基于 MySQL 实现分布式锁

前言

MySQL 实现分布式锁,常见的方案有以下几种:

  • 基于唯一索引(INSERT 操作)实现
  • 基于排他锁(SELECT FOR UPDATE)实现
  • 基于乐观锁(CAS)实现

基于 MySQL 实现分布式锁,需要注意的实现细节

  • 锁是否可重入
  • 是否会引发死锁
  • TTL 机制,即超时自动释放锁
  • 锁的公平性(公平锁、非公平锁)
  • 规定只能释放自己的锁,不能误释放别人的锁。

实现方案一

基于唯一索引(INSERT 操作),通过使用表(InnoDB)中的行级锁和事务来实现分布式锁。

创建锁表

  • 创建一个专门用于锁的表,使用 InnoDB 存储引擎,并设置锁名称所在的列为主键(唯一)。
  • 为了防止死锁,需要设置锁的过期时间。在创建锁表时,添加一个带有过期时间的列,并在获取锁时更新这一列。
  • 为了确保每个线程只能释放自己加的锁,在创建锁表时,需要添加一个唯一标识符(如线程 ID)。这样每个线程在获取锁时会记录自己的 ID,只有拥有相同 ID 的线程才能释放该锁。
1
2
3
4
5
6
7
-- 创建锁表
CREATE TABLE distributed_locks (
lock_name VARCHAR(255) NOT NULL PRIMARY KEY,
owner_id VARCHAR(255) NOT NULL,
locked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
) ENGINE=InnoDB;

获取锁

  • 在获取锁时,可以使用 INSERT 语句尝试插入一条记录。如果记录插入失败(记录已经存在),则获取锁失败。
  • 使用 INSERT IGNORE 或者 INSERT ... ON DUPLICATE KEY UPDATE 可以避免重复插入数据导致的错误。
  • 在实现分布式锁时,使用 ROW_COUNT() 可以检查获取锁的操作是否成功。ROW_COUNT() 是一个 MySQL 函数,用于返回上一个执行的 INSERTUPDATEDELETE 语句影响的行数。
  • 通过下述的 ON DUPLICATE KEY UPDATE 部分,如果锁已经存在且没有过期,则不会更新锁的过期时间和锁的唯一标识符。如果锁已经存在且已经过期,则会更新锁的过期时间和锁的唯一标识符,并重新获取锁。
1
2
3
4
5
6
7
8
9
-- 尝试获取锁,设置锁的过期时间为当前时间加上指定的过期时间(例如 10 秒)
INSERT INTO distributed_locks (lock_name, owner_id, expires_at)
VALUES ('your_lock_name', 'your_thread_id', DATE_ADD(NOW(), INTERVAL 10 SECOND))
ON DUPLICATE KEY UPDATE
owner_id = IF(expires_at < NOW(), VALUES(owner_id), owner_id),
expires_at = IF(expires_at < NOW(), VALUES(expires_at), expires_at);

-- 检查获取锁是否成功
SELECT ROW_COUNT();

释放锁

  • 通过删除对应的记录来释放锁。
  • 确保只有持有该锁的线程才能释放锁,不能释放其他线程持有的锁。
1
2
-- 释放锁
DELETE FROM distributed_locks WHERE lock_name = 'your_lock_name' AND owner_id = 'your_thread_id';

检测锁是否过期(可选)

  • 在应用层可以定期检查锁的状态,并在锁过期时删除过期的锁记录。
1
2
-- 删除过期的锁
DELETE FROM distributed_locks WHERE expires_at < NOW();

参考资料