在Spring Boot项目里接Redis,大部分Java后端同学都不陌生,但很多人一提到“Redis四种模式”就开始含混:单机、主从、哨兵、集群到底怎么选,Spring Boot里配置分别怎么写,为什么网上有的配置在yaml里报红,有的明明连上了却读到乱码。
这篇文章就直接怼着“Redis四种模式在Spring Boot框架下的配置”这个主题来,把这四种模式从原理到配置、从搭建到排坑,一条线讲清楚。你拿着文章里的配置片段,修修改改就能用到自己项目里。
1. 四种模式的整体认知:先搞清楚自己走到哪一步
1.1 四种模式分别解决了什么问题
Redis的部署模式并不是一开始就有四种,而是跟着业务需求一步步演进出来的。理解这个演进过程,你才知道自己当前该停在哪个模式,而不是盲目追求“看起来更高级”。
单机模式就是一个Redis实例扛下所有读写,部署最简单,本地开发和测试基本都是它。问题也很直白:实例挂了,缓存和临时数据全部不可用;内存有限,数据量一大就只能靠硬件往上堆。
主从模式解决的是“单点故障”和“读多写少”的问题。一个主节点负责写,一个或多个从节点实时同步数据,应用可以把读请求打到从节点上,分摊压力。但主挂了以后,从节点能不能顶上,需要人工干预,写服务依然会中断。
哨兵模式在主从之上加了一层“自动故障转移”。哨兵进程负责监控主从的健康状态,主节点挂了以后,哨兵会从从节点里选一个提升为新的主节点,并把新主节点地址通知给应用。这才算得上是真正的高可用。
集群模式则把数据分片打散到多个节点上,每个节点只存一部分数据,通过槽位(slot)机制自动分配和转发。它解决的既不是高可用,也不是读写分离,而是“单机内存不够了,得把数据摊到多台机器上”的水平扩展问题。
四种模式不是互斥的替代关系,而是层层叠加的能力升级。主从和哨兵解决的是可用性和容量,集群解决的是数据规模。生产环境里,哨兵监控的通常已经是主从架构,集群本身也会给每个分片配副本。
1.2 怎么选:别一上来就上集群
我在不少公司看到过一种倾向——项目刚起步,QPS日均才几百,Redis数据量撑死一两G,架构评审时非要上三主三从的集群,理由是“以后肯定能用上”。这个思路我劝你早打消。
集群不是白送的福利,它带来的副作用很实在:多键操作(MGET、事务、Lua脚本)受槽位限制,跨slot直接报错;批量操作必须用哈希标签把key圈到同一slot;客户端配置、监控、扩容都比单机复杂好几个量级。一个几百QPS的业务用集群,运维复杂度直接盖过了收益。
我建议的选型逻辑很简单:单机能扛住的,绝不升级;读多写少且数据量不大,上主从;对可用性有硬指标要求(比如RTO、RPO要考核),上哨兵;数据量超过单机内存承载能力,或者单机CPU已经打满,再上集群。
具体到技术判断,单机Redis的常规容量天花板受限于内存成本和RDB/AOF的恢复效率,一般业务在20G以内都比较从容。如果你预判未来三年数据量会超过这个数,或者缓存value有明显的大对象特征,再考虑集群也不迟。“先单机、再哨兵、最后集群”这个路线,是绝大多数团队最稳的演进路径。
2. 单机模式:Spring Boot集成的起点
2.1 依赖引入与配置文件
Spring Boot操作Redis的官方姿势是引入spring-boot-starter-data-redis,然后在配置里写连接参数。这里先说一个很多人踩过的坑:Spring Boot 3.x的配置前缀已经从spring.redis.*改成了spring.data.redis.*。
网上老教程大量还在用spring.redis.host这种写法。你在Spring Boot 3.x项目里照着抄,配置不报错也不生效,连的是默认的localhost:6379,等到测试环境找半天才发现原来前缀错了。具体区别如下。
Spring Boot 2.x的配置长这样:
spring: redis: host: 192.168.1.10 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3sSpring Boot 3.x的正确写法是:
spring: data: redis: host: 192.168.1.10 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3s两个版本底层用的都是Lettuce客户端,所以连接池配置的属性名是一致的,只是外层前缀不一样。如果你的项目还在2.x,想升3.x,配置文件里这一段几乎是必改项。
2.2 RedisTemplate序列化配置才是重点
配置文件写对之后,应用已经能连上Redis了,但你会发现一个很经典的问题:往Redis里存一个User对象,再取出来,变成了一串以\xAC\xED开头的乱码。这不是连错了,而是Spring Boot默认用的是JdkSerializationRedisSerializer。
JDK序列化的特点是:对象经过序列化后是一个二进制字节流,key和value都是这种格式。你在Redis客户端工具里看到的key不是自己定义的user:1001,而是前面带了一堆前缀符号的二进制串,value自然也一样。这样既不直观,也不利于调试,更别说跟别的语言互操作了。
所以,RedisTemplate一定要自定义序列化器。常规做法是key用String序列化,value用JSON序列化。这样key保持人类可读,value也能直接在Redis Desktop Manager这类可视化工具里看到JSON原貌。
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }注意一点:GenericJackson2JsonRedisSerializer会在序列化结果里额外带上@class类型信息,反序列化时才能还原成正确的对象类型。代价是value体积会比纯JSON大一些,如果对存储成本敏感,可以考虑用Jackson的ObjectMapper自定义序列化,只保留必要字段。
2.3 单机模式下的常见扩展点
单机模式下,Spring Boot能做的配置其实不多,但有几个点我建议顺手配好。
第一个是StringRedisTemplate和RedisTemplate的分工。Spring Boot已经自动注入了一个StringRedisTemplate,key和value都用String序列化。如果你存的就是普通字符串,比如验证码、token、简单的计数场景,直接注入StringRedisTemplate就行,不要什么都拿自定义的RedisTemplate操作,省不少序列化开销。
第二个是缓存注解的序列化问题。如果你用了@Cacheable这类注解,默认的RedisCacheManager依然会用二进制序列化,即使你自定义了RedisTemplate也不影响它。所以用注解缓存时,需要单独给RedisCacheManager配置序列化方式。
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); }第三个是连接池参数。Lettuce本身是线程安全的,多个线程可以共享一个连接,理论上不用池也能跑。但遇到需要阻塞执行的命令(比如BLPOP、WAIT),或者瞬间连接数飙升,没有连接池就容易拖死客户端。max-active按你接口的峰值并发量来估,一般16到32够用;max-wait不要给太长,宁可快速失败重试,也不要让请求全部堆积在获取连接上。
3. 主从模式:解决单点故障和数据冗余
3.1 主从复制的工作机制
单机Refdis最大的隐患是宕机。Redis虽然支持RDB和AOF两种持久化,但数据恢复需要时间,恢复期间缓存服务完全不可用。对于缓存场景,缓存击穿、雪崩、恢复时间过长引发的数据库压力,都是实实在在的问题。
主从模式解决的就是这个。一个主节点(Master)负责写请求,一个或多个从节点(Replica)通过复制机制同步主节点的数据。同步分为两步:
- 全量同步:从节点第一次连接主节点时,主节点生成RDB快照发给从节点,同时把快照生成期间的写命令缓存在缓冲区里;从节点加载完RDB后,再接收这段缓冲区命令,最终与主节点一致。
- 增量同步:全量同步完成后,主节点把实时的写命令通过复制积压缓冲区推送给从节点,保持持续一致。
这里有个很关键的概念叫repl_backlog,也就是复制积压缓冲区。它的大小决定了从节点断线后能不能继续增量同步。如果从节点断开太久,重连时需要的增量命令已经超出缓冲区范围,就只能重新全量同步。这个缓冲区默认是1MB,偏小,高写入场景建议调大。
主从模式默认是从节点只读。意思是你在从节点上执行写命令会直接报错,这个设计是为了保证数据一致性,避免主从两边都写导致数据分叉。
3.2 从零建一个一主一从环境
用Docker搭一个一主一从的测试环境是成本最低的方式。先跑主节点:
docker run -d --name redis-master \ -p 6379:6379 \ -v /docker/redis/master/conf:/usr/local/etc/redis \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf主节点的redis.conf里开启AOF,保证重启不丢太多数据:
appendonly yes appendfsync everysec然后跑从节点,最关键的就是replicaof配置:
docker run -d --name redis-replica \ -p 6380:6379 \ -v /docker/redis/replica/conf:/usr/local/etc/redis \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf从节点的redis.conf里加上:
replicaof 192.168.1.10 6379replicaof后面的地址和端口一定要能被从节点访问到。如果你用的是Docker容器,注意容器之间通信不能写localhost,要写宿主机IP或者容器所在网络的别名。
启动完成后,在主节点上执行info replication,你会看到如下输出:
role:master connected_slaves:1 slave0:ip=172.17.0.2,port=6379,state=online,offset=123,lag=0看到state=online就说明复制链路已经建立。
3.3 Spring Boot端配置与读写分离说明
很多人以为Spring Boot主从模式要在配置里写两个地址,其实不是。在主从架构下,应用只需要连接主节点,写入读到主节点,从节点更多时候是在主节点故障时顶替、或者承担只读分析任务。
Spring Boot的配置跟单机完全一样,把host和port指向主节点即可。具体到代码里,如果想让应用自动做读写分离,需要设置Lettuce的ReadFrom策略。比如有一部分读请求想打到从节点,可以在自定义连接工厂里做:
@Bean public LettuceConnectionFactory lettuceConnectionFactory() { LettuceClientConfiguration config = LettuceClientConfiguration.builder() .readFrom(ReadFrom.REPLICA_PREFERRED) .build(); return new LettuceConnectionFactory(new RedisStandaloneConfiguration("192.168.1.10", 6379), config); }这个策略的含义是:优先从从节点读,没有可用从节点时回落到主节点。适合读多写少的场景。但要注意,Redis主从复制是异步的,从节点的数据可能有短暂延迟。如果你的业务对一致性要求极高——比如刚写入的数据必须能立刻读到——就不要走从节点,否则会出现短暂的空值或旧数据。
主从模式本身不会解决“主节点挂掉后自动切换”的问题。你可能手动执行replicaof no one把从节点提升为主节点,但那需要人工介入,过程中写入服务是断的。这也是为什么要有哨兵。
4. 哨兵模式:真正的自动高可用
4.1 哨兵到底在哨什么
哨兵(Sentinel)的出现,就是为了把“人工切换主从”变成“自动切换主从”。它是一个独立的Redis进程,核心做三件事:
- 监控:定期向所有Redis实例发送心跳,检查主从节点是否存活。
- 通知:检测到主节点故障时,把故障事件通知给其他哨兵进程和客户端应用。
- 故障转移:在多个哨兵确认主节点不可用后,从从节点中选出一个升级为新主节点,并把其他从节点重新指向新主节点,最后把新主节点地址通知给客户端。
哨兵至少要有3个节点,彼此相互沟通,通过投票机制决定要不要执行故障转移。这里有一个反直觉的点:为什么不能用1个哨兵?因为单哨兵存在误判风险——如果哨兵自己网络抖动,误以为主节点挂了,就会触发不必要的切换,反而加剧系统不稳定。3个哨兵加上quorum参数(通常设2),代表至少2个哨兵达成一致,才认为主节点确实宕机。
哨兵模式下的架构是三层:应用层不直接连Redis主节点,而是连哨兵节点;先从哨兵这里问出“当前主节点是谁”,再与真正的主节点建立连接。主节点一旦切换,哨兵会通知应用重新获取新主节点。
4.2 部署一套最小高可用架构
哨兵不能凭空存在,它监督的是一个主从架构。所以你先把第3节里的主从搭好,然后在这两个Redis实例旁边再跑三个哨兵进程。
哨兵的配置非常简单,核心就一行:
sentinel monitor mymaster 192.168.1.10 6379 2含义是:监控名为mymaster的主节点,地址为192.168.1.10:6379,quorum为2(至少两个哨兵同意才判定故障)。
启动哨兵的方式:
redis-sentinel /path/to/sentinel.conf三个哨兵分别监听26379、26380、26381端口。这样组成一个真正的高可用架构:主节点宕机,15秒到30秒内哨兵会自动完成切换,应用无感知。
生产环境里,哨兵和Redis实例应该部署在不同的机器或不同可用区,防止一台物理机挂掉,主节点和所有哨兵一起躺平,那就没有意义了。
4.3 Spring Boot接入哨兵的配置细节
Spring Boot接入哨兵,配置上是四种模式里最特殊的。它不直接配置Redis主机地址和端口,而是配置哨兵的主节点名称和哨兵节点列表。
Spring Boot 3.x的配置:
spring: data: redis: password: yourpassword sentinel: master: mymaster nodes: - 192.168.1.30:26379 - 192.168.1.31:26379 - 192.168.1.32:26379Spring Boot 2.x的配置:
spring: redis: password: yourpassword sentinel: master: mymaster nodes: 192.168.1.30:26379,192.168.1.31:26379,192.168.1.32:26379注意几个点。哨兵模式下,password配置的是Redis主节点的密码,不是哨兵的密码。各Redis实例的密码应当保持一致,否则故障转移后应用拿到新主节点地址,却没有对应密码,连接还是会失败。
这里的nodes列表,理论上写哨兵中的一个或多个即可,Lettuce客户端会通过哨兵自动发现其他哨兵节点以及其他从节点的信息。但建议把三个哨兵都写上,任何一个哨兵不可达时,应用还能从另外两个拿到主节点信息。
配好之后,应用启动时会先连哨兵,获取当前主节点地址,再建立Redis连接。主节点切换后,客户端会自动重新解析主节点,业务代码完全感知不到故障的过程。
这里补一个排错提醒:如果配置了哨兵模式但连不上,先用redis-cli -p 26379 sentinel get-master-addr-by-name mymaster手动查一下哨兵返回的主节点地址。可能是哨兵配置里的地址对外不可达,或者哨兵还在判定故障状态,客户端拿到的地址已经不可用了。
5. 集群模式:数据分片和水平扩展
5.1 槽位、分片与重定向
集群模式是为了解决单机容量瓶颈。它会自动把数据分布到多个节点上,分布的基本单位是槽位(slot)。
Redis集群预定义了16384个槽位,初始化时把这些槽位平均分配到各个主节点。每个key通过CRC16算法计算出一个哈希值,再对16384取模,得到它属于哪个槽位。比如你执行SET user:1001 zhangsan,Redis算出这个key落在哪个槽,然后由负责该槽的节点来处理这个请求。
这里有个高频问题:客户端连接集群中的任意一个节点时,如果这个key不在该节点管理的槽位上,节点会返回一个MOVED错误,并告诉客户端正确答案在哪个节点。支持集群的客户端(如Lettuce)会自动处理MOVED重定向,但如果你用redis-cli不带-c参数,就会直接看到MOVED错误。这也是为什么一些初学者在集群里执行命令时经常报错的原因。
集群模式的完整形态是“三主三从”:三个主节点各管一段槽位,每个主节点配一个从节点作为副本。主节点挂掉后,从节点会自动提升为主节点。
在我的实践经验里,区分哨兵和集群的核心不是“高可用”,而是“数据分片”。你存不下、扛不住、单机内存告急,上集群;只是怕宕机,上哨兵。数据量没有超过单机容量之前,集群带来的复杂度通常不划算。
5.2 搭建一个三主三从的最小集群
用Docker快速搭建一个六节点的集群,命令如下。生产环境建议用k8s或云厂商的托管集群,但本地验证可以这样:
for port in 7000 7001 7002 7003 7004 7005; do docker run -d --name redis-cluster-$port \ -p $port:6379 \ -v /docker/redis/cluster-$port/conf:/usr/local/etc/redis \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf done每个节点的redis.conf至少需要这样几项:
cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes注意cluster-enabled yes是最关键的开关。没有这一项,Redis不会以集群模式启动,后面的集群创建命令也会失败。
六节点全部启动后,执行集群创建命令:
redis-cli --cluster create \ 192.168.1.10:7000 192.168.1.10:7001 192.168.1.10:7002 \ 192.168.1.10:7003 192.168.1.10:7004 192.168.1.10:7005 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配1个从节点。执行过程中会让你输入yes确认槽位分配方案,确认后集群完成初始化。
验证集群状态:
redis-cli -h 192.168.1.10 -p 7000 cluster info看到cluster_state:ok,说明集群已经可用。
5.3 Spring Boot连接集群的配置与边界
Spring Boot连接Redis集群,配置上的变化不大,核心是把集群节点地址列出来。
Spring Boot 3.x的配置:
spring: data: redis: cluster: nodes: - 192.168.1.10:7000 - 192.168.1.10:7001 - 192.168.1.10:7002 - 192.168.1.10:7003 - 192.168.1.10:7004 - 192.168.1.10:7005Spring Boot 2.x的配置:
spring: redis: cluster: nodes: - 192.168.1.10:7000 - 192.168.1.10:7001 - 192.168.1.10:7002 - 192.168.1.10:7003 - 192.168.1.10:7004 - 192.168.1.10:7005跟哨兵模式类似,这里不需要把所有节点都列出来,Lettuce会通过连接上的节点自动发现整个集群的拓扑。但我还是建议把六个节点都写上,避免某个节点不可达时,客户端只能从第一个节点开始学习集群信息,启动时间变慢。
集群模式下有几个边界你必须知道。
第一,database配置不生效。集群模式只支持db 0,你在yaml里配置database: 1不会报错,但不会被使用。很多从单机迁移到集群的项目就在这一步踩坑,业务里用了select 1,迁移后数据全都不在预期库。
第二,多键操作有限制。MGET、MSET、SUNIONSTORE这类涉及多个key的命令,要求所有key落在同一个槽位,否则集群会返回CROSSSLOT错误。解决办法是使用哈希标签,把需要一起操作的key包在{}里。比如{user}:1001和{user}:1002,CRC16只会计算user这一段的哈希,保证两个key落在同一个槽。
第三,Lua脚本和事务同样受限。脚本里访问的所有key必须映射到同一个槽位。这会让一些老业务的改造量大增,也是我不推荐小规模业务贸然上集群的原因。
6. 高频问题与排查建议
6.1 序列化导致的乱码与版本冲突
Redis可视化工具里看到key带\xAC\xED前缀,是没配置序列化器的最典型表现。解决方案前面已经给了,自定义RedisTemplate的序列化策略,key用String,value用JSON。
这里再补一个细节:如果项目里同时用了@Cacheable,你只改RedisTemplate是不够的,还要单独配置RedisCacheManager的序列化。我自己就遇到过:所有手写的缓存存取都正常,唯独注解缓存取出来反序列化报错,排查半天才想起注解缓存走的是另外一套序列化逻辑。
另一个关联问题是Redis版本和Spring Data Redis版本不匹配。老版本Spring Data Redis对RESP3协议支持不完整,连接Redis 7.x会出现握手失败或命令不识别。具体表现是启动能连上,执行命令时报ERR unknown command。优先升级Spring Boot到新版本,老版本项目如果锁定不能升级,连接Redis时建议显式配置协议版本,让Lettuce使用RESP2兼容,别让它自动协商。
6.2 连接池耗尽和超时
高并发下最常出现的错误有三种:
RedisConnectionFailureExceptionJedisDataException: Read timed outRedisCommandTimeoutException
这类问题十有八九不是Redis本身挂了,而是连接池被占满,或者Lettuce的默认超时时间太短。
排查步骤很固定:先看Redis服务端是否正常,redis-cli ping一下;再看连接池参数是否跟并发量匹配,重点看max-active和max-wait;最后看Redis的慢查询日志,slowlog get,看看是不是有异常命令把连接长时间占用。
调优时注意:max-active过大不一定是好事,连接数的增加会占用Redis服务端资源,反而可能引发慢查询。合理的方式是按照业务峰值并发量乘以每个请求平均占用连接数来估算,通常30到50就能满足绝大多数场景。
6.3 密码、前缀、模式选择里的细节问题
密码问题常见于三种情况:
NOAUTH Authentication required. ERR Client sent AUTH, but no password is set. WRONGPASS invalid username-password pair or user is disabled.第一种是服务端设置了密码但客户端没配;第二种反过来了,服务端根本没设密码,客户端却带了密码;第三种是密码本身错误,或者Redis 6的默认用户名和密码鉴权方式变了。排查思路是先用命令行直接验证密码:
redis-cli -h 192.168.1.10 -p 6379 -a yourpassword ping能返回PONG,说明服务端密码配置没问题,问题就在Spring Boot配置。
配置前缀问题我也再强调一次:Spring Boot 3.x升级后,RedisProperties的绑定前缀从spring.redis改成了spring.data.redis。如果你还在用2.x的写法,配置不会报错,但属性全部没有绑定,默认连localhost,这在多环境部署时是个隐形炸弹。项目升级之后的第一件事,就是把Redis相关的配置文件全局搜索一遍,把前缀全部改掉。
模式选择的问题,我见过不少团队在“哨兵和集群二选一”上反复纠结。其实可以看一个指标:数据量是否接近单机内存上限。数据量可控,优先哨兵;数据量不可控,优先集群。集群也可以给每个分片配多个副本,但那属于更复杂的架构设计,初期用不上。
一些实操后的个人经验
最后抛开配置本身,说三点我自己的真实体会,也算是给准备动手的读者提个醒。
第一,配置能在yaml里写清楚的事情,尽量不要在代码里写死。尤其是密码、节点地址、超时时间这些环境相关参数,全部走配置中心和环境变量。我在实际项目里不止一次看到有人把密码直接写在RedisTemplate的构造函数里,代码一提交,密码跟着泄露,后面改配置还要发版。
第二,RedisTemplate的序列化方案最好在项目初始化的时候就定下来,后面再改,线上已有的缓存数据全部都要清洗,代价非常大。新项目直接用StringRedisSerializer加GenericJackson2JsonRedisSerializer的组合,三年内都够用。
第三,无论用哪种模式,Redis实例上的内存监控和慢查询日志一定要有。哨兵能帮你切换主节点,集群能帮你分摊数据,但真正的容量规划和性能优化还得靠仪表盘数据说话。别等Redis把内存打到接近上限、频繁触发淘汰策略才想起来看。
Redis这块的配置内容看起来多,真正落地就会发现,单机和主从几乎是一套配置,哨兵和集群其实也只是在连接地址上换个写法而已。希望这篇文章能帮你把这四个模式一次性捋清楚。