Mysql分布式锁
最近在一份代码中看到基于Mysql的分布式锁实现,首先有一张锁表(脱敏):
CREATE TABLE `lock_tab`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`lock_type` int NOT NULL,
`lock_owner` varchar(127) NOT NULL,
`lock_ts` datetime NOT NULL,
`reserved` text default NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_idx_lock_type` (`lock_type`)
)
加锁流程的伪代码
// distributed lock with Mysql
hostIp := 192.168.0.1
lockType := 1
lock := db.model("lock_tab").where("lock_type = ?", some_lock_type)
if !lock {
db.model("lock_tab").insert(&Lock{LockType: lockType, LockOwner: hostIP, LockTs: time.Now()})
return true
}
// the lock belongs to someone else and not expired yet
if lock.LockOwner != hostIp && notExpire(lock.Ts) {
return false
}
// lock acquired, refresh the lease
db.model("lock_tab").update(&Lock{LockType: lockType, LockOwner: hostIP, LockTs: time.Now()})
return true
这份分布式锁的实现有几个特点:
- 以进程所在机器(容器)的ip作为标识,因此每台机器最多一个实例获取到锁
- 带过期时间
- 无锁,没有施加行锁
这份代码的本意是限制crontab任务在多实例上的运行,因此才有lockType对应不同的cronTab任务。那么这份代码有没有问题,能不能正常工作呢?
我的答案是有很大问题,但大概率能正常工作。问题在于当锁过期时,有很高的风险会出现多个实例获取到锁并错误的对锁做续期,又由于实例都是以crontab周期获取锁,当实例数超过一定值问题出现的频率会非常高。但这份代码的运行场景恰好只有2个实例,因此大概率是能正常工作。同时,这份代码还存在一些时间戳同步、ip重用的细节问题。
这份代码也引起我对一个常见问题的思考:到底如何基于Mysql实现一份可用、高效的分布式锁呢?
在Redis中,实现一个分布式锁是简单而直观的:
// 加锁
SET my_lock my_random_value NX PX 30000
// 解锁
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 my_lock my_random_value
以Redis为标杆,我对Mysql分布式锁的实现思路有:
- server side,规避本地计算可能出现的时间戳问题
- 原子性
- 无锁
在一番搜索和对比后chatgpt4给出的解是最符合要求的,同时兼容Mysql5.0/8.0。实际运用中,程序可以生成一个随机值作为lock_name,判断加锁语句返回的affectedRows来判定是否加锁成功。
CREATE TABLE `distributed_lock` (
`lock_name` VARCHAR(255) NOT NULL,
`lock_timestamp` BIGINT UNSIGNED NOT NULL,
`lock_ttl` INT UNSIGNED NOT NULL,
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# 加锁
INSERT INTO `distributed_lock` (`lock_name`, `lock_timestamp`, `lock_ttl`)
VALUES ('my_lock', UNIX_TIMESTAMP(), 10)
ON DUPLICATE KEY UPDATE
`lock_timestamp` = IF(UNIX_TIMESTAMP() - `lock_timestamp` > `lock_ttl`, VALUES(`lock_timestamp`), `lock_timestamp`),
`lock_ttl` = IF(UNIX_TIMESTAMP() - `lock_timestamp` > `lock_ttl`, VALUES(`lock_ttl`), `lock_ttl`);
# 解锁
DELETE FROM `distributed_lock` WHERE `lock_name` = 'my_lock';