本文探索Redis最新个性--客户端缓存在SpringBoot上的利用。
Redis Tracking
Redis客户端缓存机制基于Redis Tracking机制实现的。咱们先理解一下Redis Tracking机制。
为什么须要Redis Tracking
Redis因为速度快、性能高,经常作为MySQL等传统数据库的缓存数据库。但因为Redis是近程服务,查问Redis须要通过网络申请,在高并发查问情景中不免造成性能损耗。所以,高并发利用通常引入本地缓存,在查问Redis前先查看本地缓存是否存在数据。
如果应用MySQL存储数据,那么数据查问流程下图所示。
引入多端缓存后,批改数据时,各数据缓存端如何保证数据统一是一个难题。通常的做法是批改MySQL数据,并删除Redis缓存、本地缓存。当用户发现缓存不存在时,会从新查问MySQL数据,并设置Redis缓存、本地缓存。
在分布式系统中,某个节点批改数据后不仅要删除以后节点的本地缓存,还须要发送申请给集群中的其余节点,要求它们删除该数据的本地缓存,如下图所示。如果分布式系统中节点很多,那么该操作会造成不少性能损耗。
为此,Redis 6提供了Redis Tracking机制,对该缓存计划进行了优化。开启Redis Tracking后,Redis服务器会记录客户端查问的所有键,并在这些键产生变更后,发送生效音讯告诉客户端这些键已变更,这时客户端须要将这些键的本地缓存删除。基于Redis Tracking机制,某个节点批改数据后,不须要再在集群播送“删除本地缓存”的申请,从而升高了零碎复杂度,并进步了性能。
Redis Tracking的利用
下表展现了Redis Tracking的根本应用
(1)为了反对Redis服务器推送音讯,Redis在RESP2协定上进行了扩大,实现了RESP3协定。HELLO 3命令示意客户端与Redis服务器之间应用RESP3协定通信。
留神:Redis 6.0提供了Redis Tracking机制,但该版本的redis-cli并不反对RESP3协定,所以这里须要应用Redis 6.2版本的redis-cli进行演示。
(2)CLIENT TRACKING on命令的作用是开启Redis Tracking机制,尔后Redis服务器会记录客户端查问的键,并在这些键变更后推送生效音讯告诉客户端。生效音讯以invalidate结尾,前面是生效键数组。
上表中的客户端 client1 查问了键 score 后,客户端 client2 批改了该键,这时 Redis 服务器会马上推送生效音讯给客户端 client1,但 redis-cli 不会间接展现它收到的推送音讯,而是在下一个申请返回后再展现该音讯,所以 client1 从新发送了一个 PING申请。
下面应用的非播送模式,另外,Redis Tracking还反对播送模式。在播送模式下,当变更的键以客户端关注的前缀结尾时,Redis服务器会给所有关注了该前缀的客户端发送生效音讯,不论客户端之前是否查问过这些键。
下表展现了如何应用Redis Tracking的播送模式。
阐明一下CLIENT TRACKING命令中的两个参数:
BCAST参数:启用播送模式。
PREFIX参数:申明客户端关注的前缀,即客户端只关注cache结尾的键。
强调一下非播送模式与播送模式的区别:
非播送模式:Redis服务器记录客户查问过的键,当这些键发生变化时,Redis发送生效音讯给客户端。
播送模式:Redis服务器不记录客户查问过的键,当变更的键以客户端关注的前缀结尾时,Redis就会发送生效音讯给客户端。
对于Redis Tracking的更多内容,我曾经在新书《Redis外围原理与实际》中详细分析,这里不再赘述。
Redis客户端缓存
既然Redis提供了Tracking机制,那么客户端就能够基于该机制实现客户端缓存了。
Lettuce实现
Lettuce(6.1.5版本)曾经反对Redis客户端缓存(单机模式下),应用CacheFrontend类能够实现客户端缓存。
public static void main(String[] args) throws InterruptedException { // [1] RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1") .withPort(6379) .build(); RedisClient redisClient = RedisClient.create(redisUri); // [2] StatefulRedisConnection<String, String> connect = redisClient.connect(); Map<String, String> clientCache = new ConcurrentHashMap<>(); CacheFrontend<String, String> frontend = ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connect, TrackingArgs.Builder.enabled()); // [3] while (true) { String cachedValue = frontend.get("k1"); System.out.println("k1 ---> " + cachedValue); Thread.sleep(3000); }}
- 构建RedisClient。
- 构建CacheFrontend。
ClientSideCaching.enable开启客户端缓存,即发送“CLIENT TRACKING”命令给Redis服务器,要求Redis开启Tracking机制。
最初一个参数指定了Redis Tracking的模式,这里用的是最简略的非播送模式。
这里能够看到,通过Map保留客户端缓存的内容。 - 反复查问同一个值,查看缓存是否失效。
咱们能够通过Redis的Monitor命令监控Redis服务收到的命令,应用该命令就能够看到,开启客户端缓存后,Lettuce不会反复查问同一个键。
而且咱们批改这个键后,Lettuce会从新查问这个键的最新值。
通过Redis的Client List命令能够查看连贯的信息
> CLIENT LISTid=4 addr=192.168.56.1:50402 fd=7 name= age=23 idle=22 flags=t ...
flags=t
代表这个连贯启动了Tracking机制。
SpringBoot利用
那么如何在SpringBoot上应用呢?请看上面的例子
@Beanpublic CacheFrontend<String, String> redisCacheFrontend(RedisConnectionFactory redisConnectionFactory) { StatefulRedisConnection connect = getRedisConnect(redisConnectionFactory); if (connect == null) { return null; } CacheFrontend<String, String> frontend = ClientSideCaching.enable( CacheAccessor.forMap(new ConcurrentHashMap<>()), connect, TrackingArgs.Builder.enabled()); return frontend;}private StatefulRedisConnection getRedisConnect(RedisConnectionFactory redisConnectionFactory) { if(redisConnectionFactory instanceof LettuceConnectionFactory) { AbstractRedisClient absClient = ((LettuceConnectionFactory) redisConnectionFactory).getNativeClient(); if (absClient instanceof RedisClient) { return ((RedisClient) absClient).connect(); } } return null;}
其实也简略,通过RedisConnectionFactory获取一个StatefulRedisConnection连贯,就能够创立CacheFrontend了。
这里RedisClient#connect办法会创立一个新的连贯,这样能够将应用客户端缓存、不应用客户端缓存的连贯辨别。
联合Guava缓存
Lettuce的StatefulRedisConnection类还提供了addListener办法,能够设置回调办法解决Redis推送的音讯。
利用该办法,咱们能够将Guava的缓存与Redis客户端缓存联合
@Beanpublic LoadingCache<String, String> redisGuavaCache(RedisConnectionFactory redisConnectionFactory) { // [1] StatefulRedisConnection connect = getRedisConnect(redisConnectionFactory); if (connect != null) { // [2] LoadingCache<String, String> redisCache = CacheBuilder.newBuilder() .initialCapacity(5) .maximumSize(100) .expireAfterWrite(5, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { public String load(String key) { String val = (String)connect.sync().get(key); return val == null ? "" : val; } }); // [3] connect.sync().clientTracking(TrackingArgs.Builder.enabled()); // [4] connect.addListener(message -> { if (message.getType().equals("invalidate")) { List<Object> content = message.getContent(StringCodec.UTF8::decodeKey); List<String> keys = (List<String>) content.get(1); keys.forEach(key -> { redisCache.invalidate(key); }); } }); return redisCache; } return null;}
- 获取Redis连贯。
- 创立Guava缓存类LoadingCache,该缓存类如果发现数据不存在,则查问Redis。
- 开启Redis客户端缓存。
- 增加回调函数,如果收到Redis发送的生效音讯,则革除Guava缓存。
Redis Cluster模式
下面说的利用必须在Redis单机模式下(或者主从、Sentinel模式),遗憾的是,
目前发现Lettuce(6.1.5版本)还没有反对Redis Cluster下的客户端缓存。
简略看了一下源码,目前发现如下起因:
Cluster模式下,Redis命令须要依据命令的键,重定向到键的存储节点执行。
而对于“CLIENT TRACKING”这个没有键的命令,Lettuce并没有将它发送给Cluster中所有的节点,而是将它发送给一个固定的默认的节点(可查看ClusterDistributionChannelWriter类),所以通过StatefulRedisClusterConnection调用RedisAdvancedClusterCommands.clientTracking办法并没有开启Redis服务的Tracking机制。
这个其实也能够批改,有工夫再钻研一下。
须要留神的问题
那么单机模式下,Lettuce的客户端缓存就真的没有问题了吗?
认真思考一下Redis Tracking的设计,发现应用Redis客户端缓存有两个点须要关注:
- 开启客户端缓存后,Redis连贯不能断开。
如果Redis连贯断了,并且客户端主动重连,那么新的连贯是没有开启Tracking机制的,该连贯查问的键不会受到生效音讯,结果很重大。
同样,开启Tracking的连贯和查问缓存键的连贯必须是同一个,不能应用A连贯开启Tracking机制,应用B连贯去查问缓存键(所以客户端不能应用连接池)。
Redis服务器能够设置timeout配置,主动超过该配置没有发送申请的连贯。
而Lettuce有主动重连机制,重连后的连贯将收不到生效音讯。
有两个解决思路:
(1)实现Lettuce心跳机制,定时发送PING命令以维持连贯。
(2)即便应用心跳机制,Redis连贯仍然可能断开(网络跳动等起因),能够批改主动重连机制(Lettuce的ReconnectionHandler类),减少如下逻辑:如果连贯原来开启了Tracking机制,则重连后须要主动开启Tracking机制。
须要留神,如果应用的是非播送模式,须要清空旧连贯缓存的数据,因为连贯曾经变更,Redis服务器不会将旧连贯的生效音讯发送给新连贯。
- 启用缓存的连贯与未启动缓存的连贯应该辨别。
这点比较简单,上例例子中都应用RedisClient#connect办法创立一个新的连贯,专用于客户端缓存。
客户端缓存是一个弱小的性能,须要咱们去用好它。惋惜以后临时还没有欠缺的Java客户端反对,本书分享了我的一些计划与思路,欢送探讨。我后续会关注持续Lettuce的更新,如果Lettuce提供了欠缺的Redis客户端缓存反对,再更新本文。
对于Redis Tracking的具体应用与实现原理,我在新书《Redis外围原理与实际》做了详尽剖析,文章最初,介绍一下这本书:
本书通过深入分析Redis 6.0源码,总结了Redis外围性能的设计与实现。通过浏览本书,读者能够深刻了解Redis外部机制及最新个性,并学习到Redis相干的数据结构与算法、Unix编程、存储系统设计,分布式系统架构等一系列常识。
通过该书编辑批准,我会持续在集体技术公众号(binecy)公布书中局部章节内容,作为书的预览内容,欢送大家查阅,谢谢。
书籍详情:
京东链接
豆瓣链接