type_mode & LOCK_GAP"]]],["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"locks rec but not gap 表示为记录锁,非gap锁:lock->type_mode & LOCK_REC_NOT_GAP"]]],["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"insert intention 表示为插入意向锁:lock->type_mode & LOCK_INSERT_INTENTION"]]],["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"waiting 表示锁等待:lock->type_mode & LOCK_WAIT"]]],["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},""]]],["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"在mysql源码中使用了uint32类型来表示锁,"],["span",{"color":"rgb(48, 48, 48)","fonts":{"ascii":"Avenir","hAnsi":"Avenir","cs":"Avenir","eastAsia":"Avenir"},"sz":11.5,"szUnit":"pt","data-type":"leaf"}," 最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type(目前只用了 5 和 6 位,大小为 16 和 32 ,表示 LOCK_TABLE 和 LOCK_REC), 剩下的高位 bit 表示行锁的类型record_lock_type"]]]]" style="text-align: justify; margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; outline: 0px; letter-spacing: 0.544px; white-space: normal; color: rgba(0, 0, 0, 0.8); font-family: -apple-system-font, system-ui, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; background-color: rgb(255, 255, 255);">锁模型可以和锁类型任意组合,如:
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
▐ 唯一性索引插入

▐ 非唯一性索引插入

▐ 复现
表中已有数据:
----+---------------------+---------------------+----------+-------------+-------------+| 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操作引发死锁,故重点研究与讲解。 | | | | |
|
|
| |
|
| 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'); |
|
|
| |
| |
|
| 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'); |
| 存在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]之间的空间 |
| | |
|
|
|
|
| | |
|
| |
| |
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 insertingmysql tables in use 1, locked 1LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1MySQL thread id 5, OS thread handle 123145550733312, query id 93 localhost root updateinsert 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 waitingRecord 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 insertingmysql tables in use 1, locked 15 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1MySQL thread id 3, OS thread handle 123145550176256, query id 89 localhost root updateinsert 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 recRecord 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 waitingRecord 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。
针对大数据量的结算任务,主任务的分发逻辑从基于id步进分发,改为基于sellerId纬度分发,原因是结算是基于商家纬度进行的,分布式锁的纬度也是sellerId。
大事务导致独占区变大(加解锁的逻辑也归属与独占区了),增加了冲突概率,相当于变相提升了并发度。
针对大事务问题,因为本质上锁的方案和业务执行逻辑完全无关,这里将技术问题和业务逻辑进行了耦合,故按照解耦的思路将加锁和解锁操作开启独立子事务,减少冲突的概率。
MySQL锁介绍与加锁分析
insert 语句加锁机制(https://cloud.tencent.com/developer/article/1181532?from=14588)
如何阅读死锁日志(https://cloud.tencent.com/developer/article/1181190)
-
MySQL死锁案例分析(https://cloud.tencent.com/developer/article/1892524?from=article.detail.1181187)
MySQL · 引擎分析 · InnoDB行锁分析(http://mysql.taobao.org/monthly/2018/05/04/)
MySQL锁系列(七)之 锁算法详解(http://keithlan.github.io/2017/06/21/innodb_locks_algorithms/)
mysql 查看谁在持有锁(http://www.javashuo.com/article/p-wixmuvea-co.html)
行业与品牌营销团队
我们是一只有凝聚力、有活力的团队,主要负责打通淘系业务中商品,商家,前台场景等生产要素,重点围绕着运营,商家及品类架构体系的数据化、智能化、规模化和平台化做核心突破方向,同时联合各技术团队和业务团队,共同打造电商运营操作系统。
【春招校园招聘】:java开发实习生、数据实习生
【招聘范围】:2023年毕业生
【工作地点】:杭州
如果您有内推需求,可将简历发至邮箱lx240393@alibaba-inc.com或添加作者微信lixstudy进行详细咨询,欢迎来撩~
