ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

分布式锁反模式避坑指南:场景判断与Redis最佳实践

分布式锁反模式避坑指南:场景判断与Redis最佳实践 1. 看清分布式锁的底牌它到底解决什么问题分布式锁这玩意儿后端工程师基本都耳熟能详Redis、ZooKeeper、etcd各有拥趸面试题里也常驻热搜。但说实话我在一线见过太多“拿着锁当万能药”的项目——系统出现并发问题不管三七二十一先加一把分布式锁结果锁越加越多性能越来越差该出的bug一个没少甚至还多了几个新坑。先把概念捋清楚分布式锁本质上是跨进程互斥原语目标是让多个应用实例不同进程、不同机器在访问同一份共享资源时只有一把“钥匙”能被同时持有。它解决的是分布式环境下“多节点协作时的竞争问题”。但这里有个容易被忽略的前提分布式锁保护的资源往往是数据库记录、缓存数据、文件句柄这类跨节点可见的共享状态。如果只是单进程内的并发JVM的synchronized、ReentrantLock就能解决完全没必要把问题复杂化。我从实际项目里总结分布式锁真正适用的场景非常有限集中在几类定时任务防重多台机器同时跑定时任务不能让同一批数据被处理两次库存扣减和秒杀多个节点同时操作同一商品的库存分布式ID生成需要全局唯一且在并发下不冲突多节点共享资源的临时独占例如某台机器抢占“主节点”角色执行只有单节点能做的任务而抢红包、防止表单重复提交、异步消息幂等处理这类场景其实用幂等设计、数据库唯一索引、乐观锁版本号就能解决根本用不着分布式锁。用了反而把这些简单问题搞复杂。我把话说直白一点错误的分布式锁比没有锁更危险。没有锁系统最多是并发异常加了错误的锁系统在正常流量下看起来稳定一旦某个节点卡顿或宕机可能出现死锁、锁丢失、资源永远被占用的“定时炸弹”。所以这篇文章我不想讲那些教科书式的概念而是把自己实际踩过的坑、总结出的反模式、以及最终沉淀下来的一套工程化实践一次性讲透。既给准备面试的人提供一份能“说深一层”的素材也给正在做后端开发的朋友一份可落地、可直接抄作业的避坑指南。2. 这些场景其实根本不需要分布式锁在聊反模式之前先帮你省掉一半的错误决策。很多团队把分布式锁当成并发问题的“默认答案”但在我经手的项目里至少有一半所谓的“需要加锁”场景用更轻量的手段就能优雅解决。2.1 幂等设计才是防重复的王道先说最典型的接口幂等。比如支付回调、消息推送、表单提交这些场景的并发特点是“同一请求可能被重复发送”但业务本质要求“同一个请求只能生效一次”。这时候最自然的方案不是加分布式锁而是给请求一个唯一业务ID在数据库层面做幂等约束数据库唯一索引比如订单号、支付流水号插入重复数据时直接报错由应用捕获后返回“已处理”状态机校验先查订单状态只有“待支付”才能更新为“已支付”用一条带条件的UPDATE语句实现原子切换UPDATE orders SET status PAID WHERE order_no xxx AND status PENDING这条SQL本身就是一把“数据库级别的锁”UPDATE操作会对命中行加行锁多个并发请求同时执行时只有一个能成功其余返回影响行数为0应用侧根据这个结果判断“谁赢了”。这种方案实现简单、没有额外组件依赖、性能也不差在绝大多数场景下比分布式锁更可靠。2.2 乐观锁处理大部分并发更新如果业务场景是“读-改-写”且并发冲突概率没那么高乐观锁是性价比最高的选择。实现方式是在表里加一个version字段或update_time更新时带上版本号UPDATE inventory SET count count - 1, version version 1 WHERE sku_id xxx AND version #{oldVersion}如果能命中更新说明这个版本没有冲突如果影响行数为0说明版本已被别人改掉程序再根据策略重试或返回失败。乐观锁处理的是“提交时校验冲突”悲观锁分布式锁处理的是“操作前就阻塞等待”前者的吞吐量远高于后者因为没有任何等待和阻塞开销。只有在并发冲突概率很高比如爆款商品秒杀、且单次操作事务很短时悲观锁思路才更合适。2.3 什么时候才真的需要分布式锁聊完“不需要”的场景再说说什么情况下分布式锁是必需的我判断的标准有三条必须同时满足多节点同时操作同一份共享资源数据库行锁/唯一索引都hold不住临界区操作时长跨度大不是一个简单UPDATE能搞定的可能包含多个步骤的读-判-写逻辑业务要求严格的互斥语义不允许两个节点同时进入临界区举个例子一个订单超时自动关单的任务每个节点收到MQ消息后要检查订单状态、关闭库存、记录日志不是一个数据库操作能解决的需要保证同一时间只有一个节点在处理同一个订单。这时候分布式锁就是标配。记住这个判断逻辑后面很多反模式其实都是因为把“不需要锁”的场景硬上了锁结果平白引入了复杂度和故障点。3. 盘点那些年我们踩过的分布式锁反模式接下来进入主题。我按照实际项目中见过的高频事故盘点7种典型的分布式锁反模式。每条都会讲清楚这种现象长什么样、为什么错、根因在哪。3.1 反模式一用setnxexpire两条命令实现加锁这是“教科书教出来的最低级错误”但至今仍然大量存在。很多人一开始学Redis分布式锁写出来的是这样的代码// 错误示例分两步操作 Boolean lockResult redisTemplate.opsForValue().setIfAbsent(lock:order:123, 1); if (Boolean.TRUE.equals(lockResult)) { redisTemplate.expire(lock:order:123, 30, TimeUnit.SECONDS); // 业务逻辑... }问题在于setIfAbsent和expire是两条独立的Redis命令如果第一条执行成功、应用进程突然宕机或网络闪断第二条expire根本没执行这把锁就永远存在Redis里所有后续请求全部阻塞直接造成线上死锁事故。正确做法是Redis官方从2.6.12版本开始支持的原子操作SET lock_key unique_value NX PX 30000一句话同时设置“存在则失败”的占位语义和过期时间中间不会给故障留任何缝隙。所以我在代码评审时看到setIfAbsent后面跟一行expire直接打回没有商量余地。3.2 反模式二把锁当成互斥量临界区里跑长任务第二种非常常见而且隐蔽性极强。团队里有人把分布式锁当成synchronized用拿到锁之后把整个业务逻辑全包进去包括调用外部接口、执行复杂计算、甚至等待人工审核结果。锁的过期时间设了30秒但业务逻辑可能跑5分钟。结果就是第一个请求还在临界区里跑锁已经因为超时自动过期了第二个请求顺利拿到锁进入临界区两个线程同时操作共享数据分布式锁的保护效果完全失效。这里很多人困惑那过期时间设置得很长不就行了比如设1小时。但凡是设置固定过期时间就存在一个永恒的矛盾过期时间太短长任务还没跑完锁就没了保护失效过期时间太长持有锁的节点宕机后其他节点要等很久才能拿到锁系统可用性被拖垮后来我在实践里总结出两个解法后面第四章会详细展开这里先说结论把锁的粒度缩小锁内只做必要的最小操作耗时的外部调用挪到锁外引入“看门狗续期”机制让锁的有效期跟随业务执行时间动态延长业务没结束锁不释放业务结束了主动释放3.3 反模式三解锁直接DEL根本不校验是不是自己的锁这个坑我印象太深了。有一次线上事故就是两个线程因为锁被误删同时进入了同一个订单的退款处理逻辑导致用户被退了两次款。当时代码是这么写的// 错误示例拿到锁跑完业务直接删锁 try { Boolean lockResult redisTemplate.opsForValue().setIfAbsent(lock:refund:10001, 1, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(lockResult)) { // 退款逻辑耗时较长 doRefund(); } } finally { redisTemplate.delete(lock:refund:10001); }问题出在如果线程A的执行时间超过了锁的过期时间锁已经自动释放线程B拿到了锁并开始执行。这时线程A执行完毕finally中执行delete把线程B持有的锁删掉了。接着线程C也能拿锁进入三个线程同时在跑退款逻辑。解法是加锁时给value设置一个唯一标识比如UUID或者请求ID解锁时先比较value是否一致一致才删除而且要保证“比较删除”是一个原子操作。用Lua脚本实现if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end3.4 反模式四用单机Redis做主从架构主节点宕机锁直接丢失这个问题是分布式锁面试中最高频的追问点。很多团队基于上面的正确姿势用Redis做了分布式锁结构是主从模式。正常情况没问题但一旦发生主节点故障切换锁的可靠性就崩塌了。场景重现线程A在Master节点上成功写入了锁SET NX PXMaster节点还没来得及把这条数据同步到Slave节点就宕机了。哨兵机制触发主从切换Slave升级为新Master但锁数据已经丢了。此时线程B也能在新Master上成功加锁两个线程同时进入临界区。这个问题的本质是Redis的主从复制是异步的锁的持久化和同步无法保证100%。Redis官方后来给出的Redlock算法在理论层面试图解决这个问题向多个独立的Redis节点同时加锁超过半数成功才算加锁成功。但Redlock依然存在争议包括Martin Kleppmann和Salvatore Sanfilippo之间的著名论战——涉及时钟跳跃、GC停顿、网络分区场景下Redlock依然可能失效。我的个人建议是使用场景允许极小概率的锁失效比如缓存重建、非核心数据保护——用单机Redis 合理的过期时间性价比最高场景要求强一致资金操作、订单状态、库存一致性——不要硬上Redis考虑ZooKeeper或etcd如果必须用Redis至少要开启AOF持久化且刷盘策略设置为everysec甚至always降低锁数据丢失的概率同时接受极端情况下的理论风险3.5 反模式五把synchronized的“大粒度思想”平移过来单机时代用synchronized大家习惯性地把整个方法甚至整个类锁住大了也没什么感觉反正JVM够快。到了分布式锁这种“大而全”的思路继续平移于是锁的粒度从“一行数据”膨胀到“一个订单”“一个用户”“一张表”。后果是显而易见的锁的粒度越大并发能力越低。比如一个用户同时下了多个订单如果按“用户ID”维度加锁这个用户的所有订单操作都会被串行化——下单、退款、查询、修改排成一条队明明它们之间毫无关系白白牺牲了大量吞吐。正确做法是锁的粒度必须匹配业务实际需要保护的资源范围扣减商品库存锁的维度应该是sku_id而不是product_id一个商品下有多个SKU互相不影响用户余额变更锁的维度是account_id并且要区分“收入”和“支出”操作不要共用一把锁订单状态流转锁的维度是order_id不要让不同订单之间互相阻塞如果锁的粒度设计对了很多场景根本不需要“分布式”这层复杂度——热点分散了并发冲突自然就少了。3.6 反模式六锁内调用外部慢接口持锁时间完全失控有一次性能排查我发现某个接口的P99从50ms涨到了2秒查来查去问题出在团队新加的一把分布式锁上。拿到锁的线程在临界区里面调了一个外部供应链接口这个接口平时50ms返回偶尔能卡10秒。所有其他请求全在后面排队等这把锁整个接口的响应时间完全被外部依赖拖垮。这个反模式的本质是持锁时间被不可控因素主导。加锁的初衷是保护共享资源但锁内的外部调用让持锁时间取决于“别人的网络状况”“别人的机器负载”这是工程设计上的重大缺陷。我的处理原则是加锁临界区只保留本地资源和数据操作任何外部IO调用远程接口、发MQ消息、访问外部服务放到锁外如果临界区实在绕不开外部依赖至少要设置调用超时和熔断机制避免外部故障时锁无限期持有设计时问自己一个问题如果真的进不了锁我的业务代码能正常降级吗这个问题的答案决定了系统能扛多大的极端压力3.7 反模式七只锁代码不锁数据锁形同虚设这是最隐蔽、也最让人头秃的一种反模式。代码层面加锁加得密不透风但数据层还是出了问题。排查下来发现多个节点确实只有一个能执行某段代码但这段代码操作的数据对象却可能来自多个存储位置。举个例子系统在Redis中缓存了一份配置同时MySQL里也有一份配置表。更新配置时节点A持锁更新了Redis缓存但MySQL的事务还没提交节点B读到的MySQL还是旧数据于是把旧数据同步回了Redis——逻辑上只有A进了临界区但数据却被B覆盖了。这类问题的根因是锁保护的“资源边界”和数据的“一致性边界”没有对齐。锁只保护了代码路径却没有覆盖所有可能读写同一份数据的路径——后台任务、消息消费者、管理后台直接改库都可能是漏网之鱼。真正让锁发挥作用的唯一前提是所有需要访问这份共享资源的路径都必须经过同一把锁。任何一条旁路都会让锁的保护形同虚设。4. 工程化最佳实践Redis分布式锁的正确姿势反模式聊完了现在给出我在项目中最常使用、也是沉淀最久的一套Redis分布式锁实践。不敢说是银弹但经过多个项目的验证在“高吞吐极小概率失效可容忍”的场景下它足够可靠。4.1 锁选型Redis、ZooKeeper还是etcd分布式锁不只是Redis一个选择。做工程设计第一步永远是选型我基于实际经验整理了一张对比表选型一致性保证吞吐量实现复杂度适用场景Redis单机极弱主从切换可能丢锁极高低缓存重建、非核心数据防护、允许极小概率失效Redis Redlock理论强有争议高中需要多节点容灾但能接受理论争议的团队ZooKeeper强顺序节点临时节点中中高强一致场景分布式协调可靠性优先etcd强Raft协议保障中中云原生环境Kubernetes生态一致数据库行锁强事务保证低低低频操作、中小规模团队、不想引入新组件选型决策的实操经验是先问业务对一致性的容忍度。如果锁失效的时间窗口能接受比如缓存被重复重建、日志重复写入无脑选Redis单机性能最好、代码最简单。如果锁失效会造成资金损失或严重的数据不一致老老实实用ZooKeeper或etcd不要在Redis上硬撑。如果项目规模不大、团队不想维护新组件一张数据库表加唯一约束也能撑起很多低频互斥场景。4.2 标准加锁范式一次SET搞定不留给故障缝隙加锁的正确姿势上面已经提到过这里再细化。核心命令SET lock_key lock_value NX PX expire_time参数拆解lock_key要保护的资源标识比如lock:stock:sku_10001lock_value唯一标识使用UUID或“请求ID 线程ID”拼接保证每个线程加锁时value都不同NX只有当key不存在时才能设置成功实现互斥PX设置过期时间单位毫秒防止持有者宕机导致死锁Java侧代码参考String lockKey lock:order: orderId; String lockValue UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofMillis(lockExpireMillis)); if (Boolean.TRUE.equals(locked)) { try { // 临界区业务代码 doBiz(); } finally { releaseLock(lockKey, lockValue); } }过期时间怎么定我的经验是取该业务逻辑正常情况下最坏执行时间的3~5倍。如果一个订单处理正常100ms、极端情况下200ms过期时间设1秒足够如果业务涉及复杂的计算链路至少设置5秒。设太短会让长尾请求提前丢锁设太长则会让故障恢复变慢。这个参数没有绝对标准要在线上压测后微调。4.3 标准解锁范式Lua脚本保证原子性解锁比加锁更容易出错核心在于“校验value 删除key”必须作为一个原子操作否则就会踩到反模式三的坑。我常用的Lua脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endJava侧的调用示例private static final String UNLOCK_SCRIPT if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; public void releaseLock(String lockKey, String lockValue) { DefaultRedisScriptLong script new DefaultRedisScript(UNLOCK_SCRIPT, Long.class); Long result redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue); // result为1表示删除成功0表示锁已被他人持有或已过期不处理 }这里有个很多人忽略的细节解锁时如果返回0说明锁已经不属于当前线程这时候不能继续执行业务的“补偿逻辑”。比如有的团队在解锁失败后马上重试反而可能把别人的锁删掉。解锁失败就让它失败业务靠幂等和后续状态校验来兜底。4.4 看门狗续期机制让锁活到业务真正结束前面提到过期时间的矛盾终极解法是动态续期。思路类似Elasticsearch的“租约”机制守护线程在业务没执行完时定期给锁“续命”业务结束后主动释放如果持有者宕机守护线程也随之消失锁到期后自然释放。实现方案通常有两种第一种自己实现续期线程池。加锁成功后启动一个定时任务每隔过期时间的三分之一周期检查一次业务是否还在执行如果还在执行就调用EXPIRE命令延长锁的存活时间。ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); ScheduledFuture? renewTask scheduler.scheduleAtFixedRate(() - { try { // 用Lua脚本原子地比较value并续期 String renewScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(pexpire, KEYS[1], ARGV[2]) else return 0 end; redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue, String.valueOf(lockExpireMillis)); } catch (Exception e) { log.error(renew lock error, e); } }, initialDelay, renewInterval, TimeUnit.MILLISECONDS);解锁时记得把续期任务取消否则锁释放后守护线程还在跑那就是内存泄漏和游荡的幽灵线程。第二种引入Redisson作为客户端。Redisson的RLock内置了看门狗机制默认锁过期时间30秒看门狗每隔10秒自动续期业务执行完自动释放。这个方案非常适合不想自己造轮子的团队。RLock lock redissonClient.getLock(lock:order: orderId); lock.lock(30, TimeUnit.SECONDS); try { doBiz(); } finally { lock.unlock(); }我在实际项目中两种方案都用过如果是新立项直接选Redisson少踩很多轮子造到一半才发现问题的坑如果是存量项目不想引入Redisson依赖再自己实现一个精简版看门狗。4.5 加锁失败后的重试策略别做无脑自旋业务拿不到锁时如果直接返回失败用户体验很差但无脑自旋会造成大量的无效请求打到Redis热点锁的Key甚至可能被打成热点。我采用的重试策略是“指数退避 最大次数限制”。int maxRetryTimes 3; int baseSleepMillis 100; for (int i 0; i maxRetryTimes; i) { Boolean locked tryLock(lockKey, lockValue, lockExpireMillis); if (Boolean.TRUE.equals(locked)) { return; } // 指数退避100ms, 200ms, 400ms Thread.sleep((long) (baseSleepMillis * Math.pow(2, i))); } throw new BizException(获取分布式锁失败请稍后重试);这样既避免了立即自旋对Redis的压力又给了业务合理的等待时间。另外强调一点不要用“队列线程池阻塞”的方式等锁那会把问题从Redis转移到应用线程把无界队列作为“分布式锁的等待区”是另一种隐蔽反模式。4.6 锁超时与业务执行时间的关系铁律自查最后给一套我在代码评审时用的自查标准和铁律铁律一加锁请求必须带过期时间禁止使用永不过期的锁除了极少数人工介入的运维场景铁律二解锁前必须校验value且校验删除必须原子Lua脚本或Redisson封装铁律三锁的过期时间必须覆盖该临界区的最坏执行时间拿不准时用看门狗动态续期而不是盲目调大固定值铁律四临界区内禁止调用外部慢接口、禁止执行大事务、禁止在线程池里继续持有锁等待异步结果铁律五所有访问共享资源的代码路径必须经过同一把锁只要有一条旁路锁的保护就算作废这套自查清单在团队内部沿用已久基本能挡住90%以上的分布式锁低级错误。5. 锁的演进与扩展可重入、信号量与锁粒度优化解决了基础正确性之后分布式锁还涉及到一些更高阶的使用姿势这里一并讲透。5.1 分布式可重入锁同一线程重复获取也能通过单机场景下的ReentrantLock天然支持可重入同一个线程可以多次加锁而不死锁。分布式锁默认是不支持可重入的——同一个线程拿了一次锁再取一次会因为NX失败而阻塞如果是递归调用或者嵌套AOP切面直接死锁。实现可重入的常见思路是本地计数 Redis持久化标记线程第一次成功加锁后把锁的持有标识记录在ThreadLocal里同时Redis中的value存储为一个复合字符串uniqueId 重入次数同一线程再次加锁时先从ThreadLocal判断是否持有锁如果是不必再调Redis加锁直接本地重入次数1解锁时同理本地重入次数先减1减到0才真正清理Redis中的key这个思路本质上把锁的“重入判定”放在本地只有第一次加锁和最后一次解锁才会真实访问Redis。划分清楚本地活跃线程与分布式锁的关系性能不会差。Redisson的RLock也已经内置了可重入能力内部同样通过ThreadLocal记录重入次数所以如果你使用Redisson这块不用重复造轮子。5.2 锁的公平性与非公平性业务是否需要排队Redis分布式锁天然是非公平的两个线程同时抢锁谁先到达Redis谁先成功但无法保证“等待时间最长的线程优先获得锁”。大部分业务场景比如秒杀、扣库存不需要公平性因为排队和不排队的最终效果都是串行处理只是顺序问题。但如果业务对公平性有要求比如订单按时间顺序履约、群众业务中先到先处理就需要注意Redis的普通SET NX方案无法保证公平性需要借助队列结构比如ZSet按时间戳排序才能实现“FIFO抢锁”ZooKeeper的临时顺序节点天然支持公平锁每个请求在锁节点下创建一个顺序临时节点只有序号最小的节点能获得锁其他节点监听前一个节点的删除事件所以在面试中被问到“公平锁”时能讲清Redis与ZooKeeper在公平性上的差异基本就是加分项。5.3 锁粒度优化从一把大锁拆成N把小锁性能优化中性价比最高的操作之一就是对锁做“降维打击”。举一个真实场景某个积分系统一开始按照“用户ID 积分动作类型”加锁所有入账、消费、过期回收操作之间都是互斥的。结果某次大促活动几十万用户同一时间触发积分入账单个用户的积分锁虽然互相独立但每个用户的操作链条却因为串行等待而变得很慢。优化以后把锁的粒度细分到“用户ID 积分账本ID 操作类型”消费和入账使用不同的锁积分入账根本不需要锁——因为它天然是“追加”语义不会产生覆盖问题。再举一个经典场景热点商品库存扣减。如果一把锁锁住整个商品所有SKU的销量都被串行化并发量上不去。拆成“品类锁”“SKU锁”“库存流水锁”之后每个SKU独立加锁并发能力直接翻了几倍。锁粒度拆分的核心原则是寻找数据关联度最低的维度作为锁的边界让互不干扰的数据完全并行只在真正共享的数据上互斥。5.4 持有锁的降级策略Redis不可用怎么办分布式锁不是可以“死扛”的Redis集群抖动、网络分区时锁服务自身也会故障。这时候系统不能跟着一起挂掉需要降级方案。我在生产环境中实践过的降级策略分三个层级第一级Redis访问异常时快速失败。获取锁时如果Redis返回错误或者超时不要死等重试直接抛出异常或者返回“系统繁忙”由上层业务决定是重试还是降级第二级本地乐观锁兜底。即使没有拿到分布式锁业务进入数据库操作时依然执行UPDATE WHERE status ?这类条件更新依靠数据库行锁和乐观锁防止真正的数据错乱第三级人工补偿通道。对于极少数极端情况Redis完全不可用并且业务被迫继续运行保留一个手工后台“清理锁”“重置状态”的操作入口宁可人工介入也不能让业务死锁死等其实这套降级思路的本质是把分布式锁当作一种性能优化手段而不是唯一的数据安全屏障。数据最终的一致性来源应该是数据库事务和业务幂等设计分布式锁只是降低了冲突概率、减少了无谓的数据库竞争。6. 写在最后三个问题判断你的锁写得对不对回头再看看分布式锁这件事我觉得最核心的不是掌握某个开源组件的API而是建立一套“会不会用锁”的判断力。给你一个自己评估代码的三步自查法第一步删掉锁业务会不会出一致性问题如果不会这锁就不该加如果会看看能不能用唯一索引、幂等表、乐观锁解决能就不用加分布式锁只有这些都不可用才轮到分布式锁上场。第二步我的锁能容忍锁失效吗如果锁失效会导致资金错误、数据覆写这类事故那就要考虑用ZooKeeper或etcd这类强一致组件如果只是重复执行一个幂等任务Redis锁足够并且要把过期时间、续期、重试这些参数设计到位。第三步锁能保护到所有访问路径吗这是我在长期维护系统后体会最深的一点——代码里最快腐烂的就是这把锁的“使用范围”。今天你只在一个Service方法里加了锁明天同事在另一个Listener里也改了同一份状态后天后台管理脚本悄悄上线锁的保护范围就在不知不觉中千疮百孔。所以设计锁的时候要多花10分钟把所有读写共享资源的路径全部列一遍确保每一条都走同一把锁。我个人在实际操作中的体会是分布式锁这块80%的精力应该花在“如何不加锁”和“如何让锁的失效影响最小”上只有20%的精力花在“如何正确加锁”上。想通了这一点很多所谓的经典难题其实就会迎刃而解。如果你正在上手分布式锁建议从最简单的Redis SET NX开始先跑通一个最小闭环再逐步引入可重入、续期、公平性这些高级特性。遇到问题不要慌回到今天聊的这几类反模式里对照一下大概率能找到答案。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进