写点什么

实践篇 -- Redis 客户端缓存在 SpringBoot 应用的探究

用户头像
binecy
关注
发布于: 刚刚
实践篇 -- Redis客户端缓存在SpringBoot应用的探究

本文探究 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。

  3. ClientSideCaching.enable 开启客户端缓存,即发送“CLIENT TRACKING”命令给 Redis 服务器,要求 Redis 开启 Tracking 机制。

  4. 最后一个参数指定了 Redis Tracking 的模式,这里用的是最简单的非广播模式。

  5. 这里可以看到,通过 Map 保存客户端缓存的内容。

  6. 重复查询同一个值,查看缓存是否生效。


我们可以通过 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 连接不能断开。

  2. 如果 Redis 连接断了,并且客户端自动重连,那么新的连接是没有开启 Tracking 机制的,该连接查询的键不会受到失效消息,后果很严重。

  3. 同样,开启 Tracking 的连接和查询缓存键的连接必须是同一个,不能使用 A 连接开启 Tracking 机制,使用 B 连接去查询缓存键(所以客户端不能使用连接池)。


Redis 服务器可以设置 timeout 配置,自动超过该配置没有发送请求的连接。

而 Lettuce 有自动重连机制,重连后的连接将收不到失效消息。

有两个解决思路:

(1)实现 Lettuce 心跳机制,定时发送 PING 命令以维持连接。

(2)即使使用心跳机制,Redis 连接依然可能断开(网络跳动等原因),可以修改自动重连机制(Lettuce 的 ReconnectionHandler 类),增加如下逻辑:如果连接原来开启了 Tracking 机制,则重连后需要自动开启 Tracking 机制。

需要注意,如果使用的是非广播模式,需要清空旧连接缓存的数据,因为连接已经变更,Redis 服务器不会将旧连接的失效消息发送给新连接。


  1. 启用缓存的连接与未启动缓存的连接应该区分。

  2. 这点比较简单,上例例子中都使用 RedisClient#connect 方法创建一个新的连接,专用于客户端缓存。


客户端缓存是一个强大的功能,需要我们去用好它。可惜当前暂时还没有完善的 Java 客户端支持,本书说了我的一些方案与思路,欢迎探讨。我后续会关注继续 Lettuce 的更新,如果 Lettuce 提供了完善的 Redis 客户端缓存支持,再更新本文。


关于 Redis Tracking 的详细使用与实现原理,我在新书《Redis 核心原理与实践》做了详尽分析,文章最后,介绍一下这本书:

本书通过深入分析 Redis 6.0 源码,总结了 Redis 核心功能的设计与实现。通过阅读本书,读者可以深入理解 Redis 内部机制及最新特性,并学习到 Redis 相关的数据结构与算法、Unix 编程、存储系统设计,分布式系统架构等一系列知识。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。


京东链接

豆瓣链接

发布于: 刚刚阅读数: 2
用户头像

binecy

关注

还未添加个人签名 2020.08.26 加入

《Redis核心原理与实践》作者,欢迎关注个人技术公众号binecy

评论

发布
暂无评论
实践篇 -- Redis客户端缓存在SpringBoot应用的探究