当前位置: 首页 > news >正文

Mysql可以做分布式锁吗?Mysql分布式锁的应用

文章目录

  • 1. 引言
  • 2. mysql为什么可以做分布式锁?
  • 3. mysql分布式锁实现方式
  • 4. 哪些场景下适合用mysql做分布式锁?
  • 5. mysql分布式锁的局限性
  • 6. 交易系统使用mysql做读写互斥锁的案例
    • 6.1 业务背景
    • 6.1 业务抽象
    • 6.3 解决过程

1. 引言

       在分布式系统中,分布式锁是一个常见的需求,用于在多个节点之间协调对共享资源的访问。通常情况下,使用 Redis 等内存数据库实现分布式锁是更流行的选择,因为它们通常提供更快的响应时间和更高的吞吐量。但 MySQL 也可以实现分布式锁,并且在某些业务场景下,使用 MySQL 实现分布式锁是合理的选择。

       

2. mysql为什么可以做分布式锁?

MySQL能够作为分布式锁的实现基础,主要依赖于其ACID特性(特别是原子性和持久性)和唯一键约束。以下从多维度分析

       

3. mysql分布式锁实现方式

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

  • 基于唯一约束和行锁
    • 创建一个锁表,例如 distributed_lock,包含字段 lock_name(唯一索引)和 expire_time 等。
    • 获取锁时,尝试插入一条记录(锁名作为唯一键)。如果插入成功,表示获取锁成功。
    • 释放锁时,删除该记录。
    • 为了避免死锁,可以设置一个过期时间,并使用定时任务清理过期的锁。
  • 基于悲观锁(SELECT FOR UPDATE)​​
    • 使用事务,通过 SELECT … FOR UPDATE 对锁表中的一行进行锁定。
    • 成功获取锁后,执行业务逻辑,然后提交事务以释放锁。
    • 其他事务在尝试获取同一把锁时会被阻塞,直到第一个事务提交。
  • 乐观锁(版本号)
    • 在表中增加版本号字段,更新时检查版本号。但这种方法不适用于锁,因为锁需要独占,而乐观锁通常用于避免冲突的更新,不适合互斥锁的场景。

       

4. 哪些场景下适合用mysql做分布式锁?

虽然 Redis 分布式锁(如 Redlock)在性能上更有优势,但以下场景可能更适合使用 MySQL 实现分布式锁:

  • ​​对锁的可靠性要求非常高,且对性能要求不是极端敏感
    • MySQL 的事务特性和持久性(ACID)确保了锁操作的可靠性和一致性。在需要严格保证锁的正确性且可以接受毫秒级延迟的场景下,MySQL 是更好的选择。
    • 例如,金融系统中的某些关键操作,需要确保在分布式环境下操作的绝对正确性,而 Redis 锁可能因为节点故障或网络分区导致锁的不可靠(即使 Redlock 算法也存在争议)。
  • 业务本身需要基于数据库事务进行​​
    • 如果业务操作在一个数据库事务中,需要将锁的获取和业务操作放在同一个事务中,以保证原子性。
    • 例如,在一个事务中需要锁定某个资源,然后更新多个表,这时使用 MySQL 的行锁或排他锁可以天然地保证锁和业务操作的原子性

在金融支付业务中,数据的一致性和可靠性是至关重要的。MySQL锁(基于数据库的分布式锁)提供了一些关键特性,这些特性在金融场景中非常有用:

  • 强一致性:MySQL作为关系型数据库,使用ACID事务,可以保证锁操作的原子性和一致性。在支付这种对数据一致性要求极高的场景中,能够确保锁的状态和业务数据的状态在同一个事务中提交,避免锁与业务数据不一致的情况。
  • 持久性:MySQL的锁可以持久化到磁盘,即使数据库重启,锁状态也可以通过表中的记录进行恢复(或者通过redoLog恢复)。而Redis锁在重启后可能会丢失(除非使用AOF持久化并且每次操作都fsync,但这样性能会下降)。
  • 可审计性:锁的操作会记录在数据库的事务日志中或者自定义的锁记录表中,便于事后审计。在金融行业中,监管要求严格,需要能够追踪每一步操作。

       

5. mysql分布式锁的局限性

       MySQL锁在性能上通常不如Redis锁,因此在低延迟、高并发的支付场景中可能会成为瓶颈。所以,在高并发支付场景下,通常会进行权衡,对于核心资金操作(如账户扣款、生成交易记录)使用MySQL锁,而并发控制(如库存扣减)可能会用Redis锁。

       

6. 交易系统使用mysql做读写互斥锁的案例

6.1 业务背景

举一个P2P的场景,这里先介绍一下 p2p 的业务

比如小明需要 10 万买车,但是手头上没钱,此时可以在 p2p 平台上申请一个 10 万的借款,然后 p2p平台会发布一个借款项目,开始募集资金。
其他网民可以去投资这个项目,每个月借款人小明会进行还款,投资人会拿到收益。
当投资人每次投资的时候,会产生一份债权,可以把债权理解为借款人欠你钱的一个凭证。
如果投资人急着用钱,但是此时投资还未到期,此时投资人可以发起债权转让,将投资人的债权卖给给其他人,这样投资人就可以及时拿到本金了。

这里面涉及到 2 个关键的业务:

  • 借款人(小明)执行还款:借款人执行还款的时候,会将资金发到投资人账户中,涉及到投资人账户资金的变动,还有债权信息的变化等,整个还款过程涉及到调用银行系统,过程比较复杂,耗时相对比较长
  • 投资人发起债权转让:投资人发起债权转让,也涉及到债权的编号和投资人账户的资金的变动

由于这 2 个业务都会操作债权记录和投资人账户资金,为了保证资金的正确性,降低系统的复杂度,可以选择让这 2 种业务互斥

  • 某笔借款执行还款的过程中,那么这笔借款关联的所有债权记录不允许发起转让
  • 如果某笔借款记录当前没有在还款处理中,那么这笔借款记录关联的债权都可以同时发起债权转让

下文的代码也都是基于上述互斥业务进行的
       

6.1 业务抽象

上述业务所在应用如果是集群部署,那么java 中的 ReadWriteLock 读写锁就排不上用场了,把如上业务抽象成

  • X:互斥的资源id
  • W:借款人还款操作
  • R:投资人债权转让操作

对 X 资源,可以执行 2 种操作:W 操作、R 操作,2 种操作需要满足下面条件

  1. 执行操作的机器分布式在不同的节点中,也就是分布式

  2. W(借款人还款) 操作是独享的,也就是说同一时刻只允许有一个操作者对 X 执行 W 操作

  3. R (投资人债权转让)操作是共享的,也就是说同时可以有多个执行者对 X 资源执行 R 操作

  4. W 操作和 R 操作是互斥的,什么意思呢?也就是说 W 操作和 R 操作不能同时存在

       

6.3 解决过程

       mysql 中同时对一笔记录发起 update 操作的时候,mysql 会确保整个操作会排队执行,内部是互斥锁实现的,从而可以确保在并发修改数据的时候,数据的正确性,执行 update 的时候,会返回被更新的行数,这里我们就利用 mysql 这个特性来实现读写锁的功能
       

①:创建读写锁表 t_read_write_lock

create table t_read_write_lock(resource_id varchar(50) primary key not null comment '互斥资源id',w_count int not null default 0 comment '目前执行中的W(借款人还款)操作数量' ,r_count int not null default 0 comment '目前执行中的R(投资人债权转让)操作数量',version bigint not null default 0 comment '版本号,每次执行update的时候+1'
);

这里主要关注 3 个字段:

  1. resource_id:互斥资源 id,比如上面的借款记录 id

  2. w_count:当前执行 W(借款人还款)操作的数量

  3. r_count:当前执行 R (投资人债权转让)操作的数量

下面来看 W 操作和 R 操作的实现。

       

②:借款人还款操作:W

1、通过resource_id去t_read_write_lock查询,如果不存在,则插入一条记录,这里由于resource_id是主键,所以对于同一个resource_id只会有一个插入成功,这里用 $lock_record表示t_read_write_lock记录
2、判断lock_record.w_count==0 && lock_record.r_count==0,如果为true继续向下,否则返回false,业务终止
3、获取锁,过程如下{3.1、开启事务3.2int count = (update t_read_write_lock set w_count=1 where r_count = 0)3.3、提交事务;}
4、如果3.2的count==1,继续向下执行,否则终止业务
5、执行业务操作
6、释放锁,过程如下{6.1、开启事务6.2、update t_read_write_lock set w_count=0 where w_count = 16.3、提交事务}

整个过程有个问题,不知道大家发现没有,如果执行到 5 之后,系统挂了,会出现什么情况?

业务执行完毕了,但是 w 锁却没有释放,这种后果就是本次操作一直持有锁,后续 W 和 R 操作就没法执行了。

我们来看看,业务执行过程中,系统挂掉,锁未释放如何处理和优化?方案如下:

  • 创建锁日志表:业务执行需要获取锁,除了需要修改t_read_write_lock表中的加锁数据外,还要额外增加一张表t_lock_log去记录当前互斥资源id 获取锁 - 业务执行结果 - 释放锁的过程(并记录上时间),这样即使系统挂了导致了锁占用,我们也知道业务是在那一步挂掉了。
  • 同步记录锁日志表数据:然后需要修改 借款人还款操作过程:W 的业务流程, 获取锁 - 业务执行结果 - 释放锁 的每一步过程都要在同一个事务中,同步更新t_lock_log中的进度,保证和t_read_write_lock的修改操作同时成功,或者失败
  • 旁路定时任务清理锁占用:最后再增加一个定时任务去根据加锁时间是否超出阈值,来扫描 t_lock_log中已过期未释放的锁,如果锁超过阈值未释放,则修改t_read_write_lockt_lock_log 中的状态。这样即使在任务在执行过程中崩溃了,旁路的定时任务也会定时扫描清理过期的锁,这样就避免了系统挂掉导致的锁一直占有!

       
1、 创建锁日志表:需要添加一下上锁日志表,每次上锁成功,则记录一条日志,表结构如下

create table t_lock_log(id bigint primary key auto_increment comment '主键,自动增长'resource_id varchar(50) primary key not null comment '互斥资源id',lock_type smallint default 0 comment '锁类型,0:W锁,1:R锁',status smallint default 0 comment '状态,0:获取锁成功,1:业务执行完毕,2:锁被释放',create_time bigint default 0 comment '记录创建时间',version bigint not null default 0 comment '版本号,每次执行update的时候+1'
);

2、同步记录锁日志表数据:接下看 借款人还款操作过程:W 改进如下

1、通过resource_id去t_read_write_lock查询,如果不存在,则插入一条记录,这里由于resource_id是主键,所以对于同一个resource_id只会有一个插入成功,这里用 $lock_record表示t_read_write_lock记录
2、判断lock_record.w_count==0 && lock_record.r_count==0,如果为true继续向下,否则返回false,业务终止
3、获取锁,过程如下{3.1、开启事务3.2int count = (update t_read_write_lock set w_count=1 where r_count = 0)3.3、如果count==1,则插入一条上锁日志,锁类型是0,状态是0:insert t_lock_log (resource_id,lock_type,status,create_time) values (#{resource_id},0,0,'当前时间');3.4、提交事务;}
4、如果3.2的count==1,继续向下执行,否则终止业务
5、执行业务操作,业务操作过程如下{5.1、业务库开启事务5.2、执行业务5.3、更新锁日志记录的状态为1,条件中必须带上status=0int updateLogCount = (update t_lock_log set status=1 where id=#{日志记录id} and status = 0)5.4if(updateLogCount==1){5.5、提交事务}else{5.6、回滚事务【走到这里说明更新锁日志记录失败了,说明t_lock_log的status被其他地方改掉了,被防止死锁的job修改了】}}
6、释放锁,过程如下{6.1、开启事务6.2、释放锁:update t_read_write_lock set w_count=0 where w_count = 1 and resource_id = #{resource_id}6.3、更新锁日志状态为2:update t_lock_log set status=2 where id = #{日志记录id}6.4、提交事务}

3、旁路定时任务清理锁占用:此时我们需要一个 job,通过这个 job 来释放长时间还未释放的锁,比如过了 10 分钟,锁还未被释放的,job 的逻辑如下

1、获取10分钟之前锁未释放的锁日志列表:select * from t_lock_log where status in (0,1) and create_time+10分钟<=当前时
间的;
2、轮询获取的日志列表,释放锁,操作如下{2.1、开启事务2.2if(t_lock_log.lock_type==0){//lock_type为0表示是W锁,下面准备释放W锁//先将日志状态更新为2,注意条件中带上version作为条件,这里使用到了乐观锁,可以确保并发修改时只有一个count的值为1int count = (update t_lock_log set status=2 where id = #{日志记录id} and version = #{日志记录.version})if(count==1){//将w_count置为0update t_read_write_lock set w_count=0 where w_count = 1 and resource_id = #{resource_id}}}else{//准备释放R锁//先将日志状态置为2int count = (update t_lock_log set status=2 where id = #{日志记录id} and version = #{日志记录.version})if(count==1){//将r_count置为r_count-1,注意条件中带上r_count - 1>=0update t_read_write_lock set r_count=r_count-1 where r_count - 1>=0 and resource_id = #{resource_id}}}2.3、提交事务
}

       
③:投资人债权转让操作:R

1、通过resource_id去t_read_write_lock查询,如果不存在,则插入一条记录,这里由于resource_id是主键,所以对于同一个resource_id只会有一个插入成功,这里用 $lock_record表示t_read_write_lock记录
2、判断lock_record.w_count ==0,如果为true继续向下,否则返回false,业务终止
3、获取锁,过程如下{3.1、开启事务3.2int count = (update t_read_write_lock set r_count=r_count+1 where w_count = 0)3.3、如果count==1,则插入一条上锁日志,锁类型是1【表示R锁】,状态是0:insert t_lock_log (resource_id,lock_type,status,create_time) values (#{resource_id},1,0,'当前时间');3.4、提交事务;}
4、如果3.2的count==1,继续向下执行,否则终止业务
5、执行业务操作,业务操作过程如下{5.1、业务库开启事务5.2、执行业务5.3、更新锁日志记录的状态为1,条件中必须带上status=0int updateLogCount = (update t_lock_log set status=1 where id=#{日志记录id} and status = 0)5.4if(updateLogCount==1){5.5、提交事务}else{5.6、回滚事务【走到这里说明更新锁日志记录失败了,说明t_lock_log的status被其他地方改掉了,被防止死锁的job修改了】}}
6、释放锁,过程如下{6.1、开启事务6.2、释放锁:update t_read_write_lock set r_count=r_count-1 where r_count - 1 >= 0 and resource_id = #{resource_id}6.3、更新锁日志状态为2:update t_lock_log set status=2 where id = #{日志记录id}6.4、提交事务}

④:总结
使用 mysql 来实现读写锁,如何防止死锁,重点就是 2 张表,锁表日志表,2 个表配合一个 job,就把问题解决了。

大家可以将上面代码流程转换为程序,结合 spring 的 aop 可以实现一个通用的 db 读写锁

相关文章:

  • 图像处理控件Aspose.Imaging教程:用Java将 CMX 转换为 PNG
  • 第七章接入技术
  • window 显示驱动开发-处理视频帧
  • [SPDM]SPDM 证书链验证过程详解
  • 深度信念网络 (DBN, Deep Belief Network)
  • 2025 06 12 mrp
  • yolo11学习笔记
  • 强化微调技术与GRPO算法(2): 优势、应用场景与选择指南
  • Android NumberPicker使用大全
  • 支持 CHI 协议的 NOC的错误注入和边界条件测试
  • JDK各个版本新特性
  • pytorch 之 nn 库与调试
  • Spring Boot 整合 Smart-Doc:零注解生成 API 文档,告别 Swagger
  • 2025-05-07-二分查找
  • Cloudflare SaaS 功能 ip 优选原理
  • 论文略读:Large Language Models Assume People are More Rational than We Really are
  • Unity-通过Transform类学习迭代器模式
  • 给Markdown渲染网页增加一个目录组件(Vite+Vditor+Handlebars)(上)
  • Java面试题020:一文深入了解微服务之负载均衡Feign
  • 多通道信号采集分析系统 - 01 功能分解与采样子系统
  • 网站建设应当注意哪些问题/免费私人网站建设
  • 河北建设集团在哪个网站采购/电商运营怎么做如何从零开始
  • 东莞手机网站建设/不受国内限制的搜索引擎
  • 菏泽哪里做网站/互联网销售怎么做
  • 网站建设网站设计哪家专业/cpu游戏优化加速软件
  • 门头沟高端网站建设/浏览广告赚钱的平台