ARTICLE DETAIL

资讯详情

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

Redis分布式锁原理与实践:从单机到集群演进

Redis分布式锁原理与实践:从单机到集群演进 1. Redis分布式锁的演进背景第一次接触分布式锁是在2016年处理电商秒杀系统时遇到的。当时我们的PHP单体应用刚拆分为微服务架构多个订单服务实例同时操作同一个库存时出现了超卖问题。最初尝试用MySQL行锁解决但在高并发下性能急剧下降TPS从2000跌到不足300。这时团队中的架构师老张提出了使用Redis实现分布式锁的方案。单机版Redis锁的实现出奇简单 - 一个SETNX命令加过期时间就能搞定。但当我们把应用部署到三台服务器上测试时立即发现了新问题主从切换会导致锁失效。记得那天凌晨三点我们模拟网络分区故障时两个客户端同时拿到了锁导致库存数据出现严重不一致。这次事故让我深刻认识到分布式环境下没有银弹每种方案都有其适用场景和边界条件。2. 单机Redis锁的实现原理2.1 基础命令组合最基础的Redis锁实现只需要三个命令SET lock_key random_value NX PX 30000这个原子操作实现了当lock_key不存在(NX)时设置键值并赋予30秒的过期时间(PX)。random_value必须是全局唯一字符串通常用UUID或客户端ID时间戳组合。解锁时需要Lua脚本保证原子性if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end2.2 关键参数调优在实际项目中这几个参数需要特别注意过期时间建议设置为业务平均耗时的3倍。我们电商系统经过压测最终设定为普通商品500ms业务耗时 → 1500ms锁过期秒杀商品300ms业务耗时 → 1000ms锁过期重试策略采用指数退避算法初始间隔50ms最大间隔500ms锁标识使用业务前缀:资源ID的格式如seckill:sku_123456重要提示绝对不要使用固定的随机值我曾见过有团队使用1作为value结果在锁超时后新请求的1误删了其他客户端的锁。2.3 单机方案的局限性在金融级系统中我们发现单机方案存在三大致命缺陷主从切换的锁丢失异步复制导致新主节点可能缺失部分锁时钟漂移问题如果客户端时钟比Redis快可能提前释放锁单点故障Redis实例崩溃会导致所有锁不可用去年处理的一个P0级故障就是因此引发的 - 机房网络抖动触发主从切换导致支付系统的双重扣款。这促使我们开始研究集群环境下的可靠方案。3. 集群环境下的Redlock算法3.1 Redlock核心思想Redis官方推荐的Redlock算法基于以下设计部署5个独立的Redis主节点物理隔离客户端获取当前毫秒级时间戳T1依次向所有节点发送SETNX命令包含相同的随机值和过期时间当获得多数节点≥3的认可后记录获取锁耗时T2有效时间 原始过期时间 - (T2-T1)解锁时需要向所有节点发送删除请求即使某些节点获取锁失败也要尝试解锁。3.2 生产环境实现要点在我们的K8s集群中Redlock实现有几个关键配置redisson: lock: # 锁最长持有时间业务必须在此时间内完成 leaseTime: 30000 # 等待锁的最长时间 waitTime: 10000 # Redis节点地址跨可用区部署 nodeAddresses: - redis://node1:6379 - redis://node2:6379 - redis://node3:6379 - redis://node4:6379 - redis://node5:63793.3 争议与改进Martin Kleppmann曾指出Redlock依赖系统时钟的缺陷对此我们的解决方案是使用NTP服务保证时钟同步偏差5ms增加锁令牌的版本号机制关键业务配合数据库乐观锁在最近的双十一大促中改进后的Redlock成功支撑了每秒12万笔的订单创建故障率0.001%。4. 分布式锁的进阶实践4.1 锁的可重入设计对于需要递归调用的场景我们在Redis hash结构中记录线程标识和重入计数local key KEYS[1] local threadId ARGV[1] local expire ARGV[2] if (redis.call(exists, key) 0) then redis.call(hset, key, threadId, 1) redis.call(expire, key, expire) return 1 end if (redis.call(hexists, key, threadId) 1) then redis.call(hincrby, key, threadId, 1) redis.call(expire, key, expire) return 1 end return 04.2 锁续期机制对于执行时间不确定的长任务我们实现了看门狗线程自动续期private void renewExpiration() { ExpirationEntry ee EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee ! null) { Timeout task commandExecutor.getConnectionManager() .newTimeout(new TimerTask() { Override public void run(Timeout timeout) { // 异步续期逻辑 if (redis.call(pexpire, KEYS[1], ARGV[1]) 1) { renewExpiration(); } } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); } }4.3 故障处理手册根据三年来的运维经验我整理了这些常见问题的应对策略故障现象可能原因解决方案解锁失败但业务已完成网络分区增加锁的租约时间设置合理的TTL多客户端同时获取锁时钟漂移启用NTP服务采用物理时钟逻辑时钟锁释放后仍被阻塞GC停顿监控JVM状态优化垃圾回收策略Redis节点宕机硬件故障部署Redis哨兵集群设置适当的down-after-milliseconds5. 新一代分布式锁方案探索5.1 Redis作者的新思路Antirez最近提出的Disque Lock方案有几个创新点采用Quorum协议保证一致性引入锁的版本号机制客户端心跳保持活性我们在测试环境验证时发现该方案在跨地域部署场景下性能比Redlock提升40%。5.2 与ZooKeeper的对比在金融风控系统中我们针对不同场景采用混合方案维度Redis锁ZooKeeper锁性能10w QPS1w QPS一致性最终一致强一致适用场景短时高频操作低频关键事务容错能力依赖多数存活依赖Leader存活5.3 云原生时代的最佳实践在K8s环境中我们总结出这些经验使用StatefulSet部署Redis集群保证稳定的网络标识通过Pod反亲和性分散节点分布配置HPA自动扩展Redis代理层使用Service Mesh实现细粒度的流量控制去年某次线上事故中正是这种架构让我们在单个AZ故障时仍能保持锁服务的可用性。
返回列表