mysql锁机制的再研究

简介: 在分布式系统中,分布式锁已经的使用越发常见,我们系统由于较为老旧,使用数据库实现分布式锁,方案为:使用lock_key, lock_biz组成唯一索引,利用数据库对一条记录insert和delete操作的事务性来实现不可重入的db分布式锁。

1.gif

在分布式系统中,分布式锁已经的使用越发常见,我们系统由于较为老旧,使用数据库实现分布式锁,方案为:使用lock_key, lock_biz组成唯一索引,利用数据库对一条记录insert和delete操作的事务性来实现不可重入的db分布式锁。


前一段时间,发现系统分布式锁出现死锁告警,在处理完问题后,重新去看现场日志,发现数据库出现了死锁,在阿里云性能诊断系统中的锁分析里找出死锁日志,但奇怪的是不同的唯一索引值出现了死锁,如下图所示。抱着不懂就研究的心态,重新去研究了mysql Inndb的锁机制(ps:公司mysql 默认是RC隔离级别)。



图片.png


关于锁的基础知识


本文下方所有实例均依旧该表进行,建表语句如下:


CREATE TABLE `test_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `lock_key` varchar(128) NOT NULL COMMENT '锁名称',
  `lock_context` varchar(512) DEFAULT NULL COMMENT '锁上下文',
  `lock_biz` varchar(64) NOT NULL COMMENT '锁类型',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_uk_lock_name` (`lock_key`,`lock_biz`)
) ENGINE=InnoDB AUTO_INCREMENT=26229 DEFAULT CHARSET=utf8 COMMENT='分布式锁表'
;


 锁的作用范围分类

全局锁


在DB级别对整个数据库实例加锁


加锁表现:

  1. 数据库处于只读状态
  2. 阻塞对数据的增删改以及DDL


加锁方式:lock Flush tables with read lock 释放锁:unlock tables(发生异常时会自动释放)
作用场景:全局锁主要用于做全库的逻辑备份,和设置数据库只读(set global readonly=true)相比,全局锁在发生异常时会自动释放


表锁


表级别对操作的整张表加锁, 锁定颗粒度大,资源消耗少,不会出现死锁,但并发度低

分为表共享锁和表排他锁,注意:意向锁为表锁,但是由存储引擎自己维护,无需用户手工命令干预。

显示加锁方式:lock tables {tb_name} read/write 释放锁:unlock table {tb_name} (连接中断也会自动释放)


行锁


InnoDB支持行级别锁, 锁粒度小并发度高,但是加锁开销大也很可能会出现死锁

innodb行锁住的是索引项,注意当回表时,主键的聚簇索引也会加上锁。

举个栗子:


INSERT INTO `test_lock` (`id`,`gmt_create`,`gmt_modified`,`lock_key`,`lock_context`,`lock_biz`) VALUES (1,now(),now(),'123456',null,'AccountUser');


当执行下面语句,因为查询字段多余组合索引覆盖字段,会出现回表操作补齐其他字段,此时唯一索引 lock_key=123423-lock_biz=AccountUsery以及主键索引 id=1,均被锁住。


select * from test_lock where lock_key='123456' and lock_biz='AccountUser' for update;


加锁方式:

  1. 普通 select… 查询 (不加锁)
  2. 普通 insert、update、delete… (隐式加写锁)
  3. select…lock in share mode (加读锁)
  4. select…for update (加写锁)


解锁:

提交/回滚事物(commit/rollback) kill 阻塞进程

由于行锁用的最多且更容易出现死锁问题,下面会详细讲述行锁。


锁的模式分类


我们常规理解的锁分为2大类:读锁(也叫共享锁,S)和写锁(也叫排他锁,X)。


这两把锁之间的兼容性说明如下表


横轴表示已持有的锁,纵轴表示尝试获取的锁。1表示成功(即兼容,表现为正常进行下一步操作),0表示失败(即冲突,表现为阻塞住当前操作)


兼容性

X

S

X

0

0

S

0

1


总结一句话就是:排他锁和任何锁均不兼容。


如果仅有读写锁,会存在一个性能问题,思考下面这个场景,其中T代表事务,T1代表事务1,以此类推。

T1: 锁住表中的一行,只能读不能写(行级读锁)。

T2:申请整个表的写锁(表级写锁)。

如果T2申请成功,则能任意修改表中的一行,但这与T1持有的行锁是冲突的。故数据库应识别这种冲突,让T2的申请锁被阻塞,直到T1释放行锁。

若自己实现,最容易想到的识别方案就是遍历:

step1:判断表是否已被其他事物用表锁锁住。

step2: 判断表中的每一行是否已被行锁锁住。

其中step2需要遍历整个表,效率在数据库是没法接收的。因此innodb使用意向锁来解决这个问题


Innodb实现方案:

T1需要先申请表的意向共享锁IS(注意意向共享锁为表级锁,且是由存储引擎自己维护,无需用户命手工命令干预),成功后再申请一行的行锁S。

在意向锁存在的情况下,上面的判断可以改为:step1: 不变

step2:发现表上有意向共享锁,说明表中行被共享行锁锁住了,因此,事务B申请表的写锁被阻塞。


此时就引入的意向锁,加入意向锁后,锁的兼容性分析如下表:

横轴表示已持有的锁,纵轴表示尝试获取的锁。1表示成功(即兼容,表现为正常进行下一步操作),0表示失败(即冲突,表现为阻塞住当前操作)


兼容性

IX

IS

X

S

IX

1

1

0

0

IS

1

1

0

1

X

0

0

0

0

S

0

1

0

1


锁的类型分类


key代表二级索引,pk代表主键,截图二级索引一段数据,说明锁的分类。


图片.png


记录锁(Record Locks)


  1. 记录锁是最简单的行锁,仅仅锁住一行。如:
SELECT id FROM t WHERE id=1 FOR UPDATE
  1. 记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。
  2. 会阻塞其他事务对其插入、更新、删除 ;;


间隙锁(Gap Locks)


  1. 间隙锁是一种加在两个索引之间的锁(众所周知索引是有序的),或者加在第一个索引之前,或最后一个索引之后的间隙。
  2. 使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
  3. 间隙锁只阻止其他事务插入到间隙中,他们不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock 有相同的作用。


Next-Key Locks


  1. Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。


插入意向锁(Insert Intention)


顾明思义,该锁只会出现在insert操作执行前(并不是所有insert操作都会出现),目的是为了提高并发插入能力,注意虽有意向二字,但插入意向锁是行锁。

  1. 插入意向锁是在插入一行记录操作之前设置的一种特殊的间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。
    普通的Gap Lock 不容许 在 (上一条记录,本记录) 范围内插入数据
    插入意向锁Gap Lock 容许 在 (上一条记录,本记录) 范围内插入数据
  2. 假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方不冲突的插入行。


锁类型兼容矩阵,横轴表示已持有的锁,纵轴表示尝试获取的锁。1表示成功(即兼容,表现为正常进行下一步操作),0表示失败(即冲突,表现为阻塞住当前操作):


兼容性

Gap

Insert Intention

Record

Next-Key

Gap

1

1

1

1

Insert Intention

0

1

1

0

Record

1

1

0

0

Next-Key

1

1

0

0


锁组合


虽然我们了解了上述知识,但是看死锁日志时偶尔也会一脸懵,是因为实际运行他们是组合起来共同完成的锁机制。


锁的模式:

lock_s(读锁,共享锁)

lock_x(写锁,排它锁)


锁的类型:

Record_Lock              (锁记录)

Gap_Lock                  (锁记录前的GAP)

Next-Key Lock           (同时锁记录+记录前的GAP)

insert_Intention_Lock   (插入意向锁,其实是特殊的GAP锁)


锁模型可以和锁类型任意组合,如:

locks gap before rec,表示为gap锁:lock->type_mode & LOCK_GAP

locks rec but not gap,表示为记录锁,非gap锁:lock->type_mode & LOCK_REC_NOT_GAP

insert intention,表示为插入意向锁:lock->type_mode & LOCK_INSERT_INTENTION

waiting,表示锁等待:lock->type_mode & LOCK_WAIT


在mysql源码中使用了uint32类型来表示锁, 最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type(目前只用了 5 和 6 位,大小为 16 和 32 ,表示 LOCK_TABLE 和 LOCK_REC), 剩下的高位 bit 表示行锁的类型record_lock_type


insert语句加锁详解


 唯一性索引插入


图片.png


 非唯一性索引插入


图片.png


问题分析


 复现


表中已有数据:


----+---------------------+---------------------+----------+-------------+-------------+
| id | gmt_create          | gmt_modified        | lock_key | lock_biz    | lock_context|
+----+---------------------+---------------------+----------+-------------+-------------+
| 12 | 2022-02-15 19:54:42 | 2022-02-15 19:54:42 | 123      | accountUser |           0 |
| 50 | 2022-02-15 19:55:05 | 2022-02-15 19:55:05 | 150      | accountUser |           0 |
| 75 | 2022-02-15 19:55:19 | 2022-02-15 19:55:19 | 200      | accountUser |           0 |


从死锁日志可以看出,是由于insert操作引发死锁,故重点研究与讲解。

步骤 T1 T2 T3 T4
1 begin;
begin;
2 insert into test_lock( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '140', 'AccountUser');
insert into test_lock( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '144', 'AccountUser');
3
begin;

begin;
4
insert into test_lock`( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '140', 'AccountUser');
insert into `test_lock`( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '144', 'AccountUser');
5 存在lock_key=140, lock_biz='AccoutUser'的X record行锁 因为出现唯一性冲突,故加S Next-key Lock,锁住(123-140],(140,150]之间的空间, 存在lock_key=144, lock_biz='AccoutUser'的X record行锁 因为出现唯一性冲突,故加S Next-key Lock,锁住(123-144],(144,150]之间的空间
6 rollback; 申请插入意向锁,阻塞

7

rollback; 申请插入意向锁,阻塞
8
成功获取锁,并插入
deadLock


T1: insetrt后,存在lock_key=140, lock_biz='AccoutUser'的X记录锁

T2: 与T1发生唯一键冲突,故加上S Next-key Lock(也就是lock mode S waiting),锁住(123-140],(140,150]之间的空间。

T3: insert后,存在lock_key=144, lock_biz='AccoutUser'的X记录锁

T4: 与T3发生唯一键冲突,故加上S Next-key Lock(也就是lock mode S waiting),锁住(123-144],(144,150]之间的空间。

T2:T1 回滚后,T2与T4锁冲突,等待T4 S-Next-key Lock锁释放,然后申请意向锁,在日志中显示lock_mode X locks gap before rec insert intention waiting.

T4:T3回滚后,T2和T4同时申请意向锁,死锁出现。

通过show engine innodb status;命令查看死锁日志,可以看到与线上表现一致。


------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-02-15 20:34:19 0x70000ec62000
*** (1) TRANSACTION:
TRANSACTION 8501, ACTIVE 10 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 5, OS thread handle 123145550733312, query id 93 localhost root update
insert into `test_lock`( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '144', 'AccountUser')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index idx_uk_lock_name of table `dianjing_test`.`test_lock` trx id 8501 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 3; hex 313530; asc 150;;
 1: len 11; hex 6163636f756e7455736572; asc accountUser;;
 2: len 8; hex 8000000000000032; asc        2;;
*** (2) TRANSACTION:
TRANSACTION 8495, ACTIVE 31 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 123145550176256, query id 89 localhost root update
insert into `test_lock`( `gmt_create` ,`gmt_modified`  ,`lock_key` , `lock_biz` ) VALUE (now(),now(), '140', 'AccountUser')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 4 n bits 80 index idx_uk_lock_name of table `dianjing_test`.`test_lock` trx id 8495 lock mode S locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 3; hex 313530; asc 150;;
 1: len 11; hex 6163636f756e7455736572; asc accountUser;;
 2: len 8; hex 8000000000000032; asc        2;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index idx_uk_lock_name of table `dianjing_test`.`test_lock` trx id 8495 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 3; hex 313530; asc 150;;
 1: len 11; hex 6163636f756e7455736572; asc accountUser;;
 2: len 8; hex 8000000000000032; asc        2;;


小注解:mysql的锁,放在系统库information_schema的INNODB_LOCKS和INNODB_LOCK_WAITS两个表中,可直接select查看。


 业务场景分析


复现出问题,在结合业务流程去排查和分析,发现问题出自结算域的多个定时任务中,这些定时任务的使用分布式定时框架,执行模式为网格计算,结算的每条记录都有主键id, 活动id商家id以及用于计算结算金额的投放数据uv和pv等,原定时任务的处理逻辑是by最细粒度的活动数据主键id步进分发到各个机器上,每个机器通过id查找计算应结算金额,然后开启事务-》加锁-》执行扣款操作(涉及解冻等复杂逻辑,会进行多表操作)-》释放锁-》提交事务。

注:网格计算本质上是通过并发提来提升任务的处理速度,分为主任务和子任务,主任务负责按照自定义规则分发子任务到各个机器上,子任务互不干涉同步运行。


 解决方案


通过业务场景梳理,很容易就发现2个设计问题-并发和大事务。


并发


因配置不当,人为造成高并发场景。针对并发我们进行了两种方式的改造:

  1. 针对只有少量数据的结算任务,改成单机运行,实例并发数设置为1。
  2. 针对大数据量的结算任务,主任务的分发逻辑从基于id步进分发,改为基于sellerId纬度分发,原因是结算是基于商家纬度进行的,分布式锁的纬度也是sellerId。


大事务


大事务导致独占区变大(加解锁的逻辑也归属与独占区了),增加了冲突概率,相当于变相提升了并发度。


针对大事务问题,因为本质上锁的方案和业务执行逻辑完全无关,这里将技术问题和业务逻辑进行了耦合,故按照解耦的思路将加锁和解锁操作开启独立子事务,减少冲突的概率。


参考文献


  1. MySQL锁介绍与加锁分析
  2. insert 语句加锁机制(https://cloud.tencent.com/developer/article/1181532?from=14588
  3. 如何阅读死锁日志(https://cloud.tencent.com/developer/article/1181190
  4. MySQL死锁案例分析(https://cloud.tencent.com/developer/article/1892524?from=article.detail.1181187
  5. MySQL · 引擎分析 · InnoDB行锁分析(http://mysql.taobao.org/monthly/2018/05/04/
  6. MySQL锁系列(七)之 锁算法详解(http://keithlan.github.io/2017/06/21/innodb_locks_algorithms/
  7. mysql 查看谁在持有锁(http://www.javashuo.com/article/p-wixmuvea-co.html


团队介绍


行业与品牌营销团队

我们是一只有凝聚力、有活力的团队,主要负责打通淘系业务中商品,商家,前台场景等生产要素,重点围绕着运营,商家及品类架构体系的数据化、智能化、规模化和平台化做核心突破方向,同时联合各技术团队和业务团队,共同打造电商运营操作系统。

【春招校园招聘】:java开发实习生、数据实习生

【招聘范围】:2023年毕业生

【工作地点】:杭州

如果您有内推需求,可将简历发至邮箱lx240393@alibaba-inc.com或添加作者微信lixstudy进行详细咨询,欢迎来撩~


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
7月前
|
SQL AliSQL 关系型数据库
MYSQL的全局锁和表锁
本文介绍了MySQL中的锁机制,包括全局锁、表级锁及其应用场景。全局锁通过`Flush tables with read lock (FTWRL)`实现,主要用于全库逻辑备份,但会阻塞更新和结构变更操作。表级锁分为显式表锁(`lock tables`)和元数据锁(MDL),前者用于控制并发访问,后者自动加锁以确保读写正确性。文章还探讨了如何安全地为小表添加字段,建议通过设置DDL等待时间或使用MariaDB/AliSQL的NOWAIT/WAIT功能避免业务阻塞。这些方法有助于在高并发场景下优化数据库性能与安全性。
196 0
|
存储 关系型数据库 MySQL
MySQL MVCC全面解读:掌握并发控制的核心机制
【10月更文挑战第15天】 在数据库管理系统中,MySQL的InnoDB存储引擎采用了一种称为MVCC(Multi-Version Concurrency Control,多版本并发控制)的技术来处理事务的并发访问。MVCC不仅提高了数据库的并发性能,还保证了事务的隔离性。本文将深入探讨MySQL中的MVCC机制,为你在面试中遇到的相关问题提供全面的解答。
915 2
|
缓存 关系型数据库 MySQL
MySQL并发支撑底层Buffer Pool机制详解
【10月更文挑战第18天】在数据库系统中,磁盘IO操作是性能瓶颈之一。为了提高数据访问速度,减少磁盘IO,MySQL引入了缓存机制。其中,Buffer Pool是InnoDB存储引擎中用于缓存磁盘上的数据页和索引页的内存区域。通过缓存频繁访问的数据和索引,Buffer Pool能够显著提高数据库的读写性能。
567 2
|
SQL 关系型数据库 MySQL
MySQL 锁
MySQL里常见的几种锁
229 3
|
10月前
|
关系型数据库 MySQL 网络安全
如何排查和解决PHP连接数据库MYSQL失败写锁的问题
通过本文的介绍,您可以系统地了解如何排查和解决PHP连接MySQL数据库失败及写锁问题。通过检查配置、确保服务启动、调整防火墙设置和用户权限,以及识别和解决长时间运行的事务和死锁问题,可以有效地保障应用的稳定运行。
413 25
|
11月前
|
存储 关系型数据库 MySQL
MySQL进阶突击系列(06)MySQL有几种锁?| 别背答案,现场演示一下
本文详细解析了MySQL InnoDB存储引擎的锁机制,涵盖读锁、写锁、意向锁、记录锁、间隙锁和临键锁等8种锁类型。重点探讨了不同锁类型的加锁与释放方式,以及事务并发场景下的实战验证。通过具体示例,展示了在不同情况下锁的行为及其对事务的影响。文章还特别强调了锁的作用范围主要是索引,并解释了锁如何影响数据的读写操作。最后总结了并发事务中加锁规则,帮助读者深入理解MySQL的锁机制。
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
736 1
|
关系型数据库 MySQL Java
MySQL数据锁:Record Lock,Gap Lock 和 Next-Key Lock
本文基于 MySQL 8.0.30 版本及 InnoDB 引擎,深入解析三种行锁机制:记录锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-key Lock)。记录锁锁定索引记录,确保事务唯一修改;间隙锁锁定索引间的间隙,防止新记录插入;临键锁结合两者,锁定范围并记录自身,有效避免幻读现象。通过具体示例展示了不同锁的作用机制及其在并发控制中的应用。
1264 2
|
存储 关系型数据库 MySQL
MySQL数据库锁:共享锁和独占锁
本文详细介绍了`InnoDB`存储引擎中的两种行级别锁:共享锁(S锁)与排他锁(X锁)。通过具体示例展示了这两种锁的工作机制及其在`InnoDB`与`MyISAM`引擎中的表现差异。文章还提供了锁的兼容性矩阵,帮助读者更好地理解锁之间的互斥关系。最后总结了两种锁的特点及适用场景。适合希望深入了解`MySQL`并发控制机制的读者阅读。
488 1
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
358 0

推荐镜像

更多