~码铃薯~

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

怎样使用数据库实现分布式锁

面试里可以这样答:数据库实现分布式锁,核心就两种思路:基于唯一索引的 INSERT/DELETE,和基于 SELECT ... FOR UPDATE 的行锁。另外还有乐观锁版本号方式。实际面试重点讲第一种,因为最常用、也最能体现细节。

1. 总述

数据库分布式锁的本质:
利用数据库的唯一约束行锁,让同一时刻只有一个事务/线程能拿到锁。
但必须解决:锁超时、误删、可重入、续期、主从、性能这些问题。


2. 方案一:唯一索引 + INSERT/DELETE(重点)

表设计

CREATE TABLE distributed_lock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    lock_name VARCHAR(64) NOT NULL,
    holder VARCHAR(128) NOT NULL,       -- 持有者,建议 UUID + 线程ID
    reentrant_count INT DEFAULT 1,      -- 重入次数
    expire_time DATETIME NOT NULL,      -- 过期时间,防死锁
    create_time DATETIME DEFAULT NOW(),
    UNIQUE KEY uk_lock_name (lock_name)
);

加锁

public boolean tryLock(String lockName, String holder, long expireMs) {
    try {
        jdbcTemplate.update(
            "INSERT INTO distributed_lock(lock_name, holder, expire_time) VALUES (?, ?, ?)",
            lockName, holder, new Date(System.currentTimeMillis() + expireMs)
        );
        return true;
    } catch (DuplicateKeyException e) {
        // 插入失败,说明锁已存在。可尝试清理过期锁后重试
        int deleted = jdbcTemplate.update(
            "DELETE FROM distributed_lock WHERE lock_name = ? AND expire_time < NOW()",
            lockName
        );
        if (deleted > 0) {
            return tryLock(lockName, holder, expireMs);
        }
        return false;
    }
}

关键点:

  • 加锁操作要独立事务,插入后立刻提交,否则其他事务插入相同唯一键会一直阻塞等待。
  • 如果放在业务大事务里,锁记录迟迟不提交,会拖死其他线程。

释放锁

public void unlock(String lockName, String holder) {
    jdbcTemplate.update(
        "DELETE FROM distributed_lock WHERE lock_name = ? AND holder = ?",
        lockName, holder
    );
}

必须带 holder,防止误删别人的锁。

可重入

加锁时如果唯一键冲突,不要直接失败,先查:

SELECT holder, reentrant_count, expire_time FROM distributed_lock WHERE lock_name = ?

如果 holder 是自己且未过期,则:

UPDATE distributed_lock SET reentrant_count = reentrant_count + 1 WHERE lock_name = ? AND holder = ?

释放时:

UPDATE distributed_lock SET reentrant_count = reentrant_count - 1 WHERE lock_name = ? AND holder = ?

减到 0 再 DELETE

锁续期

后台定时线程,每隔一段时间:

UPDATE distributed_lock SET expire_time = ? WHERE lock_name = ? AND holder = ?

防止业务执行时间超过 expire_time,锁被误删。

过期清理

定时任务:

DELETE FROM distributed_lock WHERE expire_time < NOW()

但要注意:如果业务还在执行但没续期,可能误删。所以续期机制必须配合。


3. 方案二:SELECT ... FOR UPDATE 行锁

思路:表中先有一行锁记录,加锁时用 SELECT ... FOR UPDATE 锁住这行,事务提交后自动释放。

@Transactional
public void doWithLock(String lockName) {
    // 确保行存在
    jdbcTemplate.update("INSERT IGNORE INTO distributed_lock(lock_name) VALUES (?)", lockName);

    // 加锁,其他事务会阻塞在这里
    jdbcTemplate.queryForObject(
        "SELECT * FROM distributed_lock WHERE lock_name = ? FOR UPDATE",
        new Object[]{lockName}, (rs, rowNum) -> rs.getString("lock_name")
    );

    // 执行业务
    // ...

    // 事务提交,行锁自动释放
}

优点:

  • 不需要显式删除锁。
  • 天然阻塞等待,适合需要排队场景。
  • 事务结束自动释放,不会因为忘记 unlock 死锁。

缺点:

  • 锁持有时间等于整个事务时长,长事务会占用数据库连接。
  • 高并发下大量线程阻塞在数据库,容易连接池耗尽。
  • 必须操作主库,读写分离下不能读从库。
  • 需要先确保行存在,否则 FOR UPDATE 可能锁不住。

4. 方案三:乐观锁版本号

表里加 versionlocked 字段,用 UPDATE 影响行数判断是否拿到锁。

UPDATE distributed_lock
SET holder = ?, expire_time = ?, version = version + 1
WHERE lock_name = ?
  AND version = ?
  AND (holder IS NULL OR expire_time < NOW());

影响行数为 1,说明抢锁成功。释放时:

UPDATE distributed_lock
SET holder = NULL, version = version + 1
WHERE lock_name = ? AND holder = ? AND version = ?;

适合竞争不激烈、冲突少的场景。


5. 必须注意的坑

  1. 锁超时
    宕机后锁必须能自动释放,所以一定要有 expire_time

  2. 误删
    释放锁必须校验 holder,不能直接 DELETE WHERE lock_name=?

  3. 可重入
    holder + reentrant_count 实现,释放时减到 0 才删除。

  4. 续期
    业务执行时间可能超过过期时间,需要后台定时续期,类似 Redisson 看门狗。

  5. 独立事务
    基于唯一索引加锁时,INSERT 要独立事务提交,不能和业务大事务混在一起。

  6. 主从问题
    分布式锁必须走主库,不能走读写分离的从库,否则复制延迟会导致锁状态不一致。

  7. 性能问题
    数据库锁并发能力差,连接池有限,大量轮询会拖垮数据库。
    一般只适合并发不高、已有数据库、不想引入 Redis/ZooKeeper 的场景。


6. 与 Redis、ZooKeeper 对比

方案 优点 缺点
数据库 简单、强一致、无需额外组件 性能差、占连接、主从复杂
Redis 性能高、实现成熟(Redisson) 需处理续期、主从切换、Redlock 争议
ZooKeeper 一致性强、临时顺序节点 性能一般、需维护 ZK 集群

7. 面试收尾总结

可以这样答:

数据库实现分布式锁,最常用的是基于唯一索引:加锁时 INSERT 一条唯一记录,成功即拿到锁;释放时校验 holder 后 DELETE。必须加 expire_time 防死锁,配合定时续期防误过期,holder 防误删,reentrant_count 支持重入。另一种是 SELECT ... FOR UPDATE 行锁,事务提交自动释放,但长事务会占连接。数据库锁适合并发不高、不想引入中间件的场景;高并发更推荐 Redis 或 ZooKeeper。

常见追问:
唯一索引冲突时会发生什么?锁超时怎么续期?如何防止误删?怎么实现可重入?主从切换锁会丢吗? 这些都可以继续展开。

posted on 2026-09-20 17:30  ~码铃薯~  阅读(6)  评论(0)    收藏  举报