
1. 为什么单机Redis迟早要换成Cluster集群1.1 从一次线上告警说起去年下半年我负责维护的一个订单系统在晚高峰突然开始大量超时。排查下来发现Redis单机实例的内存已经逼近物理上限触发了maxmemory淘汰策略大量热点key被提前驱逐缓存命中率从95%直接掉到60%以下数据库瞬间被打穿。那次事故之后我花了大概两周时间把缓存层从单机主从切换到了Redis Cluster集群版。这个经历让我意识到一个问题很多团队在项目初期用单机Redis跑得好好的业务量一上来就出问题。单机Redis的瓶颈其实很明显——内存受限于单机物理内存写入吞吐受限于单线程模型主从架构虽然能读扩展但写仍然是单点。Redis Cluster就是官方给出的分布式解决方案把数据分片到多个节点上每个节点只存一部分数据同时每个分片还有从节点做高可用。这篇文章我会把Redis Cluster集群版的搭建过程、SpringBoot项目中的接入方式、以及我在实践中踩过的坑完整地梳理一遍。适合已经用过单机Redis、想往集群方向升级的开发者也适合正在做技术选型、想了解Cluster实际落地成本的团队参考。1.2 Cluster到底解决了什么问题先明确Cluster的核心能力。它把整个key空间划分成16384个哈希槽hash slot每个主节点负责一部分槽位。当你写入一个key时Redis会对key做CRC16计算然后对16384取模得到这个key属于哪个槽再路由到负责该槽的节点上。这样做的好处有三个第一数据分散到多个节点总内存容量等于所有主节点内存之和突破了单机上限第二写入请求分散到多个主节点吞吐量随节点数线性增长第三每个主节点可以挂从节点主节点故障时从节点自动晋升保证可用性。但Cluster也有代价。它不支持跨槽的多key操作事务和Lua脚本涉及的key必须在同一个槽里。客户端需要支持集群拓扑发现和请求重定向。运维复杂度比单机高不少。这些限制在选型时都要考虑清楚。提示如果你的业务QPS在几万以内、数据量在10GB以内单机主从加哨兵其实够用了。Cluster更适合数据量大、写入吞吐要求高的场景不要为了用而用。2. 集群搭建前的规划与准备2.1 节点规划与端口分配Redis Cluster最少需要3个主节点官方推荐至少3主3从共6个节点。我这次搭建用的是6台独立的服务器每台跑一个Redis实例3主3从交叉分布。如果资源有限也可以在一台机器上用不同端口跑多个实例但生产环境不建议这么做一台机器挂了会影响多个节点。端口规划上每个Redis实例需要两个端口一个是客户端通信端口比如7001另一个是集群总线端口客户端端口10000即17001用于节点间 gossip 通信。防火墙必须同时放行这两个端口很多人搭集群失败就是忘了开总线端口。节点角色IP客户端端口总线端口主节点1192.168.1.101700117001主节点2192.168.1.102700217002主节点3192.168.1.103700317003从节点1192.168.1.104700417004从节点2192.168.1.105700517005从节点3192.168.1.1067006170062.2 配置文件的关键参数每个节点的redis.conf需要重点改这几个参数。cluster-enabled yes 开启集群模式cluster-config-file nodes-7001.conf 指定集群配置文件这个文件由Redis自动维护不要手动改cluster-node-timeout 15000 设置节点超时时间单位毫秒。appendonly yes 建议开启AOF持久化集群环境下数据安全性更重要。protected-mode no 在受信任的内网环境可以关闭保护模式否则跨机访问会被拒绝。bind 0.0.0.0 监听所有网卡同样仅限内网。还有一个容易忽略的参数cluster-require-full-coverage。默认是yes意思是只要有一个槽位不可用整个集群就拒绝服务。生产环境建议改成no这样部分节点故障时其他槽位的数据仍然可读写可用性更高。# 以7001节点为例的配置片段 port 7001 cluster-enabled yes cluster-config-file nodes-7001.conf cluster-node-timeout 15000 appendonly yes protected-mode no bind 0.0.0.0 cluster-require-full-coverage no daemonize yes logfile /var/log/redis/redis-7001.log2.3 系统层面的调优在启动Redis之前有几个系统参数需要调整。vm.overcommit_memory 设置为1避免后台保存时fork失败。net.core.somaxconn 调大到1024以上应对高并发连接。还有透明大页THP建议关闭否则可能导致Redis延迟抖动。echo 1 /proc/sys/vm/overcommit_memory echo 1024 /proc/sys/net/core/somaxconn echo never /sys/kernel/mm/transparent_hugepage/enabled这些参数最好写进 /etc/sysctl.conf 和开机脚本否则重启后就失效了。我吃过这个亏有次服务器维护重启后忘了重新设置Redis延迟毛刺明显增多查了半天才想起来是THP又开了。3. 集群创建与验证实操3.1 用redis-cli创建集群6个节点都启动好之后用redis-cli的--cluster create命令一键建集群。--cluster-replicas 1 表示每个主节点配一个从节点Redis会自动分配主从关系和槽位。redis-cli --cluster create \ 192.168.1.101:7001 \ 192.168.1.102:7002 \ 192.168.1.103:7003 \ 192.168.1.104:7004 \ 192.168.1.105:7005 \ 192.168.1.106:7006 \ --cluster-replicas 1执行后会显示一个槽位分配方案让你确认。输入yes后集群开始创建节点之间握手、分配槽位、建立主从关系。看到 All 16384 slots covered 就说明集群创建成功了。这里有个细节Redis分配主从不是简单的前三个主后三个从它会打乱顺序。你可以在创建完成后用 cluster nodes 命令查看实际的主从对应关系。如果对主从分布有要求比如希望从节点和主节点不在同一机架需要手动用 cluster replicate 命令调整。3.2 集群状态检查创建完成后必须做几项验证。第一用 redis-cli -c -p 7001 cluster info 查看集群状态cluster_state 应该是okcluster_slots_assigned 应该是16384。第二用 cluster nodes 查看每个节点的角色和槽位范围。第三实际写入几个key验证读写和重定向是否正常。# 查看集群信息 redis-cli -c -p 7001 cluster info # 查看节点列表 redis-cli -c -p 7001 cluster nodes # 写入测试-c参数开启集群模式自动重定向 redis-cli -c -p 7001 set test_key hello redis-cli -c -p 7001 get test_key如果 set 的时候看到 MOVED 12345 192.168.1.102:7002 这样的提示说明客户端没有开启集群模式。加上 -c 参数后redis-cli会自动跟随重定向你就能正常读写了。3.3 槽位迁移与扩容演练集群跑起来之后扩容是迟早的事。假设要加一个新主节点7007步骤是先启动新节点然后用 redis-cli --cluster add-node 把新节点加入集群此时它没有槽位再用 --cluster reshard 从现有节点迁移一部分槽位给它。# 添加新节点 redis-cli --cluster add-node 192.168.1.107:7007 192.168.1.101:7001 # 迁移槽位交互式输入迁移数量和目标节点ID redis-cli --cluster reshard 192.168.1.101:7001reshard过程会逐个key迁移如果槽位里数据量大耗时会比较长。建议在业务低峰期操作并且迁移前做好数据备份。我一般会先用 --cluster rebalance 做一次均衡检查看看各节点槽位分布是否均匀。注意槽位迁移过程中被迁移的槽位会短暂处于MIGRATING/IMPORTING状态客户端访问这些槽位的key时可能收到ASK重定向。成熟的客户端如Lettuce会自动处理ASK但如果你用的是老版本Jedis需要确认版本是否支持。4. SpringBoot接入Cluster的完整方案4.1 客户端选型Lettuce还是JedisSpringBoot从2.x开始默认用Lettuce作为Redis客户端。Lettuce基于Netty是异步非阻塞的多个线程可以共享同一个连接在集群环境下表现更好。Jedis是同步阻塞的每个线程需要独立连接连接池开销大。所以新项目我强烈建议用Lettuce。不过Lettuce有个坑它默认只在启动时刷新一次集群拓扑如果集群发生主从切换或槽位迁移拓扑信息可能过期。需要配置一个定时刷新策略或者开启自适应刷新。这个后面会详细说。4.2 依赖引入与配置pom.xml里引入spring-boot-starter-data-redis它会自动带入Lettuce。如果需要连接池还要加commons-pool2。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependencyapplication.yml里配置集群节点列表。注意集群模式下spring.redis.cluster.nodes 列出所有主从节点客户端会自动发现拓扑。spring: redis: cluster: nodes: - 192.168.1.101:7001 - 192.168.1.102:7002 - 192.168.1.103:7003 - 192.168.1.104:7004 - 192.168.1.105:7005 - 192.168.1.106:7006 max-redirects: 3 timeout: 3000 lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 2000max-redirects 是跟随重定向的最大次数默认就是3一般够用。timeout 是命令超时时间集群环境下建议设短一点比如3秒避免慢节点拖垮整个请求。4.3 自定义Lettuce集群拓扑刷新前面提到Lettuce的拓扑刷新问题这是接入Cluster最容易踩的坑。默认情况下Lettuce只在建立连接时获取一次拓扑之后如果集群扩容或主从切换客户端可能还在往旧节点发请求导致连接失败。解决办法是配置 ClusterTopologyRefreshOptions开启周期刷新和自适应刷新。周期刷新是每隔一段时间主动拉取拓扑自适应刷新是在遇到重定向时触发拓扑更新。Configuration public class RedisClusterConfig { Bean public LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() { return builder - { ClusterTopologyRefreshOptions refreshOptions ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(Duration.ofSeconds(30)) .enableAllAdaptiveRefreshTriggers() .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10)) .build(); ClusterClientOptions clientOptions ClusterClientOptions.builder() .topologyRefreshOptions(refreshOptions) .maxRedirects(3) .build(); builder.clientOptions(clientOptions); }; } }这段配置我实测下来很稳。enableAllAdaptiveRefreshTriggers 会在MOVED、ASK、连接断开等情况下触发拓扑刷新配合30秒的周期刷新基本能保证客户端拓扑和集群实际状态一致。4.4 RedisTemplate的序列化配置默认的RedisTemplate用JDK序列化存进去的key和value都是二进制乱码用redis-cli看根本认不出来。生产环境一定要改成String序列化key、JSON序列化value。Bean public RedisTemplateString, Object redisTemplate( RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); 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有个好处它会在JSON里带上类信息反序列化时能自动还原成原对象。但要注意如果对象结构变了比如加了字段旧数据反序列化可能报错。对稳定性要求高的场景可以用Jackson2JsonRedisSerializer指定具体类型牺牲一点通用性换稳定性。5. 集群环境下的开发避坑指南5.1 跨槽操作的三种解法Cluster最让人头疼的限制就是跨槽操作。比如你想用 mget 一次取多个key如果这些key不在同一个槽就会报 CROSSSLOT 错误。同样涉及多个key的Lua脚本、事务MULTI/EXEC也受此限制。解法一用hash tag强制同槽。Redis规定如果key里包含 {}只有 {} 内的内容参与槽位计算。所以 user:{1001}:name 和 user:{1001}:age 会落在同一个槽。设计key的时候把需要一起操作的key用相同的tag包起来。解法二拆分操作。mget拆成多次get用pipeline批量发送减少网络往返。事务拆成独立的命令用分布式锁保证一致性。解法三客户端聚合。在应用层把多个key按槽分组分别请求不同节点然后合并结果。Spring Data Redis的ClusterOperations接口提供了这种能力但用起来比较繁琐。操作类型是否支持跨槽推荐解法mget/mset不支持hash tag或拆分pipelineLua脚本不支持hash tag保证同槽事务MULTI不支持分布式锁替代pipeline支持分节点客户端自动分组5.2 批量操作的性能优化集群环境下做批量写入如果一个个set网络往返次数太多。用pipeline能把多个命令打包发送大幅减少RTT。但要注意pipeline里的key如果跨槽Lettuce会自动按节点分组分别发送pipeline这个行为是透明的不用手动处理。public void batchSet(MapString, String data) { redisTemplate.executePipelined((RedisCallbackObject) connection - { StringRedisConnection strConn (StringRedisConnection) connection; data.forEach(strConn::set); return null; }); }实测下来1000个key的批量写入用pipeline比逐个set快5到8倍。但pipeline不是越大越好一次打包太多命令会占用大量内存建议每批控制在500到1000个命令。5.3 主从切换时的连接处理集群主节点故障时从节点会在 cluster-node-timeout 之后发起选举晋升为主节点。这个过程通常需要10到15秒。在这期间客户端访问故障节点的请求会失败。Lettuce的重试机制可以缓解这个问题。配置 spring.redis.lettuce.cluster.refresh 相关参数配合命令重试能在主从切换后自动恢复。但要注意重试只对幂等操作安全像 incr 这种非幂等命令重试可能导致重复累加。我的做法是对读操作开启重试对写操作根据业务判断。如果业务能容忍重复写比如set覆盖可以重试如果是计数器类操作要么用分布式锁要么在应用层做补偿。提示cluster-node-timeout 不要设太小否则网络抖动会误判节点故障引发不必要的切换。生产环境建议15秒左右具体根据网络质量调整。6. 监控、运维与常见故障排查6.1 必须监控的核心指标集群跑起来不是终点持续监控才能提前发现问题。我重点关注这几类指标集群状态cluster_state是否为ok、槽位覆盖情况是否有槽位未分配、节点连接数、内存使用率、命令延迟、主从复制延迟。用 redis-cli --cluster check 可以快速做一次健康检查它会检查槽位覆盖、主从关系、各节点状态。建议做成定时任务每天跑一次结果异常时告警。redis-cli --cluster check 192.168.1.101:7001输出会列出每个主节点的槽位范围、从节点数量、是否有孤立节点。如果看到 Not all slots are covered 或者某个节点状态是fail就要立即处理。6.2 常见故障速查表故障现象可能原因排查方法解决方案集群状态fail槽位未全覆盖cluster info查看slots_assigned用cluster addslots补全写入报CLUSTERDOWN节点故障未恢复cluster nodes看节点状态修复故障节点或手动failover频繁MOVED重定向客户端未开集群模式检查客户端配置开启集群模式或更新拓扑主从切换后连接失败拓扑未刷新查看客户端拓扑缓存配置拓扑刷新策略内存告警数据倾斜各节点info memory对比检查热点key重新分片延迟毛刺大key或慢查询slowlog get排查拆分大key优化命令6.3 数据倾斜的处理集群最容易出现的问题之一是数据倾斜——某些节点内存用得特别多其他节点很空。原因通常是热点key集中在某个槽或者key的分布本身不均匀。排查方法是逐个节点执行 info memory对比 used_memory 的差异。如果差异超过30%就要考虑重新分片。用 redis-cli --cluster rebalance 可以自动均衡槽位但它只迁移槽不解决热点key问题。热点key还得从业务层面拆分比如把一个热门商品的缓存拆成多个子key分散到不同槽。我遇到过一次数据倾斜原因是某个业务用了自增ID作为key前缀而自增ID的高位变化慢导致大量key落在相邻槽位。后来改成对ID做哈希后再拼key分布就均匀了。这个经验告诉我key的设计要提前考虑槽位分布别等出问题再改。6.4 备份与恢复策略集群的备份比单机复杂因为数据分散在多个节点。我用的方案是每个主节点定期执行 BGSAVE 生成RDB文件然后把RDB集中备份到对象存储。恢复时先按原拓扑启动节点把RDB放回对应目录再重建集群。要注意集群模式下不能用单机的 DEBUG RELOAD 来加载RDB必须让节点以集群模式启动然后通过 cluster meet 重新组网。如果拓扑变了比如节点数不同还需要重新分片。所以备份时最好记录下当前的槽位分配和主从关系恢复时照着来。# 在每个主节点执行 redis-cli -p 7001 bgsave # 备份RDB文件 cp /var/lib/redis/7001/dump.rdb /backup/redis/7001-$(date %Y%m%d).rdb备份频率根据数据重要性和可容忍的丢失量来定。对订单类数据我一般每小时备份一次RPO控制在一小时内。对缓存类数据丢了可以从数据库重建备份频率可以低一些。7. 一些个人实践体会Cluster集群搭建本身不难难的是后续的运维和业务适配。我最大的体会是上集群之前一定要先梳理清楚业务里哪些操作是跨槽的提前设计好key的命名规范。等业务跑起来再改key结构迁移成本非常高。另外客户端版本很关键。Lettuce 5.3以上对集群的支持才比较完善老版本在拓扑刷新和重定向处理上有bug。SpringBoot 2.3以上默认带的Lettuce版本基本没问题如果用的是更早的版本建议手动升级Lettuce。最后分享一个小技巧本地开发环境可以用Docker Compose一键起6个Redis节点省去手动配置的麻烦。把集群搭建脚本化每次重建环境只要跑一个脚本几分钟就能搞定。这样测试集群相关功能时随时可以推倒重来不用担心把环境搞坏。# docker-compose.yml 片段起6个节点 version: 3 services: redis-7001: image: redis:7.0 command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes ports: - 7001:7001 - 17001:17001 # 其余5个节点类似改端口即可用Docker起集群时网络模式建议用host否则集群总线通信可能因为NAT出问题。如果必须用bridge要确保容器间能通过配置的IP互相访问cluster announce ip 要设成宿主机IP。