本文探索 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 LIST
id=4 addr=192.168.56.1:50402 fd=7 name= age=23 idle=22 flags=t ...
flags=t
代表这个连贯启动了 Tracking 机制。
SpringBoot 利用
那么如何在 SpringBoot 上应用呢?请看上面的例子
@Bean
public 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 客户端缓存联合
@Bean
public 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)公布书中局部章节内容,作为书的预览内容,欢送大家查阅,谢谢。
书籍详情:
京东链接
豆瓣链接