1. 一次“明明加了锁”的线上事故,让我重新认识分布式锁
先讲个让我加班到凌晨的真实事故。
当时我们有个订单超时自动关闭的定时任务,部署在三个实例上,逻辑很简单:每分钟扫一次待支付订单,超时就把订单置为关闭状态。测试环境一直没问题,结果上线后第二天,凌晨两点半,告警群炸了——用户反馈说订单明明已经支付成功,却被系统自动关闭了。
排查日志发现,同一批订单被任务执行了两次:第一个实例扫描时订单还没超时,处理到一半时发生了Full GC,停顿了几秒;第二个实例在同一时间醒来,发现这批订单已经达到超时条件,于是直接关闭了订单。两个实例操作的是同一批数据,却没有互相感知对方的存在。
这个事故的本质,就是多个进程同时访问了共享资源,却没有做互斥控制。所谓的互斥控制,落到工程上就是分布式锁。
很多人一想到分布式锁就说“Redis SETNX一句话的事”,真这么简单的话,就不会有那么多线上事故了。分布式锁要解决的,表面上是一个“加锁”动作,实际上是一整套围绕“互斥、容错、性能、业务语义”的设计问题。这篇我结合自己做分布式锁服务的完整过程,从方案选型、核心设计、代码实现到生产环境的坑,一次讲清楚。
不管你是在面试被问到“Redis分布式锁怎么实现”,还是正在接手一个需要自己实现分布式锁服务的项目,这篇都适合你。我尽量把“为什么这么做”也讲透,而不是只丢结论。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种主流实现方案的演进逻辑与选型决策:数据库、Redis、ZooKeeper
2.1 数据库锁:能满足需求,但上限很低
我接触到的很多老项目,最早的分布式锁就是用数据库实现的。思路很简单:建一张锁表,在需要加锁的时候插入一条记录,释放时删除这条记录。谁插入成功,谁就拿到了锁。
sql复制CREATE TABLE `distributed_lock` (
`lock_key` varchar(64) NOT NULL COMMENT '锁的Key,加唯一索引',
`owner_id` varchar(64) NOT NULL COMMENT '持有者标识',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
加锁就执行 INSERT INTO distributed_lock(lock_key, owner_id) VALUES('order:123', 'instance-1')。由于 lock_key 是主键,多个实例同时插入时只有一个能成功,其他会报主键冲突,这就是互斥了。释放就执行 DELETE FROM distributed_lock WHERE lock_key = 'order:123'。
这套方案的优点是直观,团队里任何一个人都能看懂,不需要额外引入中间件。但它的缺点非常明显:
- 性能差。每次加锁解锁都是一次数据库读写,高并发下来数据库会变成瓶颈。
- 数据库本身可能成为单点。如果数据库挂了,业务也就挂了。
- 没有自动过期机制。如果持有锁的进程崩溃了,锁记录永远留在表里,其他进程永远拿不到锁。虽然可以设计一个
expire_time字段靠定时任务清理,但那已经是补丁式的方案了。
所以数据库锁只适合低并发、对一致性要求不极端的内部系统。比如一个每天跑几次的后台报表任务,用数据库锁完全够用,没必要引入Redis。
2.2 Redis锁:性能与复杂度的平衡点
Redis锁是目前使用最广泛的方案,原因很简单:性能高(纯内存操作,单次加锁耗时通常在1ms内),而且Redis本身在很多团队的基础设施里已经存在,不需要额外运维一个新组件。
它经历了几个演进阶段。最初大家用 SETNX key value 加锁,再用 EXPIRE key seconds 设置过期时间。后来发现这两条命令不是原子的,如果 SETNX 之后、EXPIRE 之前进程崩溃了,锁就永远不会过期。于是Redis提供了 SET key value NX PX expireTime 一条命令原子完成。
再往后,业界又发现删除锁时直接 DEL 会误删别人的锁,于是引入Lua脚本保证“先判断再删除”的原子性。再到Redisson这类客户端,又引入了看门狗自动续期。这些细节我会在下一节展开,这里先记住一个结论:Redis锁的成熟度已经很高,但它的可靠性建立在“Redis主节点不丢数据”的前提下,这不是100%能保证的。
2.3 ZooKeeper/etcd锁:一致性优先的更重选择
如果你对锁的可靠性要求极高,比如同一个风控规则绝对不能并发执行两次,那么ZooKeeper或etcd这类强一致性组件更合适。
ZooKeeper实现分布式锁的原理是临时顺序节点:多个进程同时在一个锁目录下创建临时顺序节点,编号最小的那个获得锁,其他进程监听自己前一个节点的删除事件。持有锁的进程挂了,ZooKeeper会通过会话超时自动删除临时节点,锁自动释放,不需要额外设置过期时间。
etcd的实现方式类似,基于它的Lease租约机制加事务写入来实现。它们的共同点是:锁状态的判定由服务端来保证,而不是靠客户端超时时间。 在“进程失联”这个场景下,ZooKeeper/etcd的确认机制比Redis的“赌一把你会在过期时间内恢复逻辑”更严谨。
代价也很明显:ZooKeeper/etcd本身是高可用的分布式系统,部署、运维、监控的成本都更高;加锁的RTT(往返时延)和数据一致性协议的复杂度决定了它比Redis慢一个量级。
2.4 选型决策:三个问题帮我判断用哪个
我自己的选型习惯是,先问自己三个问题:
- 锁的持有时间通常是多长时间?几毫秒还是几秒?
- 如果锁意外失效,导致两个请求同时执行,后果有多严重?
- 团队里有没有人会维护ZooKeeper/etcd集群?
把答案放到下面的对比表里,基本就能定下来了:
| 维度 | 数据库锁 | Redis锁 | ZooKeeper/etcd锁 |
|---|---|---|---|
| 实现复杂度 | 低 | 中 | 高 |
| 性能 | 差,秒级QPS上限 | 高,万级QPS上限 | 中,千级QPS上限 |
| 自动释放 | 需额外设计 | 过期时间+看门狗 | 会话过期自动释放 |
| 一致性强弱 | 取决于数据库部署 | 主从模式下有窗口 | 强,服务端保证 |
| 依赖的中间件 | 通常已有 | 通常已有 | 可能需要额外引入 |
| 适合场景 | 低并发后台任务 | 大多数业务并发控制 | 对可靠性要求极高的核心链路 |
我见过不少团队在Redis锁和ZooKeeper锁之间反复横跳,其实大可不必。对绝大多数互联网业务来说,Redis锁的可靠性已经足够,你要做的不是追求“绝对不丢锁”,而是把锁的基础实现做对,再在业务层补一道幂等。这一套组合下来,能覆盖99%以上的场景。
3. 实现Redis分布式锁的关键设计细节
这一节是全文的重头戏。如果你已经决定用Redis实现分布式锁服务,那下面每一小节都是你不能跳过的设计点。这些点单看都不难,但组合在一起,才能真正支撑起一个“服务”。
3.1 加锁必须是一条原子命令
用Redis加锁,正确姿势是这一条命令:
bash复制SET lock:order:123 uuid-xxxx NX PX 30000
参数拆开解释:
lock:order:123是锁的key,一般按业务维度设计。uuid-xxxx是持锁者的唯一标识,每次加锁都生成一个。NX表示只有key不存在时才设置成功,这是互斥的关键。PX 30000表示锁的过期时间为30秒,防止持锁进程崩溃后锁永远不释放。
为什么不分成 SETNX + EXPIRE 两条命令?因为Redis命令是单条原子的,两条命令之间不是原子操作。如果 SETNX 执行成功,EXPIRE 还没来得及执行,进程就崩了,锁就会变成一个没有过期时间的永久锁。后面的所有请求都进不来,这就是死锁。
3.2 value必须携带唯一标识,否则会误删别人的锁
很多初版实现里,加锁时value随便填了个数字,比如 SET lock:order:123 1 NX PX 30000,解锁时直接 DEL lock:order:123。这个版本在并发量上来之后必出问题。
场景是这样的:线程A拿到锁,业务处理超过了30秒,锁被Redis自动过期。线程B拿到锁,开始处理同一个订单。此时线程A处理完了,执行 DEL lock:order:123,把B正在持有的锁删掉了。紧接着线程C也拿到锁……三个线程同时进入临界区,分布式锁形同虚设。
解决办法就是给value设一个全局唯一的随机数,解锁时先比较value是不是自己生成的,再决定要不要删除。我在实际项目里用一个 UUID.randomUUID().toString(),考虑到锁数量很大,也可以用一个雪花算法生成的ID,保证唯一性即可。
3.3 解锁要做Lua脚本原子判断
有了唯一标识之后,“判断+删除”这个组合动作依然需要保证原子性。不能先 GET 再 DEL,因为中间可能穿插其他命令。正确做法是用一段Lua脚本:
lua复制if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这段脚本的意思是:只有key的value等于当前持锁者标识时,才删除key;否则直接返回。Redis会把整个Lua脚本作为一条命令执行,中间不会穿插其他命令,所以它是原子性的。
我见过有人问“为什么不能用事务MULTI/EXEC”,区别在于Lua脚本支持复杂的业务判断逻辑,而且执行期间的输入对脚本是可见的,而事务只是批量执行。加锁解锁这种场景,Lua脚本是标准解法,没有争议。
3.4 可重入与锁粒度:给锁加上“主人身份证”
如果你的业务里存在一个方法内部再调用另一个同样需要锁的方法,就会遇到可重入问题。比如方法A加了锁,方法A内部调用方法B,方法B也尝试加同一把锁。没有可重入机制的话,方法B会发现自己把自己锁在外面,导致死锁。
实现可重入有两种常见方式。一种是在value里保存持有者ID加重入计数,比如 uuid-xxxx:2,每次重入加锁时如果发现value的前缀是自己的ID,就把计数加一。另一种是保存一个持有者线程ID的哈希结构,记录每个持锁者的重入次数。
现实中我建议优先审代码,尽量避免在锁内嵌套加锁。因为可重入会把锁的逻辑搞复杂,一旦计数没对上,排查起来的成本远超那点代码量。如果确实需要可重入,用Redisson的 RLock,它原生支持,没必要自己造轮子。
锁粒度又是一个容易被忽略的点。同一个业务里,锁key到底是“全局一把锁”,还是“一个用户一把锁”,性能差异可能是几十倍。比如订单操作,正确设计是 lock:order:userId,而不是 lock:order:all。如果用全局锁,所有用户的操作都要串行,等于把系统的并发能力直接降级成了单线程。锁粒度设计要尽量细,细到锁住最小粒度的临界资源即可。
3.5 看门狗续期:处理锁持有时间超过过期时间的问题
锁过期时间设置成30秒,但业务执行需要60秒,怎么办?锁到期自动释放,其他请求进来了,临界区被并发进入——这是Redis分布式锁最大的隐患,也是最容易被面试官追问的点。
看门狗(WatchDog)机制就是用来解决这个问题的:在锁过期之前,周期性检查持锁线程是否还在运行,如果还在,就自动把锁的过期时间重置。Redisson里默认锁的超时时间是30秒,看门狗每10秒(即锁过期时间的三分之一)执行一次续期。
看门狗的实现不复杂,但有一个关键前提:续期线程必须和持锁线程生命周期绑定。持锁线程执行完,马上取消续期;持锁线程所在的进程崩溃了,JVM退出,续期线程自然停止,锁会在原有过期时间之后自动释放——这才是没有死锁的保证。很多人把看门狗理解成“无限续期”,那是错的。它的目的是让锁在业务需要的时间内持续有效,而不是永远有效。
4. 一套带看门狗续期的分布式锁服务完整实现
这一节给出我自己在项目中落地的一套实现,不是Redisson源码的完整拷贝,而是核心流程的简化版,逻辑完整,可以直接照着搭。代码用Java风格写,原理同样适合其他语言。
4.1 整体模块划分
我把分布式锁服务拆成四个模块:
LockKeyGenerator:负责生成标准化的锁key,统一前缀,避免和其他redis数据冲突。RedisLock:核心锁对象,包含锁的key、持有者标识、过期时间、是否持有锁等状态。WatchDog:后台定时线程,负责在锁过期前自动续期。LockTemplate:提供上层API,tryLock/unlock,屏蔽底层实现细节。
模块之间单向依赖,LockTemplate 依赖 RedisLock,RedisLock 依赖 WatchDog。这样设计的好处是:业务方只需要和 LockTemplate 打交道,方便后期把底层Redis客户端替换成Lettuce或Jedis,业务代码不用动。
4.2 加锁与续期核心代码
这是整个服务的核心。我直接给出关键代码:
java复制public boolean tryLock(String key, String ownerId, long expireMillis, long waitMillis) {
String lockKey = LockKeyGenerator.generate(key);
// 在waitMillis时间内不断尝试加锁
long deadline = System.currentTimeMillis() + waitMillis;
while (System.currentTimeMillis() < deadline) {
String result = jedis.set(lockKey, ownerId, "NX", "PX", expireMillis);
if ("OK".equals(result)) {
// 加锁成功,启动看门狗线程
startWatchDog(lockKey, ownerId, expireMillis);
return true;
}
// 如果锁还没被释放,短眠后重试
Thread.sleep(50);
}
return false;
}
private void startWatchDog(String lockKey, String ownerId, long expireMillis) {
// 续期间隔为过期时间的三分之一
long interval = expireMillis / 3;
WatchDog watchDog = new WatchDog(lockKey, ownerId, expireMillis, interval);
Thread thread = new Thread(watchDog, "watchdog-" + ownerId);
thread.setDaemon(true);
watchDog.setThread(thread);
watchDogThreadMap.put(ownerId, watchDog);
thread.start();
}
看门狗线程的核心循环:
java复制public void run() {
while (!quit.get()) {
try {
Thread.sleep(interval);
// 用Lua脚本原子续期:只允许锁的当前持有者续期
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ "return redis.call('pexpire', KEYS[1], ARGV[2]) "
+ "else return 0 end";
List<String> keys = Arrays.asList(lockKey);
List<String> args = Arrays.asList(ownerId, String.valueOf(expireMillis));
jedis.eval(lua, keys, args);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
这里有个细节值得讲:看门狗续期也必须用Lua脚本判断持有者标识。如果锁已经被其他线程获取(比如很极端地跨过了过期时间),这时续期会把别人的锁的过期时间延长,反而制造问题。加了 if redis.call('get', KEYS[1]) == ARGV[1] 判断,就只给自己的锁续期。
4.3 释放锁与守护线程关闭链路
解锁的逻辑很多人只写 DEL,漏掉了停看门狗。我见过一个事故,就是解锁代码没停看门狗,导致看门狗线程一直续期一个已经不存在的key,等这个key被其他请求重新创建时,续期直接作用在了新锁上。排查了半天才找到原因。
正确的解锁流程应该是:
- 从
watchDogThreadMap中取出当前锁的看门狗,调用stop(),把退出标记置为true并中断线程。 - 执行Lua脚本,先判断持有者标识再删除锁。
- 清理线程缓存,避免线程泄漏。
java复制public void unlock(String key, String ownerId) {
stopWatchDog(ownerId);
String lockKey = LockKeyGenerator.generate(key);
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ "return redis.call('del', KEYS[1]) "
+ "else return 0 end";
List<String> keys = Arrays.asList(lockKey);
List<String> args = Arrays.asList(ownerId);
jedis.eval(lua, keys, args);
}
private void stopWatchDog(String ownerId) {
WatchDog watchDog = watchDogThreadMap.remove(ownerId);
if (watchDog != null) {
watchDog.stop();
}
}
解锁和停看门狗之间有非常短暂的窗口期:看门狗可能还会继续续期一次,但锁已经被删了,下次续期时发现锁不存在,自然就停止了。Redis key不存在时 PEXPIRE 返回0,Lua脚本返回0,不影响任何逻辑。只要 stop() 把退出标记置位,循环必然退出,不会出现线程无限续期的问题。
4.4 封装成注解,让业务方无感接入
落地到业务代码时,直接暴露 tryLock / unlock API虽然灵活,但容易出错——有团队把 unlock 忘在 finally 里了。我后来改成基于AOP的注解方式,把锁获取和释放从业务代码里彻底隔离出去。
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key(); // 锁key,支持SpEL表达式
long expireMillis() default 30000;
long waitMillis() default 1000;
}
java复制@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
String key = parseKey(distributedLock.key(), joinPoint);
String ownerId = UUID.randomUUID().toString();
boolean locked = lockTemplate.tryLock(key, ownerId,
distributedLock.expireMillis(), distributedLock.waitMillis());
if (!locked) {
throw new RuntimeException("获取锁超时");
}
try {
return joinPoint.proceed();
} finally {
lockTemplate.unlock(key, ownerId);
}
}
业务代码就变成一行注解,不用关心加锁解锁细节了。
java复制@DistributedLock(key = "order:close:#userId", waitMillis = 200)
public void closeOrder(Long userId) {
// 业务逻辑
}
如果用的Spring Boot,配置好AOP切面即可直接翻用。如果项目里没有Spring AOP,也可以自己写一个装饰器模式的代理,思路是一样的。
5. RedLock这场争论,给我的实际教训
5.1 争议双方到底在争什么
提到Redis分布式锁,绕不开RedLock。RedLock是Redis作者Antirez提出的分布式锁算法:在多个独立的Redis节点上尝试获取锁,只有当超过半数的节点都加锁成功时,才认为整体加锁成功。
这个方案引来了分布式系统专家Martin Kleppmann的质疑。他的核心论点是:即使使用RedLock,在GC(垃圾回收)停顿或进程暂停的场景下,锁仍然可能失效。 他举了个例子:客户端A请求所有Redis节点加锁成功,随后发生了一次长时间GC停顿,锁在GC期间过期了。客户端B同样也拿到了锁。GC结束后,客户端A继续执行,这时候A和B同时处于临界区。RedLock在这种模型下并没有解决客户端进程停顿带来的问题。
Antirez的回应则强调,现实中不存在一个能在所有极端条件下都严格正确的分布式锁,RedLock在合理的安全模型下是可靠的,如果威胁模型包含“客户端本身暂停任意长的时间”,那任何基于超时的锁都无法保证安全。
5.2 这场争论对工程实践的启示
我自己从头到尾做了RedLock的调研和实践,结论是:RedLock能解决的是“Redis节点故障导致锁丢失”的问题,不能解决“客户端进程暂停”的问题。 你要清楚自己面对的是哪一种风险。
如果团队现状是只有一个Redis主从集群,最现实的优化方案不是上RedLock,而是:
- 在业务层增加幂等兜底,比如数据库唯一约束、状态机校验。
- 对锁的过期时间做精细化设置,过期时间应该是业务预计最大耗时的1.5倍以上。
- 引入看门狗机制,让锁的续期由客户端自动处理。
如果你的业务场景是“锁的失效会造成资金损失或者数据污染”,比如支付回调、库存扣减、风控规则执行,那我建议直接用etcd或者ZooKeeper。不要为了用Redis而用Redis,系统设计里没有银弹,只有最合适的权衡。
5.3 我最后选择的方案
在大多数业务项目里,我的默认方案是:Redis单主节点 + 唯一value + Lua原子解锁 + 看门狗续期,业务层补幂等。只有在满足“锁失效后果极其严重”并且“团队有能力运维etcd”这两个条件时,才上强一致性组件。这套组合下来的成本收益比最合理。
6. 我踩过的四个锁的坑:完整排查链路与修复方案
这一节写的都是真实踩坑经历。分布式锁的坑往往不会出现在刚上线的第一天,而是出现在业务量增长后的某个凌晨。
6.1 坑一:锁被A线程删掉了B线程的执行
现象:线上出现同一订单被并发处理,日志显示两个线程都进入了临界区。
排查过程:我最初的疑惑是“加锁明明成功才进临界区,为什么会有两个?” 后来看日志发现两个线程的锁value不一样,但解锁代码里的判断逻辑已经被我加上了啊。查代码才发现,我加的唯一标识判断只加在新写的 unlock 方法里,但还有一个历史遗留的调用点直接调了 jedis.del(lockKey),没有走新的解锁逻辑。等于有个后门没关。
修复方案:全局搜索所有 del 的调用,统一走 LockTemplate.unlock;在Redis的慢查询日志里也确认了解锁走的是Lua脚本。这个坑给我最大的教训是:分布式锁的所有代码路径,必须保证只有一个入口一个出口。
6.2 坑二:业务执行超过锁过期时间,导致锁提前失效
现象:某个报表生成任务,数据量达到百万级,业务执行时长从平时的10秒涨到了50秒,但锁的过期时间设的是30秒。结果两个实例同时开始生成报表,数据库IO被打满。
排查过程:一开始怀疑是看门狗没生效,后来看代码发现,这个任务根本没走我的 LockTemplate,而是直接用RedisTemplate SETNX加的锁,压根没接看门狗。而接看门狗的业务都正常。
修复方案:所有加锁操作统一走 LockTemplate,修正过期时间为业务预估最大耗时的2倍,同时保留看门狗作为兜底。另外,我对所有长时间任务加了慢日志告警,超过锁过期时间80%就能提前发现。
6.3 坑三:Redis主从切换的窗口期锁丢失
现象:差点出事。Redis主节点发生故障,从节点在几毫秒内提升为新的主节点。就在这个窗口期,一个持锁者的锁记录还没来得及同步到从节点,另一个请求对新主节点加锁成功。两个请求同时进入临界区。
排查过程:这个场景如果只查应用日志,你看到的是两个请求的加锁命令都返回了成功。最后是通过Redis的复制日志和故障切换记录定位到的。那一刻我才意识到,Redis主从之间的异步复制,决定了锁在这种极端情况下不是绝对可靠的。
修复方案:当时业务对锁的失效容忍度较低,我在两个方向做了处理。一是把锁的过期时间适当调大,减少窗口期的概率;二是在业务层增加了一个“执行前检查订单状态”的幂等判断,即便锁失效了,后到的请求也会因为状态不对直接返回。这是成本最低的兜底。
6.4 坑四:锁粒度太大,接口性能出现断崖式下跌
现象:压测时发现下单接口的TPS从500掉到了30,数据库连接池被打满。
排查过程:刚开始以为是数据库慢查询,看了一遍SQL都没问题。后来用Arthas在线分析线程堆栈,发现大量线程阻塞在同一个Redis请求上。再一看锁key的设计,原来是把整个下单流程设计成了 lock:order:all 这种全局锁,所有用户的订单操作全部被串行排队。一个用户操作慢,所有人跟着等。
修复方案:按用户维度重新设计锁key为 lock:order:{userId},同时通过SpEL表达式从请求参数里动态解析用户ID。改造后TPS恢复到接近压测前的水平。
6.5 排查分布式锁问题的通用思路
统计下来,识别“是不是锁的问题”最快的方式是看两点:一是并发量有没有突增,二是日志里同一关键业务是否出现两个不同线程同时执行的记录。确认是锁的问题后,按这个顺序排查:
- 加锁代码有没有走统一入口,有没有绕过封装的方法。
- 锁的value是否唯一,解锁是否用了Lua脚本。
- 锁的过期时间、看门狗续期有没有按预期工作。
- 锁key的粒度是否合理,是否存在全局锁。
- Redis的部署架构是单机、主从还是集群,主从切换会不会造成锁丢失。
把这一套检查完,90%的分布式锁问题都能定位到根因。
最后,关于分布式锁的本质
在我自己做过这个服务之后,最大的体会是:分布式锁的可靠性不取决于某一行加锁代码,而取决于整个链路上所有细节的完整性。 从原子加锁、唯一标识、Lua解锁,到看门狗续期、锁粒度设计、业务幂等兜底,任何一环漏掉,锁都会在某些极端时刻失效。
如果你也准备自己实现一个分布式锁服务,我建议不要一上来就追求把所有功能都做进去,先实现“SET原子加锁 + UUID标识 + Lua解锁”这个最小可用版本,跑通之后再逐步加看门狗、可重入、注解封装这些增强功能。每加一个功能,都要想清楚它解决的是什么问题、引入什么新风险。
还有个小技巧,生产环境一定要监控锁的申请耗时和锁等待时长。锁正常的时候,申请耗时通常在1ms以内,一旦超过10ms,就要看是不是锁粒度问题,或者Redis出现了网络抖动。提前暴露问题,比出了事故再排查要省心得多。
