如何解决死锁和锁失效等问题
解决 死锁 和 锁失效 需要从预防、检测和恢复三个方面入手: - 死锁:通过避免循环等待(如资源排序)、设置超时、检测并中断。 - 锁失效:通过续期机制、冗余锁(如 Redlock)或强一致性工具(如 Zookeeper)解决。
关键事实
- 死锁:
- 定义:多个线程/进程互相等待对方释放锁,形成循环。
- 条件:互斥、占有等待、不可抢占、循环等待。
- 锁失效:
- 定义:锁未正确持有(如过期、误删),导致互斥失败。
- 场景:分布式锁超时、未续期。
解决死锁
1. 避免循环等待(资源排序)
- 原理:对资源编号,按顺序获取锁,打破循环。
- 示例(Java):
// 按资源 ID 顺序加锁
synchronized (resource1.compareTo(resource2) < 0 ? resource1 : resource2) {
synchronized (resource1.compareTo(resource2) < 0 ? resource2 : resource1) {
// 操作
}
}
- Redis 分布式锁:多资源锁按 key 排序。
2. 设置超时
- 原理:锁获取失败时超时退出,避免无限等待。
- 示例(Redis):
if (!lock(jedis, "lock:resource", clientId, 30000)) {
throw new TimeoutException("Lock timeout"); // 30秒超时
}
- Java:
Lock.tryLock(timeout)
。
3. 检测与中断
- 原理:检测死锁后中断线程或回滚事务。
- 示例(Java):
- 使用
ThreadMXBean
检测:
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
// 中断线程或记录日志
}
- 数据库:MySQL InnoDB 自动检测死锁并回滚。
4. 预防策略
- 减少锁粒度:锁小范围资源。
- 单一锁:避免多锁嵌套。
解决锁失效
1. 锁续期(防超时失效)
- 问题:Redis 锁过期(如 30 秒),业务未完成,锁被他人获取。
- 解决:客户端定时续期。
- 实现(Redisson 示例):
RLock lock = redisson.getLock("lock:resource");
lock.lock(30, TimeUnit.SECONDS); // 自动续期
// 业务逻辑
lock.unlock();
- 手动续期:
new Thread(() -> {
while (locked) {
jedis.pexpire("lock:resource", 30000); // 每 10 秒续期
Thread.sleep(10000);
}
}).start();
2. 防误删(原子解锁)
- 问题:Redis 锁被其他客户端误删。
- 解决:解锁时校验 value,用 Lua 脚本。
- 代码:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
3. 冗余锁(Redlock)
- 问题:单 Redis 节点故障,锁失效。
- 解决:Redlock 算法,多节点投票。
- 原理:
- 向 N 个 Redis 实例加锁,超过 N/2+1 成功则有效。
- 示例:5 节点,3 个成功即锁住。
- 实现:Redisson 支持 Redlock。
4. 强一致性替代
- 问题:Redis 锁是 AP 模型,弱一致性。
- 解决:用 Zookeeper(CP 模型)。
- 实现:
- 创建临时顺序节点,序号最小者获锁。
- 自动续期,无需手动处理。
完整示例(Redis 锁)
public class SafeDistributedLock {
private Jedis jedis = new Jedis("localhost", 6379);
private String key = "lock:resource";
private String clientId = UUID.randomUUID().toString();
private volatile boolean locked = false;
public boolean tryLock(int expireMs) {
if ("OK".equals(jedis.set(key, clientId, "NX", "PX", expireMs))) {
locked = true;
startRenewal(expireMs); // 续期线程
return true;
}
return false;
}
public void unlock() {
if (!locked) return;
String script = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(clientId));
locked = false;
}
private void startRenewal(int expireMs) {
new Thread(() -> {
while (locked) {
jedis.pexpire(key, expireMs);
try { Thread.sleep(expireMs / 3); } catch (Exception e) {}
}
}).start();
}
}
延伸与面试角度
- 死锁预防:
- 数据库:缩短事务,优化查询。
- Java:
ReentrantLock
替代synchronized
。 - 锁失效场景:
- Redis 主从切换,锁丢失。
- 网络抖动,续期失败。
- 性能对比:
- Redis:高性能,弱一致。
- Zookeeper:低性能,强一致。
- 实际应用:
- 秒杀:Redis 锁 + 续期。
- 任务调度:Zookeeper 锁。
- 面试点:
- 问“死锁条件”时,提四条件。
- 问“锁失效改进”时,提 Redlock 或 Zookeeper。
总结
死锁通过资源排序、超时和检测解决;锁失效通过续期、原子解锁和冗余锁解决。Redis 锁用续期和 Lua 脚本优化,必要时用 Redlock 或 Zookeeper 替代。面试时,可写代码或提 Redisson,展示实践能力。