本文探索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);    }}
  1. 构建RedisClient。
  2. 构建CacheFrontend。
    ClientSideCaching.enable开启客户端缓存,即发送“CLIENT TRACKING”命令给Redis服务器,要求Redis开启Tracking机制。
    最初一个参数指定了Redis Tracking的模式,这里用的是最简略的非播送模式。
    这里能够看到,通过Map保留客户端缓存的内容。
  3. 反复查问同一个值,查看缓存是否失效。

咱们能够通过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;}
  1. 获取Redis连贯。
  2. 创立Guava缓存类LoadingCache,该缓存类如果发现数据不存在,则查问Redis。
  3. 开启Redis客户端缓存。
  4. 增加回调函数,如果收到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客户端缓存有两个点须要关注:

  1. 开启客户端缓存后,Redis连贯不能断开。
    如果Redis连贯断了,并且客户端主动重连,那么新的连贯是没有开启Tracking机制的,该连贯查问的键不会受到生效音讯,结果很重大。
    同样,开启Tracking的连贯和查问缓存键的连贯必须是同一个,不能应用A连贯开启Tracking机制,应用B连贯去查问缓存键(所以客户端不能应用连接池)。

Redis服务器能够设置timeout配置,主动超过该配置没有发送申请的连贯。
而Lettuce有主动重连机制,重连后的连贯将收不到生效音讯。
有两个解决思路:
(1)实现Lettuce心跳机制,定时发送PING命令以维持连贯。
(2)即便应用心跳机制,Redis连贯仍然可能断开(网络跳动等起因),能够批改主动重连机制(Lettuce的ReconnectionHandler类),减少如下逻辑:如果连贯原来开启了Tracking机制,则重连后须要主动开启Tracking机制。
须要留神,如果应用的是非播送模式,须要清空旧连贯缓存的数据,因为连贯曾经变更,Redis服务器不会将旧连贯的生效音讯发送给新连贯。

  1. 启用缓存的连贯与未启动缓存的连贯应该辨别。
    这点比较简单,上例例子中都应用RedisClient#connect办法创立一个新的连贯,专用于客户端缓存。

客户端缓存是一个弱小的性能,须要咱们去用好它。惋惜以后临时还没有欠缺的Java客户端反对,本书分享了我的一些计划与思路,欢送探讨。我后续会关注持续Lettuce的更新,如果Lettuce提供了欠缺的Redis客户端缓存反对,再更新本文。

对于Redis Tracking的具体应用与实现原理,我在新书《Redis外围原理与实际》做了详尽剖析,文章最初,介绍一下这本书:
本书通过深入分析Redis 6.0源码,总结了Redis外围性能的设计与实现。通过浏览本书,读者能够深刻了解Redis外部机制及最新个性,并学习到Redis相干的数据结构与算法、Unix编程、存储系统设计,分布式系统架构等一系列常识。
通过该书编辑批准,我会持续在集体技术公众号(binecy)公布书中局部章节内容,作为书的预览内容,欢送大家查阅,谢谢。

书籍详情:
京东链接
豆瓣链接