网易架构师心得:Springboot下使用redis踩过的坑

2,961 阅读10分钟

分享一下我的网易架构师同事在spring boot下使用redis的心得~

首先总结了redis服务端单线程工作模型,redis四种部署方式及使用场景,然后从源码的角度上,分析springboot在jedis和lettuce客户端下使用redis的一些坑~尤其是在集群模式下的一些不兼容问题!

最近整理的Java架构学习视频和大厂项目底层知识点,需要的同学欢迎私信我发给你~一起学习进步!

1 Redis服务端单线程模型

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/b3645477e82644bfa65c5dea095496cf?from=pc)

redis 内部使用文件事件处理(file event handler)处理客户端的请求,文件事件处理器是单线程的,所以redis才叫做单线程的模型。

文件事件处理器的结构包含4个部分:多个socket、IO 多路复用程序、文件事件分派器、事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

文件事件处理器采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

Redis客户端通过socket连接reids服务端,多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

redis 单线程模型也能效率高的原因:

  1. 纯内存操作
  2. 基于非阻塞的 IO 多路复用机制
  3. 单线程反而避免了多线程的频繁上下文切换问题

为什么redis采用单线程模型呢?

如果采用多线程模型,cpu需要进行上下文切换,假设1MB的数据由多个线程读取了1000次,那么就有1000次时间上下文的切换,那么就有1500ns * 1000 = 1500us,而单线程的读完1MB数据才250us ,所以完全没必要使用多线程。

什么时候适合采用多线程的方案呢?

对于慢速设备:磁盘,网络,SSD 等等,将请求和处理的线程不绑定,请求的线程将请求放在一个buff里,然后等buff快满了,处理的线程再去处理这个buff。然后由这个buff 统一的去写入磁盘,或者读磁盘,这样效率最高。

Redis线程安全吗?

redis实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis操作的复合操作来说,依然需要锁,而且有可能是分布式锁。

2 redis部署方式

2.1 单节点模式

单节点模式只有一个节点,一般用来测试

2.2 主从模式

主从模式包括一个主节点和多个从节点,一般来说,主节点用来读写操作,从节点用户读操作,主节点的数据可以同步到从节点,所以从节点即便支持写操作也没有意义。

2.3 哨兵模式

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/418ef7116c464c428bfd6879c44ea748?from=pc)

哨兵模式是基于主从模式的,哨兵模式为了实现主从模式的高可用,监控主从节点的状态,当sentinel发现master节点挂了以后,sentinel就会从slave中重新选举一个master。

一般来说,通过sentinel集群可以管理多个主从redis,sentinel最好不要和Redis部署在同一台机器,不然redis的服务器挂了以后,sentinel也挂了。使用sentinel集群也是为了保证redis的高可用,避免哨兵节点挂了之后影响redis的使用。

当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。

2.4 集群模式:

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/3cecb10dab164cfa980cb7fb4c910b9f?from=pc)

cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。

如图所示,部署了三主三从的redis集群,redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot,从而将数据存储至对应的slot上。

3 Springboot使用redis总结

spring-boot-starter-data-redis支持两种redis客户端:jedis和lettuce

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/90e381e478b74dc3aa7089e59cd8a234?from=pc)

Springboot2.0默认使用的客户端是lettuce,下面跟踪源码来分析springboot如何加在lettuce客户端的,首先找到springboot自动加载的jar包下redis相关的加载配置类RedisAutoConfiguration

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/e12b2305e3dd44748a044cbf2816ef97?from=pc)

这里采用@Configuration @bean的方式向容器中注入RedisTemplate和StringRedisTemplate,注入两者的方法中需要传入RedisConnectionFactory,RedisConnectionFactory通过@Import导入的LettuceConnectionConfiguration和JedisConnectionConfiguration生成

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/8a26a755349e47efb60941c82d5201c5?from=pc)
![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/380d0aa2f11e4e9c85a05bc1acfee4c7?from=pc)

可以看到在没有RedisConnectionFactory的情况下,会默认向Spring容器中注入LettuceConnectionFactory,如果要使用jedis客户端,只需要手动配置一个JedisConnectionFactory并注入容器即可。

3.1 jedis和lettuce的区别

  • Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接。
  • Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问, StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

3.2 jedis非线程安全分析:

从源码角度分析jedis客户端执行每个命令的过程

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/4213190a7240423389c52e3334d3aff9?from=pc)

首先借助于Client类的对应方法去执行命令

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/d710c432546f4782bcd66f1b46a8b814?from=pc)

然后借助于Connection类的sendCommand方法执行

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/9b383959d0444b55950c08306df92f9d?from=pc)

sendCommand方法每次执行都会调用connect方法

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/6d1eda17b8a64557bba50ccd8f29bdc9?from=pc)

从connect方法中可以看到,socket是一个共享变量,假如两个线程公用一个jedis实例,当前还没有建立socket连接,两个线程同时进入建立socket连接

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/ca386a73ca5b48ad88a94c8849b19ce8?from=pc)

线程1建立socket连接后,开始获取输入输出流,于此同时,线程2重新初始化socket,并且没有执行到建立socket连接,此时线程1获取输入输出流将失败,因为此时的socket并没有连接。

jedis本身不是多线程安全的,这并不是jedis的bug,而是jedis的设计与redis本身就是单线程相关,jedis实例抽象的是发送命令相关,一个jedis实例使用一个线程与使用100个线程去发送命令没有本质上的区别,所以没必要设置为线程安全的。但是如果需要用多线程方式访问redis服务器怎么做呢?那就使用多个jedis实例,每个线程对应一个jedis实例,而不是一个jedis实例多个线程共享。一个jedis关联一个Client,相当于一个客户端,Client继承了Connection,Connection维护了Socket连接,对于Socket这种昂贵的连接,一般都会做池化,jedis提供了JedisPool。

3.3 集群模式下jedis和lettuce使用的一些坑

1. Lettuce在集群模式下主节点宕机,从节点更新为主节点,lettuce如何更新集群拓扑结构

集群中每个节点只负责部分slot, slot可能从一个节点迁移到另一节点,造成客户端有可能会向错误的节点发起请求。因此需要有一种机制来对其进行发现和修正,这就是请求重定向。

集群拓扑刷新是在ClusterTopologyRefreshScheduler中进行,下面进入类中一探究竟

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/8e13b9f8135f4c899e16200d86471019?from=pc)
![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/ed42c38f1b9b427d91181574d7204e60?from=pc)

ClusterTopologyRefreshScheduler类实现了ClusterEventListener接口,用来监听redis集群事件,集群事件包括ask重定向,move重定向,以及重新连接等。

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/ff0d027024024f12bcdb734893aa03cd?from=pc)

在重定向方法中首先调用isEnabled方法判断是否开启刷新集群拓扑,然后调用indicateTopologyRefreshSignal方法刷新集群拓扑

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/ae9671ebe1cc4ffa9ef0927de1d5b3f1?from=pc)

判断集群是否开启刷新拓扑结构,依据ClusterTopologyRefreshOptions中自适应刷新的trigger中是否包含指定的重定向trigger,在默认配置下,这个trigger是什么样的呢?

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/ba4aee2f4bee44ecb4d91f13adc10ac9?from=pc)

可以看到默认情况下自适应刷新的trigger是空的,所以在集群模式下,使用默认的lettuce配置,如果主节点宕机,是不会刷新集群拓扑的,也就是会导致redis连接失败。

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/753a9441bb7340158a7a5fd212ecf544?from=pc)

在enableAllAdaptiveRefreshTriggers方法中可以开启自适应刷新集群拓扑。开启自适应刷新集群拓扑后,又是如何刷新的呢?

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/3a55d9c2955f46ce8887977692190ae9?from=pc)

在indicateTopologyRefreshSignal方法中提交一个刷新集群拓扑的clusterTopologyRefreshTask

![网易架构师心得:Springboot下使用redis踩过的坑](https://p3-tt.byteimg.com/origin/pgc-image/bffc5c6d11a145b291163ed4973f57f7?from=pc)

在task中调用RedisClusterClient类的reloadPartitions方法重新加载集群拓扑信息,达到刷新的效果。

除了通过开始自适应刷新集群拓扑之外,还可以通过开启周期刷新的方式刷新集群拓扑

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/cf1450853b1a42a086c8d19e73d06094?from=pc)

开启周期刷新集群拓扑后,在初始化集群拓扑的时,会调用activateTopologyRefreshIfNeeded开启周期刷新集群拓扑任务

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/629b7cbac700444bbc0cecc72f9943b8?from=pc)
![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/dc2fef6cfd814f63b2a790474fe1542d?from=pc)

这里会判断是否开启周期刷新,开启后才会提交一个定时任务。

周期刷新和自适应刷新比较:周期刷新和自适应刷新两种方法,最好还是使用自适应刷新,因为周期刷新的周期需要设置,设置太长会导致服务可能一段时间不可用,设置太短对资源是一种浪费,而自适应刷新根据服务端的响应来刷新集群拓扑。

两种刷新方法没必要都开启,都开启对资源也是一种浪费。

2.Jedis客户端执行lua脚本的坑

redis使用lua脚本的好处:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

那Jedis客户端是如何支持lua脚本的呢?

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/c329997eb76341c39e1632656259f93b?from=pc)

Jedis执行lua脚本是通过ScriptExecutor类的execute方法执行的,在方法中进一步调用eval方法

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/720ac6755ee244ae86bb1512833c862e?from=pc)

进一步调用RedisScriptingCommands类的eval方法,因为实在集群模式下使用jedis客户端,所以调用JedisClusterScriptingCommands实现类的eval方法

![网易架构师心得:Springboot下使用redis踩过的坑](https://p1-tt.byteimg.com/origin/pgc-image/515bc169ff354de9b6d9913c1e22f5b8?from=pc)

再看JedisClusterScriptingCommands实现类的eval方法,居然直接抛出异常,集群模式下不支持脚本。

解决方法是使用lettuce客户端,LettuceScriptingCommands类中的eval方法支持脚本

![网易架构师心得:Springboot下使用redis踩过的坑](https://p6-tt.byteimg.com/origin/pgc-image/e085c7ea5ce54ca38ed7e782da5faa0a?from=pc)

看到这里的小伙伴,如果你喜欢这篇文章的话,别忘了转发、收藏、留言互动

如果对文章有任何问题,欢迎在留言区和我交流~

最近我新整理了一些Java资料,包含面经分享、模拟试题、和视频干货,如果你需要的话,欢迎私信我

还有,关注我!关注我!关注我!