lettuce学习分享
基本情况
lettuce
1.基于netty的java客户端,支持通过同步、异步、响应式API和redis进行交互
2.所有连接都会从其 RedisClient 继承默认超时,超时默认为 60 秒,可在 RedisClient 中或针对每个连接进行更改,默认和redis服务端只使用单条连接通信。
Spring Data Redis和lettuce的关系
1.springboot客户端库: Lettuce 是一个独立的 Redis 客户端库,用于 Java 应用程序。它提供了同步、异步和反应式的 API 来与 Redis 服务器通信,Lettuce 是基于 Netty 的,这使得它非常适合需要高并发处理的应用程序。
2.Spring Data Redis 集成: 在 Spring Data Redis 中,Lettuce 被用作连接和操作 Redis 服务器的底层客户端之一(另一个常用的客户端是 Jedis,但在最新版本中,Spring Data Redis 推荐使用 Lettuce 由于其非阻塞和线程安全的特性)。Spring Data Redis 提供了一个统一的 API,这个 API 在内部可以使用 Lettuce 或其他 Redis 客户端来实现与 Redis 的交互。
3.抽象层: org.springframework.data.redis.core.RedisTemplate 和 org.springframework.data.redis.core.StringRedisTemplate 是 Spring Data Redis 提供的两个核心类,它们提供了一个高级的模板方法 API,用于 Redis 数据访问,抽象了底层的 Redis 访问细节。这些模板类使用配置好的 RedisConnectionFactory 来获取连接,这个连接工厂可以配置为使用 Lettuce。
实现原理
netty NIO
Lettuce使用netty作为其与redis交互的底层框架
上图展示了Netty NIO的核心逻辑。NIO通常被理解为non-blocking I/O的缩写,表示非阻塞I/O操作。图中Channel表示一个连接通道,用于承载连接管理及读写操作;EventLoop则是事件处理的核心抽象。一个EventLoop可以服务于多个Channel,但它只会与单一线程绑定。EventLoop中所有I/O事件和用户任务的处理都在该线程上进行;其中除了选择器Selector的事件监听动作外,对连接通道的读写操作均以非阻塞的方式进行 —— 这是NIO与BIO(blocking I/O,即阻塞式I/O)的重要区别,也是NIO模式性能优异的原因。
Lettuce实现原理与Redis管道模式
虽然一个Netty的EventLoop可以服务于多个套接字连接,但是Lettuce仅凭单一的Redis连接即可支持业务端的大部分并发请求 —— 即Lettuce是线程安全的。这有赖于以下几个因素的共同作用:
1.Netty的单个EventLoop仅与单一线程绑定,业务端的并发请求均会被放入EventLoop的任务队列中,最终被该线程顺序处理。同时,Lettuce自身也会维护一个队列,当其通过EventLoop向Redis发送指令时,成功发送的指令会被放入该队列;当收到服务端的响应时,Lettuce又会以FIFO的方式从队列的头部取出对应的指令,进行后续处理。
2.Redis服务端本身也是基于NIO模型,使用单一线程处理客户端请求。虽然Redis能同时维持成百上千个客户端连接,但是在某一时刻,某个客户端连接的请求均是被顺序处理及响应的。
3.Redis客户端与服务端通过TCP协议连接,而TCP协议本身会保证数据传输的顺序性。
如此,Lettuce在保证请求处理顺序的基础上,天然地使用了管道模式(pipelining)与Redis交互 —— 在多个业务线程并发请求的情况下,客户端不必等待服务端对当前请求的响应,即可在同一个连接上发出下一个请求,省去很多等待网络的时间开销。这在加速了Redis请求处理的同时,也高效地利用了TCP连接的全双工特性。
相关好文:https://developer.aliyun.com/article/1064562
集群拓扑相关
集群拓扑更新
连接到 Redis 集群需要一个或多个初始种子节点。第一次连接时即可获得完整的集群拓扑视图(分区),因此您无需指定所有集群节点。指定多个种子节点有助于提高弹性,因为即使没有种子节点,Lettuce 也能够连接集群。Lettuce 拥有多个连接,这些连接可根据需要打开。您可以自由操作这些连接。
lettucet通过三种更新方式
下面是如何通过不同的方式更新Redis集群拓扑的代码示例:
1. 调用 RedisClusterClient.reloadPartitions()
这个方法可以手动触发拓扑的重新加载。
RedisClusterClient clusterClient = RedisClusterClient.create("redis://localhost:6379");
clusterClient.reloadPartitions();
2. 基于时间间隔的周期性更新
你可以在创建 RedisClusterClient 实例时配置周期性更新。
RedisClusterClient clusterClient = RedisClusterClient.create("redis://localhost:6379");
clusterClient.setOptions(ClusterClientOptions.builder()
.topologyRefreshOptions(ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh(30, TimeUnit.SECONDS)
.build())
.build());
3. 基于持续断开连接和MOVED/ASK重定向的自适应更新
这种方式会在检测到持续的断开连接或MOVED/ASK重定向时自动更新拓扑。
RedisClusterClient clusterClient = RedisClusterClient.create("redis://localhost:6379");
clusterClient.setOptions(ClusterClientOptions.builder()
.topologyRefreshOptions(ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers()
.build())
.build());
●MOVED: 当尝试访问一个键,而这个键已经被迁移到另一个节点时,你会收到一个 MOVED 错误。这通常发生在集群重新分配哈希槽时。
源码分析
lettuce部分
主要是在ClusterTopologyRefreshScheduler这个类里面
3.定时刷新
看到开启定时刷新配置,lettuce会开启一个定时任务的线程定时执行
4.自适应刷新
在向通道内写数据时,也就是给redis发送命令时,如果失败了,可以得到一些错误信息,例如 : MOVED 重定向,当客户端向一个节点发送命令,而该节点不包含命令所需的键的哈希槽时,节点会返回一个 MOVED 错误。这个 MOVED 错误包含了正确节点的地址,客户端应该重新向这个地址发送请求。触发 onMovedRedirection 的情况
触发条件:
1.客户端连接到错误的节点: 当客户端尝试执行一个操作,但连接的节点不包含该操作所需的数据时,该节点会返回一个 MOVED 错误。这个错误告诉客户端正确的节点地址。
2.集群拓扑变化: 如果集群的拓扑(节点和哈希槽的关系)发生变化,比如因为节点故障、添加新节点或节点维护,客户端可能会连接到一个不再负责特定哈希槽的节点。这时,节点也会返回 MOVED 错误。
处理各种事件的类:
处理前判断是否开启处理开关
从上面的代码看,判断是否处理该时间,就是将设置的事件触发器和当前事件比较,如何集合里面有该时间类型,这继续处理
大致调用流程
1.CommandHandler,通道写入和读数据的核心部分在这里
2.CommandHandler.channelRead()方法读取redis server的响应。然后调用ClusterCommand的complete()方法,然后调用ClusterDistributionChannelWriter的write(RedisCommand<K, V, T> command)方法,在这个方法中,处理各种错误。
springframework使用部分(默认设置)
lettuce主要的组件
1.RedisURI:连接信息。
2.RedisClient:Redis客户端,特殊地,集群连接有一个定制的RedisClusterClient。
3.Connection:Redis连接,主要是StatefulConnection或者StatefulRedisConnection的子类,连接的类型主要由连接的具体方式(单机、哨兵、集群、订阅发布等等)选定,比较重要。
4.RedisCommands:Redis命令API接口,基本上覆盖了Redis发行版本的所有命令,提供了同步(sync)、异步(async)、反应式(reative)的调用方式,对于使用者而言,会经常跟RedisCommands系列接口打交道
spring-data-redis
这里是org.springframework.data.redis.connection.lettuce包的代码,看一下springboot是如何集成lettuce并注入各种属性的,设置是怎么样子的
分析源码后发现RedisClusterClient是在构建LettuceConnectionFactory时构建的,这时依赖了LettuceConnectionFactory构造方法中的传参clientConfig,下面具体来看一下
1.我们代码中是使用ReactiveRedisTemplate进行redis的各种操作的,这里ReactiveRedisTemplate的构造方法中注入的,就是刚刚构建的LettuceConnectionFactory,而构造LettuceConnectionFactory时里面有注入我们自定义的属性,包括定时和自适应刷新
例如ReactiveRedisTemplate.opsForValue().set()方法的源码如下:
看到是使用了LettuceConnectionFactory创建的链接
2.首先我们代码里面手动指定开启自适应刷新
可以看到,这里将枚举里面所有的触发器全部添加进去了
构建LettuceConnectionFactory,并将前面自定义的配置注入LettuceConnectionFactory
LettuceConnectionFactory创建客户端的源码:
集群模式下,走这里的分支,将我们之前配置的属性注入,如果我们没配置,这不注入,使用默认值。
接下来应该看一下默认值是什么,以及配置是怎么生效的
3.RedisClusterClient创建的源码:
可以看到,如果没有手动注入拓扑刷新的配置,默认是不开启定时刷新拓扑的。而事件触发器这里源码默认的是一个空集合
通过打断点也可以看到,触发器都是空的:
redis故障分析
前段时间,发生了一个小故障,redis集群的一个主节点宕机,服务随之报错,知道10分钟后,该节点恢复连接(宕机同时就已经发生主从切换,现在是从节点),而后服务才也随之恢复
redis主从切换后,服务依然报错的原因
由于我们没有开启配置,没有添加任何触发器,所以以下事件不会发生,lettuce客户端并不会处理,如果是集群一个主节点宕机了,应该触发的是PERSISTENT_RECONNECTS
此时,去获取该节点的redis连接会一直报错,但不会去处理
反之,如果配置了配置了对应的触发器,会提交一个拓扑刷新的任务,刷新本地的拓扑
打断点可以看到,在重试5次没有连接上down掉的节点后, 开始刷新拓扑的本地缓存
连接断开后的大致处理过程:
1.CommandHandler,通道写入和读数据的核心部分在这里
2.CommandHandler.channelRead()方法读取redis server的响应。然后调用ClusterCommand的complete()方法,然后调用ClusterDistributionChannelWriter的write(RedisCommand<K, V, T> command)方法,在这个方法中,处理各种错误。
3.ConnectionWatchdog监控发布重连事件
4.ClusterTopologyRefreshScheduler进行各种事件的处理,进行拓扑刷新(如果有开启配置)
降级为从节点的机器恢复,服务也随之恢复的原因
从节点恢复后,服务和该节点的连接也随之恢复,向该节点发送命令,由于已经降级,故返回MOVED错误,并指定了自己主节点的信息
而处理MOVED错误,代码如下,虽然下面onMovedRedirection只会在配置了对应触发器的情况下打开,但对应该错误,在ChannelWriter这里已经做了一些处理,这时可以看做是发生了主从切换事情,服务可以感知到。