1. 分布式锁失效的典型场景剖析
最近在排查一个线上事故时发现,明明系统已经加了Redis分布式锁,但在促销活动的高并发场景下,库存数据仍然出现了超卖。这个现象让我意识到,很多开发者(包括曾经的我)对分布式锁存在严重的认知误区。今天我们就来深挖这个90%开发者都会踩的坑。
分布式锁失效通常发生在以下三种典型场景:
- 锁过期时间设置不合理,导致业务未执行完锁已释放
- 锁被其他线程误删,出现非持有者的删除操作
- 锁未实现可重入特性,造成同一线程多次获取锁失败
1.1 锁过期时间的陷阱
Redis分布式锁通常使用SETNX命令实现,但90%的初级开发者会忽略一个关键点:锁必须设置过期时间。更可怕的是,即便设置了过期时间,仍然可能出问题。比如:
java复制// 错误示范1:未设置过期时间
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock_key", "1");
// 错误示范2:设置过期时间但与加锁非原子操作
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock_key", "1");
redisTemplate.expire("lock_key", 30, TimeUnit.SECONDS);
正确的做法应该是使用原子性操作:
java复制// 正确写法:原子性加锁并设置过期时间
Boolean result = redisTemplate.opsForValue().setIfAbsent(
"lock_key",
"1",
30,
TimeUnit.SECONDS
);
关键经验:锁的过期时间应该大于业务执行的最长时间。建议通过压测确定合理的过期时间,通常设置为平均业务耗时的3倍。
1.2 锁误删的隐蔽风险
另一个常见问题是锁被非持有者删除。假设线程A获取锁后执行时间超过锁过期时间,此时锁自动释放,线程B获取到锁。接着线程A执行完毕尝试删除锁,就会误删线程B的锁。
解决方案是为每个锁设置唯一标识:
java复制String clientId = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue().setIfAbsent(
"lock_key",
clientId,
30,
TimeUnit.SECONDS
);
// 删除前校验
if (clientId.equals(redisTemplate.opsForValue().get("lock_key"))) {
redisTemplate.delete("lock_key");
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分布式锁的高级实现方案
2.1 Redisson的看门狗机制
Redisson客户端提供了更完善的分布式锁实现,其核心是看门狗机制——后台线程会定期检查锁是否仍被持有,并在接近过期时自动续期。
java复制RLock lock = redissonClient.getLock("lock_key");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
2.2 分段锁优化性能
在高并发场景下,单个分布式锁可能成为性能瓶颈。可以采用分段锁策略,比如将商品库存拆分为多个段:
java复制// 原始锁
String lockKey = "product_123_lock";
// 分段锁(假设分为10段)
int segment = id % 10;
String segmentLockKey = "product_123_lock_" + segment;
这样可以将锁竞争分散到多个段,显著提升并发能力。
3. 分布式事务的配合使用
3.1 本地事务与分布式锁的配合
即使使用了分布式锁,仍需注意本地事务的提交时机。错误的顺序可能导致锁释放后事务还未提交:
java复制@Transactional
public void updateStock(Long productId) {
// 错误顺序:先获取锁再开启事务
RLock lock = redissonClient.getLock("lock_" + productId);
lock.lock();
try {
// 业务操作
} finally {
lock.unlock();
}
}
正确的做法应该是让事务注解包含整个锁范围:
java复制public void updateStock(Long productId) {
RLock lock = redissonClient.getLock("lock_" + productId);
lock.lock();
try {
// 将业务逻辑移到事务方法内
doUpdateStockInTransaction(productId);
} finally {
lock.unlock();
}
}
@Transactional
public void doUpdateStockInTransaction(Long productId) {
// 业务操作
}
3.2 分布式事务方案选型
对于更复杂的场景,可能需要引入分布式事务。常见方案对比:
| 方案 | 原理 | 适用场景 | 性能影响 |
|---|---|---|---|
| 2PC/XA | 两阶段提交 | 数据库层面跨库事务 | 高 |
| TCC | Try-Confirm-Cancel | 高一致性要求 | 中 |
| SAGA | 长事务拆分为本地事务 | 最终一致性 | 低 |
| 本地消息表 | 异步确保型 | 跨系统最终一致性 | 最低 |
4. 实战中的避坑指南
4.1 锁等待超时处理
永远要为获取锁设置超时时间,避免系统死锁:
java复制// 设置获取锁的超时时间
boolean locked = lock.tryLock(3, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
4.2 锁的可重入实现
同一线程多次获取锁时需要考虑可重入性。Redisson的RLock已经实现了可重入,如果自己实现可以参考:
java复制// 伪代码:可重入锁实现
public boolean tryLock(String key, String clientId, int expire) {
String currentValue = redis.get(key);
if (currentValue != null && currentValue.equals(clientId)) {
// 重入计数+1
redis.incr(key + ":count");
return true;
}
// 正常获取锁逻辑...
}
4.3 锁的监控与告警
生产环境必须对分布式锁进行监控:
- 锁等待时间超过阈值告警
- 锁持有时间异常告警
- 锁竞争激烈时自动扩容
可以使用Redis的INFO命令获取锁相关指标,或通过Redisson的监控接口。
5. 性能优化实战技巧
5.1 锁粒度控制
锁的粒度越细,并发性能越好。对比两种方案:
java复制// 粗粒度锁:整个库存操作加锁
RLock lock = redisson.getLock("inventory_lock");
// 细粒度锁:按商品ID加锁
RLock lock = redisson.getLock("inventory_lock_" + productId);
5.2 读写锁分离
对于读多写少的场景,可以使用读写锁:
java复制RReadWriteLock rwLock = redisson.getReadWriteLock("read_write_lock");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();
5.3 锁降级策略
在某些场景下,可以从写锁降级为读锁:
java复制writeLock.lock();
try {
// 写操作
readLock.lock(); // 锁降级
} finally {
writeLock.unlock();
}
try {
// 读操作
} finally {
readLock.unlock();
}
我在实际项目中发现,合理使用这些技巧可以将分布式锁的性能提升3-5倍。特别是在秒杀场景下,通过分段锁+读写锁的组合,我们成功将QPS从500提升到了3000+。
