1. 一个锁引发的线上事故:为什么要聊分布式锁
先说个我自己的真实经历。几年前做电商订单系统,用户下单后要扣库存、发优惠券、生成履约单,好几个服务要协同改数据。单机时代靠synchronized就能锁住临界区,后来拆了微服务,库存服务单独部署了三台实例,问题立刻来了。
某次大促,同一个商品SKU的库存瞬间被打穿,超卖了几十单。查日志发现,三个实例同时执行了库存扣减逻辑,synchronized锁住的只是各自JVM内部的线程,跨进程完全失控。那次事故之后,我花了整整一周研究分布式锁,踩了不少坑,也总结出一套相对成熟的落地思路。
所谓分布式锁,本质就是在多个进程、多台机器之间达成"互斥"共识的一种机制。它要解决的核心问题很直接:当多个客户端同时操作共享资源时,如何保证同一时刻只有一个客户端能拿到执行权限。
这类问题几乎每个做后端开发的都会遇到,尤其是涉及订单、支付、库存、优惠券这类强一致场景。不管你是刚入门分布式系统的新人,还是正在准备面试的候选人,把分布式锁的原理和实现吃透,都是一个绕不开的核心技能点。
这篇文章不打算罗列一堆理论,而是从实际踩坑经验出发,把分布式锁的几种主流实现方案、核心原理、完整落地步骤,以及我在生产环境里遇到的各种诡异问题,一次讲清楚。无论你是要用Redis、ZooKeeper还是数据库实现,读完都能直接上手去搭一套能用的方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方案选型背后:Redis、ZooKeeper、数据库到底怎么选
2.1 为什么Redis能成为分布式锁的主流选择
先聊聊技术选型。目前业界主流的分布式锁实现方案有三种:基于Redis、基于ZooKeeper、基于数据库。我见过很多团队一上来就用Redis,理由是"我们本来就有Redis集群,顺手就用了"。这个理由听起来随意,但实际从原理上看,Redis确实是性价比最高的选择。
Redis实现分布式锁的核心依赖是它的单线程命令执行模型和原子操作。Redis是单线程处理命令的,多个客户端的命令到达服务端后会排队依次执行,这天然就避免了并发竞争问题。再配合SETNX(SET if Not eXists)这类原子指令,多个客户端同时抢锁时,Redis能保证只有一个客户端设置成功,互斥性就有了着落。
另一个关键点是性能。分布式锁的获取和释放通常频率很高,Redis基于内存操作,单次加锁、解锁的耗时在毫秒级别,对业务请求的影响微乎其微。
对比其他方案:
- 基于ZooKeeper的锁,依靠临时顺序节点和watcher机制实现,可靠性确实更强,但ZK集群的部署运维成本高,一次会话建立、节点创建、监听通知的完整链路,耗时比Redis多一个量级,高并发场景下不占优势。
- 基于数据库的锁,通常用唯一索引或
for update行锁实现,实现简单、没有额外组件依赖,但性能是三者中最差的,而且数据库本身就是瓶颈高发点,拿它做锁,等于给核心链路又加了一层压力。
我对选型的建议是:业务优先、组件次之。如果系统里本来就有Redis,且对性能要求高,选Redis;如果对可靠性要求极高,比如涉及资金类的强一致场景,且能接受ZK的运维成本,选ZK;如果只是低并发的内部管理系统,不想引入额外组件,数据库唯一索引方案也完全够用。
2.2 Redis分布式锁要满足的四个核心条件
无论用哪种方案,一个合格的分布式锁都必须满足四个条件,这也是面试官最常考的点:
互斥性。同一时刻只能有一个客户端持有锁。这是分布式锁的立身之本,做不到互斥,整个方案都是空中楼阁。
安全性。锁只能被持有者自己释放,不能出现客户端A的锁被客户端B释放的情况。这个条件非常容易被忽略,很多生产事故都是因为释放锁时没有校验持有者身份导致的。
死锁避免。锁必须设置过期时间,防止客户端获取锁后崩溃或网络异常,导致锁永远无法释放,后续所有请求全部阻塞。
容错性。只要Redis集群中的大部分节点正常运行,客户端就能正常加锁、解锁。这一点在集群模式下尤其重要,如果主节点挂掉后锁数据还没同步到从节点,就可能出现"锁丢失"的经典问题。
后面讲代码实现时你会发现,每一个条件都对应着一个具体的代码细节,缺一个都会在特定场景下翻车。这也是我建议大家在设计阶段就把这四个条件写在文档里的原因,写代码时逐条对照,能避开至少一半的坑。
3. 从SETNX到Redlock:Redis分布式锁的原理拆解
3.1 SETNX加锁的演进过程与参数设计
Redis加锁的API经历了一个演进过程。早期版本的SETNX key value只能设置键,无法直接设置过期时间,所以标准的加锁代码长这样:
java复制// 早期写法:两步操作,非原子
boolean locked = redisTemplate.opsForValue().setIfAbsent(key, value);
if (locked) {
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
}
这段代码有一个致命问题:setIfAbsent和expire是两个独立的Redis命令,中间如果客户端宕机,锁就永远没有过期时间,直接变成死锁。
后来Redis官方针对这个问题,在2.6.12版本后支持了SET key value NX EX seconds的多参数形式,把"不存在才设置"和"设置过期时间"合并成一个原子操作。对应的Spring Data Redis写法是:
java复制// 推荐写法:原子操作
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);
这个接口底层执行的就是SET key value NX EX timeout,一次网络请求完成加锁和过期时间设置,彻底规避了死锁风险。
这里有两个细节要重点说明。第一个是关于value的设计。value必须是一个能标识客户端唯一身份的字符串,通常用UUID或IP + 线程ID生成。目的是为了释放锁时做持有者校验——只有value匹配,才能执行删除操作。第二个是过期时间的取值。时间设置太短,业务还没执行完锁就自动释放了,其他客户端趁虚而入;设置太长,客户端崩溃后锁要等很久才能被回收,阻塞时间拉长。
没有一个通用的标准值,核心思路是:根据业务逻辑的最大执行耗时,留出20%-50%的冗余。比如你的业务逻辑经过压测,最慢一次耗时2秒,那过期时间至少设3秒。但这种方式只解决了大部分场景,极端情况下仍可能超时,这就需要看门狗机制来兜底,后面会专门讲。
3.2 解锁为什么要校验身份:一个经典误删问题
解锁操作比加锁更容易踩坑。很多人第一版代码是这么写的:
java复制redisTemplate.delete(key);
这行代码看起来很合理,但仔细想想:如果客户端A加锁后,业务执行时间超过了锁的过期时间,锁自动失效了,此时客户端B加锁成功。A执行完业务后执行delete(key),删除的其实是B的锁。结果就是:A和B同时进入临界区,互斥性被彻底破坏。
正确的解锁逻辑必须两步走:先比较value是否是自己持有的锁,是才删除。而且这两步也要保证原子性,否则比较和删除之间可能被其他客户端插入操作。这里需要用Lua脚本把判断和删除串成一个原子操作:
lua复制-- 解锁Lua脚本:先比对value再删除
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
对应的Java代码:
java复制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";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(key),
value);
注意,这里一定不能用先get再delete的两步Java代码,因为两步操作不原子,依然存在时间窗口。用了Lua脚本,Redis会保证脚本内的多条命令连续执行、不被打断,这才是解锁的正确姿势。
3.3 锁超时了怎么办:看门狗机制的原理与局限
锁超时是个永恒的矛盾:设短了,业务还没跑完锁就没了;设长了,死锁恢复时间太长。Redisson给出的解决方案是"看门狗"机制。
Redisson的RLock默认过期时间是30秒,但它在加锁成功后,并不会傻等30秒,而是启动一个后台定时任务,每隔10秒检查一次:如果锁还被当前线程持有,就把过期时间重新刷新为30秒。这相当于给锁自动续期,业务没执行完,锁就永远不会超时。
这个机制的原理是:加锁成功后,Redisson会启动一个netty定时任务,调度周期是lockWatchdogTimeout / 3,也就是默认10秒执行一次,每次执行时使用Lua脚本判断锁是否仍被当前线程持有,如果是则执行PEXPIRE key 30000重置过期时间。如果客户端崩了,看门狗线程也随之消失,锁在30秒后自动释放,不会死锁。
java复制// Redisson的看门狗机制
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("order:stock:10001");
// 加锁成功后自动启动看门狗
lock.lock();
try {
// 业务逻辑:即使耗时超过30秒,锁也不会释放
doBusiness();
} finally {
lock.unlock();
}
这里要提醒一句:看门狗机制虽然好用,但它不是万能的。如果你的业务逻辑确实出现长时间卡顿,比如调用的下游接口响应要1分钟,看门狗续期也扛不住——它每10秒续期一次,理论上可以无限续下去,但长时间持锁会让其他请求长时间阻塞。所以在真实业务中,我通常会在锁内部再设置一个业务超时时间,双重保险,避免单个慢请求拖垮整个系统的吞吐量。
3.4 Redlock算法:主从切换下的锁安全方案
前面所有的方案,都是基于单实例Redis。但生产环境不可能只用单实例,一般至少是主从架构。主从架构引入了一个经典问题:客户端A在主节点加锁成功,主节点还没来得及把数据同步到从节点就宕机了,哨兵把从节点提升为新的主节点,但新的主节点上并没有A的锁数据。此时客户端B对新主节点加锁,发现可以加锁成功——锁丢失了。
Martin Kleppmann(《设计数据密集型应用》的作者)和Redis作者antirez曾经针对这个问题进行过一场非常著名的论战。antirez提出的解决方案就是Redlock算法。
Redlock的核心思想是:不要在一个Redis实例上加锁,而是向多个独立的Redis节点(通常5个)同时发起加锁请求。只有超过半数节点(比如5个中的3个)加锁成功,并且总耗时小于锁的过期时间,才算加锁成功。由于多个节点同时发生主从切换的概率远低于单节点,锁的安全性大幅提升。
Redlock的加锁步骤如下:
- 获取当前系统时间。
- 依次向N个Redis节点发送
SET key value NX EX ttl加锁请求,每个请求设置一个比锁总过期时间短得多的超时时间(比如锁总过期时间5秒,单节点请求超时设在50毫秒),目的是快速失败,避免在宕机节点上长时间等待。 - 计算加锁总耗时:当前时间减去步骤1的时间。
- 如果成功加锁节点数 >= N/2 + 1,且总耗时 < 锁过期时间,认为加锁成功。
- 加锁成功后,锁的实际有效时间 = 锁过期时间 - 加锁总耗时。
- 加锁失败或未达到半数节点,则向所有节点发送释放锁的Lua脚本。
Redlock在理论上是安全的,但在业界也有不少争议。最主要的质疑点是:它依赖所有节点的时间是同步的,如果某个节点的时钟发生跳变,锁的过期时间就会失真。此外,Redlock无法解决"GC停顿导致锁过期"的问题——客户端A加锁后发生Full GC,停顿了10秒,锁过期了,客户端B加锁成功,A恢复后又进入临界区,这时互斥性已经被破坏。
我对Redlock的落地态度比较务实:如果业务场景用单实例Redis加锁,配合主从节点、哨兵或Cluster,在高并发下偶尔出现"两个客户端同时拿到锁"的概率很低,且业务可以容忍这种极小概率的重复操作(比如幂等性兜底),那完全没必要上Redlock。只有那种绝对不能出现并发重复操作的场景,才需要考虑Redlock或直接改用ZooKeeper。
4. 生产级落地实践:三条核心路径的完整实现
4.1 路径一:Spring Boot + RedisTemplate自研锁
如果你的项目用的Spring Boot,并且不想引入额外的分布式锁组件,可以基于StringRedisTemplate自己封装一个工具类。这个方案的优势是对现有项目的侵入最小,几行代码就能实现。
一个完整的自研锁工具类要考虑这几个问题:
- 原子加锁:
setIfAbsent(key, value, timeout, timeUnit),value用UUID保证唯一。 - 原子解锁:Lua脚本比对value后删除。
- 自动续期:开启一个
ScheduledExecutorService定时任务,每timeout/3时间执行一次续期,业务结束后关闭定时任务。 - 阻塞等待:加锁失败时,可以自旋重试,但要注意重试间隔和最大尝试次数,避免无脑死循环。
java复制public class RedisDistributedLock {
private static final StringRedisTemplate REDIS_TEMPLATE = ...; // 注入
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";
private final String key;
private final String value;
private final long expireTime;
private final TimeUnit timeUnit;
private volatile boolean locked = false;
private ScheduledExecutorService watchdog;
public boolean tryLock(long waitTime, long expireTime, TimeUnit timeUnit)
throws InterruptedException {
long start = System.currentTimeMillis();
long waitMillis = timeUnit.toMillis(waitTime);
this.value = UUID.randomUUID().toString();
while (System.currentTimeMillis() - start < waitMillis) {
Boolean acquired = REDIS_TEMPLATE.opsForValue()
.setIfAbsent(key, value, expireTime, timeUnit);
if (Boolean.TRUE.equals(acquired)) {
locked = true;
startWatchdog(expireTime);
return true;
}
Thread.sleep(50); // 重试间隔
}
return false;
}
public void unlock() {
if (!locked) return;
stopWatchdog();
REDIS_TEMPLATE.execute(
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(key), value);
locked = false;
}
// 看门狗:定期续期
private void startWatchdog(long expireTime) {
watchdog = Executors.newSingleThreadScheduledExecutor();
watchdog.scheduleAtFixedRate(() -> {
if (locked) {
REDIS_TEMPLATE.expire(key, expireTime, TimeUnit.MILLISECONDS);
}
}, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
}
}
上面这版代码已经能支撑大部分业务场景了。但说实话,除非你有特殊需求,我不建议自己造轮子。自研锁最麻烦的地方在于边界情况非常多:续期任务线程池怎么管理、异常时怎么保证锁一定释放、Redis连接异常时怎么降级……这些问题都要考虑周全,否则就是埋雷。生产环境中,我更推荐直接用Redisson,它是目前功能最完整、bug最少的高阶封装。
4.2 路径二:Redisson开箱即用
Redisson是Java生态里对Redis分布式锁封装最成熟的客户端。它不仅实现了java.util.concurrent.locks.Lock接口,还提供了公平锁、读写锁、红锁等高级功能。
公平锁:redisson.getFairLock(key),按请求顺序分配锁,适合对公平性有要求的场景。
读写锁:redisson.getReadWriteLock(key),读锁共享、写锁互斥,适合读多写少的场景。
红锁:redisson.getRedLock(lock1, lock2, lock3),对多个RLock实例执行Redlock算法。
java复制RLock lock = redisson.getLock("inventory:10001");
boolean acquired = false;
try {
// 尝试加锁,最多等待3秒,加锁后10秒自动释放
acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (acquired) {
// 业务逻辑
deductInventory(10001, 1);
} else {
throw new BizException("获取锁失败,请重试");
}
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这里我用的是tryLock(waitTime, leaseTime, timeUnit)方法,显式指定了锁的租期是10秒,此时Redisson不会启动看门狗,10秒后锁过期自动释放。如果改成lock()方法或tryLock(waitTime, timeUnit),才会启用看门狗自动续期。
4.3 路径三:ZooKeeper与数据库实现
ZK实现分布式锁的经典方式是利用临时顺序节点。核心流程:
- 在锁的根节点下创建临时顺序节点,比如
/locks/myLock/seq-0001。 - 检查自己创建的节点是否是最小序号节点,如果是,获取锁成功。
- 如果不是,对前一个节点注册watcher监听,前一个节点删除后唤醒当前客户端。
- 释放锁时删除对应临时节点,会话断开后临时节点也会自动消失,天然防止死锁。
java复制InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
if (lock.acquire(3, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.release();
}
}
ZK方案的优点是没有锁过期的问题,临时节点跟随会话生命周期,客户端崩溃后锁自动释放;节点监听机制可以实现公平锁。缺点是性能差一些,每次加解锁都要经历磁盘同步和节点监听机制,高并发下的TPS远低于Redis方案。
数据库方案主要有两种。一种是基于唯一索引:
sql复制CREATE TABLE distributed_lock (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
lock_key VARCHAR(64) NOT NULL,
owner VARCHAR(64) NOT NULL,
expire_time DATETIME NOT NULL,
UNIQUE KEY uk_lock_key (lock_key)
);
加锁就是INSERT INTO distributed_lock (lock_key, owner, expire_time) VALUES (?, ?, ?),唯一索引保证同一时刻只能插入一条相同lock_key的记录。释放锁就是DELETE FROM distributed_lock WHERE lock_key = ? AND owner = ?。第二种是SELECT ... FOR UPDATE行锁,开启事务后对特定行加悲观锁。
数据库方案的优点是实现直观、无额外组件依赖、数据持久化有保障;缺点也非常明显:数据库IO性能是瓶颈,锁释放依赖事务,出现异常时容易留下脏数据。
三种方案的对比,我用一个表格总结:
| 维度 | Redis | ZooKeeper | 数据库 |
|---|---|---|---|
| 性能 | 最高,毫秒级 | 中等,有网络和磁盘开销 | 最低,受限于DB IO |
| 可靠性 | 依赖主从同步,存在锁丢失可能 | 高,临时节点会话隔离 | 高,事务保证 |
| 实现复杂度 | 低 | 中 | 低 |
| 自动释放机制 | 过期时间/看门狗 | 临时节点+会话超时 | 需要定时任务清理 |
| 适用场景 | 高并发、对性能敏感 | 强一致、可靠性要求极高 | 低并发、简单场景 |
5. 生产环境里的坑:我踩过的七个分布式锁问题
5.1 锁误删:线上最先踩到的坑
这个前面提过,但还是要单独列出来,因为它太经典了。我记得当时上线第一版自研锁的时候,就因为没有校验value,导致大促时出现并发扣减,最后只能手动回滚数据。从那以后,我在团队里立了一个规矩:所有操作Redis锁的代码,必须用Lua脚本校验owner后再删除,Code Review看到redisTemplate.delete(key)直接打回。
5.2 过期时间设太短:高并发下锁提前失效
有一次压测,业务逻辑里有个批量操作,极限场景下耗时接近4秒,而我当时把锁的过期时间设成了3秒。压测结果惨不忍睹:大量请求同时进入临界区,数据错乱。排查了半天,最后在监控里看到锁的平均持有时间已经超过了过期时间,才意识到问题。
解决思路有两个:一是用Redisson的看门狗自动续期,二是给业务逻辑设置独立的超时熔断。另外,在压测阶段就要观察锁持有时间的P99(99分位)耗时,用这个值加上安全系数来设置过期时间,而不是拍脑袋定一个数。
5.3 Redis主从切换引起的锁丢失
有一次半夜Redis主节点挂了,哨兵自动完成主从切换,结果第二天早上收到告警——有两条订单数据重复处理了。复盘发现,就是主从切换导致锁数据丢失,两个节点同时抢到了锁。
如果业务完全不能容忍这种问题,那就得上Redlock或者ZK。如果业务可以做幂等,比如在数据库层面加唯一索引做兜底,那单Redis主从的方案也能接受,毕竟极端情况概率很低。
5.4 可重入问题
分布式锁默认是不可重入的。你在业务代码里获取了锁,然后调用的方法里又去获取同一把锁,会直接失败或阻塞。解决方式:
- Redisson的
RLock天然支持可重入,内部用一个ThreadLocal记录当前线程加锁次数,unlock时递减,减到0才真正删除锁。 - 自研方案需要自己维护重入计数器和线程标识。
工具类里如果要用可重入,建议在JVM层面加一个ThreadLocal<Map<String, Integer>>的重入计数,配合Redis锁一起使用。
5.5 线程阻塞导致看门狗失效
看门狗本身是后台线程在续期,但如果你在业务代码里调用了Thread.sleep()或者阻塞等待某个资源,看门狗线程不受影响,依然会续期。也就是说,锁不会因为业务线程阻塞而过期,这反而可能导致锁一直被持有,其他请求一直阻塞。解决这个问题,关键在业务层做超时控制,避免单条请求长时间占用锁。
5.6 锁的粒度太粗,拖垮性能
有些同学图省事,对用户ID取模后再加锁,实际上两个不同的用户如果哈希到同一个桶,就会互相阻塞。这就是锁粒度太粗导致的性能问题。正确做法是尽量细粒度加锁,比如订单维度就锁订单号、库存维度就锁SKU ID、用户维度就锁用户ID,而不是整个服务或整个表加一把大锁。
5.7 忘了加随机重试导致的惊群效应
用自旋方式抢锁时,如果所有线程都在同一个固定时间间隔重试,解锁一瞬间所有线程同时打向Redis,会造成请求风暴。解决方法是加随机退避,比如每次重试的间隔在50ms到100ms之间随机,让请求分散开来。
java复制// 自旋重试时,加入随机退避
long sleepTime = 50 + ThreadLocalRandom.current().nextLong(50);
Thread.sleep(sleepTime);
6. 面试考点汇总:分布式锁怎么聊才能拿到高分
分布式锁是Java后端面试的高频考点,几乎每到大厂面试都会碰到。面试官通常不会只问"你用过吗",而是会层层深入,考察你对原理的理解深度。
最常被问到的问题,我整理了一下:
问题一:分布式锁的实现方式有哪些?
回答思路:三种方式——Redis、ZooKeeper、数据库,分别简述实现原理和优缺点,用表格回答更清晰。注意不要只背结论,要结合场景分析,比如"Redis性能高适合高并发""ZK可靠性强适合资金类场景"。
问题二:Redis分布式锁的实现原理是什么?
回答思路:SETNX + EXPIRE为什么能实现互斥,value为什么必须唯一,Lua脚本为什么能保证原子性。最好能把加锁、解锁的完整命令流程口头画一遍。
问题三:Redis分布式锁过期了业务还没执行完怎么办?
回答思路:先说这个问题会带来什么危害——锁被其他线程抢走,引发并发冲突。再说解决方案:Redisson看门狗自动续期;或业务代码里做时间戳校验;或干脆用ZK方案。
问题四:Redlock算法是怎么实现的?它真的安全吗?
回答思路:Redlock的流程要说清楚——多节点加锁、多数派成功、计算锁有效时间。然后客观评价它的争议点——时钟跳跃、GC停顿问题。如果能提到Martin Kleppmann和antirez的论战,会是一个亮点。
问题五:如何避免锁被其他线程误删?
回答思路:value存唯一标识,释放锁前用Lua脚本校验value再删除,保证原子性。回答时最好直接背出Lua脚本内容,面试官会觉得你是真写过代码的。
问题六:分布式锁和事务的边界问题?
这个稍微深一点。锁保证了并发控制,事务保证了数据一致性。但两者要结合考虑:事务还没提交时锁就释放了,另一个事务拿到锁读到的是未提交数据,可能出现脏读。解决思路是让锁的释放时机晚于事务提交——比如先提交事务,再在finally块里释放锁。
面试时还有个很加分的点是聊"取舍":不要吹得天花乱坠,而是针对不同业务场景说清楚每种方案的适用边界。面试官最怕的就是候选人只会背八股文,说不出来"为什么",所以平时的项目积累和技术深度思考才是真正的底牌。
7. 最后分享一个排查技巧
调试分布式锁问题,最有效的办法是给锁的关键操作加上日志和监控。我习惯在加锁、解锁、续期三个节点各打一条日志,包含key、owner、耗时,同时在监控面板上展示锁的持有时间、等待时间、获取失败次数三个指标。
这样一旦线上出问题,一查监控就能快速判断:是锁竞争激烈导致等待超时?是锁持有时间过长?还是锁获取失败频繁?对症下药,比盲猜高效得多。
另外一个经验:分布式锁能用现成组件就不要自己写,Redisson这类的成熟方案踩过的坑比你想象的要多得多,它内部对异常场景的处理,比我们自己临时写一版靠谱太多了。
