您当前的位置:首页 > 电脑百科 > 数据库 > MYSQL

如何用MySQL设计一个分布式锁?

时间:2023-03-06 12:08:26  来源:微信公众号  作者:JAVA旭阳
关于如何实现分布式锁,大家可能对基于redis​实现比较熟悉,但是往往很多情况是一些并发量不大的项目用不上Redis,Redis往往适用于并发量比较大的场景。但是MySQL基本都是有的,所以今天我来谈谈如何基于MySQL实现我们的分布式锁。

​前言

分布式锁想必大家都不陌生,可以用来解决在分布式环境下,多个用户在同一时间读取/更新相同的资源带来的问题。比如秒杀场景下的库存问题、redis key失效情况下请求直接打到MySQL中造成MySQL负载过大的问题,这些问题都可以通过分布式锁来解决。

图片

关于如何实现分布式锁,大家可能对基于Redis​实现比较熟悉,但是往往很多情况是一些并发量不大的项目用不上Redis,Redis往往适用于并发量比较大的场景。但是MySQL基本都是有的,所以今天我来谈谈如何基于MySQL实现我们的分布式锁。

设计目标

  1. 互斥。不同机器上许多进程/线程中只有一个可以访问特定资源,其他进程/线程应该等到锁被释放才可以用。
  2. TTL。从CAP理论我们知道,网络总是不可靠的,任何一台服务器都有可能宕机一段时间。所以我们在设计分布式锁服务的时候,需要考虑到可能有一个持有锁的客户端宕机,无法释放锁,从而阻塞所有等待获取同一个锁的客户端。所以我们需要一种机制,可以在这种情况下自动释放锁来解锁其他客户端。
  3. 相关API
  • lock():获取锁
  • unlock():释放锁
  • tryLock(): 可选,更高级的API,例如:客户端可以指定获取锁的最大等待时间。如果不能在窗口内获得锁,则错误返回而不是继续等待。
  1. 高性能
  • 低延迟:在正常情况下,锁定和解锁应该非常快。比如实际的业务逻辑处理只需要1ms,而单纯的获取和释放锁,处理一个请求又需要100ms,那么最大QPS只能达到10,这对于现在的很多服务来说已经很低了。在这种情况下,服务器可以处理的最大 QPS 受到锁性能的限制。
  • 通知机制:分布式锁理想情况下应该提供通知机制。如果服务器进程A由于被另一个服务器进程B持有而无法获得锁,那么A不应该一直等待并占用CPU。相反,A 应该空闲以避免浪费 CPU资源 。然后当锁可用时,锁服务通知A,A将获得CPU资源并恢复运行。
  • 避免惊群效应。假设有 100 个进程想要获取同一个锁,当锁可用时,理想情况下应该只通知队列中的“下一个”进程,而不是突然调用所有 100 个进程来竞争锁。
  1. 公平。先到先得。等待时间最长的人应该下一个获得锁。如果是这样,则该锁被认为是公平锁。否则就是非公平锁。这两种锁在现实中都有实际使用。
  2. 重入锁。 想象一下,一个节点或服务器进程获取了一个锁,开始处理业务逻辑,然后遇到一个代码片段要求再次获取同一个锁,在这种情况下,节点或进程不应死锁,相反,它应该能够再次获取相同的锁,因为它已经持有锁。

MySQL如何实现分布式锁?

1. 唯一键约束

我们可以使用MySQL的唯一性约束来实现分布式锁,整体的思路如下:

  • 客户端 A 正在尝试获取锁。此时没有其他客户端持有锁,所以客户端A成功获取到了锁,并向MySQL表中插入一行数据。
  • 现在客户端 B 想要获取相同的锁,先查询DB,发现客户端A插入的行已经存在。在这种情况下,客户端B无法获取到锁。然后客户端 B 将等待一段时间后重试。客户端 B 会在指定的 TTL 窗口内不断重试几次,最终要么在客户端 A 释放锁后成功获取锁,要么因为 TTL 而失败。
  • 一旦客户端 A 完成其任务,它将通过简单地删除 DB 表中的行来释放锁lock。现在其他客户端能够获取锁。

现在我们来简单实现下,创建一个lock​表,其中lock_key字段有唯一性约束。

CREATE TABLE `lock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `lock_key` varchar(256) NOT NULL,
  `holder` varchar(256) NOT NULL,
  `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_lock_key` (`lock_key`)
);
  • lock_key​ 是锁的唯一名称。我们可以使用 project_name + resource_id 作为锁的名称,表明要抢的资源是什么,具备唯一性。
  • holder​是当前持有锁的客户端ID。我们可以使用service_name +IP 地址 + thread_id 来标识分布式环境中的客户端。

获取锁:

INSERT INTO `lock`(`lock_key`, `holder`) VALUES ('project1_uid1', 'server1_ip1_tid1');

释放锁:

DELETE FROM `lock` WHERE `lock_key` = 'project1_uid1';

上面的方案已经基本满足通过MySQL实现分布式锁的基本要求。现在让我们考虑一些特殊情况,看看它是否对分布式系统中的常见故障具有鲁棒性。

如果客户端 A 获取了锁,向 DB 中插入了一行,但后来客户端 A 崩溃了,或者网络分区和客户端 A 无法访问 DB 怎么办?在这种情况下,该行将保留在数据库中,不会被删除。换句话说,对于其他客户端来说,就好像客户端 A 仍然持有锁(即使 A 已经崩溃了!)。其他客户端将无法获取锁,并返回错误。

一种常用的方法是为每个锁分配一个 TTL。这个想法很简单:如果客户端 A 崩溃并且无法释放锁,那么其他人应该执行删除 DB 中的行从而释放锁的工作。假设通常客户端 A 需要 3 分钟才能完成任务。我们可以将 TTL 设置为 5 分钟。然后我们需要构建另一个服务来不断扫描lock表,并删除超过 5 分钟前创建的任何行。但是,还有其他问题:

  • 如果 A 没有崩溃,它只需要比平时多一点时间来完成任务怎么办?
  • 如果我们为扫描lock表而构建的这项新服务本身崩溃了怎么办?

第一个问题用MySQL很难完全解决。我们可以考虑A在获取到分布式锁后,新起个线程去检查锁是否快要过期了,比如发现TTL还剩下1/3时间,但是A还没有结束,这时候去扩大TTL时间,这就是锁的续签机制。但是在现实中,对于大部分的业务案例,我们总是可以设置一个足够大的TTL,使得这种情况很少发生,以至于对公司业务的影响几乎察觉不到。

现在让我们看看第2个问题怎么解决?

2. 使用时间戳+唯一键约束

我们可以在lock​表中添加一列来存储上次获取锁的时间戳last_lock_time。

CREATE TABLE `lock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `lock_key` varchar(128) NOT NULL,
  `holder` varchar(128) NOT NULL DEFAULT '',
  `version` int(11) not null,
  `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_lock_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_lock_key` (`lock_key`)
);

现在我们用${timeout}表示分布式锁的TTL。

获取锁:

当客户端 B 试图获取锁时,我们可以添加`last_lock_time` < ${now} - ${timeout}​作为where条件的一部分。

UPDATE `lock` SET `holder` = 'server1_ip1_tid1', `last_lock_time` = ${now} WHERE `lock_key` = 'project1_uid1' and `last_lock_time` < ${now} - ${timeout};

在这种情况下,只有当`last_lock_time` < ${now} - ${timeout}​客户端 B 可以获取锁、将 holder​ 更改为其 ID 并将其重置last_lock_time​为当前时间戳时。假设后面客户端 B 挂了,不能释放锁,最坏的情况是等待${timeout}TTL时间以后,其他客户端就能拿到锁。

释放锁:

我们可以把last_lock_time​更新为一个很小时间戳,例如‘1970–01–01 00:00:01’。

UPDATE `lock` SET `holder` = '', `last_lock_time` = ${min_timestamp} WHERE `lock_key` = 'project1_uid1' and `holder` = 'server1_ip1_tid1';

在WHERE语句中,我们添加了`holder` = ‘server1_ip1_tid1’,这是为了避免其他客户端不小心释放了当前客户端持有的锁。

成功释放锁后,holder​将其设置为空,并将last_lock_time设置为最小时间戳,以便其他客户端可以轻松获取锁。

现在我们解决了TTL问题,但是在上面的实现中,如果持有锁,其他客户端将需要一直循环重试,等待锁释放后再获取锁。如果分布式锁服务可以通知等待的客户端锁可用,那就更好了,我们思考下在MySQL中该如何实现。

3.使用FOR UPDATE​实现锁释放通知

MySQL具有行级锁功能,在RC隔离级别下,当我们使用FOR UPDATE​时,MySQL会为所有符合过滤条件的行加行级锁。当一个客户端会话获得锁时,所有其他客户端都将等待锁。此外,等待客户端唤醒并获取锁的顺序与它们首次尝试获取锁时的顺序相同。只要持有锁的客户端在 SQL 事务内执行逻辑,FOR UPDATE 就可以执行多次。换句话说,锁是重入锁。

另外,针对FOR UPDATE​,MySQL还支持两种模式:NOWAIT​ 和 SKIP LOCKED。

  • NOWAIT:不等待锁的释放。如果锁被其他客户端持有,无法获取,则立即返回锁冲突消息。
  • SKIP LOCKED:读取数据时,跳过行级锁被其他客户端持有的行。

通过这两个选项,我们可以实现tryLock行为,即客户端尝试获取锁,获取不到锁则立即返回,而不是等待。

我们可以简化我们的lock表以仅包含两个字段:

CREATE TABLE `lock` ( 
  `id` bigint unsigned NOT NULL AUTO_INCREMENT, 
  `lock_key` varchar(128) NOT NULL, 
  PRIMARY KEY (`id`), 
  UNIQUE KEY `uniq_lock_key` (`lock_key`) 
);

获取锁:

BEGIN;
SELECT * FROM `demo`.`lock` WHERE `lock_key` = 'project1_uid1' FOR UPDATE;

这里关于启动新事务BEGIN​ 做一个说明,只有在第一次获取锁时才需要它。后续重入时,不要执行BEGIN,否则会启动一个新的事务,现有的事务结束,实际上是在事务结束时释放锁。

非阻塞尝试锁tryLock():

BEGIN;
SELECT * FROM `demo`.`lock` WHERE `lock_key` = 'project1_uid1' FOR UPDATE NOWAIT;

释放锁:

COMMIT;

提交事务就可以释放锁。

总结

我们现在回头来看看基于MySQL实现分布式锁,是否满足我们一开始定下的设计目标:

  1. 互斥,最基本的功能,肯定是可以的。
  2. TTL 机制,MySQL 本地管理客户端会话。如果客户端由于机器故障或网络故障而断开连接,MySQL 将自动释放行级锁。
  3. 支持所有 3 个 API:获取/尝试/释放锁。
  4. 高性能:释放锁时,MySQL只会通知队列中等待的下一个客户端,而不是一次性通知所有客户端,避免雷群问题。
  5. 公平。MySQL 行锁本身支持。
  6. 重入。MySQL 行锁本身也支持。记住第一次获取锁就开始事务,以后再入时不要再开始新的事务。

看来基本上是没什么问题的,但是还有一点,我们需要提前向lock表中插入资源锁的数据,然后获取/尝试/释放锁的 API 才能按预期工作。

参考:https://medium.com/@bb8s/design-distributed-lock-with-mysql-9bc28ac59629



Tags:MySQL   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
关于如何实现分布式锁,大家可能对基于Redis​实现比较熟悉,但是往往很多情况是一些并发量不大的项目用不上Redis,Redis往往适用于并发量比较大的场景。但是MySQL基本都是有的,所...【详细内容】
2023-03-06  Tags: MySQL  点击:(0)  评论:(0)  加入收藏
那不知道大家有没有深入了解过,当我们的一条SQL命令被执行时,MySQL是如何把数据从硬盘/内存中查出来并展示到用户面前的呢?对于很多开发小伙伴来说,每天写SQL是必不可少的一项工...【详细内容】
2023-03-03  Tags: MySQL  点击:(8)  评论:(0)  加入收藏
本次分享了一次关于 MySQL 连接挂死问题排查的心路历程,由于环境搭建的工作量巨大,而且该问题复现存在偶然性,整个分析过程还是有些坎坷的(其中也踩了坑)。一、背景近期由测试反...【详细内容】
2023-03-03  Tags: MySQL  点击:(5)  评论:(0)  加入收藏
本文通过逻辑方式、物理方式原地升级来介绍MySQL5.6 升级至MySQL5.7的方法,并介绍其使用场景。当前不少系统的数据库依旧是MySQL5.6,由于MySQL5.7及MySQL8.0在性能及安全方面...【详细内容】
2023-02-27  Tags: MySQL  点击:(9)  评论:(0)  加入收藏
MySQL中如何优化LIMIT分页?这个问题我们今天一起来聊一聊。以下是一个示例,演示如何优化MySQL 中limit 分页查询的性能:假设我们有一个名为 users 的表,其中存储了 1,000,000 条...【详细内容】
2023-02-27  Tags: MySQL  点击:(9)  评论:(0)  加入收藏
MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默 认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,从节点可...【详细内容】
2023-02-27  Tags: MySQL  点击:(8)  评论:(0)  加入收藏
基本组件Nginx、Gateway、Nacos、Sentinel、Ribbon、Feign、Seata、Redis、RabbitMQ、MySQL、docker、Vue。大家好,我是哪吒。今天分享一篇一站式微服务架构,读哪吒编程,品技术...【详细内容】
2023-02-27  Tags: MySQL  点击:(15)  评论:(0)  加入收藏
随着数据量的不断增长,MySQL千万级数据查询的优化问题也日益引人注目。在这篇文章中,我们将深入探讨MySQL千万级数据查询优化的方法和技巧,以帮助开发者更好地优化MySQL性能。...【详细内容】
2023-02-25  Tags: MySQL  点击:(4)  评论:(0)  加入收藏
在大型的数据应用场景下,MySQL作为一个关系型数据库管理系统(RDBMS)是非常受欢迎的。然而,MySQL在处理大量数据时会遇到瓶颈,为了解决这个问题,分库分表是一种有效的解决方案。分...【详细内容】
2023-02-24  Tags: MySQL  点击:(4)  评论:(0)  加入收藏
Redis 和 MySQL 都是常用的数据存储工具,但它们的设计和使用场景不同,因此在一些特定的情况下,可能会有不同的表现。Redis 的队列本质上是一种内存数据结构,它是基于内存的,所以...【详细内容】
2023-02-22  Tags: MySQL  点击:(3)  评论:(0)  加入收藏
▌简易百科推荐
关于如何实现分布式锁,大家可能对基于Redis​实现比较熟悉,但是往往很多情况是一些并发量不大的项目用不上Redis,Redis往往适用于并发量比较大的场景。但是MySQL基本都是有的,所...【详细内容】
2023-03-06  JAVA旭阳  微信公众号  Tags:MySQL   点击:(0)  评论:(0)  加入收藏
那不知道大家有没有深入了解过,当我们的一条SQL命令被执行时,MySQL是如何把数据从硬盘/内存中查出来并展示到用户面前的呢?对于很多开发小伙伴来说,每天写SQL是必不可少的一项工...【详细内容】
2023-03-03  程序员拾山  今日头条  Tags:MySQL   点击:(8)  评论:(0)  加入收藏
本次分享了一次关于 MySQL 连接挂死问题排查的心路历程,由于环境搭建的工作量巨大,而且该问题复现存在偶然性,整个分析过程还是有些坎坷的(其中也踩了坑)。一、背景近期由测试反...【详细内容】
2023-03-03    dbaplus社群  Tags:MySQL   点击:(5)  评论:(0)  加入收藏
本文通过逻辑方式、物理方式原地升级来介绍MySQL5.6 升级至MySQL5.7的方法,并介绍其使用场景。当前不少系统的数据库依旧是MySQL5.6,由于MySQL5.7及MySQL8.0在性能及安全方面...【详细内容】
2023-02-27  数据库干货铺  今日头条  Tags:MySQL   点击:(9)  评论:(0)  加入收藏
MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默 认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,从节点可...【详细内容】
2023-02-27  Java极客技术    Tags:MySQL   点击:(8)  评论:(0)  加入收藏
随着数据量的不断增长,MySQL千万级数据查询的优化问题也日益引人注目。在这篇文章中,我们将深入探讨MySQL千万级数据查询优化的方法和技巧,以帮助开发者更好地优化MySQL性能。...【详细内容】
2023-02-25  Java编程世界  今日头条  Tags:MySQL   点击:(4)  评论:(0)  加入收藏
在大型的数据应用场景下,MySQL作为一个关系型数据库管理系统(RDBMS)是非常受欢迎的。然而,MySQL在处理大量数据时会遇到瓶颈,为了解决这个问题,分库分表是一种有效的解决方案。分...【详细内容】
2023-02-24  Java编程世界  今日头条  Tags:MySQL   点击:(4)  评论:(0)  加入收藏
由于本人水平有限,有错误的地方还请大家帮忙指正.我们知道MySQL是一个插件式存储引擎的数据库,不同存储引擎的对象的元数据的存储方式是不一样的.例如:InnoDB的表的元数据信...【详细内容】
2023-02-21  朱德润  微信公众号  Tags:MySQL   点击:(12)  评论:(0)  加入收藏
按标准化运维,同一集群同一角色有且仅有一个域名,但线上集群存在一套集群使用多个主库、从库域名的情况。在流量切换时,需要兼容处理多域名问题1 背景作为国内领先的循环经济产...【详细内容】
2023-02-21  黄建波  51CTO  Tags:MySQL   点击:(12)  评论:(0)  加入收藏
1.1 安装前准备1.2 安装MySQL1.3 配置数据库1.4 数据库主从配置1.4.1 检查配置server-id参数值1.4.2 记录主从状态1.4.3 启动主从1.4.4 验证主从状态MySQL为什么需要主从复...【详细内容】
2023-02-16  互联网技术学堂   网易号  Tags:MySql   点击:(14)  评论:(0)  加入收藏
站内最新
站内热门
站内头条