☰
Redis分布式锁从手写到Redisson:超卖问题、死锁避坑与看门狗续期
2026/10/8 9:55:05 网站建设 项目流程

有次半夜线上告警,我维护的商品秒杀系统出现超卖:库存明明只剩 3 件,同时涌进来的 5 个请求却都扣单成功了。代码翻来覆去查了几遍,本地用synchronized锁得好好的,怎么一部署到多实例集群就失效?这个问题几乎是每个后端都会撞上的坎:你锁住的是当前 JVM 里的线程,却管不住集群另一台机器上的请求。Redis 分布式锁的价值就在这里——用 Redis 作为多实例共享的锁协调者,让不同进程之间也能互斥。这篇文章我会从手写实现开始,把死锁、误删、不可重入、无法续期这些坑一个一个踩过,再对比 Redisson 的成熟方案,讲清楚为什么生产环境最终都会落到 Redisson 上。

1. 从一次超卖事故看分布式锁的边界

1.1 单机锁在多实例环境下的失效原因

先还原当时的现场。秒杀接口的伪代码大概是这样的:

synchronized (this) { int stock = getStockFromDB(); if (stock <= 0) { throw new RuntimeException("已售罄"); } reduceStock(); }

单机部署时没有任何问题,因为 JVM 内部只有一个锁对象。但上了 Nginx 负载均衡之后,请求被分发到 A、B 两台服务实例上。A 机器在synchronized里扣库存的同时,B 机器上的线程根本感知不到 A 的锁——它们是两个不同的 JVM 进程,锁对象天然隔离。库存被扣成负数,往往就是 A、B 同时进入了临界区。

所以要明确一点:分布式锁并不是替代synchronized/Lock,而是解决"跨进程互斥"这个单机锁根本覆盖不了的问题。判断一个场景是否需要引入分布式锁,就看你操作的资源是否被多个服务实例同时访问、并且需要保证互斥。

1.2 分布式锁需要满足的三个核心约束

很多教程一上来就丢代码,容易让人忽略背后的设计目标。我把分布式锁的基本要求拆成三点,后续不管是手写还是选型,都围绕它们展开:

  • 互斥性:任意时刻,只能有一个客户端持有锁,这是锁的立身之本。
  • 安全性:锁必须最终可释放,不能因为持有者宕机就变成死锁,所以要有过期时间兜底。
  • 可用性:加锁、解锁过程要快,不能因为锁服务本身拖垮业务;同时要尽力避免把锁错误释放。

这三条听起来简单,实操中每条都有暗坑。第一条对应 Redis 的原子命令,第二条对应过期时间的设计,第三条对应释放锁时的持有者校验。缺了任何一条,你都会在某个凌晨被线上事故叫醒。

1.3 Redis 为什么能承担锁的职责

在分布式锁的选型中,Redis 不是唯一答案:ZooKeeper 用临时顺序节点 + Watch 机制做锁,etcd 用租约和 Revision 机制,都能实现。但 Redis 胜在简单、高性能、基础组件普及率高。大部分团队本来就有 Redis 做缓存,不需要为锁单独引入一套新中间件,加锁解锁的一次内存操作通常是亚毫秒级。代价是 Redis 的锁不如 ZooKeeper/etcd 那样具备强一致语义——这个缺陷后面讲主从切换时再展开。

2. 手写第一版分布式锁:能跑和能用是两回事

2.1 最简实现:Redis setnx 加锁 + delete 释放

如果你搜过"Redis 分布式锁最简实现",大概率会看到setnx加锁、del解锁的版本。我最初也是这么写的:

// 加锁 Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1"); if (Boolean.TRUE.equals(locked)) { // 业务逻辑 } finally { // 释放锁 redisTemplate.delete(lockKey); }

setnx的意思是 set if not exists:key 不存在时写入成功,返回 true;key 已存在时写入失败,返回 false。这天然满足互斥性。解锁用delete,把 key 删掉,下一次请求就能加锁成功。在"只有一个线程、从不宕机、业务秒级完成"的理想世界里,这个版本够用了,但现实不是。

2.2 第一个致命问题:持有者宕机导致死锁

上面代码最大的隐患是:如果加锁成功的线程在执行业务时进程崩溃,或者发生了长时间的 GC / 网络抖动,finally里的delete根本不会执行。锁 key 会一直留在 Redis 里,后续所有请求都会因为加锁失败而被卡死。这就是死锁。

解决思路也很直接:给锁设置过期时间,让 Redis 在超时后自动清理。很多人会写成两步:

Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1"); redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); // 单独设置过期时间

这样写有一个原子性问题:setIfAbsent和expire是两次 Redis 命令,中间如果服务宕机,过期时间没设置成功,锁照样不会被自动清理。正确做法是用一条命令同时完成"不存在才写入"和"设置过期时间"两个动作:

Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);

Redis 从 2.6.12 开始支持在一条SET命令中组合NX和EX参数,setIfAbsent带超时时间底层就是这个命令,原子性有保障。

2.3 第二个致命问题:误删别人持有的锁

加了过期时间之后,又冒出新问题:假设线程 A 获取锁后业务执行超过了 30 秒,锁自动过期释放。此时线程 B 获取到同一把锁,开始执行自己的业务。A 终于跑完了,执行delete把锁删掉——但它删的是 B 的锁。

这就是误删。后果比死锁更隐蔽:A 释放锁后,线程 C 也能马上获取锁。同一时刻 B 和 C 同时持有锁,互斥性被彻底破坏。

要解决误删,必须在加锁时给锁设置一个只有自己知道的标识,删除前校验这个标识是否还属于自己。这就是下一版本手写锁的核心改进。

3. 手写锁进阶:UUID 标识、Lua 脚本与可重入

3.1 加锁时写入唯一标识,解锁前校验持有者

改进后的逻辑很清晰:加锁时value使用一个全局唯一的 UUID,解锁前先查询当前锁的 value 是否等于自己的 UUID,相等才删除。

String lockKey = "lock:order:" + orderId; String lockValue = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); // 解锁 String currentValue = redisTemplate.opsForValue().get(lockKey); if (lockValue.equals(currentValue)) { redisTemplate.delete(lockKey); }

这段代码比第一版严谨,但还不够。get和delete是两次独立操作,中间存在着时间窗口:如果当前锁刚好在这个窗口内过期,其它线程获取了锁并且写入了新的 value,那当前的delete依然会删掉别人的锁。只是概率变低了,不等于解决了。

3.2 Lua 脚本保证"校验 + 删除"的原子性

要彻底解决这个窗口,得让"比较 value 是否相等,相等才删除"这两个步骤在 Redis 端原子执行。Redis 的 Lua 脚本天然具备原子性,因为脚本执行期间不会被其它命令插入。

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

Java 侧用DefaultRedisScript执行:

DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>( "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class ); Long result = redisTemplate.execute(unlockScript, List.of(lockKey), lockValue); // result == 1 表示成功释放

这是手写锁的一个重要分水岭:判断和删除必须作为一个原子操作提交给 Redis。只要这两个动作还是分开的两次命令,你就永远有一个理论上的误删漏洞。这里我踩过一个坑:用DefaultRedisScript执行 Lua 时,如果lockValue走的是 JDK 序列化,到了 Redis 端字符串比较可能对不上,最好把 key 和 value 的序列化方式统一成字符串,用StringRedisTemplate或者显式配置StringRedisSerializer。

3.3 可重入锁的 Hash 结构实现

上面的锁还不支持可重入。什么叫可重入?就是一个线程已经持有锁的情况下,方法递归或者嵌套调用同一个锁时,应该能再次获取成功。synchronized天然具备这个能力,但SETNX方案不行:同一个线程第二次加锁时,key 已经存在,会加锁失败。如果一个锁会锁住另一个需要同一把锁的方法,就会直接死锁。

支持可重入的经典实现是把锁数据从简单字符串改成 Hash 结构。Hash 的 field 记录持有者的唯一标识,value 记录重入次数:

-- 加锁 if redis.call("exists", KEYS[1]) == 0 then redis.call("hset", KEYS[1], ARGV[1], 1) redis.call("pexpire", KEYS[1], ARGV[2]) return 1 end if redis.call("hexists", KEYS[1], ARGV[1]) == 1 then redis.call("hincrby", KEYS[1], ARGV[1], 1) redis.call("pexpire", KEYS[1], ARGV[2]) return 1 end return 0
-- 释放 if redis.call("hexists", KEYS[1], ARGV[1]) == 0 then return nil end local counter = redis.call("hincrby", KEYS[1], ARGV[1], -1) if counter > 0 then redis.call("pexpire", KEYS[1], ARGV[2]) return 0 else redis.call("del", KEYS[1]) return 1 end

每次重入把计数加一,释放时计数减一,减到零才真正删除 key。这个思路和 JDK 的ReentrantLock几乎一致,只不过状态从 JVM 内存搬到了 Redis。注意:重入次数必须是完整"加锁几次、释放几次"的严格配对,否则计数永远减不到零,会出现锁泄漏。

3.4 手写锁无法优雅解决的最后一块拼图:续期

到这里,手写锁已经解决了死锁、误删、原子性和可重入,但还剩一个最头疼的问题:锁的过期时间定多少合适?

定太短,业务一慢锁就自动过期,导致并发进入临界区;定太长,持有者宕机后锁要很久才能被释放,其它请求会被阻塞很久。更麻烦的是业务耗时本身不可控:一次完整的数据库查询、远程调用、消息发送,实际耗时可能从几十毫秒到几秒。无论你预设一个多大的值,都无法覆盖所有场景。

成熟的方案是"自动续期":加锁后开启一个后台任务,在锁快过期时主动延长它的过期时间;解锁或任务结束后停止续期。这个机制说难不难,但涉及定时任务的启停、线程安全、Redis 异常后的容错,自己做很容易写出 bug。手写锁到这一步基本到了极限,这也是我最终转向 Redisson 的直接原因。

4. 为什么生产环境要选 Redisson:别重复造锁的轮子

4.1 Redisson 解决的不只是续期问题

Redisson 是 Redis 官方推荐的 Java 客户端,锁只是它众多分布式服务中的一个模块。它把前面手写锁演进过程中踩过的坑全部封装好了:

  • RLock接口贴合 JDK 的Lock设计,加锁、解锁、尝试锁的语义和ReentrantLock高度一致,学习成本低。
  • 加锁、可重入计数、释放锁全部基于 Lua 脚本实现,原子性由 Redis 保证。
  • 内置看门狗(WatchDog)自动续期机制,不用自己维护定时任务。

4.2 选 Redisson 而不选自己维护锁代码的理由

我在团队里做过一次小调查,绝大多数项目最终都有各自的手写锁工具类,但普遍存在几个问题:加锁解锁逻辑分散在多个 Service 里、异常分支处理不一致、Redis 连接池参数和业务代码耦合、Lua 脚本没有统一管理。用 Redisson 之后,锁的获取和释放是一个标准动作,配置集中管理,出了问题有社区兜底,遇到疑难杂症还能翻源码。除非你的诉求只是"临时加一个不重要的锁,不想引入依赖",否则我不建议长期维护一套自研锁。

4.3 引入 Redisson 的成本有多低

成本其实非常低。Maven 加一个依赖:

<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.27.2</version> </dependency>

然后注入RedissonClient就能用。相比自己写工具类、自己处理序列化、自己写 Lua,这个迁移成本几乎可以忽略。

5. Redisson 分布式锁的工作机制拆解

5.1 lock() 与 tryLock():阻塞与超时控制

Redisson 的RLock提供了两组核心方法:

RLock lock = redissonClient.getLock("lock:order:" + orderId); // 方式一:阻塞等待 lock.lock(); // 方式二:超时等待 boolean locked = lock.tryLock(3, TimeUnit.SECONDS);

lock()会一直阻塞当前线程,直到获取锁成功。这种方式适合内部短任务,但如果 Redis 不可用或锁被人长时间持有,当前线程就会一直挂起,生产环境慎用。tryLock(waitTime, timeUnit)会最多等待waitTime时间,超时后加锁失败返回 false,你可以根据返回值决定走"系统繁忙"分支。这两种方式如果都没有显式指定 leaseTime,Redisson 会启用看门狗自动续期,锁不会因为业务跑太慢而中途失效。

另一个重载是tryLock(2, 30, TimeUnit.SECONDS),第二个参数表示锁的最大持有时间。一旦指定了 leaseTime,看门狗就不会续期,到时间锁强制释放。所以如果你选择这个重载,leaseTime 必须大于预估的最大业务耗时,否则会出现锁被强制回收导致的并发问题。

5.2 看门狗续期原理:默认 30 秒锁,每 10 秒续一次

看门狗是 Redisson 分布式锁最核心的卖点。默认配置下,lockWatchdogTimeout是 30 秒。当没有显式指定锁超时时间时,Redisson 加锁成功后,会启动一个后台定时任务:每隔内部锁租约时间 / 3,也就是大约 10 秒,把锁的剩余过期时间重新刷新为 30 秒。

这意味着什么?你的业务如果跑了 20 秒,看门狗会在第 10 秒、第 20 秒各续期一次,锁在业务执行期间不会过期。当业务执行完,你调用unlock()主动释放锁,看门狗任务同步取消。这个机制把"锁超时时间"这个问题彻底从业务代码里抹掉了。

但要注意看门狗不是万能的:如果 Redis 发生长时间网络故障,续期命令发不出去,锁到了时间照样会过期;如果业务线程在持锁期间一直阻塞(比如lock()之后又去等一个永远不会完成的网络调用),看门狗会一直续期,锁就一直释放不了。所以后面我会推荐在生产上用tryLock(waitTime)加显式解锁组合,而不是裸用lock()。

5.3 加锁解锁的 Lua 脚本:一个操作串起重入与销毁

Redisson 的加锁、解锁逻辑封装在RedissonLock里,核心是几个 Lua 脚本。加锁脚本的思路和我前面手写的 Hash 可重入锁一致:判断 key 是否存在、判断持有者标识是否存在、重入计数加一、设置过期时间。区别在于它把"锁 key、持有者标识、过期时间"规范化成了完整的上锁模板。

解锁脚本中判断hexists和hincrby减一、计数归零才del,这些动作在 Redis 端一次原子完成,天然不会误删别人的锁。而且 Redisson 在解锁时会校验当前线程是否持有这个锁,避免在 finally 里对一把已经不归自己的锁执行unlock()抛出异常。

5.4 多实例下 Redisson 的部署形态

Redisson 支持单节点、哨兵、集群、主从等多种模式。日常开发用单节点最省事:

@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setConnectionPoolSize(32) .setConnectionMinimumIdleSize(8); return Redisson.create(config); } }

生产环境如果是哨兵 / 集群部署,改成useSentinelServers()或useClusterServers()就行。注意一点:RedissonClient全局只创建一个实例,不要每次加锁都new Config(),连接池会被打爆。

6. 生产落地:Redisson 接入与业务代码范式

6.1 标准加锁模板:tryLock、业务、finally 的黄金组合

我所在项目里最终沉淀了一套标准写法,所有使用分布式锁的地方都按这个模板走:

@Component public class OrderLockService { @Autowired private RedissonClient redissonClient; public void deductStock(Long orderId) { String lockKey = "lock:stock:" + orderId; RLock lock = redissonClient.getLock(lockKey); boolean locked = false; try { locked = lock.tryLock(2, TimeUnit.SECONDS); if (!locked) { throw new BizException("系统繁忙,请稍后再试"); } // 真正的业务逻辑:查库存、扣库存、写订单 doDeductStock(orderId); } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }

几个值得解释的细节:

  • 使用tryLock(2, TimeUnit.SECONDS),最多等两秒。拿不到锁就快速失败,不让请求无限阻塞。
  • finally里必须判断isHeldByCurrentThread(),防止当前线程加锁失败时去解锁一把不存在的锁,或释放掉其它线程持有的锁。
  • lockKey使用lock:前缀加上业务标识,便于在 Redis 里排查问题。业务标识尽量用订单号、用户 ID、商品 ID 这类有业务含义的值,不要让无关请求互相阻塞。
  • 锁粒度越细越好。对"lock:stock:" + orderId加锁,不同订单的并发互不影响;对全局"lock:stock"加锁,性能会急剧下降。

6.2 锁超时后业务还没跑完怎么办

很多人担心一种情况:业务用到tryLock(waitTime)时没有指定 leaseTime,看门狗会自动续期,最终锁会一直活着直到unlock()。这样写其实是安全的,因为锁的拥有者始终是你自己。但如果你显式指定了leaseTime = 10,业务跑了 15 秒,锁在第 10 秒过期,另一个线程拿到锁进入临界区——业务数据就有被重复处理的风险。

针对这类场景,我的做法分两层:第一层,能用看门狗续期的就尽量不指定 leaseTime;第二层,确实要指定 leaseTime 的话,必须结合业务耗时压测给出余量,并配套状态字段做幂等(比如订单状态位 change by 条件更新)。分布式锁不能包治百病,幂等和乐观锁永远是最后一道防线。

6.3 用自定义注解 + AOP 收敛锁逻辑

业务代码散落着tryLock/unlock模板也很容易失控。我后来用自定义注解做了一层封装,把锁逻辑收敛在切面里:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { String key(); long waitTime() default 2; }
@Aspect @Component public class RedisLockAspect { @Autowired private RedissonClient redissonClient; @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String lockKey = redisLock.key(); RLock lock = redissonClient.getLock(lockKey); boolean locked = lock.tryLock(redisLock.waitTime(), TimeUnit.SECONDS); if (!locked) { throw new BizException("系统繁忙,请稍后再试"); } try { return joinPoint.proceed(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }

使用的时候只需要在方法上标注:

@RedisLock(key = "lock:order:#orderId") public void deductStock(Long orderId) { // 业务逻辑 }

这个方案看起来清爽,但有一个坑:切面里拿不到方法参数时,key 没法动态拼接。上面的示例用了 SpEL 表达式#orderId,需要在切面里解析参数名和参数值,实现起来略繁琐。如果觉得暴力,可以约定key()支持#参数名占位,在切面里用SpelExpressionParser解析。需要我细讲这块可以单独开一篇,这里先点到为止。

7. 分布式锁的高危边界与真实踩坑记录

7.1 锁没等来就超时返回:waitTime 设计误区

有一次压测时发现部分请求频繁抛出"系统繁忙",我打开链路日志一看,加锁失败全是因为waitTime设成了 100 毫秒。高并发场景下同一把锁被持有的时间稍微长一点,后续请求就会大量超时失败。这不是锁的问题,是等待时间参数不合理。

waitTime的设定取决于两个因素:单次获取锁后的平均持有时间、你希望请求在锁竞争时阻塞多久。如果业务大多在 100 毫秒内完成,waitTime设为 300~500 毫秒是比较合理的;如果业务要跑 1 秒以上,waitTime至少要留到 2 秒。压测时观察一下加锁成功率和响应时间分位数,再反推调整参数。

7.2 看门狗续期失败的真实表现

一次生产事故中,Redis 主节点出现短暂抖动,持续了约 15 秒。那段时间有业务线程已经持有锁超过 25 秒,看门狗续期命令因连接异常没发出去。锁到期释放后,同一资源被两个线程同时处理,产生了重复退款。

复盘时确认:看门狗并不是"一旦启用就永远不死锁"的银弹,它依赖 Redis 的可用性。如果你们的业务对极端一致性要求极高,只靠 Redis 锁不够,还需要在业务侧加状态机约束。我把这次事故的结论写进了团队规范:分布式锁只能作为常规互斥手段,不能作为唯一的一致性保障;涉及资金、库存这类强校验场景,一定要配数据库行锁或乐观锁做兜底。

7.3 主从切换、故障转移导致的锁丢失

这是 Redis 分布式锁最出名的软肋。场景是这样的:客户端 A 在主节点上获取了锁,但这条写命令还没同步到从节点,主节点就挂了。哨兵把所有从节点中的某一个提升为新主节点,但新主节点上没有 A 的锁记录。此时客户端 B 去加锁,它能成功加锁,导致 A 和 B 同时持有锁。

面对这个问题的业界方案叫 RedLock:同时向多个独立的 Redis 节点请求锁,超过半数节点加锁成功才认为获得锁。Redisson 也提供了RedissonRedLock。但 RedLock 本身也有很大争议,因为它把问题从"一个 Redis 的强一致"转移成"多个 Redis 之间的多数派一致",实现复杂、性能开销大,生产环境真正采用的很少。我个人的做法是:单 Redis 实例部署时接受这个风险,因为锁服务本身就不是强一致存储;对一致性要求苛刻的场景,直接考虑 ZooKeeper / etcd 那套方案,不要在 Redis 上硬扛。

7.4 锁内做远程调用、锁外做事务提交的组合风险

最容易被忽略的一个坑是锁的粒度覆盖不到事务提交。很多代码长这样:方法上有@Transactional,事务提交发生在方法返回之后;但unlock()是在方法内的 finally 里执行的。也就是说,锁先释放了,事务后提交。另一个线程在锁释放后立刻拿到锁去查数据库,它可能查不到刚才那个事务还没提交的数据,又会重复处理一笔订单。

解决思路有几个:一是把锁和事务边界收窄,锁内只做必要操作,事务提交也尽量放在锁内;二是利用 Spring 的TransactionSynchronizationManager注册事务提交完成后的回调,在事务真正提交后再unlock()。这个坑做支付对账时尤其致命,我建议团队里所有涉及分布式锁 + 数据库事务的代码都按"先事务后解锁"的原则评审。

8. 面试官真正想考的点,和你该记住的判断标准

8.1 高频问题:手写锁哪里不行

面试官问"用 Redis 实现分布式锁",通常不是真想听你背出SETNX的用法,而是想看你有没有踩过后续的坑。一个合格的回答链路是:先给出最简版,然后主动指出死锁问题,加上过期时间;再指出误删问题,引入 UUID;再指出"判断 + 删除"不是原子操作,引入 Lua;再指出不可重入,引入 Hash 计数;最后指出过期时间不好定,引出 Redisson 的看门狗。能完整走完这条链路,基本就展示出了真实的分布式锁实践经验。

8.2 高频问题:看门狗的实现细节

看门狗的底层实现大致是:加锁成功且没有指定 leaseTime 时,Redisson 启动一个ScheduleService定时任务,每隔lockWatchdogTimeout / 3毫秒执行一次续期 Lua 脚本,把 key 的过期时间重新设为lockWatchdogTimeout;每次执行业务解锁时,取消续期任务。关键点在于:续期是基于独立的定时任务触发的,不是基于业务线程的 sleep,所以业务方法里怎么耗时都不影响续期。但定时任务执行失败时,锁会过期,所以不能完全依赖看门狗。

8.3 高频问题:Redisson 为什么用 Lua 脚本

这个问题的标准答案是:Redis 执行 Lua 脚本是原子的,脚本执行期间不会被其它命令插入,所以"判断持有者、重入计数、设置过期时间"这些多个步骤合并成一个操作,避免了并发条件下的竞态。如果你把答案展开到"手写版本的缺陷 + 自研脚本的维护成本 + Redisson 社区版成熟度",就比死记硬背强很多。

8.4 我判断一个分布式锁方案是否成熟的五个标准

最后分享一个我总结的判断标准,新项目引入任何分布式锁方案时都会对着过一遍:

  • 互斥是否可靠,加锁和解锁是否都通过原子命令 / Lua 完成。
  • 持有者宕机后锁是否能自动释放,有没有过期时间。
  • 是否支持重入,会不会在同线程嵌套调用时死锁。
  • 锁的超时策略是否可控,业务耗时变化时有没有续期机制兜底。
  • 异常分支是否处理干净,比如tryLock失败、unlock抛错、Redis 故障等场景。

按这个标准回头看我手里曾有过的自研锁工具类,第四条就直接被判不合格。这其实也是我最终死心塌地切到 Redisson 的原因——看起来只是"换一个客户端",实际上是把一套经过大规模验证的分布式锁语义完整接到项目里。用的时候少一点自己发挥,把标准模板严格落实,线上就能少折腾很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询