[面试题] 怎么使用MySQL实现一个分布式锁?
📖 版块 A:知识讲解版 (温故知新)
使用 MySQL 实现分布式锁主要有三种常见方案:
-
基于唯一索引(最常用):
- 创建一个锁表,包含
lock_name(锁名,加唯一索引)、owner_id(持有者标识)、recount(重入次数)和expire_at(过期时间)等字段。 - 加锁: 执行
INSERT INTO method_lock (lock_name, owner_id, ...) VALUES ('order_lock', 'thread_1', ...)。如果插入成功,则获取锁成功;如果唯一索引冲突,则获取锁失败。 - 释放锁: 执行
DELETE FROM method_lock WHERE lock_name = 'order_lock' AND owner_id = 'thread_1'。
- 创建一个锁表,包含
-
基于排他锁(for update):
- 在查询语句后面加上
FOR UPDATE。 - 加锁:
SELECT * FROM method_lock WHERE lock_name = 'order_lock' FOR UPDATE。由于 MySQL 的 InnoDB 引擎会给该记录加行级排他锁,其他线程执行此语句会被阻塞。 - 释放锁: 提交事务(
COMMIT)即可自动释放排他锁。
- 在查询语句后面加上
-
基于乐观锁(CAS 思想):
- 在业务表中增加
version字段。 - 更新:
UPDATE table SET ..., version = version + 1 WHERE id = #{id} AND version = #{old_version}。通过影响行数是否为 1 来判断操作是否成功。
- 在业务表中增加
MySQL 分布式锁面临的三个核心挑战:
- 死锁问题: 如果获取锁的服务挂了,锁没释放,会导致死锁。需引入
expire_at字段并配合定时任务清理过期数据。 - 不可重入: 需记录
owner_id和recount,加锁时判断是否为同一持有者。 - 性能瓶颈: 频繁的磁盘 IO 读写,支撑不了超高并发。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:怎么使用 MySQL 实现一个分布式锁?
💬 高手逐字稿:
关于 MySQL 实现分布式锁,我主要从三种思路来考虑:最简单的唯一索引方案、利用数据库底层能力的排他锁方案,以及乐观锁方案。
首先是基于唯一索引的实现,这是最直观的。 我们会创建一张锁表,核心字段是 lock_name 并且一定要加上唯一索引。当多个节点尝试加锁时,其实就是执行一条 INSERT 语句。由于唯一性约束,只有一个节点能插入成功,即代表获取锁。释放锁时,直接 DELETE 掉这条记录即可。
但这个方案在生产环境有两个致命点必须解决:第一是死锁。如果某个节点拿到了锁但在释放前宕机了,这个记录会永久留在数据库里。我的解决办法是在表中增加一个 expire_at 过期时间字段,并启动一个后台定时任务,定期清理掉表中已过期的记录。第二是可重入性。为了让同一个线程能多次获取同一把锁,我们需要在表中维护 owner_id(比如 IP+线程ID)和 recount(重入计数)。每次加锁前先查一下,如果是同一持有者,就把 recount 加 1。
其次是利用 MySQL 自身的行锁,即 SELECT … FOR UPDATE。 这种方式依赖于数据库的事务。我们在一个事务内执行这条语句,如果没查到或者被其他事务占用,当前线程会阻塞直到拿到锁。这种方式的好处是利用了成熟的数据库锁机制,但也存在问题:一旦并发量过大,大量的连接被阻塞在数据库侧,很容易把数据库的连接池耗尽,导致系统雪崩。
最后是乐观锁方案。 通常是在业务数据表里加一个 version 字段。这种方式其实并不属于真正的“独占锁”,它更像是一种并发冲突检测机制。在高并发竞争激烈的情况下,乐观锁会导致大量的更新失败和重试。
高手自问自答: 可能会有人问,既然有了 Redis 这种高性能锁,为什么还要用 MySQL?其实在一些对强一致性要求极高、但并发量并不大的后台管理系统或金融清算场景下,MySQL 方案反而更有优势。因为它支持 ACID 特性,数据持久化在磁盘上,不会像 Redis 那样存在主从切换导致丢锁的极端情况。
总结来说,MySQL 实现分布式锁主要靠唯一索引加过期时间清理。如果系统追求极致吞吐量,我会优先选择 Redis 或 ZooKeeper;如果是为了快速实现且不引入额外中间件,MySQL 是一个可靠的选择。