RedisCacheWriter
RedisCacheWrite它有Spring内建唯一实现类DefaultRedisCacheWriter,并且这个类是内建的非public的:
// @since 2.0 public interface RedisCacheWriter { // 两个静态方法用于创建一个 有锁/无锁的RedisCacheWriter // 它内部自己实现了一个分布式锁的效果~~~~ Duration可以在去获取锁没获取到的话,睡一会再去获取(避免了频繁对redis的无用访问) static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { return new DefaultRedisCacheWriter(connectionFactory); } static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50)); } @Nullable byte[] get(String name, byte[] key); // 注意:这两个put方法,都是带有TTL的,因为Redis是支持过期时间的嘛 // 多疑依托于此方法,我们其实最终可以定义出支持TTL的缓存注解,下篇博文见 void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); @Nullable byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl); void remove(String name, byte[] key); void clean(String name, byte[] pattern); } // @since 2.0 class DefaultRedisCacheWriter implements RedisCacheWriter { // 一切远程操作的链接,都来自于链接工厂:RedisConnectionFactory private final RedisConnectionFactory connectionFactory; private final Duration sleepTime; DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory) { this(connectionFactory, Duration.ZERO); } DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) { this.connectionFactory = connectionFactory; this.sleepTime = sleepTime; } // 拿到redis连接,执行命令。 当然执行之前,去获取锁 private <T> T execute(String name, Function<RedisConnection, T> callback) { RedisConnection connection = connectionFactory.getConnection(); try { checkAndPotentiallyWaitUntilUnlocked(name, connection); return callback.apply(connection); } finally { connection.close(); } } private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) { if (!isLockingCacheWriter()) { return; } try { // 自旋:去查看锁 若为true(表示锁还cun'z) while (doCheckLock(name, connection)) { Thread.sleep(sleepTime.toMillis()); } } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other participants to react. Thread.currentThread().interrupt(); throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), ex); } } ... // 关于它内部的lock、unlock处理,有兴趣的可以看看此类,因为不是本文重点,所以此处不说明了 // 其余方法,都是调用了execute方法 @Override public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { execute(name, connection -> { // 只有ttl合法才会执行。注意:单位MILLISECONDS 是毫秒值 if (shouldExpireWithin(ttl)) { connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); } else { connection.set(key, value); } return "OK"; }); } @Override public byte[] get(String name, byte[] key) { return execute(name, connection -> connection.get(key)); } // putIfAbsent是根据内部的分布式锁来实现的 @Override public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { ... } @Override public void remove(String name, byte[] key) { execute(name, connection -> connection.del(key)); } // 注意批量删,第二参数是pattern @Override public void clean(String name, byte[] pattern) { ... } ... }
整体上看,DefaultRedisCacheWriter的实现还是比较复杂的,它是2.x重写的实现。
1.x版本的RedisCache类的实现没有这么复杂,因为它依托于RedisOperations去实现,复杂度都在RedisOperations本身了。
2.x版本把注解操作缓存和RedisTempate操作缓存完全分离开了,也就是说注解缓存再也不用依赖于RedisTempate了~
为何能使用RedisCacheWriter代替RedisOperations
使用过1.x版本的都知道,创建一个RedisCacheManager实例的时候,都必须想准备一个RedisTempate实例,因为它强依赖于它。
但是如上2.0的配置可知,它现在只依赖于RedisConnectionFactory而不用再管RedisOperations了。
为何Spring要大费周章的重写一把呢?此处我可以大胆猜测一下:
- Cache抽象(缓存注解)只会操作k-v,不会涉足复杂的数据结构
- RedisOperations功能强大,能够操作任意类型。所以放在操作Cache上显得非常的笨重
- 重写的RedisCacheWriter完全独立不依赖,运用在Cache上,能够使得缓存注解保持非常高的独立性。并且此接口也非常的轻
Demo示例
说了这么多,是时候来些实战的代码了。这里我自己给出一个Demo供以参考:
@Service public class CacheDemoServiceImpl implements CacheDemoService { @Cacheable(cacheNames = "demoCache", key = "#id") @Override public Object getFromDB(Integer id) { System.out.println("模拟去db查询~~~" + id); return "hello cache..."; } } @EnableCaching // 使用了CacheManager,别忘了开启它 否则无效 @Configuration public class CacheConfig extends CachingConfigurerSupport { // 配置一个CacheManager 来支持缓存注解 @Bean public CacheManager cacheManager() { // 1.x是这么配置的:仅供参考 //RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); //cacheManager.setDefaultExpiration(ONE_HOUR * HOURS_IN_ONE_DAY); //cacheManager.setUsePrefix(true); // --------------2.x的配置方式-------------- // 方式一:直接create //RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory()); // 方式二:builder方式(推荐) RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) //Duration.ZERO表示永不过期(此值一般建议必须设置) //.disableKeyPrefix() // 禁用key的前缀 //.disableCachingNullValues() //禁止缓存null值 //=== 前缀我个人觉得是非常重要的,建议约定:注解缓存一个统一前缀、RedisTemplate直接操作的缓存一个统一前缀=== //.prefixKeysWith("baidu:") // 底层其实调用的还是computePrefixWith() 方法,只是它的前缀是固定的(默认前缀是cacheName,此方法是把它固定住,一般不建议使用固定的) //.computePrefixWith(CacheKeyPrefix.simple()); // 使用内置的实现 .computePrefixWith(cacheName -> "caching:" + cacheName) // 自己实现,建议这么使用(cacheName也保留下来了) ; RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory()) // .disableCreateOnMissingCache() // 关闭动态创建Cache //.initialCacheNames() // 初始化时候就放进去的cacheNames(若关闭了动态创建,这个就是必须的) .cacheDefaults(configuration) // 默认配置(强烈建议配置上)。 比如动态创建出来的都会走此默认配置 //.withInitialCacheConfigurations() // 个性化配置 可以提供一个Map,针对每个Cache都进行个性化的配置(否则是默认配置) //.transactionAware() // 支持事务 .build(); return redisCacheManager; }
配置好后,运行单测:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class, CacheConfig.class}) public class TestSpringBean { @Autowired private CacheDemoService cacheDemoService; @Autowired private CacheManager cacheManager; @Test public void test1() { cacheDemoService.getFromDB(1); cacheDemoService.getFromDB(1); System.out.println("----------验证缓存是否生效----------"); Cache cache = cacheManager.getCache("demoCache"); System.out.println(cache); System.out.println(cache.get(1, String.class)); } }
打印结果如下:
模拟去db查询~~~1 ----------验证缓存是否生效---------- org.springframework.data.redis.cache.RedisCache@788ddc1f hello cache...
并且缓存中有值如下(注意key有统一前缀):
就这样非常简单的,Redis分布式缓存就和Spring Cache完成了集成,可以优雅的使用三大缓存注解去操作了。对你有所帮助
备注:DefaultRedisCacheWriter使用的写入都是操作的Bytes,所以不会存在乱码问题~
总结
Redis作为当下互联网应用中使用最为广泛、最为流行的分布式缓存产品,相信本文叙述的应该是绝大多数小伙伴的场景吧。既然它使用得这么普遍,你是否想过怎么和你的同事拉开差距呢?
其实我这里有个不太成熟的小建议:盘它。只有你踩的坑多了,碰到的事多了,你才有足够的资本说你比你的同事懂得多,熟练得多,能够解决他们解决不了的问题。
至于有的小伙伴提到的想让每一个缓存注解也支持自定义过期时间,我觉得能有这个想法是很不错的,至于如何实现,请待下文分解~