ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Redis分布式锁从原理到生产实践:锁粒度、坑与Redisson选型

2026/10/8 15:08:33 拓冰建站 浏览量
Redis分布式锁从原理到生产实践:锁粒度、坑与Redisson选型 先说结论分布式锁这个技术点面试背八股很简单真到生产环境写起来处处是坑。我自己就经历过凌晨三点被线上告警叫醒、库存扣成负数、订单重复支付的场景所以这篇不打算给你堆一堆SETNX EXPIRE 删除的老生常谈而是从原理一路讲到生产落地包括我踩过的坑和现在团队还在用的方案你可以直接拿去参考。1. synchronized救不了微服务分布式锁到底锁的是什么1.1 一个经典的超卖问题假设你负责一个电商系统的库存扣减接口核心逻辑大概是这样的public Boolean deductStock(Long skuId, Integer count) { // 1. 查询当前库存 Integer stock stockMapper.getStock(skuId); // 2. 判断库存是否充足 if (stock count) { return false; } // 3. 扣减库存 return stockMapper.deductStock(skuId, count); }当流量只有每秒几十的时候这段代码什么问题都没有。一旦遇到大促同一个SKU被几百个请求同时打到你会在数据库里看到库存变成了负数或者扣减次数明显超出实际库存。单体应用时代这个问题的解法很简单加一把synchronized或者ReentrantLock让同一时刻只有一个线程执行扣减逻辑就行了。但是微服务架构下用户请求被负载均衡分发到三个不同实例上每个实例各有一把JVM锁——它们彼此完全隔离锁住的只是各自进程内的线程。这个场景我用一个生活化的例子给你捋一下一栋楼只有一个共享车位但每一层都装了一道门禁。每层的人都能看到车位信息但各自楼层的人只会用自己楼层的门禁卡。A层的人在锁门以为占住了车位B层的人照样刷卡进入抢车位。分布式锁要锁的从来不是某个实例里的某个线程而是所有实例共享的某个资源本身。这个资源可能是数据库里的一条记录、缓存里的一个key也可能是第三方接口的调用额度。锁的本质是一次跨进程的互斥协商让所有节点都认同这个资源当前被谁占用了。1.2 分布式锁的三个硬性要求一个能被生产环境接受的分布式锁至少要满足三个条件互斥性任意时刻只有一个客户端持有锁。这是分布式锁的底线做不到这条后面都白谈。防死锁持有锁的客户端崩溃、网络分区、超时锁必须能自动释放否则整个系统会卡死。可重入性视业务场景而定同一个客户端在持锁期间如果再次加锁能够成功这在递归方法和嵌套调用中特别重要。往下走你会发现这三个要求在实现层面会引出一连串细节问题——锁的过期时间怎么设、释放时怎么避免误删别人的锁、主节点宕机了锁会不会丢等等。这些才是生产环境真正要面对的难题。2. 三条技术路线的选型数据库、ZooKeeper、Redis为什么最后选了Redis2.1 三条主流路线的对比分布式锁的实现方案业界基本分成三类基于数据库、基于 ZooKeeper、基于 Redis。我做了一张对比表你可以直观看到差异维度数据库唯一索引/悲观锁ZooKeeper临时顺序节点RedisSETNX/Redisson性能最差依赖磁盘IO和事务中等节点监听机制有一定开销最好纯内存操作单线程模型可靠性依赖数据库事务隔离级别高内置ZAB协议保证一致性中主从切换或哨兵模式可能丢锁实现复杂度简单建表加唯一索引即可中等需要理解Znode和Watcher低Redisson封装得非常好天然特性没有死锁问题数据库会加锁等待临时节点 序列号自动实现公平锁需要自己处理过期时间、续期等问题运维成本零额外成本复用业务库需要单独维护一套ZooKeeper集群如果已有Redis成本很低2.2 各方案的适用场景数据库锁实现最粗暴的方式是建一张锁表把method_name设为唯一索引要加锁就插入一条记录释放锁就删除这条记录。优势是简单到不用动脑子但性能上限很感人——每个加锁操作都伴随着一次数据库IO高并发下数据库会成为新的瓶颈而且它还会直接影响主库的连接池占用。我见过有团队用这种方式做定时任务调度锁一天跑几次没问题但绝对扛不住高频的业务调用。ZooKeeper锁的原理是创建临时顺序节点多个客户端同时去创建节点时序号最小的那个算拿到锁其它客户端监听自己的前一个节点。它胜在一致性有ZAB协议兜底天生不会出现主从切换丢锁这类问题而且临时节点的特性保证了客户端崩溃时锁会自动消失。代价是性能比Redis差一个量级另外引入了一套新组件维护成本上升。2.3 我的选型决策我当时所在的团队已经有了一套高可用的Redis集群主从加哨兵再为分布式锁单独引入ZooKeeper运维上完全没有必要。而且经过评估我们的核心业务场景库存扣减、订单防重、任务调度对秒级甚至毫秒级的锁冲突容忍度很高Redis在性能和实现成本上的优势非常明显。这里我插一句自己的判断如果你们的核心业务对锁的正确性极其敏感比如金融对账、资金流转丢了锁会造成真金白银的损失那不要犹豫直接上ZooKeeper或etcd。如果你跟我一样做的是电商交易、内容平台这类业务Redis方案经过合理设计可靠性足够覆盖绝大多数场景。分布式锁没有银弹只有适不适合。还有一个非常重要的点大部分团队已经有Redis了复用基础设施意味着你不需要引入新的中间件也不需要额外培训运维同学。我见过太多团队为了一个听起来更完美的方案就盲目引入新组件最后运维成本和故障面都在涨。技术选型永远要算总账而不是单点对比。3. Redis锁的核心原理拆解从两条命令到一枚Lua脚本3.1 最初的写法以及它为什么是个错误示范这是网上流传了很久的分布式锁入门版// 第一步尝试加锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:sku: skuId, 1); if (Boolean.TRUE.equals(locked)) { // 第二步设置过期时间 redisTemplate.expire(lock:sku: skuId, 30, TimeUnit.SECONDS); try { doBiz(); } finally { redisTemplate.delete(lock:sku: skuId); } }问题一望即知SETNX和EXPIRE是两条独立的Redis命令不是原子操作。如果客户端执行完SETNX之后、在执行EXPIRE之前突然崩溃比如进程被杀、机器宕机、网络抖动返回超时这个key就永远不会过期——其他客户端再也拿不到锁出现死锁。Redis官方在2.6.12版本之后就给出了标准答案把两条命令合并成一条Boolean locked redisTemplate.opsForValue().setIfAbsent( lock:sku: skuId, uuidValue, 30, TimeUnit.SECONDS );这里set key value NX EX的NX表示只有当key不存在时才能写入EX表示同时设定过期时间。一条命令完成加锁和设超时原子性由Redis单线程模型保证。这是所有Redis分布式锁的基础——加锁操作必须是一条原子命令。3.2 释放锁的坑为什么不能直接delete锁加上了业务跑完了释放锁的时候又有一个广大程序员踩过无数次的坑——直接delete。想象一下这个时间线线程A拿到锁value是UUID-A过期时间30秒。线程A业务执行超过30秒比如调了外部接口对方响应超时锁自动过期了。线程B过来发现锁没了成功加锁value是UUID-B。线程A终于执行完了执行delete(lock:sku: skuId)。线程A把线程B的锁删掉了。线程C立即加锁成功和线程B同时进入临界区。这个问题叫锁误删。解决方式也很明确释放锁之前必须先检查这个key的value是不是自己设置的那串唯一标识是才删不是就不能删。但这里又出现一个细节问题先GET再DELETE这是两条命令中间依然有被线程B介入的窗口期没错所以需要用Lua脚本把判断删除做成一个原子操作。Redis执行Lua脚本时不会穿插执行其他命令这是它的执行模型保证的。if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endJava侧调用String luaScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; RedisScriptLong redisScript new DefaultRedisScript(luaScript, Long.class); redisTemplate.execute(redisScript, Collections.singletonList(lock:sku: skuId), uuidValue);看到这里你应该已经理解了分布式锁的关键不是一条SETNX而是加锁、解锁各自都要保证原子性并且解锁时要验证持有者身份。这也是我面试候选人时必问的一个点能清楚讲出这套演化过程的说明是真的写过而不是背了八股。3.3 value为什么要用UUID还有一个容易被忽略的细节加锁时value的值怎么生成。用一个全局唯一的随机值比如UUID.randomUUID().toString()它的作用就是锁的所有者标识。自己生成的才能确保释放时能通过身份校验如果用固定值1所有客户端都能解锁锁误删的概率会大得多。4. 手写一把教学级分布式锁看得见的代码才是真理解4.1 核心代码实现理解了原理我建议你亲手写一版简单的Redis分布式锁哪怕不用于生产完整的编码过程能帮你把前面讲的概念全部串起来。下面这版代码是教学级实现包含了加锁、解锁、超时重试单看逻辑是完整的public class RedisDistributedLock { private static final String LOCK_PREFIX lock:; private static final long DEFAULT_LOCK_TIME 30L; private static final long DEFAULT_WAIT_TIME 3L; private final StringRedisTemplate redisTemplate; private final String lockKey; private final String lockValue; private final long lockTime; private final TimeUnit timeUnit TimeUnit.SECONDS; // 解锁的Lua脚本校验value 删除key原子操作 private static final String UNLOCK_LUA if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockKey) { this.redisTemplate redisTemplate; this.lockKey LOCK_PREFIX lockKey; this.lockValue UUID.randomUUID().toString().replaceAll(-, ); this.lockTime DEFAULT_LOCK_TIME; } public boolean tryLock() { // 尝试加锁等待时间DEFAULT_WAIT_TIME long deadline System.currentTimeMillis() DEFAULT_WAIT_TIME * 1000; while (System.currentTimeMillis() deadline) { Boolean success redisTemplate.opsForValue().setIfAbsent( lockKey, lockValue, lockTime, timeUnit); if (Boolean.TRUE.equals(success)) { return true; } // 没抢到锁休息一会儿再试避免空转打爆Redis try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } return false; } public void unlock() { redisTemplate.execute(new DefaultRedisScript(UNLOCK_LUA, Long.class), Collections.singletonList(lockKey), lockValue); } }对应的使用方式RedisDistributedLock lock new RedisDistributedLock(redisTemplate, order: orderId); if (lock.tryLock()) { try { // 业务逻辑 doBiz(); } finally { lock.unlock(); } } else { // 拿不到锁快速失败或者走降级逻辑 throw new BizException(系统繁忙请稍后重试); }4.2 这版代码的设计取舍这里有几个设计细节值得展开讲讲。第一tryLock为什么用循环重试而不是直接返回因为在实际业务里如果拿不到锁就直接告诉用户系统繁忙体验极差。在等待时间范围内稍作重试能平滑地度过锁冲突高峰期。但要注意重试间隔建议50到200毫秒不要直接死循环否则高并发下Redis会被你的自旋请求打满。第二unlock放在finally里这是底线要求。不管业务逻辑是成功还是抛出异常锁都必须释放否则一个异常就把锁资源永久占住了。第三这版代码没有实现自动续期和可重入性。真实生产环境如果业务执行时间不稳定30秒的锁过期时间可能不够用业务没跑完锁就先没了这会导致两个线程同时进入临界区。所以教学版只能算能用离生产级还差几步——正是那几步构成了后面章节的踩坑故事。4.3 为什么建议你一定要手写一遍很多同学直接上Redisson代码两行就完事这没问题。但我还是强烈建议你手写一遍这个教学版。原因有两条手写过程中你会自然理解为什么会演化出SET NX EX单命令、为什么会引出一个UUID、为什么删除要用Lua脚本。这些理解会在你真正排查线上问题时发挥巨大作用因为线上问题往往不是锁没加上而是锁被错误地释放了或者锁提前过期了没有底层理解你对着监控面板会毫无头绪。面试时候能把我基于Redis实现过一套分布式锁以及为什么没用Redisson生产环境会有什么问题讲清楚比背十道面试题管用得多。讲清踩过的坑和经验才是打动人的核心。5. 生产环境最要命的五个坑每一个都值得重视5.1 锁提前过期业务没跑完锁先没了这是Redis分布式锁最经典的坑。锁设为30秒过期但业务逻辑调用了一个慢接口响应花了40秒。30秒时锁自动消失另一个线程拿到锁进来两个线程同时执行同一份资源。结果就是——你原本想互斥保护的东西在某个瞬间彻底失去了保护。网上最常听到的方案是启动一个定时任务在锁快过期时自动续期。的确这就是Redisson看门狗Watchdog机制在做的事情。但续期机制并不能解决所有问题比如业务线程在执行中发生长时间GC停顿Stop The World整个JVM都停了定时续期的任务当然也停了锁依然会过期。续期任务本身如果依赖同一个线程池线程池被打满时续期任务会被延迟执行锁同样会提前消失。这里我想说句掏心窝的话分布式锁保护的临界区执行时间必须短且可控。如果你的业务逻辑动辄几十秒甚至几分钟你该考虑的不是怎么给锁续期而是重新设计流程——把长事务拆短、把外部慢调用提前预计算、把一次性的大事务改成多阶段异步。分布式锁不是万能膏药,拿它去保护一个执行时间不可控的临界区本身就是设计失误。5.2 误删他人锁多线程互相踩踏前面讲原理时已经提到过这个坑这里再补充一个生产环境的真实案例。我们曾经有一个服务两个线程同时处理同一个用户的请求线程A和线程B先后都调用了tryLock。A拿到锁开始执行业务B没抢到锁在自旋等待。A业务执行过程中Redis主节点发生了一次短暂的网络抖动A与Redis的连接中断但此时A并不知道锁还在自己手里锁未过期A自认为释放锁失败网络原因于是认为业务失败开始重试——重试时它又去调了一次unlock而这次网络恢复了它成功地把B刚拿到的锁删掉了。B和C同时进入临界区问题爆发。这个案例的根因是因为网络抖动客户端把正常执行业务误判为失败继而走了重试逻辑。解决方案有几个层面解锁必须用Lua脚本校验value这已经做了它能防止不是你的锁你删了但在这个案例里value是A自己的Lua判断通过照样删掉。问题关键其实在于业务重试逻辑必须等锁真正释放后再重新加锁不能无脑重试。加锁时value还可以带上线程ID、请求ID等业务标识让锁的owner信息更丰富排查问题时能快速定位是谁删了谁的锁。5.3 主从切换丢锁Redis高可用的代价这是Redis分布式锁又一个著名的硬伤。当你用主从架构一主一从时加锁操作写的是主节点主节点通过异步复制把数据同步给从节点。如果这个瞬间发生主从切换线程A在主节点加锁成功锁的数据还没同步到从节点。主节点宕机。从节点被哨兵提升为新主节点但它没有A的锁记录。线程B来加锁成功——它在新主节点上写入了一把新锁。现在A和B同时认为自己持有锁。这个问题的本质是加锁的确认机制发生在主节点但主节点的数据可能还没来得及同步就被切换走了。用Redis做分布式锁在高可用场景下你必须在性能与绝对可靠之间做取舍。应对方案是业界著名的 Redlock 算法核心思想是同时向独立的Redis节点通常是5个发送加锁请求只要超过半数节点返回成功就认为加锁成功。节点之间互不依赖单一节点的故障不会影响整体判断。Redlock在理论层面经常被人质疑尤其是分布式系统大牛Martin Kleppmann写过一篇很有名的文章专门批它这里不展开辩论我提供一下我自己在实际生产中的判断如果你的系统要求绝对不丢锁这个量级的要求通常说明你最好直接用ZooKeeper或etcd而不是在那纠结Redlock的实现细节。如果你的场景是99.9%情况下不丢锁偶尔丢一次也能通过补偿机制兜底比如订单重复支付有对账程序能发现那Redlock足够了。5.4 可重入性缺失导致死锁我们遇到过这样一个线上事故一个方法内部嵌套调用了另一个加锁方法外层加了order:123的锁内层又去加order:123的锁结果内层永远等不到外层释放锁形成死锁。这个问题的本质是锁不具备可重入性。Java自身的synchronized重入锁早就解决了这个问题但Redis分布式锁默认没有这个能力——同一个客户端第二次SETNX同一个key时会因为key已经存在而失败。解决方案有两种加锁时value记录持有者重入次数每次加锁时如果发现自己持有锁就把次数加1释放时减1减到0才真正删除key。这就是Redisson的实现方式。在业务设计上避免嵌套加锁统一把加锁操作收敛到一个服务层方法里从设计层面消灭问题。第一种方案实现成本略高但是通用第二种方案成本低但需要团队有良好的编码规范。我个人在实际团队里是两条并用框架层支持可重入业务层也严格约定尽量不要嵌套加锁。5.5 时钟跳跃你以为的过期时间不是你以为的Redis节点上的系统时间如果发生跳跃比如运维手动调整NTP时间同步或者虚拟机时间跳变会直接影响key的过期判定。如果Redis服务器时间突然向后跳了一大截那些本应过期的key会复活锁的有效期被意外拉长。这个坑比较隐蔽也很少有人聊但在虚拟化环境频繁做快照回滚、热迁移里真实存在。我建议你在生产环境关注一下Redis所在服务器的NTP配置同时可以设置Redis的minimal-clock-drift-scan参数在启动时检测时钟漂移的幅度一旦超过阈值就报警。有人会说Redis本身提供了clock:skew时钟检查功能但默认是关闭的。谁也不想在凌晨两点被这种问题叫醒所以我在搭建Redis集群时一般会把时钟相关的参数显式配置并纳入巡检脚本每台Redis节点都要确认NTP同步正常。6. Redisson接管了一切生产环境正确姿势与Redlock讨论6.1 为什么不建议你在生产环境自己维护锁说实话我见过不少团队推荐自己写Redis分布式锁不用Redisson理由是Redisson依赖引入太重、代码看不懂、出了问题不好排查。对这种说法我的观点是如果你的业务比较简单、对极端可靠性要求不高,自己封装一套也未尝不可安全风险可控。但一旦业务复杂度上来自己实现的成本会指数级上升你要实现自动续期看门狗需要额外维护一个定时任务和Task队列。你要实现可重入value要设计成一个结构而不是简单字符串。你要处理等待锁时的公平性问题、阻塞/非阻塞语义。你要考虑加锁失败时的异常链路。你要排查死锁、锁泄漏、锁误删等各类问题。Redisson把这些全都做完了而且是经过大规模生产验证的。用成熟的轮子才是工程正道。6.2 Redisson的核心用法Redisson最常用的API如下RLock lock redissonClient.getLock(order: orderId); // 方式一阻塞等待默认拿不到锁一直等容易挂住业务线程 lock.lock(); try { doBiz(); } finally { lock.unlock(); } // 方式二尝试获取锁最多等待3秒拿不到就放弃 if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { try { doBiz(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这里建议生产环境优先用tryLock而不是lock原因很简单lock.lock()在拿不到锁时会被阻塞直到获取到锁如果Redis发生了长时间的主从切换或者网络分区调用lock()的线程会一直等下去业务线程池会被锁卡死。而tryLock(waitTime, leaseTime, unit)在指定时间内拿不到锁会直接返回false你可以决定立即失败还是做降级兜底。有一点需要注意tryLock里我传了leaseTime10秒这种情况下Redisson不会启动看门狗自动续期10秒后锁强制释放。如果你不想设置固定过期时间就让leaseTime用默认值-1这样Redisson会启动看门狗把锁的有效期维持在默认30秒并且每10秒检查一次业务还在执行就续期业务结束了就停止续期。6.3 Redlock什么时候用什么时候真没必要Redlock的问题在前面提过一嘴这里展开说说我的取舍标准。如果你的系统并不存在同一个锁会有多个客户端疯狂竞争的场景比如就是定时任务集群防止重复执行几个实例每天抢一次锁那Redlock完全没必要。普通Redis锁的实力绰绰有余。如果存在低频但严重影响正确性的场景比如支付回调防重、对账系统加锁并且团队能接受偶尔一次丢失锁带来的人工补偿成本那普通Redis锁 补偿机制也是可行的。如果业务接受不了任何丢锁连一次都不行那就应该用ZooKeeper或者etcd来做分布式锁它们的一致性模型更适合此类场景。Redlock在实现上虽然解决了单点故障问题但它依赖多个独立Redis节点部署成本和运维复杂度都不低。对于绝大多数团队来说升级到Redlock是一件性价比很低的事情。7. 锁粒度、性能测试与监控一把好锁的另一半功夫7.1 锁粒度设计把全局锁变成分片锁很多人用分布式锁容易犯一个错误一个接口统一加一个固定的锁key比如lock:deductStock。这样会导致所有请求都串行化性能急剧下降。正确的做法应该是按业务维度拆分锁的粒度。以库存扣减为例合理的key设计是lock:stock:{skuId}——每个SKU的库存互相独立不同SKU的扣减互不干扰只有同一个SKU的扣减会被锁保护。如果还是觉得粒度粗可以进一步按仓库IDSKU拆lock:stock:{warehouseId}:{skuId}因为实际库存是按仓维度隔离的。订单创建的防重锁可以是lock:order:{userId}:{goodsId}防止同一个用户对同一件商品重复下单不同用户之间的下单完全并发。锁的key想清楚之后你还要想一个问题锁保护的到底是哪个资源。锁的粒度应该与资源的粒度严格匹配。如果锁的粒度大于资源粒度会造成不必要的串行如果锁的粒度小于资源粒度会出现两把锁保护同一份资源的情况互斥保护就失效了。7.2 性能测试别等线上出事故才后悔我踩过的最疼的一次坑就是分布式锁上线前没做压测结果大促当晚Redis的QPS打到瓶颈加锁耗时从零点几毫秒飙升到几十毫秒接口P99直接爆掉。这里给你一套相对完整的压测思路先压本次要用的核心接口统计加锁、解锁各耗时多少锁冲突率大概多少。压测Redis单节点能支撑多少QPS的SETNX操作通常单节点纯内存操作可以支撑到5万到10万QPS但你要留出运维冗余一般压测到峰值的3倍即可。重点观察锁冲突时的等待耗时和业务成功率确认tryLock的超时策略是否符合预期。我曾经用一个模拟接口做了实验直接不加锁压测TPS轻松到2000加锁后压测同一接口TPS掉到800左右。听起来差距很大但800 TPS对大多数业务已经完全够用了。你要关注的永远不是锁带来的绝对损耗而是在可接受的延迟内锁能不能保证正确性。7.3 监控告警与日常巡检分布式锁的监控指标讲真没有太多但有三项你值得盯紧加锁失败率如果一段时间内tryLock返回false的比例突然升高说明锁冲突严重要么是锁的粒度太粗要么是你在错误地拿锁去保护长事务。锁等待时长线程在tryLock里自旋等待的时间如果持续走高说明临界区资源的占用率在提升。解锁失败数通过日志和metric统计Lua脚本返回删除结果的异常情况如果出现异常解锁通常意味着value校验失败或网络异常这种往往是线上问题爆发的前奏。我建议你在接入分布式锁的同时就把这些指标打到统一的监控系统并配置对应的告警阈值。别等到线上出问题才去翻日志那是最痛苦的排查方式。7.4 一个完整的生产级加锁写法如果你用的是Redisson参考下面这个写法基本够用RLock lock redissonClient.getLock(lock:stock: skuId); boolean locked false; try { locked lock.tryLock(3, -1, TimeUnit.SECONDS); if (!locked) { // 记录告警抢锁超时 log.warn(acquire lock failed, skuId{}, skuId); return Result.fail(当前排队人数较多请稍后再试); } // 业务逻辑确保执行时间短、可控不要在里面调长时间的外网服务 return deductStock(skuId, count); } finally { if (locked) { lock.unlock(); } }这个写法的最核心保障在于锁的获取尽量短锁的内部业务逻辑也尽量短锁的释放一定放在finally里。做到这三点锁引发的大部分线上问题都不会找上你。你自己手写的那套教学级锁如果用于生产请一定再补上自动续期的能力然后把监控指标加上。否则我建议还是直接用Redisson省心也稳。最后说点题外话分布式锁这个东西理论知识说破天就那几句真正的功夫全在细节里。我在生产环境摸爬滚打过之后最大的体会是能用简单方案解决的就不要追求复杂的完美方案Redlock看起来比单机Redis锁完美但你为此付出的运维成本和复杂度的增长可能远超那一点可靠性收益。再分享一个小技巧如果你的业务可以被设计成幂等最终一致其实可以不依赖分布式锁。把请求带上全局唯一业务号比如订单号、流水号在数据库唯一索引上做防重或者用消息队列的幂等消费机制来兜底很多所谓的并发问题在架构层面就化解了。分布式锁是最后的手段也是你最应该谨慎使用的工具——能用数据约束解决的就不要引入锁能用消息异步削峰的就不要让业务线程去抢锁。今天的分享就到这里如果这篇文章对你有帮助也欢迎在评论区聊一聊你在生产环境遇到过的分布式锁坑一起交流。