方式二:自定义cacheNames方式
虽然我觉得方案一已经能够满足我们需求了,但是广大小伙伴还是觉得使用起来不太自由,毕竟大多数Cache都希望是通过在注解指定CacheNames让其自动生成就行(其实提前追备好有助于提升初次访问的性能)。但是为了便用性摆第一位的话,那就应广大小伙伴的要求,写出本例供以参考:
其实最终我们期望的使用方式如下:
@Cacheable(cacheNames = "demoCache#3600", key = "#id + 0"),
通过#
分隔,后面部分表示此Cache
的TTL(单位:秒)
为了实现这个效果,其实并不难,只需要对RedisCacheManager
稍稍的改造一下即可达到目的:
public class MyRedisCacheManager extends RedisCacheManager { public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { super(cacheWriter, defaultCacheConfiguration); } @Override protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { String[] array = StringUtils.delimitedListToStringArray(name, "#"); name = array[0]; if (array.length > 1) { // 解析TTL long ttl = Long.parseLong(array[1]); cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl)); // 注意单位我此处用的是秒,而非毫秒 } return super.createRedisCache(name, cacheConfig); } }
使用我自定义的MyRedisCacheManager配置CacheConfig如下:
@EnableCaching // 使用了CacheManager,别忘了开启它 否则无效 @Configuration public class CacheConfig extends CachingConfigurerSupport { @Bean public CacheManager cacheManager() { RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) .computePrefixWith(cacheName -> "caching:" + cacheName); MyRedisCacheManager redisCacheManager = new MyRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory()), defaultCacheConfig); return redisCacheManager; } @Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); configuration.setHostName("10.102.132.150"); configuration.setPort(6379); configuration.setDatabase(0); LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration); return factory; } @Bean public RedisTemplate<String, String> stringRedisTemplate() { RedisTemplate<String, String> redisTemplate = new StringRedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory()); return redisTemplate; } }
使用示例如下:
@Service public class CacheDemoServiceImpl implements CacheDemoService { @Cacheable(cacheNames = { "demoCache#3600", "demoCar#600", "demoFsx" }, key = "#id") @Override public Object getFromDB(Integer id) { System.out.println("模拟去db查询~~~" + id); return "hello cache..."; } }
打印结果:
模拟去db查询~~~1 ----------验证缓存是否生效---------- org.springframework.data.redis.cache.RedisCache@53f4c1e6 hello cache...
缓存生效。Redis Server里查到缓存结果如图(TTL都分别生效了):
说明:demoFsx没有指定TTL,所以走了默认值ttl=1天
小细节
- 同样的,禁用前缀并不影响它的TTL的生效与否
- 若在CacheManager里已经配置了Cache对应的TTL配置,那就以CacheManager里配置的为准
- 若多个方法里配置了同一个CacheName,TTL以第一个执行的生成Cache的方法配置的为准
总之一个原则:TTL是和Cache绑定的,且是在Cache在首次被初始化的时候就被指定好了
关于此方案,其实还可以扩展一下,比如可以扩展成可配置的如下:
@Cacheable(cacheNames = "demoCache#${cache.timeout:1800}",key = "#id")
其实这么想的小伙伴,我觉得根本原因是不太能理解cacheName和Redis的key的关系导致的(本文以Redis为例~)
我此处不直接解答这个问题,但我对此额外抛出3个问题,相信答案就不攻自破了:
- 为何同一个Cache下管理的key你需要不同的TTL???(这种设计本身就不合理吧)
- 在不禁用前缀的情况下,cacheName默认都会反映到key上。因此即使你有这种特殊需求,你也可以通过定义特殊的CacheName来实现
- 若你真想控制到key这种细粒度,我只能说:实现成本太高了且会打破一定的封装性,后续扩展受限
综合来说,不管从场景上还是技术上,我都是极力不推荐这种行为的。
总结
本文主要介绍了让缓存注解支持TTL失效时间,提供的两种方式都可以用在生产环境中。合理的使用、控制失效时间,能让你的应用更加的高效,缓存利用得更合理。
另外关于Spring缓存其实还有一个重要知识点:缓存即将过期时主动刷新缓存:
因为缓存失效后,就会有一些请求会打到DB上,这段时间如果是高并发的话DB压力就很大(sync=true可以有一定的缓解作用),DB就很危险,容易造成雪崩。
因此我们是期望在缓存即将过期的某一时间点,后台主动去更新缓存以确保前端请求的缓存命中率。关于这部分的实现,只有在高并发系统下才有需求,有兴趣和有需要的小伙伴可往这方面考虑一把~