ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis实战指南:分布式锁、缓存治理与高可用方案

Redis实战指南:分布式锁、缓存治理与高可用方案 这两年Redis相关的文章已经多到泛滥但大多数都在讲命令、讲数据结构真到了生产环境一跑问题全在最容易被忽略的“设计”和“边界情况”上。这篇实战篇3接着前面两篇的基础专门聊我在真实项目里反复踩过的几个场景分布式锁怎么做才不会锁错、缓存怎么加才能不把数据库打穿、以及单机Redis扛不住的时候怎么从容地做主从和哨兵。如果你已经在用Redis或者正准备把它用于线上业务这几块内容应该能帮你省掉不少事故群里的“深夜艾特”。我习惯先交代一下背景。这个系列的前两篇覆盖了Redis的数据类型、过期策略、内存淘汰这些基本功属于“知道怎么写命令”的阶段。而实战篇3要解决的是“知道怎么设计”的问题——同样是set一个key为什么有的系统能扛住双十一的流量有的系统第二天就被一个缓存击穿拖垮。答案不在命令行里在架构思维里。下面这些内容没有按官方文档的顺序讲全是按照我在生产环境里实际处理过的先后顺序来的。1. 生产事故复盘Redis分布式锁踩过的坑先说我遇到的一个真实事故。有一年我接手了一个库存服务代码里已经写了一套基于Redis的分布式锁。看起来没什么问题加锁用SETNX释放锁用DEL简单直接。结果上线第三天半夜被压测打出了一个超卖bug上千件库存被扣成负数。查到最后问题就出在这个“简单直接”上。第一版代码里加锁和设置过期时间是两条命令SETNX lock:sku:123 1 EXPIRE lock:sku:123 3如果第一条命令执行完之后Redis进程或者应用进程突然挂了EXPIRE根本没有机会执行。这把锁就变成了一个永不过期的死锁所有后续请求全部阻塞。这就是典型的“分布式锁必须考虑防死锁”问题——锁必须有过期时间而且过期时间和加锁必须是一条原子命令。Redis 2.6.12之后SET命令支持了NX和EX参数可以一次性搞定所以正确写法应该是SET lock:sku:123 1 NX EX 3但你以为这就完了还早。这事过去没多久同一个服务又出了个更隐蔽的bug线程A拿到锁后执行时间超过了3秒锁自动过期了线程B拿到新锁开始干活这时候线程A终于干完了顺手执行了DEL——它把线程B的锁删了。于是线程C又拿到锁三个线程同时写同一个库存。这次连压测都不用线上真实流量就直接炸了。这个问题的根源在于“释放锁的人”和“持有锁的人”不是同一个人。解决办法是在锁的value里塞一个唯一标识释放之前先比对一下确认是自己持有的锁再删。比对和删除这两个操作也必须是原子的不能分两条命令否则中间又会被别的线程插进来。所以生产上推荐用Lua脚本像下面这样if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段脚本的意思是锁的value必须等于我传入的标识比如UUID才允许删除不相等说明锁已经换了主人我无权删除直接放弃。整段逻辑在Redis服务端原子执行不会被打断。我用Jedis写过对应的Java代码核心是这个样子public boolean unlock(String lockKey, String requestId) { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Object result jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return 1L.equals(result); }除了误删问题还有一个非常现实的问题业务执行时间超过锁的过期时间怎么办。我见过不少团队直接把过期时间设成10秒、30秒觉得“业务肯定跑不完”结果赶上GC停顿、慢SQL、或者一次网络超时重试锁就提前失效了。真正面向生产的方案主要有两条路线。一条是使用Redisson这类客户端库它内置了看门狗机制默认锁的leaseTime是30秒只要你没主动释放Redisson会每隔10秒自动续期一次把过期时间重置回30秒。这个机制照顾了大多数业务场景但也不是银弹——如果Redis主节点挂了锁数据还没来得及同步到从节点就会产生短暂的双花风险极端金融场景要用RedLock算法不过这个方案因为实现复杂、争议也大中小项目我的建议是别轻易上。另一条是自己做续期适合锁粒度极细、量级很大的场景。我写过一版基于Guava的ScheduledExecutorService续期实现加锁成功后启动一个定时任务每隔过期时间的三分之一检查一次如果当前线程还持有锁就执行PEXPIRE续期释放锁时取消定时任务。这里要给一句忠告自己写续期逻辑一定要做好线程标识与锁状态的绑定否则特别容易把别人的锁续上命。还有一个容易被忽略的维度是锁的粒度。我见过有人用一个全局锁保护所有SKU的库存扣减并发直接退化成串行TPS从几千掉到几百。正确思路是按业务维度拆锁——库存锁按sku维度加锁用户积分锁按userId维度加锁而不是一把锁锁住全库。2. 缓存治理三板斧穿透、击穿、雪崩一次说透Redis做缓存最怕的不是Redis这层挂掉而是缓存失效的那一瞬间流量全部涌到底层数据库。我在做缓存治理时总结了三类问题名字都差不多但成因完全不同处理手段也不一样缓存穿透、缓存击穿、缓存雪崩。缓存穿透是指查询一个根本不存在的数据。缓存里没有数据库里也没有每个请求都绕过了Redis直接打到MySQL。这种问题多半来自恶意攻击或者程序bug——比如有人拿不存在的订单号批量刷接口存储层每天要抗几百万次废查询。最常见的兜底方案是缓存空值查不到数据时在缓存里写一个空对象设置较短的过期时间比如60秒。这样做的好处是简单直接几分钟就能上线缺点是会给Redis塞进大量无用Key如果攻击者每次构造不同的ID空值缓存会变成新的内存杀手。所以我在生产项目里换用了布隆过滤器。布隆过滤器的原理可以理解为用一个巨大的位数组通过多个哈希函数来判断某个数据“一定不存在”还是“可能存在”用来在请求到达Redis之前就把不存在的ID挡住。像订单号、用户ID这类数据量百万级以内的场景布隆过滤器的误判率可以控制在千分之一左右内存开销也不大。如果你们团队成员少、技术储备有限我的建议是先做空值缓存并配合接口层的参数校验把明显非法的请求挡掉再逐步引入布隆过滤器。缓存击穿是指某个热点Key在过期的一瞬间有大量并发请求同时打到数据库。注意这里和穿透的区别这个Key在缓存里是有数据的只是刚好在某个时刻过期了。我处理过最典型的场景是首页推荐位商品某个爆款商品详情页的缓存配置是2小时凌晨12点缓存一过期正好赶上用户晚高峰数据库直接被打满。解决击穿问题常用的方案是“互斥锁”和“逻辑过期”两条路。互斥锁的思路是当发现缓存过期后先尝试获取一个重建锁抢到锁的线程查数据库并回填缓存其他线程在锁外等待然后再次读取缓存。这种做法能严格保证缓存重建只有一次但代价是如果回填数据库很慢排队线程的RT会明显上升。逻辑过期则是另一个极端缓存值里存的基本结构包含一个逻辑过期时间字段每次读取时判断这个字段发现过期后先返回旧数据同时异步发起一个后台线程去重建缓存。我做过的一个真实项目里首页Feed流就是用逻辑过期方案旧数据返回十几毫秒后台重建缓存用不到100毫秒用户完全无感知数据库的峰值压力还降了一多半。但要记住逻辑过期方案会引入短暂的数据不一致接口对数据准确性要求极高、比如库存和余额这种场景就别用了老老实实走互斥锁。缓存雪崩就更凶猛了大量Key在同一时间集体失效或者Redis实例本身宕机造成整体性的数据库冲击。一个很常见的诱因是缓存Key设置了相同的过期时间——比如所有商品都设为整点过期一到整点所有流量全部穿透到数据库。治标的手段是过期时间做随机化TTL 固定过期时间 随机数让Key在分布式时间轴上分散开我一般是加0到300秒的随机偏移量。治本的手段一是给Redis做高可用主从加哨兵避免单点二是在应用层加本地缓存用Caffeine或者Google Guava做一级缓存Redis做二级缓存热点数据先在本地挡一层。我屡试不爽的组合是低频冷数据直接走Redis高频热点数据用本地缓存扛两层缓存都设置不同TTL底层数据库的尖峰流量就这么被削平了。这里还要提一个和缓存治理强相关的细节序列化问题。Redis存取的数据往往会经过一层序列化如果你用的是JDK原生序列化存进去的是二进制对象在可视化工具里看是一堆乱码并且结构升级时容易出兼容问题。我后来统一改成了JSON序列化配合RedisTemplate自定义的GenericJackson2JsonRedisSerializer缓存可读性大幅提升排查问题时能直接看到Key里的业务含义。新手在做缓存治理时不妨先确认一下自己的Redis客户端用的是哪种序列化策略。3. 重启不丢数据RDB与AOF持久化方案选型实录聊完缓存再说一个线上故障里极其常见的痛点Redis重启之后数据丢了。表面上看起来是运维故障本质上是持久化方案一开始就没设计好。Redis的持久化分为RDB快照和AOF日志两种绝大多数团队都是默认配置跑着但默认配置并不是万能策略。RDB是内存快照把某一时刻的全部数据写入磁盘文件。它的执行方式是fork一个子进程由子进程完成数据落盘主进程继续处理请求。RDB的特点是恢复快文件紧凑几十G数据恢复也就几秒。问题在于快照的间隔决定了数据丢失的窗口默认配置下如果900秒内有1次写操作300秒内有10次写操作60秒内有10000次写操作才会触发一次快照。也就是说即使你配置得很激进RDB也不可能精确记录每一秒的写入。我经历过一次典型的RDB事故当时某个缓存节点没开AOF只靠默认的RDB配置顶着半夜服务器突然断电早上起来发现丢了近30分钟的数据。查了日志原因就是那半小时内的写入频率没达到save 60 10000的阈值所以一直没有生成新快照。从那以后凡是数据敏感的场景我再没敢只开RDB。AOF则完全不同它记录的是每个写操作命令本身类似MySQL的binlog。追加频率由appendfsync参数控制可选值有always、everysec、no。always表示每条写命令都立刻同步到磁盘最安全但吞吐量下降明显everysec表示每秒批量刷盘一次性能接近纯内存最多丢失最后一秒数据no则完全交给操作系统刷盘崩溃时可能丢好几秒数据。生产环境我最常用的组合是appendonly yes appendfsync everysec这个配置在性能和安全性之间取得了很好的平衡绝大多数业务都适用。AOF还有一个机制叫重写也就是当AOF文件膨胀得比较厉害时Redis会根据当前内存中的实际数据重新生成一份精简的AOF文件。我记得有个项目因为频繁更新同一个KeyAOF文件从200MB涨到8G后来配置了auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb重写机制激活后文件被压回了200MB左右。如果你发现AOF文件异常膨胀先查这两项配置。RDB和AOF可以同时开启Redis重启时会优先加载AOF文件还原数据因为AOF的数据完整性更高。Redis 4.0之后还有混合持久化模式AOF文件数据换成RDB二进制头部加上增量AOF日志尾部兼顾了RDB的紧凑恢复速度和AOF的细粒度记录。我在生产上验证了一段时间后最终采用的推荐配置是这样的appendonly yes appendfilename appendonly.aof appendfsync everysec no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes save 900 1 save 300 10 save 60 10000这个组合的意思是AOF保证秒级数据安全RDB负责快速恢复两者互不冲突。如果业务数据允许丢失较长时间比如纯缓存场景也可以只开RDB但至少要保证手工或脚本能定期触发BGSAVE别把命运全押在默认触发条件上。3.1 AOF文件损坏修复实操AOF在写盘过程中如果遭遇断电或磁盘故障文件可能损坏Redis启动时通常会拒绝加载。遇到这种情况别慌官方提供了修复工具在Redis安装目录的src下执行redis-check-aof --fix appendonly.aof它会扫描AOF文件定位并移除损坏的命令然后重启Redis加载修复后的文件。我实测下来修复后一般能恢复绝大部分数据但最后几条可能已经丢失这正是AOF everysec策略的固有边界。所以我一直强调备份意识不能丢重要节点要定时把RDB和AOF文件复制到独立存储。4. 从单机到高可用Docker部署主从与哨兵集群实录单机Redis到期再看怎么玩都有天花板一是单点故障没有容错二是读流量大了扛不住。我在多个项目里从单机演进到主从加哨兵的架构这个过程其实并不复杂但坑也不少。主从复制的核心逻辑简单说就是从节点向主节点发起同步请求主节点先做一次全量RDB快照发给从节点同时把生成快照期间的新写入命令缓存到repl_backlog缓冲区快照传完后把增量命令继续发给从节点最终两者数据对齐。后续主节点每收到一条写命令都会实时转发给从节点。这个过程自动完成不需要业务代码做任何特殊改造。实操层面的一个重点是控制复制延迟。Redis的复制是异步的主库写入成功后不会等从库同步完成就返回所以从库存在几十毫秒级别的滞后。如果业务要求强一致刚写入的数据立刻读取那就必须让这部分请求走主库不能压到从库上。我之前处理过一个查询订单详情的接口刚下单成功就跳转详情页结果从库还没同步到订单数据页面直接404排查到最后才发现是读写分离惹的祸。这个问题的解决思路就一句话区分场景关键链路读主库报表分析、低频查询才走从库。在Docker环境下搭建主从我贴一套实测过的命令和配置。先起一个主节点docker run -d --name redis-master -p 6379:6379 \ -v /data/redis-master/redis.conf:/etc/redis/redis.conf \ redis:7.0 redis-server /etc/redis/redis.conf再起一个从节点关键配置是replicaof指向主节点的宿主机IP和端口docker run -d --name redis-slave -p 6380:6379 \ -v /data/redis-slave/redis.conf:/etc/redis/redis.conf \ redis:7.0 redis-server /etc/redis/redis.conf从节点的redis.conf里加一行replicaof 172.16.1.10 6379配置完成后连上任意一个节点执行INFO replication能看到role:master或role:slave以及master_link_status:up就说明主从关系已经建立。这里我建议把repl-backlog-size调大一点默认的1MB太小了。如果从节点断开连接超过网络分区的时间backlog被覆盖主从之间又要做一次全量同步数据量大的时候会非常痛苦。我一般调到16MB起步。主从只能解决读扩展和半自动故障转移真正的自动故障切换要靠哨兵Sentinel。哨兵的作用是监控主节点状态发现主节点宕机后在从节点里选出一个升级为新主节点并把新主节点的地址通知给客户端。为了避免哨兵自己成为单点生产上至少要部署3个哨兵节点它们之间投票仲裁防止网络抖动导致误判。我这里以Docker Compose为例一个简单的哨兵配置长这样sentinel monitor mymaster 172.16.1.10 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000 sentinel parallel-syncs mymaster 1这里解释一下几个关键参数2代表至少需要2个哨兵同意主节点下线才会触发故障切换down-after-milliseconds是主观下线判断时间5秒内联系不上就算可疑parallel-syncs是故障切换后同时允许几个从节点同步新主节点设为1防止瞬间同步压力过大。客户端接入哨兵集群时连接的不再是Redis地址而是哨兵地址列表。像Jedis的SentinelPool、Spring Boot的哨兵配置本质上都是先向哨兵询问当前主节点地址然后建立连接。如果主节点发生切换客户端会自动重新获取新主节点地址这个过程对外部调用方完全透明。再往上就是Redis Cluster集群模式了。Cluster用哈希槽分配数据一共16384个slot每个主节点负责一部分slot。它的好处是数据自动分片单机内存瓶颈通过水平扩展解决。但多Key操作受限mget这类跨slot查询需要用hash tag让相关Key分布到同一个slot。我在项目里从单机迁移到Cluster之后踩过一个特别实在的坑有个定时任务用scan遍历所有Key做批量清理结果每次都只遍历到一部分slot的Key最后改成遍历每个节点、每个slot才搞定。Cluster适合数据量大到单机放不下的场景如果只是读写压力大哨兵加上从库扩容已经能覆盖绝大多数业务。5. 线上Redis故障排查速查表这节是我自己的运维手记每次线上Redis报警我基本都按下面这张表来排查。整理成表格方便你直接抄作业现象、核心排查命令、处理建议三列配合我常用的操作命令一起看。故障现象核心排查方法处理建议客户端连接不上Redis检查bind配置、protected-mode、requirepass是否匹配生产环境关闭protected-modebind内网网段客户端配置正确密码连接被频繁重置/断开查看服务端日志CLIENT LIST看连接数检查timeout配置调大timeout或配置0不超时客户端连接池合理设置maxTotal和maxIdle内存飙升到maxmemoryINFO memory观察used_memory和峰值CONFIG GET maxmemory-policy设置allkeys-lru淘汰策略定期用redis-cli --bigkeys排查大Key响应耗时出现尖刺SLOWLOG GET 10查看慢命令结合bigkeys找大Key拆分大Key、换更高效数据结构避免KEYS和hgetall全量操作持久化导致主线程阻塞INFO stats检查latest_fork_usec看RDB生成耗时调低save频率或者把持久化放到从节点避免主节点fork开销过大缓存与数据库不一致对比业务日志排查缓存更新顺序采用双删加消息队列补偿或订阅数据库binlog异步刷新缓存AOF文件损坏无法启动redis-check-aof --fix修复后再加载修复后立即备份并评估丢失数据范围这里我特别想单独强调一下大Key问题它几乎能引发上面一半的故障内存不均衡、慢查询、持久化阻塞、集群迁移超时。我习惯用redis-cli自带的bigkeys扫描redis-cli --bigkeys它会按Key类型扫描并输出占用空间最大的Key。string类型超过10KB、list或者set元素超过几千个就应该考虑拆分了。比如把大list拆成多个小list或者把大string换成hash结构分拆之后内存占用和响应时间都有明显改善。另外很多刚接触Redis的同事会问我能不能用可视化工具做排查。我自己的习惯是GUI工具负责“看”命令行负责“操作”。Redis Insight、Another Redis Desktop Manager这类工具用来观察Key结构、内存分布、实时监控都很方便但真要执行复杂Lua脚本、分析慢日志或者处理大Key还是redis-cli更顺手。可视化和命令行并不冲突合理搭配效率最高。再补一个问题排查时经常忽略的点慢日志的阈值设置。Redis默认的slowlog-log-slower-than是10000微秒也就是10毫秒以上的命令才会被记录这个阈值太粗了没法发现那些5毫秒到10毫秒之间的隐性问题。我一般调到5000微秒并用SLOWLOG GET配合业务低谷期分析能找到一个时间段内的命令热点。排查中断言一句Redis这个系统性能强大但也特别诚实问题几乎都能从INFO、SLOWLOG、MONITOR这些命令里找到蛛丝马迹。排查靠数据不要靠猜。我自己的体会是真正把Redis用好的人未必记得住所有命令的参数但他一定理解每个机制背后的权衡分布式锁要在可用性和一致性之间权衡缓存治理要在性能和数据准确性之间权衡持久化要在性能和恢复速度之间权衡高可用方案要在成本和复杂度之间权衡。这些权衡下来系统的稳定性就慢慢建立起来了。如果你正在搭建或者优化自己的Redis生产环境建议先从主从加哨兵和持久化配置入手这两项是性价比最高的改造分布式锁和缓存治理则要在写代码时反复审查边界场景千万别让一行看似无害的SETNX毁掉一个晚上。
返回列表