ARTICLE DETAIL

建站实战干货

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

Redis分布式锁全解:从SETNX到Redisson与RedLock

2026/9/26 18:26:17 拓冰建站 浏览量
Redis分布式锁全解:从SETNX到Redisson与RedLock 这个系列走到了第八篇前面我们一起过完了Redis的基础数据结构、持久化、主从复制、哨兵、集群、缓存设计、Lua脚本。按照正常的进阶路线接下来最适合聊的就是分布式锁——它既是Redis使用频率极高的场景也是面试官最喜欢深挖的一环。这些年的面试里分布式锁几乎成了Java技术岗的必问项而且问的深度一直在加码从最早的SETNX加锁问到过期时间、原子性、看门狗再问到RedLock的争议。如果你能把这篇文章里的内容完整讲清楚这一块基本就能过关了。分布式锁解决的问题很直接在多实例部署的情况下多个进程同时操作同一个共享资源怎么保证同一时刻只有一个进程在动它。单机锁管不住跨进程的并发数据库锁性能又撑不住高并发这时候Redis分布式锁就登场了。这篇文章会从最原始的实现讲起一步一步推到生产级方案全程无废话直接上能用的经验。1. 为什么要用分布式锁单机锁在分布式环境下的失效逻辑1.1 从synchronized到分布式锁锁的本质是什么锁的本质实际上就一句话让多个执行单元按顺序访问临界区。临界区就是那段不该被并发执行的代码比如扣库存、发放优惠券、更新账户余额。在单机时代这个约束很好实现。我们用synchronized、ReentrantLock它们靠的是JVM进程内的共享内存。同一个进程内的多个线程都能看到同一把锁的标记所以能互相约束。但问题来了一旦服务做集群部署三个实例同时运行每个实例有自己的JVM、自己的锁标记A实例的synchronized根本管不住B实例的线程。说个最典型的场景。订单服务部署了三个实例上游同时来了三笔订单恰好命中不同实例。三个线程同时执行库存扣减各自在自己JVM里拿到了锁然后一起把库存从100扣到了97。从单实例看没有任何问题但整体库存被重复扣减了。这就是锁的管辖范围和临界区所在的范围不匹配造成的。所以锁要跨进程生效就必须有一个所有进程都能访问到的公共载体。Redis就是那个载体。大家把锁的标记放到Redis里谁能在Redis里成功写入这个标记谁就获得了进入临界区的资格。这个思路就是分布式锁最底层的逻辑。1.2 分布式锁必须满足的四个条件设计或者考察一个分布式锁核心就看四个条件是否满足。条件含义不满足的后果互斥性同一时刻只能有一个客户端持有锁多个线程同时进入临界区数据出错安全性锁最终一定能被释放不会死锁持有者宕机后所有线程永久等待可用性加锁/解锁操作性能好、稳定锁成为性能瓶颈拖垮整个业务可重入性同一线程可以重复获取同一把锁视场景递归或嵌套调用时自我死锁绝大多数手写分布式锁方案出问题都出在安全性和互斥性上。安全性解决的是“宕机了怎么办”答案是过期时间兜底互斥性解决的是“误删别人的锁怎么办”答案是唯一标识校验。这两个问题后面的章节会逐一展开。1.3 哪些场景真的需要分布式锁先说清楚一个判断标准能用数据库唯一索引、乐观锁、版本号解决的问题尽量不要引入分布式锁。分布式锁是最后的手段不是第一选择。但有几类场景分布式锁确实是标准答案。一类是分布式定时任务。集群部署环境下同一个定时任务每个实例都会触发如果不加锁数据就会被重复处理。XXL-Job这类框架自带分片和调度锁但如果你的任务是自己写的那就必须用分布式锁保证同一时刻只有一个实例在执行。一类是热点缓存重建。某个热点key过期后大量请求同时打到数据库。可以用分布式锁控制只有一个线程去重建缓存其他线程短暂等待或直接返回旧数据避免缓存击穿。还有一类是秒杀、抢购这类高并发扣减场景。库存扣减需要严格的互斥控制分布式锁锁住商品的库存key保证扣减操作串行执行。2. 手写一个分布式锁从SETNX到SET NX EX的演进2.1 第一版实现SETNX加锁DEL释放最早的分布式锁方案非常朴素。Redis提供了一个命令SETNX全称是SET if Not eXists只有key不存在的时候才能设置成功设置成功返回1失败返回0。利用这个特性加锁就是一条命令的事。SETNX lock_key 1执行成功说明没人持有锁执行失败说明锁已被别人持有。业务执行完毕之后用DEL把锁删掉其他线程就能继续抢了。DEL lock_key这个方案在面临面试官“有什么问题”的追问时第一个暴露的死穴是如果拿到锁的服务实例突然宕机DEL永远执行不到这把锁就永远留在Redis里了其他所有线程只能干等整个服务相当于锁死了。这就是前面说的安全性问题死锁。我见过不少团队早期线上就是这么干的平时一切正常一旦某个节点因为内存溢出或强制kill挂掉线上立刻出现大面积请求阻塞重启都不管用因为锁key还在Redis里躺着。2.2 引入EXPIRE给锁加上“自动销毁”机制解决死锁的思路很直接给锁设置一个过期时间让锁在极端情况下也能自动释放。SETNX lock_key 1 EXPIRE lock_key 30这样即使持有锁的实例宕机最多30秒后锁自动消失其他线程可以继续抢锁。过期时间相当于给锁上了一道保险。但细想一下这里还有个隐藏的坑SETNX和EXPIRE是两个独立的命令它们之间没有原子性保证。如果SETNX执行成功之后EXPIRE还没来得及执行实例进程就崩溃了锁依然会永久留在Redis里。这个缺口在低概率情况下出现但后果和第一版一模一样死锁。为什么会这样因为Redis是单线程执行命令但两条命令之间完全可能插入其他操作。进程崩溃发生在任意一条命令执行完之后都有可能。所以只要加锁和设过期不是一条命令就永远存在这个窗口期。2.3 一步到位SET NX EX 实现原子加锁这个原子性缺口直到Redis 2.6.12版本才算真正补上。这个版本给SET命令增加了NX和EX选项可以把“加锁”和“设置过期时间”合并成一条命令。SET lock_key unique_value NX PX 30000NX只有key不存在时才设置成功相当于SETNX。PX设置过期时间单位毫秒。unique_value调用方生成的唯一标识后面解释用途。命令返回OK说明加锁成功返回空结果说明锁被其他人持有加锁失败。这里给一个Java侧的示例用Spring Data Redis实现String requestId UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lock:order: orderId, requestId, Duration.ofMillis(30000)); if (Boolean.TRUE.equals(locked)) { try { // 执行临界区业务逻辑 } finally { // 释放锁下一节讲正确写法 } }这个方案解决了原子性问题也保留了过期时间兜底是手写分布式锁的正确起点。看起来已经能用了但离生产级还有两个关键缺口过期时间怎么定以及释放锁怎么做到安全。2.4 释放锁的隐患为什么不能直接DEL很多人加锁写得没问题结果在释放锁上栽了跟头。直接DEL的问题在于一种经典误删场景线程A获取锁后因为业务执行时间超过了锁的过期时间锁在A还在运行时就自动过期了。此时线程B加锁成功开始执行自己的业务。A终于执行完毕直接发送DEL命令删掉的却是B持有的锁。B的锁被误删后线程C又加锁成功两个线程同时进入临界区互斥性被破坏。怎么解决加锁时在value里放一个当前线程的唯一标识释放前先GET一下比对value是不是自己的是才DEL不是就不动。String requestId UUID.randomUUID().toString(); if (requestId.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); }但这里又冒出来一个原子性问题GET和DEL是两步操作中间完全可能插入其他命令。如果线程A在GET之后、DEL之前发生GC停顿或网络延迟锁恰好在这段时间内过期并被线程B获取A恢复后DEL掉的还是B的锁。所以释放锁同样需要原子操作这就必须请出Lua脚本了。3. 释放锁与续期背后Lua脚本与看门狗思想3.1 Lua脚本为什么能保证释放锁的原子性Redis从2.6版本开始支持内嵌Lua脚本。执行Lua脚本时Redis会把整个脚本作为一个整体在单线程中执行执行期间不会插入任何其他客户端命令。这正好解决了“先比对再删除”的原子性需求。标准的释放锁脚本长这样if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endKEYS[1]是锁的keyARGV[1]是加锁时放入的唯一标识。如果当前锁的value等于这个唯一标识说明锁确实是自己的删除成功否则说明锁已经被别人持有不操作。Java侧的执行方式DefaultRedisScriptLong unlockScript new DefaultRedisScript(); unlockScript.setScriptText( if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end); unlockScript.setResultType(Long.class); Long result stringRedisTemplate.execute(unlockScript, Collections.singletonList(lockKey), requestId);这里有个实战细节要提醒如果Redis部署方式是集群模式Lua脚本中的KEYS和ARGV有讲究KEYS必须落在同一个slot上。锁key本身是同一个自然满足要求。但如果你在一个脚本里想同时操作多个key就得用Hash Tag把key强制放在同一个slot里否则会报CROSSSLOT错误。这一点不实操的话很难注意到。3.2 锁过期时间怎么定宁可长也不可短手写锁时过期时间是最考验经验的参数。设太短业务超时执行还没结束锁就过期了后续线程提前进入临界区设太长一旦持有锁的实例宕机其他线程等待的时间也相应拉长系统恢复慢。我的经验是先统计业务在临界区内的平均耗时把过期时间设置为平均耗时的3到5倍。比如业务平均执行200毫秒过期时间放到1秒。这样既给了波动余量又不会在宕机时让其他线程等太久。但无论怎么设置固定过期时间都有一个无法绕开的矛盾你无法准确预知业务什么时候结束。如果业务依赖外部接口响应时间可能从几百毫秒波动到几十秒固定值必然会失效。要彻底解决就得引入动态续期机制也就是看门狗思想。3.3 看门狗思想的雏形动态续期解决长任务问题看门狗的核心思路是不给锁设置一个固定的长过期时间而是设置一个较短的时间同时启动一个后台任务在锁快要过期时自动续期把过期时间往后推。只要持有者还活着且业务没结束锁就一直续下去一旦持有者宕机后台任务随之消亡锁在短时间后自动释放。这个过程很像家里的看门狗主人不按铃铛它就安静待着一旦有异常情况它就叫唤提醒。Redisson把这种机制做成了标准实现后面会详细讲。手写方案的续期逻辑其实不复杂就是定时任务加一个PEXPIRE但难点在于分布式环境下的边界处理续期任务本身不能和锁的生命周期脱节续期操作也要保证只对自己的锁生效。到这里手写分布式锁的完整链路已经清晰了。但说实话如果生产环境让我选我不会自己写这一套。Java生态里Redisson已经把这些边界问题都处理好了直接用它才是工程上最稳妥的选择。4. Redisson生产级分布式锁的正确打开方式4.1 为什么生产环境不建议继续手写手写锁走到Lua脚本这一步看起来已经很完备了。但继续往下深挖还有一堆问题在等着可重入怎么实现续期怎么做主从切换时锁丢失怎么兜底锁等待超时如何处理这些细节单独解决都不难但组合在一起就是一个不小的工程。Redisson本身就是为这些问题而生的。它提供了一个开箱即用的RLock接口语义上对标JDK的Lock能用几乎相同的方式调用同时把加锁、解锁、续期、可重入、等待超时这些复杂逻辑全部封装掉了。你不需要自己拼Lua脚本不用自己维护UUID更不用自己写续期任务。引入方式也很简单。以Spring Boot项目为例dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency如果想要更多控制权可以手动创建RedissonClientConfig config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setConnectionPoolSize(10); RedissonClient redisson Redisson.create(config);4.2 RLock加锁与看门狗机制锁的生命周期自动管理编写中接下段使用RLock的代码风格和JDK的Lock非常接近RLock lock redisson.getLock(lock:order: orderId); try { boolean locked lock.tryLock(5, 30, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException(获取锁失败); } // 执行临界区业务 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }tryLock接收三个参数等待时间、锁过期时间、时间单位。还有更简单的lock()调用不传过期时间时会启用看门狗机制。这一点值得展开细说。Redisson的加锁底层用的是Hash结构key是锁名field是客户端唯一标识UUID加线程IDvalue是重入计数。加锁的Lua脚本大致逻辑是判断锁是否存在不存在则创建并设置过期时间同时计数为1存在且field是自己的则计数加1实现可重入。当调用lock()不指定leaseTime时Redisson会使用默认的30秒锁过期时间并启动一个定时任务每隔10秒检查一次当前线程是否还持有锁。如果持有就把过期时间重新设置为30秒。这个过程就是看门狗。看门狗解决了两个问题长业务场景下锁不会因为业务没执行完而过期减少了线程B提前进入临界区的概率。持有者宕机后定时任务停止锁最多30秒后自动释放不会有死锁风险。续期操作的Lua脚本逻辑if redis.call(hexists, KEYS[1], ARGV[1]) 1 then return redis.call(pexpire, KEYS[1], ARGV[2]) else return 0 end这个脚本判断锁的持有者是否还是当前线程是则续期。需要特别提醒如果调用lock(long leaseTime, TimeUnit unit)显式传入了过期时间看门狗不会启动。锁到了时间就自动释放不会续期。所以传leaseTime等于你关闭了看门狗保护长业务场景要谨慎使用。4.3 可重入锁与公平锁Redisson的进阶功能Redisson的RLock天然支持可重入。同一个线程可以连续多次加锁同一把锁每次加锁计数加一每次解锁计数减一只有计数归零锁才真正释放。RLock lock redisson.getLock(lock:demo); lock.lock(); lock.lock(); // 同一线程第二次获取计数变为2 lock.unlock(); // 计数变为1锁仍然被持有 lock.unlock(); // 计数归零锁释放这个特性非常关键。实际业务中一个方法调用了另一个也需要加同一把锁的方法如果没有可重入性第二次加锁会把自己阻塞住形成死锁。JDK的ReentrantLock正是因为有可重入设计才支持这种嵌套调用。Redisson还提供FairLock公平锁。普通锁是非公平的线程获取锁的顺序和请求顺序无关抢到就是谁的。公平锁通过ZSet按请求顺序排队先到先得。代价是性能明显下降因为每次加锁都要维护队列的排序信息。绝大数业务场景用不到公平锁知道有这个东西就够。4.4 集群模式下的隐患普通RLock的真实现状Redisson的普通RLock在单节点Redis环境下是可靠的。但一旦部署为哨兵模式或者Cluster模式会有一个绕不开的短板Redis主从复制是异步的。线程A在Master节点上加锁成功Master还没来得及把这条数据同步给Slave就宕机了哨兵把Slave提升为新Master。此时线程B去新Master上加锁因为锁数据不存在加锁成功。A和B同时持有锁互斥性被破坏。这不是Redisson的bug而是Redis本身异步复制模型的固有限制。那怎么办这就引出了分布式锁领域最著名也最有争议的RedLock算法。5. 主从切换与RedLock分布式锁的安全性边界5.1 主从切换为什么必然丢锁先把这个场景完整复述一遍线程A在Master上加锁成功锁数据写入Master。Master向Slave异步同步数据但同步尚未完成Master宕机。哨兵检测到Master不可用将一个Slave提升为新Master。线程B向新Master请求加锁锁key不存在加锁成功。线程A和线程B同时持有同一把锁的“合法”凭证。这个问题的根源在于Redis选择的是AP模型保证可用性和分区容错性牺牲强一致性。主从节点之间的数据同步是最终一致不是强一致。所以你无论怎么优化Master到Slave的同步策略只要异步复制存在锁就有丢失的窗口期。5.2 RedLock算法的设计思路Redis原作者Salvatore Sanfilippo提出了RedLock算法试图解决这个问题。思路很精巧部署多个完全不相关的Redis实例这些实例之间不复制数据。加锁时客户端依次向所有实例发送加锁请求只要在有效时间内成功锁定超过一半的实例就认为加锁成功。RedLock要求使用N个独立Redis实例官方建议N等于5。算法流程如下获取当前时间戳。依次向所有实例发送SET lock NX PX设置一个较短的有效期。计算所有实例的加锁耗时可忽略不计如果在有效期内成功锁定了至少3个实例且总耗时小于锁的有效期加锁成功。加锁成功后的实际持有时间等于锁有效期减去加锁总耗时。加锁失败或持有时间不足则向所有实例发送DEL释放部分成功的锁。这个算法的理论基础是分布式共识里的“过半数”原则只要有超过一半的实例确认了锁状态即使个别节点故障也不会出现两个客户端都拿到超过半数锁的情况。但RedLock在提出后不久就遭到了分布式领域著名研究者Martin Kleppmann的公开质疑。核心争议点包括进程的GC暂停可能导致线程A持有的锁已经过期但A自己不知道恢复后继续执行临界区造成数据不一致。不同实例的时钟可能漂移导致锁有效期的计算不准确。RedLock本身依赖同步时钟假设而这一点在分布式系统中并不总是成立。Redis作者也做了回应他在文章里强调了一个关键点分布式锁的安全性和活性需要区分业务场景很多场景下Redis锁加上业务侧的幂等保障已经足够。5.3 工程上的折中方案什么场景信Redis锁什么场景不信聊完争议回到工程决策。按我的实际经验选型参考这张表业务场景推荐方案原因缓存重建、定时任务互斥Redis分布式锁即使偶发锁失效后果可控秒杀扣库存允许极端超卖或可对账Redis分布式锁 库存预扣性能优先事后补偿账户余额、资金流转ZooKeeper/etcd分布式锁强一致优先不能接受并发覆盖跨数据库分布式事务不建议用Redis锁需要分布式事务中间件如果业务对强一致有硬性要求比如资金类操作就用ZooKeeper或etcd这类CP模型组件。它们要用更多的通信开销换取读写强一致锁的安全性更高但性能和复杂度也上去了。如果业务能接受“极端场景下锁失效并靠业务自身兜底”Redis锁是完全够用的。实际项目中我通常会叠加三层保障锁本身用Redisson的看门狗续期业务侧带上请求ID做幂等校验Redis主从用哨兵或Cluster三节点起步尽量降低单点故障概率。6. 分布式锁使用场景与实战陷阱6.1 典型场景拆解缓存击穿、定时任务、库存扣减缓存击穿场景。一个热点key在缓存中过期瞬间涌入大量请求。如果没有锁所有请求都穿透到数据库数据库压力瞬间飙升。用锁控制只有一个线程去查数据库并重建缓存其他线程在短暂等待后读取刚写入的新缓存数据库的并发压力就控制住了。定时任务场景。订单超时未支付自动关单多个实例的定时器都在跑。没有锁的话同一个订单可能被多个实例扫描到并重复关单。用锁保证同一时刻只有一个实例在扫描其他实例直接跳过本周期。库存扣减场景。这个要提醒的是锁粒度。如果锁key设计成lock:stock:all所有SKU的扣减都串行执行并发能力直接归零。正确做法是锁到单个SKUlock:stock:{skuId}。不同SKU的扣减互不干扰同一个SKU内部串行既保证安全性又保留并发度。RLock lock redisson.getLock(lock:stock: skuId); boolean locked lock.tryLock(1, 10, TimeUnit.SECONDS); if (locked) { try { int stock getStock(skuId); if (stock buyCount) { updateStock(skuId, stock - buyCount); } } finally { lock.unlock(); } }注意这里的tryLock等待时间设成了1秒含义是如果拿不到锁最多等1秒拿不到就快速失败而不是无限阻塞等待。6.2 实战中踩过的坑四个必须警惕的细节第一个坑锁内做远程调用。这是新手最容易犯的错误。在临界区里调用外部HTTP接口或者RPC服务一旦下游服务响应慢锁的持有时间被无限拉长其他线程只能一直等待。更糟的是如果锁有超时时间下游还没返回锁就过期了后面的线程提前进入临界区。锁内应该只做必要的内存操作和快速Redis操作外部IO尽量移出临界区。第二个坑unlock不校验持有者。直接lock.unlock()的问题和手写DEL一样可能把别人刚获取的锁释放掉。Redisson的unlock内部有校验逻辑但如果你用的是手写方案一定要按Lua脚本的比对方式释放锁。第三个坑锁过期时间拍脑袋。既不统计业务耗时也不设置看门狗随便设个60秒。业务平时300毫秒跑完等于给宕机恢复留了60秒的等待期业务高峰期可能要跑80秒锁在第60秒过期后面的线程提前进入。正确做法是优先用Redisson的watch dog如果手写按平均耗时的3到5倍设置。第四个坑tryLock等待时间设成永久或过长。有些开发不熟API直接lock()拿锁拿不到就无限等线程长时间处于阻塞状态。建议等待时间设成500毫秒到1秒拿不到锁就快速失败让上层判断是重试还是返回提示。6.3 设计一把锁时顺手想清楚的五个问题每次给业务加锁我都习惯在动手前把下面这五个问题过一遍锁的key怎么设计有没有按照业务维度拆细能不能锁到订单维度而不是锁全表谁持有锁解锁时怎么能确认锁是自己的锁最多等多久等待策略是快速失败还是自旋重试锁过期时间怎么定业务跑不完怎么办有没有续期机制锁失效的兜底方案是什么业务层面有没有幂等控制这五个问题想清楚代码写起来基本不会有大方向问题。很多线上事故回头看都是这五个问题里有一两个没想明白。7. 高频面试题与线上问题排查实录7.1 分布式锁面试连环问五道必考题面试考分布式锁核心就五道题。我把回答要点列出来。面试题回答要点加分表达Redis分布式锁的实现原理SET NX EX 唯一标识 Lua脚本释放提到锁的过期时间与原子性两个核心为什么设置过期时间防止持有者宕机导致死锁提到看门狗续期机制SETNX和EXPIRE为什么不建议分开用非原子崩溃窗口期导致锁永久存在提到Redis 2.6.12后的SET合并方案锁过期了业务没执行完怎么办Redisson看门狗自动续期说出看门狗默认30秒、每10秒续期RedLock为什么有争议主从复制异步丢失、GC暂停、时钟漂移提到Martin Kleppmann与Redis作者的论战7.2 线上排查实录锁等待导致的线程阻塞说一个我实际处理过的案例。某个服务大促期间偶发接口响应时间突刺线程池被打满大量请求超时。日志里出现RedisLockException提示获取锁超时。排查过程分三步走。第一步查看Redis监控确认锁相关的key命令量是否异常偏高。用Redis MONITOR命令观察了几分钟发现锁key的请求频率非常高且单个锁的持有时间普遍超过1秒。第二步查看业务日志定位到持锁时间长的那个请求发现临界区内有一个外部HTTP调用单次调用的P99就有800毫秒。第三步确认锁key设计发现当时用的是全局锁所有订单抢同一把锁并发能力被锁粒度严重限制。最终处理方式把锁粒度从全局改成订单维度临界区内的外部调用改成了异步化处理锁等待时间从永久等待改成500毫秒快速失败。改动上线后接口P99从2秒降到200毫秒线程池也恢复了健康。这个案例的核心问题不在Redis而在锁的设计。分布式锁本身不会成为瓶颈使用方式不当才是瓶颈。7.3 排查分布式锁问题的快速速查表现象可能原因检查方向大量线程等待锁超时锁粒度太粗/锁内耗时长检查锁key设计、临界区代码锁一释放立刻被其他线程获取持有者处理时间超过锁过期时间检查看门狗是否启用偶发数据不一致主从切换时锁丢失考虑RedLock或换CP组件死锁可重入场景用非可重入锁检查是否嵌套调用同一把锁加锁成功率极低Redis连接池配置过小检查连接池大小与等待时间顺带提一个很多人忽略的排查点加锁失败后的业务行为。很多代码加锁失败直接抛异常抛异常后的响应码和提示信息设计不合理用户看到的是晦涩的报错体验很差。加锁失败应该被视为一种正常业务分支返回“系统繁忙请稍后重试”这类提示而不是让用户看到一堆堆栈。写在最后这篇文章从最早期的SETNX讲到Redisson的看门狗再讲到RedLock的安全边界这个脉络本身就是分布式锁知识体系的完整骨架。我在实际项目中见过太多团队反复踩同一类坑手写锁不考虑原子性、锁过期时间随意设置、锁内做远程调用。每次线上出问题根因都不是Redis不够快而是用锁的人没想清楚锁的边界条件。所以最后给大家留三句话。第一能用数据库唯一索引和版本号解决的问题别用分布式锁。第二要用分布式锁就用Redisson自己手写要付出很高的维护成本不要觉得写个SET NX EX就是分布式锁的全部。第三锁的设计要考虑的是整个系统的兜底方案锁失效了业务能不能自愈这比锁本身更重要。如果这个系列能坚持看到这里下一篇继续聊Redis相关的高阶话题。