MySQL 实现分布式锁
CREATE TABLE `x_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`key` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
`version` bigint(20) NOT NULL COMMENT '乐观锁',
`uuid` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT 'uuid',
`expired_time` bigint(20) NOT NULL COMMENT '过期时间',
`create_time` bigint(20) NOT NULL COMMENT '创建时间',
`update_time` bigint(20) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='资源锁';
要求 MySQL 的 binlog_format = row,且主从同步也是使用 binlog_format = row。
1. 预先创建,不删除
1.1 tryLock
- 锁需要预先创建。
- uuid 为 空或者
expired_time
过期的即可锁定。 - 锁定资源,提前执行
SELECT
语句获取版本号。 - 执行后影响条数为 0 则意味着锁定资源失败。
- 锁定资源成功后,需要后台定时运行续约。
tryLock ${name} lease #{lease} with ${uuid}
SELECT *
FROM `x_lock`
WHERE `key` = #{key}
AND (
`uuid` = ''
OR
`expired_time` < #{now}
)
UPDATE `x_lock`
SET
`uuid` = #{uuid},
`version` = `version` + 1,
`expired_time` = #{now} + #{lease},
`update_time` = #{now}
WHERE `key` = #{key}
AND `version` = #{version}
1.2 leaseLock
leaseLock ${name} lease #{lease} with ${uuid}
- 执行
SELECT
语句获取版本号。 - 续约资源,提前
- 执行后影响条数为 0 则意味着续约资源失败。
UPDATE `x_lock`
SET
`version` = `version` + 1,
`expired_time` = #{now} + #{lease},
`update_time` = #{now}
WHERE `key` = #{key}
AND `version` = #{version}
AND `uuid` = #{uuid}
1.3 releaseLock
releaseLock ${name} with ${uuid}
- 执行
SELECT
语句获取版本号 - 终止续约的定时器
- 释放资源,SQL 如下
UPDATE `x_lock`
SET
`version` = `version` + 1,
`expired_time` = 0,
`uuid` = '',
`update_time` = #{now}
WHERE `key` = #{key}
AND `version` = #{version}
AND `uuid` = #{uuid}
2. 插入+删除
2.1 tryLock
tryLock ${name} lease #{lease} with ${uuid}
INSERT IGNORE INTO `x_lock`
(`id`, `key`, `version`, `uuid`, `expired_time`, `create_time`, `update_time`)
VALUE
(0, #{key}, 0, #{uuid}, #{now} + #{lease}, #{now}, #{now})
- 判断返回值是否大于 0
2.2 leaseLock
leaseLock ${name} lease #{lease} with ${uuid}
UPDATE `x_lock`
SET
`version` = `version` + 1,
`expired_time` = #{now} + #{lease},
`update_time` = #{now}
WHERE `key` = #{key}
AND `version` = #{version}
AND `uuid` = #{uuid}
2.3 releaseLock
releaseLock ${name} with ${uuid}
releaseLock 前需要判断 uuid、expired_time,只有在有效期内,才可以删除
DELETE FROM `x_lock`
WHERE `key` = #{key}
AND `uuid` = #{uuid}
AND `version` = #{version}
3. ChatGPT 的评价
对于上述两个 MySQL 分布式锁实现方案,评估其优缺点如下:
方案一:预先创建,不删除
优点:
锁的状态在数据库中进行维护,方便管理和监控。
通过预先创建锁的方式,避免了频繁地插入和删除操作,减少了数据库的负载。
通过续约机制,确保锁在一定时间内不会过期,提高了锁的可靠性。
缺点:
需要定时任务来执行续约操作,增加了系统的复杂性。
锁的粒度较大,一次只能锁定一个资源,不能实现更细粒度的锁定。
方案二:插入+删除
优点:
简单直观,使用插入和删除操作来实现加锁和释放锁。
锁的粒度可以更细,可以实现对特定资源的锁定。
缺点:
需要频繁地插入和删除操作,增加了数据库的负载。
对于高并发的场景,可能会出现竞争条件,导致锁的获取不稳定。
如果锁的操作不正确或者异常,可能会出现死锁或者无法释放锁的情况。
综合评估,方案一相对于方案二更加可靠和高效,通过预先创建锁并使用续约机制,可以减少数据库的操作次数和并发竞争的可能性,提高了分布式锁的性能和可靠性。然而,方案一需要额外的定时任务来执行续约操作,增加了系统的复杂性。因此,在选择实现方案时,需要综合考虑系统的具体需求和对性能和复杂性的权衡。
3.1 方案一的优缺点
方案一的主要缺陷和潜在的问题包括:
- 定时任务的调度:方案一中需要使用定时任务来执行续约操作,确保锁不会过期。如果定时任务的调度不准确或者出现故障,可能会导致锁过期,从而影响系统的正常运行。
- 锁的持有者失效:如果持有锁的服务或节点发生故障或宕机,无法及时释放锁,可能会导致其他服务或节点无法获取到锁,进而影响系统的可用性。
- 锁的争用和性能问题:由于方案一中使用的是悲观锁机制,每次续约操作都需要执行数据库的更新操作,可能导致大量的数据库请求和争用,降低系统的性能。
- 锁的过期时间设置:续约操作的频率和过期时间的设置需要合理,如果设置过短的过期时间或者频繁地执行续约操作,会增加数据库的负载和网络通信的开销;而设置过长的过期时间可能导致锁无法及时释放,影响其他服务的运行。
- 数据库故障恢复:如果数据库发生故障或者崩溃,可能会导致锁的状态丢失,无法保证锁的一致性和可靠性。需要进行数据库备份和恢复等措施来处理故障情况。
总体而言,方案一需要注意定时任务的调度和容错机制,以及锁的持有者失效和性能问题。合理设置锁的过期时间,并确保数据库的稳定性和高可用性,以保证分布式锁的正确性和可靠性。