1. 为什么同一个Spring Boot项目要切换四种Redis模式
前阵子有个朋友问我,说他在本地起了一个Redis,Spring Boot项目里配了spring.redis.host和spring.redis.port,结果程序一启动就报连接失败。我问他Redis是不是在虚拟机里、端口有没有被占用,他说这些都查过了。后来我让他看一眼项目里是不是引入了某个集群依赖,果然,他之前照着某篇文章配过spring.redis.cluster.nodes,虽然注释掉了,但配置文件里还留着个空格——Spring Boot一读到这个属性,就觉得你要用集群模式,根本不会走单机连接逻辑。
这就是Redis四种模式在Spring Boot框架下配置时最容易踩的坑:你以为你配的是单机,框架却以为你要连集群。所以这篇文章我把四种模式的配置完整梳理了一遍,包括每种模式在application.yml里怎么写、底层连接工厂如何创建、常见的启动失败和运行时异常怎么排查。适合刚接触Redis的Spring Boot初学者,也适合已经用上单机模式、准备往主从、哨兵或集群方向升级的团队。
先统一说背景:下面所有配置都以Spring Boot 2.7和Spring Boot 3.2为例,Redis服务端版本是7.0。如果是更老的项目,比如Spring Boot 1.5,配置方式差异比较大,建议先升级依赖再参考本文。
Redis的四种模式,本质上不是四种数据类型,而是四种部署架构:
- 单机模式:一台Redis服务器,最简单,适合开发环境和并发量不高的业务。
- 主从复制模式:一台主库负责写,多台从库负责读,数据实时同步到从库。
- 哨兵模式:在主从复制基础上加一层监控,主库挂了能自动把某个从库提升为新主库。
- 集群模式:多台Redis节点组成一个整体,数据按槽位分片存储,每个节点还带从节点保证高可用。
每种模式在Spring Boot里的配置差异很大,不过核心入口都是RedisConnectionFactory。只要把连接工厂搞清楚,换模式其实就是换一套配置的事。
1.1 从最容易出问题的配置优先级说起
Spring Boot 2.0之后,Redis自动配置类在处理连接工厂时,会按照一个固定顺序判断当前应该创建哪种模式。这也是我那位朋友项目出问题的根因。
优先级大致是这样:
- 如果配置了
spring.redis.cluster.nodes,优先创建集群连接工厂。 - 否则,如果配置了
spring.redis.sentinel.master和spring.redis.sentinel.nodes,创建哨兵连接工厂。 - 否则,走单机模式,读取
spring.redis.host和spring.redis.port。
所以,哪怕你把单机配置写得好好的,只要配置文件里残留了任何集群相关属性,框架就会鬼使神差地连不上。我在实际项目里还见过有人把集群节点地址写进了哨兵配置里,结果客户端一直尝试连接哨兵端口,报错信息却是Connection refused,让人一头雾水。
排查这类问题,我一般先看生效的配置是什么:
# 启动时打印Redis配置 java -jar your-app.jar --debug然后去日志里找RedisProperties相关的输出,看cluster、sentinel字段是不是有值。如果看到clusterNodes=[]但sentinel有值,说明配置被意外读取了。
1.2 选型的核心判断依据
四种模式不是越复杂越好。我给一个非常朴素但管用的判断方法:
- 只有一台Redis,不需要考虑挂掉的情况,选单机。
- 读请求远高于写请求,写库压力不大,但读库撑不住,选主从复制。
- 需要在主库故障时自动切换,让应用不感知Redis节点变化,选哨兵。
- 数据量已经超过单机内存容量,或者未来明确会超过,选集群。
最容易误判的是数据和读写压力之间的关系。我见过不少团队数据量不到2GB、QPS也就几千,却急匆匆上了集群,结果运维成本和客户端连接问题一大堆,性能反而还不如单机跑得快。Redis集群模式里,一次mget可能被拆分到多个节点,跨槽位的操作还要额外开销,不是免费的。
下表是我自己整理的一个选型速查表:
| 模式 | 部署节点数 | 自动故障转移 | 数据分片 | 客户端连接复杂度 | 推荐场景 |
|---|---|---|---|---|---|
| 单机 | 1 | 不支持 | 不支持 | 最低 | 开发测试、低并发、可容忍宕机 |
| 主从复制 | 至少2 | 不支持 | 不支持 | 偏低 | 读多写少,备份诉求强 |
| 哨兵 | 至少3 | 支持 | 不支持 | 中等 | 生产环境单机容量够用但需高可用 |
| 集群 | 至少6 | 支持 | 支持 | 较高 | 数据量超过单机内存或高并发写入 |
现在回看开头的那个问题,他其实只需要单机模式,却因为残留配置走了别的分支。所以后面每一节,我都会放一份完整的、可复制的YAML配置,并标注哪些字段是可选的、哪些是必须的,防止配置文件里出现“幽灵配置”。
2. 动手前先打好的地基:连接池选择与Spring Boot版本差异
先别急着写YAML。Redis在Spring Boot里有两种客户端实现,一个是Lettuce,一个是Jedis,它们在连接池、集群拓扑感知上差别非常大。Spring Boot 2.0之后的默认客户端是Lettuce,但依然保留了Jedis的自动配置支持。
2.1 Spring Boot版本决定配置前缀
这是最基础却最容易被忽视的点。Spring Boot 2.x的Redis配置前缀是spring.redis,从Spring Boot 3.0开始,配置前缀统一改成了spring.data.redis。如果你拿3.x的配置去跑2.x项目,或者反过来,启动时会发现配置完全不生效,但又不报明显错误,因为属性名对不上时Spring Boot只会在debug日志里打一条提示。
我通常维护两个基线版本:
- Spring Boot 2.7:用
spring.redis.* - Spring Boot 3.x:用
spring.data.redis.*
下面所有举例我统一使用Spring Boot 3.x的写法。如果你还在用2.x,把前缀前加上data去掉即可。为了省事,有些项目会在application.yml里同时保留两份,但我不建议,因为Spring Boot 3.x已经不再绑定spring.redis开头的配置,留在那里只会误导人。
2.2 Lettuce与Jedis的关键差异
| 维度 | Lettuce | Jedis |
|---|---|---|
| 底层模型 | Netty异步,单连接可多线程复用 | BIO,每个线程一个连接 |
| 默认连接池 | 不强制,但推荐配commons-pool2 | 必须配连接池 |
| 集群拓扑刷新 | 支持自动刷新,可配置触发条件 | 需要手动重建连接 |
| 哨兵故障切换感知 | 可自动重新获取master地址 | 旧版本的Jedis需要手动刷新 |
Lettuce的核心优势是连接复用,一个物理连接可以被多个线程并发使用,在高并发场景下内存占用比Jedis低。但有个隐藏问题:因为连接是共享的,如果你在代码里对同一个连接执行阻塞性操作,比如大数据量的scan或者慢查询,可能影响其他线程。
Jedis就简单粗暴一些,每次从连接池拿连接,用完归还。它的行为更符合直觉,但在连接池耗尽时容易出现线程阻塞。
Spring Boot的默认选择是Lettuce,除非有明确原因,我不建议为了“熟悉”而换回Jedis。不过后面讲集群模式时,我会专门说一个Jedis在集群环境下的优势,那是Lettuce的一个痛点。
2.3 引入依赖,别漏掉连接池
使用Lettuce时,很多人以为只要引入spring-boot-starter-data-redis就行,结果一开高并发就报RedisConnectionFailureException。原因是默认没引入连接池实现。
下面是完整的Maven依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Lettuce连接池,记得一定要加 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>如果要用Jedis,先把默认的Lettuce排除掉,再加Jedis依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>我个人建议,第一次接触这个技术栈时,先用默认的Lettuce跑通,不要一开始就切换客户端。因为网上大量教程和踩坑文章都是针对默认客户端的,你换掉它,遇到问题能参考的资料会少很多。
3. 单机模式配置:最容易被忽略的默认行为
单机模式是四种模式里配置量最小的,但偏偏越简单越容易出问题。我先给出一份实际项目里在用的配置,然后逐个字段解释为什么要这样写。
3.1 一份可以直接抄的单机YAML
spring: data: redis: host: 192.168.1.100 port: 6379 password: your-redis-pass database: 0 timeout: 3s lettuce: pool: enabled: true max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3s shutdown-timeout: 200ms看似平平无奇,但每一个字段都有人翻过车。database默认是0,如果你用的是云服务商的Redis,实例通常只开放db0,配置其他数据库号启动时不会报错,但存入的数据全部写到另一个逻辑库里,业务读取时又查不到。我自己排查过一个诡异问题,开发环境一切正常,测试环境数据就是不对,最后发现测试环境的配置里写了database: 2,而云Redis默认只有db0。
timeout指的是连接超时和读取超时,默认好像是60秒(不同版本不太一样),对于要求快速失败的业务来说太久了。我习惯配成3秒,让调用方尽早感知Redis异常,而不是卡住线程等一个注定失败的请求。
lettuce.pool这块要特别注意,Spring Boot 2.x后期版本和3.x都有一个enabled字段,不写的话,即使你配置了max-active,连接池也不一定启用。很多初学者的配置里只写了max-active: 8,结果压测时发现连接数飙升到几百,才发现没启用。
3.2 启动时检查是否真的连上了
配置完成后,我不建议直接跑业务逻辑,可以先用一个最简单的测试用例验证配置有效性:
@SpringBootTest class RedisConnectionCheckTest { @Autowired private StringRedisTemplate stringRedisTemplate; @Test void shouldConnectToRedis() { stringRedisTemplate.opsForValue().set("health:check", "ok"); String value = stringRedisTemplate.opsForValue().get("health:check"); System.out.println("read from redis: " + value); } }如果启动时日志里出现Creating a shared RedisConnection,说明Lettuce连接工厂已经初始化成功。如果出现Unable to connect to Redis,先把防火墙、Redis配置文件里的bind和protected-mode检查一遍。
3.3 单机模式最隐蔽的坑:RedisTemplate序列化
这题看起来跟模式配置无关,但它会影响你在任何模式下使用Spring Boot的RedisTemplate。默认的RedisTemplate用的是JDK序列化,往Redis里写一个字符串,key会变成\xac\xed\x00\x05t\x00...这样的二进制字符。你在命令行里完全看不懂存了什么。
我建议使用连接工厂的时候,同时自定义一个统一的RedisTemplate,方便在所有模式里复用。下面这个配置类可以直接复制,里面用到了StringRedisSerializer和GenericJackson2JsonRedisSerializer:
@Configuration public class RedisTemplateConfig { @Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }这里不详细展开序列化的所有细节,但有一点提醒:GenericJackson2JsonRedisSerializer会在序列化结果里带上@class信息,反序列化时用它还原类型。如果你把Redis作为缓存中间件,允许其他地方直接修改数据,可能会遇到类型转换异常。这时候可以考虑换成Jackson2JsonRedisSerializer,并用一个公共DTO去读。
4. 主从复制模式配置:Spring Boot原生并不替你做读写分离
主从复制模式下,你会有至少两台Redis实例,一台主库(master),一台或多台从库(replica)。主库负责写,从库负责读,数据通过复制协议实时同步。Spring Boot本身没有任何自动路由逻辑,它只是帮你创建一个连接到“某个Redis地址”的连接工厂,不会因为你写了两个地址就自动区分读写。
4.1 先搭好Redis主从结构
在Spring Boot配置之前,先确保Redis服务端已经正确组好主从关系。测试环境可以用最简单的方式:
主库节点,修改redis.conf:
bind 0.0.0.0 port 6379 requirepass master-password从库节点,修改replica.conf:
bind 0.0.0.0 port 6380 replicaof 192.168.1.100 6379 masterauth master-password注意masterauth必须配置。如果主库开启了密码,从库同步数据时需要用这个密码向主库认证。这一步漏了,从库日志会不断报MASTER aborted replication with error: NOAUTH Authentication required,而且Spring Boot应用是正常启动的,因为应用连接的是从库端口,从库本身不需要密码。
验证主从是否正常,在主库执行:
redis-cli -p 6379 -a master-password info replication输出里的connected_slaves:1表示从库连上了,master_repl_offset和从库的偏移保持一致说明数据在正常同步。
4.2 Spring Boot配置一对读写连接工厂
主从模式下,我会写两个LettuceConnectionFactory,一个指向主库,一个指向从库。这里放一个核心配置示例,基于Spring Boot 3.2:
@Configuration public class RedisReadWriteConfiguration { @Bean @Primary public LettuceConnectionFactory writeConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName("192.168.1.100"); config.setPort(6379); config.setPassword("master-password"); return new LettuceConnectionFactory(config); } @Bean public LettuceConnectionFactory readConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); config.setHostName("192.168.1.101"); config.setPort(6380); config.setPassword("replica-password"); return new LettuceConnectionFactory(config); } @Bean @Primary public RedisTemplate<String, Object> writeRedisTemplate( @Qualifier("writeConnectionFactory") RedisConnectionFactory factory) { return buildRedisTemplate(factory); } @Bean public RedisTemplate<String, Object> readRedisTemplate( @Qualifier("readConnectionFactory") RedisConnectionFactory factory) { return buildRedisTemplate(factory); } private RedisTemplate<String, Object> buildRedisTemplate( RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 序列化配置同上一节 return template; } }注意两个点:主库工厂上加了@Primary,这样其他模块注入的RedisTemplate默认走主库写入;从库工厂单独命名,业务代码里需要读多写少时,显式注入readRedisTemplate。
@Service public class OrderService { private final RedisTemplate<String, Object> writeTemplate; private final RedisTemplate<String, Object> readTemplate; public OrderService( @Qualifier("writeRedisTemplate") RedisTemplate<String, Object> writeTemplate, @Qualifier("readRedisTemplate") RedisTemplate<String, Object> readTemplate) { this.writeTemplate = writeTemplate; this.readTemplate = readTemplate; } public void saveOrderCache(Order order) { writeTemplate.opsForValue().set("order:" + order.getId(), order); } public Object getOrderCache(String orderId) { return readTemplate.opsForValue().get("order:" + orderId); } }这种写法比网上常见的“只配一个从库地址,把主库地址写死在代码里”要干净得多,后续如果从库扩容,只需加一个Bean,不需要改业务代码。
4.3 主从复制模式必须面对的延迟和故障问题
主从复制是异步复制。主库接写入后立刻返回成功,但数据同步到从库需要时间。这会导致刚写完主库、立刻读从库时,读不到数据。对于订单号这类能容忍最终一致性的场景没问题,但如果业务要求“写后必读一致”,就要在读操作前强制走主库,或者在业务上做短暂缓存穿透。
另一个大问题是,主从复制本身不提供故障转移。主库挂了,从库虽然有全部数据,但它不会自动变成主库,应用也不知道该连谁。这时候如果连接的是主库,整个写入链路就断了。真正要解决高可用,得靠哨兵模式。
5. 哨兵模式配置:客户端如何拿到变化的master地址
哨兵模式是在主从复制基础上增加了一层哨兵进程(Sentinel),它的核心职责是监控主库和从库的健康状态。当主库发生故障时,哨兵集群会选举一个新的主库,并通知客户端更新连接地址。
5.1 哨兵模式的典型部署结构
最少需要三个Sentinel节点,加上一个主库、一个从库,一共五个Redis进程。注意,部署Sentinel的机器不一定非要单独一台,可以和Redis节点混部,但生产环境最好独立部署。
Sentinel的配置文件可以很短:
port 26379 sentinel monitor mymaster 192.168.1.100 6379 2 sentinel auth-pass mymaster master-password sentinel down-after-milliseconds mymaster 5000重点看sentinel monitor mymaster 192.168.1.100 6379 2:mymaster是主库逻辑名称,后面两个哨兵节点认为主库不可达时,才会触发故障转移。这个数字也叫“quorum”,配2意味着至少要两个哨兵达成一致。
5.2 Spring Boot连接哨兵的方式
在Spring Boot里配置哨兵模式,不是直接指定主库地址,而是把哨兵节点地址告诉客户端。客户端连接任意一个哨兵,询问“mymaster当前的主库是谁”,然后拿着真正的主库地址去建立连接。
spring: data: redis: password: master-password sentinel: master: mymaster nodes: - 192.168.1.11:26379 - 192.168.1.12:26379 - 192.168.1.13:26379 password: sentinel-password timeout: 3s lettuce: pool: enabled: true max-active: 16 max-idle: 8配置文件里有两处密码,非常容易搞混:
spring.data.redis.password:Redis数据节点的密码。哨兵模式下,主库和从库可能都有requirepass,它是客户端向数据节点认证用的。spring.data.redis.sentinel.password:哨兵节点自身开启认证时的密码,只有当哨兵的requirepass被设置时才需要。
如果哨兵没有设置requirepass,只写spring.data.redis.password就可以。最常遇到的报错是NOAUTH Authentication required,明明密码没错,应用就是连不上,原因多半是把两个密码的位置搞反了。
5.3 故障转移时,应用侧发生了什么
这一点是很多人理解上的盲区。你配好了哨兵,以为主库挂了之后Spring Boot会自动切换,但Lettuce的行为并不是瞬间完成的。
当主库发生故障,哨兵集群会先做主观下线判定,再根据quorum做客观下线,然后触发故障转移。这个过程少说也要几秒钟。在这几秒内,应用连的还是旧主库地址,每次请求大概率报连接超时。
故障转移完成后,Lettuce连接工厂需要感知到“master地址变了”。Lettuce默认会在连接断开后重新向哨兵获取master地址,但如果你用的是旧连接池里的空闲连接,可能需要等到下个心跳周期才刷新。有一种办法是开启哨兵拓扑刷新配置,但不是所有版本都有友好的YAML参数。我在项目里是自定义一个监听器,定期调用哨兵的SENTINEL get-master-addr-by-name mymaster去确认当前主库地址,然后把旧连接工厂全部关闭重建。
简单做法是让应用监听RedisConnectionFailureException,捕获后主动调用:
StringRedisTemplate.getConnectionFactory().getConnection().close();强制Lettuce丢弃旧连接,下次获取连接时重新走哨兵查询流程。这个方法不太优雅,但关键时刻能救急。
5.4 只配一个哨兵地址的隐患
很多项目在配置nodes时只写了一个哨兵节点。单点故障风险很大:如果这个哨兵挂了,应用就不知道该找谁问master地址了。
哨兵的意义就是通过多个节点达成共识,所以nodes最少写三个。开发环境可以只有一个,但生产环境一定不要省。
另外特别注意:哨兵模式要求客户端连接的Redis数据库index为0。如果设置spring.data.redis.database为非0值,某些版本的Lettuce在切换master时会出问题。因为Redis哨兵模式下通常只用db0,我建议这个字段保持默认。
6. 集群模式配置:16384个槽位和MOVED重定向
当单机内存不够、写入吞吐量需求远超单实例能力时,就该考虑集群模式。Redis集群采用无中心化架构,数据按key的哈希结果散列到16384个槽中,每个主节点负责一部分槽位,同时每个主节点可以挂一个或多个从节点。
6.1 集群节点规划
生产环境最常见的最小集群是6个节点:3个主节点、3个从节点。每个主节点负责约5461个槽位,从节点分别做备份。
搭建测试集群可以任意节点数,我一般会在本机起6个端口:7000、7001、7002作为主,7003、7004、7005作为各自的从节点。用redis-cli --cluster create一条命令就能组起来:
redis-cli --cluster create \ 192.168.1.20:7000 192.168.1.21:7000 192.168.1.22:7000 \ 192.168.1.23:7000 192.168.1.24:7000 192.168.1.25:7000 \ --cluster-replicas 1每个节点的redis.conf至少要开启:
cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 150006.2 Spring Boot集群配置
Spring Boot连接集群,不需要列出所有节点,只要给出一组“种子节点”即可。客户端连上其中一个,就能通过CLUSTER命令获取整个集群的拓扑。不过为了冗余,我一般写三个种子节点。
spring: data: redis: password: cluster-pass cluster: nodes: - 192.168.1.20:7000 - 192.168.1.21:7000 - 192.168.1.22:7000 max-redirects: 3 timeout: 3s lettuce: pool: enabled: true max-active: 16 max-idle: 8max-redirects表示客户端在遇到MOVED重定向时最多追踪几次。默认是5,配成3一般够用。如果集群拓扑变化频繁,可以稍微调大。
集群模式下没有database概念,只有db0。配置了非0的database,客户端要么启动报错,要么直接忽略,Redis集群本身就不支持多逻辑库,这一点要格外提醒。
6.3 Lettuce处理MOVED/ASK的机制
Redis Cluster中,客户端写一个key到任何节点,如果该key所对应的槽不在当前节点,节点会返回MOVED错误,并给出正确的目标节点地址。Lettuce收到MOVED后会自动重定向到目标节点,并更新内存中的槽位映射。
大多数情况下,你不需要自己处理MOVED。但有一个场景会让Lettuce吃大亏:使用pipeline或事务时,一批命令可能在同一个pipeline里跨槽位执行,中间有一个命令触发MOVED,整批提交就可能失败。
我还在生产环境碰到过Lettuce的拓扑刷新问题。集群里有一个从节点被手动切换成主节点后,Lettuce并没有及时更新槽位映射,好几个key的请求持续打到旧节点,返回MOVED后虽然能跟随重定向,但每次都多一次网络往返,延迟明显变高。
解决办法是开启Lettuce的拓扑周期刷新。在配置类里手动构建连接工厂:
@Configuration public class RedisClusterConfig { @Bean public RedisConnectionFactory lettuceClusterConnectionFactory() { RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(); clusterConfig.clusterNode("192.168.1.20", 7000); clusterConfig.clusterNode("192.168.1.21", 7000); clusterConfig.clusterNode("192.168.1.22", 7000); clusterConfig.setPassword("cluster-pass"); ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(java.time.Duration.ofSeconds(30)) .enableAllAdaptiveRefreshTriggers() .build(); ClusterClientOptions clientOptions = ClusterClientOptions.builder() .topologyRefreshOptions(topologyRefreshOptions) .build(); LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .clientOptions(clientOptions) .build(); return new LettuceConnectionFactory(clusterConfig, clientConfig); } }这段配置的作用是:每30秒自动向集群要一次拓扑信息,同时在节点故障、MOVED过多等触发条件下刷新。生产环境建议至少开周期刷新。
6.4 集群模式下命令限制:MGET、pipeline、事务、Lua
这一节是集群模式最常见的业务改造点。单机模式下随便用的命令,在集群里不一定能用。
MGET/MSET:如果多个key不在同一个槽,执行失败。需要用哈希标签{user:123}把相关key固定到同一槽,或者改成循环单key操作。pipeline:一批命令跨多个节点时,Lettuce会自动拆分成多个节点pipeline,但一致性没法保障。MULTI/EXEC事务:只支持同一个节点上的key。跨节点事务是做不到的,集群本身就不支持跨节点原子性。Lua脚本:脚本里访问的所有key必须落在同一个槽,否则报CROSSSLOT Keys in request don't hash to the same slot。
我见过一个项目从单机迁到集群后,所有缓存查询都报错,最后发现是代码里有一个地方在循环里调MGET传了5个key。改成循环单个GET之后性能反而没降多少,因为网络开销本来也没想象中那么大,但代码可读性差了很多。更稳妥的做法是使用哈希标签,把这几个key固定到同一个key所在槽:
user:123:profile user:123:orders这两个key前面部分user:123用花括号包起来,比如{user:123}:profile和{user:123}:orders,就能保证在同一个槽。Redis只对花括号内的内容做哈希计算。
6.5 Jedis还是Lettuce:我的切身体会
如果你使用Redis Cluster,并且业务中大量使用事务、Lua、复杂pipeline,Lettuce的自动拓扑刷新机制虽然先进,但它对连接共享的控制更复杂,出问题后排查难度更高。Jedis的行为直白,每次从连接池拿连接、执行完归还,Cluster模式下同样能自动处理MOVED,缺点是要维护连接池参数,没有Lettuce省心。
我个人的建议是:
- 用Spring Boot 3.x + Lettuce,能覆盖90%的业务场景。
- 如果你的项目高度依赖Lua脚本和跨槽位操作,先用哈希标签解决槽位问题,不要急着换Jedis。
- 如果确认要换Jedis,保留一份Lettuce的配置在注释里,方便回溯对比。
7. 横评四种模式的实测结论与避坑清单
文章写到这里,四种模式的配置基本都过了一遍。这一节我直接分享一组在模拟项目里压测得到的结论,以及一份踩坑清单。测试环境是虚拟机里四个Redis实例,项目是一个简单的订单缓存模拟项目X,压测工具是个人脚本,数据仅供横向参考。
7.1 四种模式在同一台机器上的表现对比
| 模式 | 单次GET平均延迟 | 单次SET平均延迟 | 主库故障恢复时间 | 配置复杂度 |
|---|---|---|---|---|
| 单机 | 0.4ms | 0.5ms | 需要人工介入 | 低 |
| 主从复制 | 0.5ms(从库读) | 0.5ms | 需要人工介入 | 中 |
| 哨兵 | 0.6ms | 0.6ms | 5~10秒自动切换 | 中高 |
| 集群 | 0.7ms | 0.8ms | 秒级感知+手动确认 | 高 |
单机和主从复制在低并发下差距很小,但主从复制在读取压力大时可以横向扩展从库,这是单机无法做到的。哨兵模式因为多了一层查询开销,比直接连主库稍慢,但这个延迟在可接受范围内。集群模式因为涉及槽位计算和可能的重定向,单次延迟稍微高一点,不过换来的是横向扩容能力和更高的总吞吐量。
我压测时发现一个有意思的现象:在并发只有200左右的场景下,集群模式反而比单机模式慢,原因是每次命令都要做CRC16哈希计算,Lettuce还要维护槽位路由表,这些都是额外开销。但并发到2000以上时,单机开始出现CPU尖刺和连接排队,集群的吞吐优势就出来了。所以选型真不是越高级越好,你的业务并发和容量要匹配。
7.2 我从这些配置里总结出的避坑清单
把四种模式遇到的典型问题汇总成一张表,每次配置完可以对照排查:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 配置了单机地址,但连接的是其他模式的节点 | 配置文件残留cluster.nodes或sentinel.nodes | 删除无关配置,只保留当前模式字段 |
启动报NOAUTH Authentication required | 密码配置只写一个,或者写错字段 | 区分数据节点密码和哨兵节点密码 |
| 主从复制正常,但从库读不到新数据 | 主从复制延迟,或从库没有配置masterauth | 检查info replication偏移量,确认主从认证 |
| 哨兵模式下主库切换后应用仍报连接超时 | 旧连接未及时刷新 | 开启拓扑周期刷新或捕获异常后重建连接工厂 |
| 集群模式运行一段时间后部分key读写变慢 | 客户端槽位映射过期 | 检查Lettuce拓扑刷新配置,考虑缩短刷新周期 |
集群模式下执行MGET报CROSSSLOT | 目标key散落到不同槽位 | 使用哈希标签重新设计key |
| RedisTemplate存储的内容在命令行显示乱码 | 默认JDK序列化导致 | 自定义RedisTemplate,改用String和JSON序列化 |
Spring Boot 3.x项目配置spring.redis不生效 | 配置前缀已变成spring.data.redis | 统一使用新前缀 |
7.3 从零到生产的一条稳妥升级路径
如果你现在还在跑单机,想往高可用方向走,我不建议一步跳到集群。更稳妥的路径是:
- 先把单机模式跑通,包括连接池、序列化、异常处理,确保应用层没有任何Redis连接问题。
- 加一台从库,做纯手动的主从复制,把主从延迟摸清楚。
- 再上哨兵,验证故障转移后应用能否自动恢复。
- 最后,当数据量或并发量明确超过单机能力时,再迁移到集群。
整个过程中,Spring Boot的RedisConnectionFactory接口是不变的,业务代码应尽量依赖RedisTemplate而不是具体连接工厂。这么一来,模式升级时通常只需要改YAML和连接工厂配置类,不需要大规模改业务代码。
另外建议把每种模式的配置文件放在Git仓库的不同目录里,比如config/single、config/sentinel、config/cluster,启动时通过--spring.config.additional-location指定。少了手动改配置的环节,也避免配置串味。
我在这个项目里踩掉最多的坑,就是配置残留和密码分离这两个问题。这两种问题都不会导致启动立刻失败,而是在运行一段时间后才爆发,所以每次上线前,我会在部署环境里跑一个30分钟的稳定性测试,专门观察连接池、慢查询和异常日志。等你把四种模式都亲手配一遍,再回头看一开始那些复杂报错,会发现它们其实都指向同一个点:客户端连接工厂没有拿到它所期望的那一套配置。