Skip to content

如何解决死锁和锁失效等问题

解决 死锁锁失效 需要从预防、检测和恢复三个方面入手: - 死锁:通过避免循环等待(如资源排序)、设置超时、检测并中断。 - 锁失效:通过续期机制、冗余锁(如 Redlock)或强一致性工具(如 Zookeeper)解决。


关键事实

  1. 死锁
  2. 定义:多个线程/进程互相等待对方释放锁,形成循环。
  3. 条件:互斥、占有等待、不可抢占、循环等待。
  4. 锁失效
  5. 定义:锁未正确持有(如过期、误删),导致互斥失败。
  6. 场景:分布式锁超时、未续期。

解决死锁

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秒超时
}
  • JavaLock.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,展示实践能力。