缓存在开发中是一个必不可少的优化点,近期在项目重构中,有关缓存问题,花费大量的时间去做优化,比如在加载数据比较所的场景中,使用缓存机制来提高接口的响应速度,间接的提高用户体验。对于缓存,很多开发者对它都是既爱又恨,爱它的是:能够大幅度提升响应效率,恨的是:如果缓存处理不好、没有用好缓存策略,没有及时更新数据库的数据就会导致数据产生滞后,从而导致用户体验较差。这是一个很严重的老大难的问题,例如我在开发某项功能需要和其他项目进行对接的时候,线上总是出现我推送接口数据成功但是网站数据滞后的现象,询问对方的技术人员,得到的回复是缓存问题,只要删除缓存就没事了,我只能无奈了……所有说如何处理好缓存,对于我们开发人员来说,是一个极其棘手的问题。值得庆幸的是,SpringBoot已经提供给开发者很便捷的开发工作,废话不多了,接下来开始探索SpringBoot的缓存如何使用吧!
Spring 从3.1 版本开始就定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,Cache 接口下 Spring 提供了各种的xxxCache的实现,例如RedisCache、EhCacheCache、ConcurrentMapCache等。每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过。如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。
重要概念以及缓存注解
缓存注解
名称 | 解释 |
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其进行缓存。主要实现的功能在进行一个读操作的时候,先从缓存中查询,如果查找不到,在读取数据库。这是缓存的注解最重要的一个方法,基本上我们的所有缓存实现都要依赖于它 |
@CacheEvict | 主要是配合@Cacheable一起使用的,它的主要作用就是清除缓存。当方法进行一些更新、删除操作的时候,就需要删除缓存 |
@CachePut | 这个注解它总是会把数据缓存,而不会去每次做检查它是否存在 。与@Cacheable区别在于是否每次都调用方法,常用于更新 |
@EnableCaching | 开启基于注解的缓存,主要作用就是全局配置缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
@Cacheable/@CachePut/@CacheEvict 主要的参数
名称 | 解释 |
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个。例如: @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。例如:@Cacheable(value=”testcache”,key=”#id”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存。@Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
unless | 否定缓存。当条件结果为TRUE时,就不会缓存。@Cacheable(value=”testcache”,unless=”#userName.length()>2”) |
allEntries(@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。 @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation(@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存@CachEvict(value=”testcache”,beforeInvocation=true) |
springBoot开启注解
开始使用前需要导入依赖
<!-- Springboot devtools热部署配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- 引入lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- 引入cache 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--MySQL依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 引入 MyBatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
在启动类注解@EnableCaching开启缓存
@SpringBootApplication
@EnableCaching //开启缓存
public class CacheApplication{
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
缓存@Cacheable
@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存
@Cacheable(key = "#id", unless = "#result.state==0")
public list<SysUser> getLists(Integer uid){
return cacheDao.getListsByUid(uid);
}
到这里,可以运行程序检验缓存功能是否实现了。如有兴趣的话,也可以深入源码,查看其他的属性,这里简单的说下。
String[] cacheNames() default {}; //和value注解差不多,二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存
boolean sync() default false; //是否使用异步模式
配置@CacheConfig
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"myCache"})注解来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
@Service
@CacheConfig(cacheNames = "articleCache")
public class ArticleService {
……
}
CacheConfig 的属性
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
更新@CachePut
@CachePut注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。但需要注意的是该注解的value 和 key 必须与要更新的缓存相同,也就是与@Cacheable 相同。示例:
@CachePut(value = "cacheTemp", key = "targetClass + #p0")
public SysUser updata(SysUser sysUser) {
SysUser sysUser = sysUserDao.findAllById(sysUser.getId());
sysUserDao.updata(sysUser);
return sysUser;
}
@Cacheable(value = "cacheTemp", key = "targetClass +#p0")//清空缓存
public SysUser save(SysUser sysUser) {
sysUserDao.save(sysUser);
return sysUser;
}
CachePut 其他的属性
String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存
清除@CacheEvict
@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。
@Cacheable(value = "cacheTemp",key = "#id")
public SysUser save(SysUser job) {
……
}
//清除一条缓存,key为要清空的数据
@CacheEvict(value="cacheTemp",key="#id")
public void delect(int id) {
……
}
//方法调用后清空所有缓存
@CacheEvict(value="accountCache",allEntries=true)
public void delectAll() {
……
}
//方法调用前清空所有缓存
@CacheEvict(value="accountCache",beforeInvocation=true)
public void delectAll() {
……
}
其他属性
String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则清空
测试
这里使用postman模拟接口请求
执行插入操作
首先我们来增加一篇文章:请求add接口
后台返回表示成功:
从后台数据库看到已经插入数据,它的id是1
执行查询操作
在查询操作中,getArticle,我使用线程睡眠的方式,模拟了5秒的时间来处理耗时性业务,第一次请求肯定会查询数据库,理论上第二次请求,将会走缓存,我们来测试一下:首先执行查询操作
接口响应成功,再看一下后台打印:表示执行了一次查询操作,耗时5212秒
好,重点来了,我们再次请求接口看看会返回什么?
与上面的比对,这次没有打印执行数据库查询操作,证明没有走数据库,并且耗时只有3ms,成功了!缓存发挥作用,从5212秒减小到3秒!大大提升了响应速度,
执行更新操作
当我们进行修改操作的时候,我们希望缓存的数据被清空:
后台控制台打印:
接下来,连续请求三次请求查询接口,看看会返回什么?
第一次请求以及结果
连续三次调用查询接口,虽然每次返回的都是相同的结果,看上去没有什么不同,但是我却有很明显的感受,就是第一次查询最慢,第二次、第三次查询特别快。第一次查询耗时是5063毫秒,第二次、第三次耗时确实0,这是因为当我们第一次执行id=2的数据库查询操作,那个时候缓存已被清空,是从数据库中获取的最新数据并放进缓存,然后后面的几次查询都是直接从缓存中获取数据,所以查询速度很快。
执行删除操作
在删除操作中,执行了一次删除,那么缓存也会被清空,查询的时候会再次走数据库,这里就不在具体展示了。
总结
如何在开发中使用缓存?怎样合理的使用都是值得我们学习的地方,缓存能大大提升程序的响应速度,提升用户体验,不过它适用的场景也是读多写少的业务场景,如果数据频繁修改,缓存将会失去意义,每次还是执行的数据库操作!这里只是探讨了Spring的注解缓存,相对来说还是比较简单的,希望起到抛砖引玉的作用.