spring缓存注解,除常用的@Cacheable,还有@CachePut、@CacheEvict、@CacheConfig、@Caching等注解,组成了一个完整的缓存注解集。
缓存的重要性、地位就不说了,不能狭义理解,缓存就是对数据库的数据缓存,比如说CPU缓存、互联网CDN服务都有它的影子,把一些耗时的计算结果存储下来,形成直接可利用的产品数据,避免重复计算,也可以称为计算缓存。可以泛泛理解为,缓存就是为突破稀缺资源的性能瓶颈,而采取的一种方法、策略。这些资源是数据库、第三方接口、网络带宽、一段业务逻辑等。
有一个问题,在软件开发时,缓存经常用到,不管是本地缓存,还是redis缓存,直接用这些缓存类库提供工具类,已经很方便、很灵活,为什么说还要用spring提供的缓存注解,被束缚。说心里话spring提供的缓存注解,功能上很完整,也很灵活。但实际开发中用的最多的还是通过工具类方式,程序员多少都有些控制综合证,就是不放心委托给第三方去处理。
我的理解,采用spring缓存注解的价值:
(1) 业务逻辑、缓存逻辑二者进行分离,使其职责更清晰。
(2) 它是一种规范(个人认为是最重要的),大家都采用这种方式,而不是五花八门,更有利于对系统长期迭代、维护。
@Cacheable:创建、查询缓存
@CachePut:更新缓存
@CacheEvict:删除缓存
@CacheConfig:类级别共享配置
@Caching: 组合缓存配置
该注解,可以使方法返回结果被缓存,再次通过相同参数调用时,会直接从缓存获取,而不再执行该方法逻辑。
参数:
cacheNames:缓存名称,用来划分不同的缓存区,避免相同key值互相影响。
key:缓存key值,格式为spring EL 表达式,从方法参数获取值,例如:"#id","#user.id"等。
keyGenerator:自定义key生成类,通过反射方式自己构建key,细节参考“4 自定义”部分内容,跟key参数不能同时赋值。
cacheManager:指定缓存管理器,不赋值时,为默认管理器,常用于多缓存源场景。
cacheResolver:自定义获取缓存源,跟keyGenerator类似,细节参考“4 自定义”部分内容。
condition:设置匹配条件,针对请求参数,满足条件的进行缓存,格式为spring EL 表达式。
unless:设置排除条件,针对返回值,满足条件的不进行缓存,格式为spring EL 表达式。
sync:是否开启同步底层方法的调用,如果开启,可避免相同key值,多个线程同时加载数据,默认为false没有开启。(开启后,会对参数有些限制,细节可参考Cacheable源码描述)
例子:
@Cacheable(cacheNames="users", key="#id")
public User get1(int id) {
System.out.println("do get1: " + id);
return new User(id);
}
@Cacheable(cacheNames="users", key="#user.id")
public User get2(User user) {
System.out.println("do get2: " + user.getId());
return new User(user.getId());
}
@Cacheable(cacheNames="users", key="#user.id",
condition="#user.id > 300", unless="#result.id > 500")
public User get3(User user) {
System.out.println("do get3: " + user.getId());
return new User(user.getId());
}
@Cacheable(cacheNames="users", key="#user.id", sync=true)
public User get4(User user) throws InterruptedException {
System.out.println("do get4: " + user.getId());
Thread.sleep(10000);
return new User(user.getId());
}
@Cacheable(cacheNames="users", keyGenerator="myKeyGenerator", cacheManager="myCacheManager")
public User get5(int id) {
System.out.println("do get5: " + id);
return new User(id);
}
该注解,在功能上跟@Cacheable基本相同,不同之处就是,每次都会执行方法逻辑,更新缓存。
参数:
除不包含sync参数外,其它跟@Cacheable一致。
例子:
@CachePut(cacheNames="users", key="#id")
public User put(int id) {
System.out.println("do put: " + id);
return new User(id);
}
该注解,对符合参数条件的缓存,作删除处理。
参数:
除跟@Cacheable类似的参数外,还包含另外allEntries,beforeInvocation两个参数。
allEntries:删除指定cacheNames区域内,所有的缓存。
beforeInvocation:如果true时,在执行方法之前做删除缓存处理,false时,在执行方法之后做删除处理,默认为false。
例子:
@CacheEvict(cacheNames="users", key="#id")
public void delete(int id) {
System.out.println("do delete: " + id);
}
@CacheEvict(cacheNames="users", allEntries=true)
public void clear() {
System.out.println("do clear");
}
@CacheConfig是一个类级别的注解,类下所有被缓存注解的方法都会继承所配置的参数,避免方法上相同参数重复配置。
参数:
只包含cacheNames,keyGenerator,cacheManager,cacheResolver四个参数。
例子:
@Service
@CacheConfig(cacheNames="users", cacheManager="myCacheManager")
public class UserService2 {
@Cacheable(key="#id")
public User get(int id) {
System.out.println("do get: " + id);
return new User(id);
}
@CachePut(key="#id")
public User put(int id) {
System.out.println("do put: " + id);
return new User(id);
}
@CacheEvict(key="#id")
public void delete(int id) {
System.out.println("do delete: " + id);
}
@CacheEvict(allEntries=true)
public void clear() {
System.out.println("do clear");
}
}
该注解,可同时组合、配置多个@Cacheable, @CachePut, @CacheEvict注解。
例子:
@Caching(evict={
@CacheEvict(cacheNames="users", key="#id"),
@CacheEvict(cacheNames="roles", key="#id")
})
public void delete2(int id) {
System.out.println("do clear");
}
开启缓存功能,需要先添加使能注解@EnableCaching,通常习惯在启动类配置,否则缓存注解@Cacheable等不起作用。
org.springframework.boot
spring-boot-starter-cache
org.springframework.boot
spring-boot-starter-data-redis
com.github.ben-manes.caffeine
caffeine
既然是对数据进行缓存,就会涉及数据缓存到哪里问题,是进程本地内存,还是进程外远程存储,就需要配置缓存源,spring提供了丰富的缓存源种类,以下是常用的几种。
如果项目没有第三方缓存源依赖时,spring boot 会默认配置ConcurrentMapCacheManager缓存管理器,其内部由ConcurrentHashMap存储缓存数据,如果有第三方缓存依赖,例如:caffeine、redis时,就会相应的配置CaffeineCacheManager或RedisCacheManager做默认缓存管理器,但只配置一个,具体优先级,推测是按CacheType枚举顺序,没有细查。
其中ConcurrentMapCacheManager无法设置缓存时间,通常不建议使用。
下面是java配置方式(配置文件方式可参考通用配置类CacheProperties定义)
@Bean(name = "simpleCacheManager")
public SimpleCacheManager simpleCacheManager() {
SimpleCacheManager result = new SimpleCacheManager();
CaffeineCache users = new CaffeineCache("users",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
CaffeineCache roles = new CaffeineCache("roles",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
result.setCaches(Arrays.asList(users, roles));
return result ;
}
@Bean(name = "redisCacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.prefixCacheNameWith("mtr");
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
@Configuration
public class CacheConfig {
@Bean(name = "simpleCacheManager")
public SimpleCacheManager simpleCacheManager() {
SimpleCacheManager result = new SimpleCacheManager();
CaffeineCache users = new CaffeineCache("users",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
CaffeineCache roles = new CaffeineCache("roles",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
result.setCaches(Arrays.asList(users, roles));
return result ;
}
@Bean(name = "redisCacheManager")
@Primary
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.prefixCacheNameWith("mtr");
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
}
例子(不完善仅做参考):
@Component("keyGenerator")
public class CacheKeyGenerator implements KeyGenerator {
public static final int NO_PARAM_KEY = 0;
public static final int NULL_PARAM_KEY = 53;
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(".").append(method.getName()).append(":");
if (params.length == 0) {
key.append(NO_PARAM_KEY);
} else {
int count = 0;
for (Object param : params) {
if (0 != count) {
key.append(',');
}
if (param == null) {
key.append(NULL_PARAM_KEY);
} else if (ClassUtils.isPrimitiveArray(param.getClass())) {
int length = Array.getLength(param);
for (int i = 0; i < length; i++) {
key.append(Array.get(param, i));
key.append(',');
}
} else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
key.append(param);
} else {
//Java一定要重写hashCode和eqauls
key.append(param.hashCode());
}
count++;
}
}
String finalKey = key.toString();
System.out.println("using cache key=" + finalKey);
return finalKey;
}
}
SimpleCacheResolver,NamedCacheResolver 是spring内部的CacheResolver接口实现类,可根据实际情况参考实现,就不提供例子。
也就是大家常讨论的问题:雪崩、穿透、击穿,下面一一说一下:
(1) 雪崩
缓存雪崩,就是某一时刻发生大规模的缓存失效,这时,大量请求就会直接打到DB上,严重时会导致DB撑不住、挂掉。
方案:
a. 保证缓存服务的高可用。
b. 针对大量key同时过期的情况,解决起来比较简单,只需要将每个key的过期时间打散即可.
c. 本地缓存保底,以及限流&降级等措施。
(2) 穿透
当查询DB不存在的数据时,而缓存也不保存空值,就会导致请求每次都会打到数据库上面去。这种查询不存在数据,而导致直接访问DB的现象称为缓存穿透。
方案:
a. 缓存保存空值,常见的redis缓存都提供相关配置参数。
b. 采用布隆过滤器等,进行前端拦截等。
(3) 击穿
在高并发场景,大量请求同时查询一个 key 时,而此时该key正好失效,就会导致多个请求同时加载该key数据场景。这种现象我们称为缓存击穿。
方案:
a. 可以采用锁的方式,限制对相同key的数据同时加载,可尝试通过缓存注解@Cacheable中sync参数处理。
(1) 配置缓存注解参数时,最好通过cacheManager明确缓存源,避免项目后期迭代,添加第三方依赖时,可能导致默认缓存管理器变动,发生不可预期的问题。
(2) 如果缓存涉及key、value序列化,当value是复杂类型时,不可避免会有属性变动,此时需要考虑已缓存数据,与新数据类型兼容问题,在多项目共享数据类型场景,尤其需要注意。