
1. 为什么单机Redis不够用从一次故障说起很多团队在项目初期都是单机Redis一把梭缓存、会话、排行榜全塞一个实例里。业务量小的时候确实没问题QPS几千、内存占用几个G跑得稳稳当当。但真到了线上环境单机Redis有三个绕不过去的坎单点故障、性能瓶颈、数据丢失风险。我见过一个电商项目大促期间Redis实例因为内存碎片率过高触发OOM整个商品详情页直接白屏损失惨重。事后复盘发现如果当初做了主从加哨兵故障切换能在几十秒内自动完成根本不会演变成事故。主从复制的核心思路很简单一个主节点负责写多个从节点负责读主节点的数据异步同步到从节点。这样做的好处是读写分离读请求可以分摊到多个从节点上写请求集中在主节点保证一致性。但主从模式有个致命问题——主节点挂了不会自动切换需要人工介入。这时候哨兵就登场了哨兵是一个独立的进程专门用来监控Redis节点的健康状态一旦发现主节点不可达就会从从节点中选举出新的主节点并通知客户端连接新地址。这套架构适合谁呢我认为凡是生产环境用Redis的团队都应该掌握。不管是中小型项目还是大型分布式系统主从加哨兵都是性价比最高的高可用方案。它不需要额外的中间件不依赖外部协调服务用Docker编排起来也就几十行配置。接下来我会从架构设计、Docker编排、配置细节、故障演练四个维度把整套方案拆开揉碎讲清楚。2. 架构设计与Docker编排思路2.1 节点规划与端口分配在动手写Docker Compose之前先把节点规划想清楚。我建议的最小生产级配置是1主2从3哨兵总共6个容器。为什么是3个哨兵因为哨兵选举需要多数票3个哨兵允许1个挂掉还能正常选举2个哨兵一旦挂1个就失去多数派无法完成故障转移。从节点2个是为了保证读性能和高可用主节点挂掉后至少还有一个从节点可以顶上来。端口分配上Redis默认6379从节点可以用6380、6381哨兵默认26379依次用26380、26381。这里有个细节要注意哨兵之间需要互相通信所以每个哨兵的端口必须独立且可访问。我见过有人把哨兵都映射到同一个宿主机端口结果哨兵集群根本建立不起来排查了半天才发现是端口冲突。角色容器名内部端口宿主机端口说明Masterredis-master63796379主节点负责写Slave1redis-slave163796380从节点1负责读Slave2redis-slave263796381从节点2负责读Sentinel1redis-sentinel12637926379哨兵1监控集群Sentinel2redis-sentinel22637926380哨兵2监控集群Sentinel3redis-sentinel32637926381哨兵3监控集群2.2 网络模式选择自定义bridge还是hostDocker网络模式的选择直接影响哨兵能否正确发现节点。我强烈建议使用自定义bridge网络而不是默认的bridge或host模式。原因有三第一自定义bridge网络内置DNS解析容器之间可以直接用容器名通信配置里写redis-master就能解析到对应IP不用硬编码IP地址第二host模式下端口冲突问题很头疼尤其是宿主机上已经有Redis在跑的时候第三自定义bridge网络隔离性好不会影响宿主机其他服务。但这里有个坑哨兵配置里需要写主节点的IP地址而Docker容器的IP在重启后可能会变。解决方案是在哨兵配置中使用容器名而不是IPRedis 5.0之后的哨兵支持主机名解析。如果用的是老版本那就得在启动脚本里动态获取IP并写入配置。我实测下来Redis 6.2和7.0对主机名支持都很好直接写容器名就行。2.3 数据持久化策略主从架构下持久化策略需要仔细考虑。我的建议是主节点开启AOF从节点开启RDB。主节点用AOF保证数据不丢从节点用RDB做冷备和快速恢复。为什么不全部开AOF因为AOF文件通常比RDB大很多从节点如果也开AOF磁盘IO和网络传输压力会翻倍。而且从节点的数据是从主节点同步过来的主节点AOF已经保证了数据完整性从节点用RDB做备份足够了。具体配置上主节点设置appendonly yes、appendfsync everysec从节点设置save 900 1、save 300 10。这里有个经验值appendfsync everysec是性能和安全的平衡点最多丢1秒数据对绝大多数业务够用了。如果业务要求零丢失那就得用appendfsync always但性能会下降一个数量级得权衡。3. Docker Compose编排文件详解3.1 主节点配置与启动参数先看主节点的Docker Compose配置。我习惯把配置拆成两部分docker-compose.yml负责容器编排redis.conf负责Redis参数。这样修改配置不用动编排文件职责清晰。version: 3.8 services: redis-master: image: redis:7.0-alpine container_name: redis-master ports: - 6379:6379 volumes: - ./master/redis.conf:/usr/local/etc/redis/redis.conf - ./master/data:/data command: redis-server /usr/local/etc/redis/redis.conf networks: - redis-net restart: always networks: redis-net: driver: bridge主节点的redis.conf关键配置如下bind 0.0.0.0 protected-mode no port 6379 daemonize no appendonly yes appendfsync everysec requirepass YourStrongPassword masterauth YourStrongPassword这里有几个点要解释。bind 0.0.0.0是让Redis监听所有网卡Docker容器内必须这样配否则外部连不上。protected-mode no是关闭保护模式因为我们在Docker网络里有网络隔离不需要额外保护。requirepass和masterauth都设成一样的密码前者是客户端连接密码后者是从节点同步密码设成一样省事生产环境也可以用不同密码但配置会复杂一些。注意daemonize no必须设置因为Docker容器需要前台进程如果设成yes容器启动后Redis会后台运行Docker以为进程退出了容器直接停止。3.2 从节点配置与主从同步从节点的配置和主节点大同小异核心区别是多了replicaof指令。在Redis 5.0之前叫slaveof5.0之后推荐用replicaof语义更准确。bind 0.0.0.0 protected-mode no port 6379 daemonize no appendonly no save 900 1 save 300 10 requirepass YourStrongPassword masterauth YourStrongPassword replicaof redis-master 6379 replica-read-only yesreplicaof redis-master 6379这行是关键告诉从节点去同步哪个主节点。这里直接写容器名redis-masterDocker的DNS会解析成正确IP。replica-read-only yes是从节点只读防止误写导致数据不一致。我见过有人从节点没设只读结果程序连错地址写入了从节点主从数据直接乱掉排查了一整天。从节点的Docker Compose配置和主节点类似只是端口映射和挂载路径不同redis-slave1: image: redis:7.0-alpine container_name: redis-slave1 ports: - 6380:6379 volumes: - ./slave1/redis.conf:/usr/local/etc/redis/redis.conf - ./slave1/data:/data command: redis-server /usr/local/etc/redis/redis.conf networks: - redis-net depends_on: - redis-master restart: alwaysdepends_on保证主节点先启动但注意这只是启动顺序控制不代表主节点已经准备好接受连接。从节点启动时如果主节点还没就绪会不断重试同步最终会成功所以不用太担心。3.3 哨兵配置与监控参数哨兵配置是整个方案里最容易出错的部分。先看配置文件port 26379 daemonize no sentinel monitor mymaster redis-master 6379 2 sentinel auth-pass mymaster YourStrongPassword sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1逐行解释。sentinel monitor mymaster redis-master 6379 2这行定义了一个监控组名字叫mymaster监控的主节点是redis-master:6379最后的2是quorum值表示至少2个哨兵认为主节点挂了才触发故障转移。3个哨兵设2是合理的既保证可靠性又不会太敏感。sentinel auth-pass mymaster YourStrongPassword是哨兵连接Redis的密码必须和Redis的requirepass一致否则哨兵连不上节点监控直接失效。sentinel down-after-milliseconds mymaster 5000是主观下线时间哨兵在5秒内ping不通主节点就认为它挂了。这个值不能太小网络抖动容易误判也不能太大故障转移太慢。5000毫秒是我实测比较平衡的值。sentinel failover-timeout mymaster 60000是故障转移超时时间60秒内没完成转移就重新尝试。sentinel parallel-syncs mymaster 1表示故障转移后同时只有1个从节点去同步新主节点避免新主节点被多个从节点同时同步压垮。哨兵的Docker Compose配置需要额外注意一点哨兵端口必须暴露且互相可访问。因为哨兵之间要互相通信来交换监控信息。redis-sentinel1: image: redis:7.0-alpine container_name: redis-sentinel1 ports: - 26379:26379 volumes: - ./sentinel1/sentinel.conf:/usr/local/etc/redis/sentinel.conf command: redis-sentinel /usr/local/etc/redis/sentinel.conf networks: - redis-net depends_on: - redis-master - redis-slave1 - redis-slave2 restart: always三个哨兵的配置除了端口号不同其他完全一样。这里有个细节哨兵启动后会自动发现其他哨兵不需要在配置里手动指定。哨兵通过发布订阅机制在__sentinel__:hello频道上交换信息自动组成集群。所以只要三个哨兵都监控同一个主节点它们会自动找到彼此。4. 完整部署流程与验证步骤4.1 目录结构与文件准备先把目录结构建好我习惯这样组织redis-cluster/ ├── docker-compose.yml ├── master/ │ ├── redis.conf │ └── data/ ├── slave1/ │ ├── redis.conf │ └── data/ ├── slave2/ │ ├── redis.conf │ └── data/ ├── sentinel1/ │ └── sentinel.conf ├── sentinel2/ │ └── sentinel.conf └── sentinel3/ └── sentinel.conf创建目录的命令mkdir -p redis-cluster/{master,slave1,slave2}/{data} mkdir -p redis-cluster/{sentinel1,sentinel2,sentinel3} cd redis-cluster然后把前面写的配置文件分别放到对应目录。这里有个容易忽略的点哨兵配置文件的权限。哨兵在运行时会重写配置文件比如故障转移后更新主节点地址如果文件权限不对哨兵会报错。建议把哨兵配置文件权限设成644并且确保容器内用户有写权限。4.2 启动顺序与健康检查启动顺序很重要我建议按这个顺序来先启动主节点等主节点完全就绪后再启动从节点最后启动哨兵。虽然Docker Compose的depends_on能控制启动顺序但控制不了就绪状态。更稳妥的做法是分步启动# 第一步启动主节点 docker-compose up -d redis-master # 等待5秒确认主节点就绪 sleep 5 docker exec redis-master redis-cli -a YourStrongPassword ping # 第二步启动从节点 docker-compose up -d redis-slave1 redis-slave2 # 等待5秒确认从节点同步完成 sleep 5 docker exec redis-slave1 redis-cli -a YourStrongPassword info replication # 第三步启动哨兵 docker-compose up -d redis-sentinel1 redis-sentinel2 redis-sentinel3验证主从同步是否正常看从节点的info replication输出docker exec redis-slave1 redis-cli -a YourStrongPassword info replication输出里应该看到role:slave、master_host:redis-master、master_link_status:up。如果master_link_status是down说明同步失败检查密码和网络连通性。验证哨兵是否正常连上任意哨兵执行docker exec redis-sentinel1 redis-cli -p 26379 sentinel master mymaster输出里会显示主节点的IP、端口、从节点数量等信息。再执行docker exec redis-sentinel1 redis-cli -p 26379 sentinel sentinels mymaster应该能看到另外两个哨兵的信息。如果只看到自己说明哨兵之间没发现彼此检查网络和端口配置。4.3 读写分离验证与数据一致性测试部署完成后做个简单的读写测试。先往主节点写数据docker exec redis-master redis-cli -a YourStrongPassword set test_key hello_redis然后从从节点读docker exec redis-slave1 redis-cli -a YourStrongPassword get test_key docker exec redis-slave2 redis-cli -a YourStrongPassword get test_key两个从节点都应该返回hello_redis。再测试从节点只读docker exec redis-slave1 redis-cli -a YourStrongPassword set test_key2 should_fail应该返回READONLY You cant write against a read only replica.说明只读配置生效。数据一致性方面主从复制是异步的所以从节点数据可能比主节点慢几毫秒到几十毫秒。如果业务对一致性要求高可以在写完后等一小会儿再读或者用WAIT命令阻塞等待同步完成docker exec redis-master redis-cli -a YourStrongPassword wait 1 1000这个命令表示等待至少1个从节点确认超时1000毫秒。返回的数字是实际确认的从节点数量。5. 故障转移演练与常见问题排查5.1 模拟主节点宕机与自动切换故障转移演练是验证整套架构是否可靠的关键步骤。先手动停掉主节点docker stop redis-master然后观察哨兵日志docker logs -f redis-sentinel1正常情况下5秒后哨兵会标记主节点为主观下线然后询问其他哨兵。当2个以上哨兵都确认主节点不可达时标记为客观下线触发故障转移。选举新主节点的过程大概需要10到30秒取决于failover-timeout配置。转移完成后连上哨兵查看新的主节点docker exec redis-sentinel1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster返回的应该是某个从节点的IP和端口。再检查原从节点是否变成了新主节点docker exec redis-slave1 redis-cli -a YourStrongPassword info replication如果role:master说明切换成功。原来的主节点恢复后会自动变成新主节点的从节点这个行为是哨兵自动处理的不需要人工干预。5.2 常见问题速查表问题现象可能原因排查方法解决方案从节点master_link_status:down密码错误或网络不通检查masterauth和requirepass是否一致统一密码配置哨兵无法发现彼此端口未暴露或网络隔离docker exec进入容器ping其他哨兵确保哨兵端口映射正确故障转移不触发quorum值设置过高检查sentinel monitor的quorum值调整为哨兵数量的多数派切换后客户端连不上客户端未使用哨兵模式检查客户端连接方式改用哨兵连接池数据丢失主从异步复制延迟检查master_repl_offset差值关键数据用WAIT命令5.3 实操心得与避坑指南第一个坑哨兵配置文件的写权限。哨兵在故障转移后会重写配置文件如果文件是只读的哨兵会报错但不会退出只是切换后配置不持久化重启后回到旧配置。我建议把哨兵配置文件放在可写卷里并且定期备份。第二个坑Docker重启后IP变化。虽然哨兵会自动发现新IP但如果所有节点同时重启哨兵可能来不及更新配置。解决方案是给容器分配固定IP或者在哨兵配置里用容器名而不是IP。我实测下来用容器名最省心。第三个坑客户端连接池配置。很多客户端连接池只配置了主节点地址故障转移后不会自动切换。必须使用支持哨兵的客户端比如Jedis的JedisSentinelPool、Lettuce的RedisSentinelConfiguration。配置时传入哨兵地址列表和主节点名称客户端会自动从哨兵获取当前主节点地址。第四个坑脑裂问题。网络分区时旧主节点可能还在接受写请求新主节点也在接受写请求导致数据不一致。缓解方案是配置min-replicas-to-write和min-replicas-max-lag让主节点在从节点数量不足或延迟过大时拒绝写入min-replicas-to-write 1 min-replicas-max-lag 10这两个参数表示至少1个从节点延迟不超过10秒主节点才接受写入。这样网络分区时旧主节点会自动拒绝写入减少数据不一致的风险。6. 生产环境加固与性能调优6.1 安全加固措施生产环境不能裸奔几个基本的安全措施必须做。第一修改默认端口虽然Docker映射了端口但容器内还是6379建议改成非标准端口减少被扫描的风险。第二启用密码认证前面已经配了requirepass但密码要足够复杂建议用32位以上的随机字符串。第三限制网络访问Docker网络虽然隔离但宿主机端口映射后外部还是能访问建议用防火墙限制来源IP只允许应用服务器访问。第四禁用危险命令比如FLUSHALL、FLUSHDB、KEYS这些命令在生产环境可能被误用或恶意利用。在配置里重命名或禁用rename-command FLUSHALL rename-command FLUSHDB rename-command KEYS 第五开启慢查询日志监控执行时间超过阈值的命令slowlog-log-slower-than 10000 slowlog-max-len 128slowlog-log-slower-than 10000表示超过10毫秒的命令记录到慢查询日志slowlog-max-len 128表示最多保留128条。定期检查慢查询日志能发现很多性能问题。6.2 性能调优参数Redis性能调优涉及内存、网络、持久化多个方面。内存方面设置maxmemory和maxmemory-policymaxmemory 2gb maxmemory-policy allkeys-lrumaxmemory根据容器内存限制设置建议留20%余量给系统。maxmemory-policy推荐allkeys-lru淘汰最近最少使用的键。如果数据都不允许丢失那就用noeviction但内存满了会报错得配合监控告警。网络方面调整TCP backlog和keepalivetcp-backlog 511 tcp-keepalive 300tcp-backlog是TCP连接队列长度高并发场景可以调到1024。tcp-keepalive是TCP保活时间300秒表示5分钟没通信就发探测包。持久化方面AOF重写策略影响性能auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb表示AOF文件比上次重写后增长100%且至少64MB时触发重写。重写期间会fork子进程内存占用翻倍所以maxmemory要留足余量。6.3 监控与告警配置没有监控的架构就是裸奔。Redis自带info命令能获取大量指标建议用Prometheus的redis_exporter采集Grafana展示。关键监控指标包括used_memory、connected_clients、instantaneous_ops_per_sec、keyspace_hits、keyspace_misses、master_link_status、master_repl_offset。告警规则建议设置内存使用率超过80%告警、主从同步断开告警、哨兵数量少于2告警、慢查询数量突增告警。我踩过的坑是只监控了Redis本身没监控哨兵。结果哨兵进程挂了没人知道主节点真挂了才发现没有哨兵来切换。所以哨兵的监控同样重要至少监控哨兵的存活状态和sentinel master命令的响应时间。7. 客户端接入与代码示例7.1 Java客户端配置Java生态里最常用的是Jedis和Lettuce。Jedis的哨兵配置SetString sentinels new HashSet(); sentinels.add(192.168.1.100:26379); sentinels.add(192.168.1.100:26380); sentinels.add(192.168.1.100:26381); JedisPoolConfig poolConfig new JedisPoolConfig(); poolConfig.setMaxTotal(100); poolConfig.setMaxIdle(20); poolConfig.setMinIdle(5); JedisSentinelPool pool new JedisSentinelPool(mymaster, sentinels, poolConfig, YourStrongPassword); Jedis jedis pool.getResource(); jedis.set(key, value);注意JedisSentinelPool的第一个参数是主节点名称必须和哨兵配置里的mymaster一致。密码是Redis的requirepass不是哨兵的密码。Lettuce的配置更简洁RedisSentinelConfiguration config new RedisSentinelConfiguration() .master(mymaster) .sentinel(192.168.1.100, 26379) .sentinel(192.168.1.100, 26380) .sentinel(192.168.1.100, 26381) .password(YourStrongPassword); LettuceConnectionFactory factory new LettuceConnectionFactory(config); factory.afterPropertiesSet(); RedisTemplateString, String template new RedisTemplate(); template.setConnectionFactory(factory); template.afterPropertiesSet();Lettuce默认是异步的性能比Jedis好但配置稍复杂。Spring Boot项目里直接用spring-boot-starter-data-redis配置spring.redis.sentinel.master和spring.redis.sentinel.nodes就行。7.2 连接池参数调优连接池参数直接影响性能。maxTotal是最大连接数根据应用并发量设置一般QPS的1.5倍左右。maxIdle是最大空闲连接建议是maxTotal的20%到30%。minIdle是最小空闲连接建议5到10个避免频繁创建连接。maxWaitMillis是获取连接的最大等待时间建议1000到3000毫秒超时直接抛异常避免请求堆积。有个经验值连接池不是越大越好。我见过有人把maxTotal设成1000结果Redis端connected_clients爆表反而影响性能。Redis单线程处理命令连接数太多会增加上下文切换开销。一般应用maxTotal设50到200就够了具体看压测结果。7.3 故障转移时的客户端行为哨兵故障转移时客户端会短暂报错。Jedis和Lettuce的处理方式不同。Jedis在获取连接时如果发现主节点变了会重新从哨兵获取新地址但已经获取的连接会失效抛JedisConnectionException。Lettuce会自动重连对应用透明但会有短暂的命令失败。应用层需要做重试。建议在Redis操作外层包一层重试逻辑捕获连接异常后重试2到3次间隔100到500毫秒。Spring Retry或Resilience4j都能实现。注意重试只对幂等操作安全非幂等操作要谨慎避免重复写入。8. 扩展思路与架构演进8.1 从哨兵到Cluster的演进路径哨兵模式解决了高可用问题但没解决水平扩展问题。单主节点写入能力有限内存也受单机限制。当数据量超过单机内存或写入QPS超过单节点上限时就需要考虑Redis Cluster。Cluster把数据分片到多个主节点每个主节点负责一部分槽位写入能力线性扩展。从哨兵迁移到Cluster不是无缝的客户端需要改用Cluster模式数据需要重新分片。迁移方案有两种一是停机迁移用redis-cli --cluster工具重新分片二是双写迁移新旧集群同时写逐步切流量。我建议新项目直接上Cluster老项目如果哨兵够用就别折腾迁移成本不低。8.2 多机房部署考虑如果业务有多机房需求哨兵模式可以跨机房部署但要注意网络延迟。哨兵的down-after-milliseconds要大于机房之间的网络延迟否则容易误判。比如同城机房延迟1到2毫秒设5000没问题异地机房延迟30到50毫秒就得设10000以上。跨机房部署还有个问题脑裂风险更高。网络分区时两个机房可能各自选举主节点。缓解方案是设置min-replicas-to-write并且把多数哨兵放在同一个机房保证只有多数派机房能完成选举。8.3 容器化部署的注意事项Docker部署Redis有几点要特别注意。第一内存限制Docker容器的内存限制和Redis的maxmemory要匹配容器限制2GBRedis就设1.6GB左右留余量给系统。第二CPU限制Redis是单线程的给1到2个CPU核心就够了给多了浪费。第三磁盘IOAOF持久化对磁盘IO要求高建议用SSD并且把数据卷挂载到宿主机的高性能磁盘上。第四时钟同步哨兵依赖时间来判断节点是否下线容器时钟必须和宿主机同步。Docker默认使用宿主机时钟一般没问题但如果是跨主机部署要确保所有宿主机时钟同步。第五日志收集容器日志默认存在宿主机上建议配置日志驱动转发到集中日志系统方便排查问题。我个人在实际操作中的体会是Docker部署Redis主从哨兵最难的不是配置本身而是理解每个参数背后的含义和边界条件。配置文件抄一份很容易但出了问题知道去哪里找原因才是真正掌握这套架构的标志。建议大家在测试环境多演练几次故障转移把各种异常情况都模拟一遍比如主节点宕机、从节点宕机、哨兵宕机、网络分区观察系统行为积累排查经验。最后再分享一个小技巧哨兵的日志级别设成notice就够了debug级别日志量太大生产环境容易把磁盘写满。