您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

从一个死锁问题分析优化器特性

时间:2023-09-22 11:55:03  来源:  作者:OSC开源社区

作者:李锡超,一个爱笑的江苏苏宁银行 数据库工程师,主要负责数据库日常运维、自动化建设、DMP 平台运维。擅长 MySQLPython/ target=_blank class=infotextkey>Python、Oracle,爱好骑行、研究技术。

爱可生开源社区出品

本文约 2100 字,预计阅读需要 7 分钟。

1问题现象

自发布了 INSERT 并发死锁问题的文章,收到了多次死锁问题的交流。一个具体案例如下:

研发反馈应用发生死锁,收集如下诊断内容:

------------------------

LATEST DETECTED DEADLOCK

------------------------

2023-07-04 06:02:40 0x7fc07dd0e700

*** (1) TRANSACTION:

TRANSACTION 182396268, ACTIVE 0 sec fetching rows

mysql tables inuse 1, locked 1

LOCK WAIT 21 lock struct(s), heap size 3520, 2 row lock(s), undo logentries 1

MySQL thread id 59269692, OS thread handle 140471135803136, query id 3738514953 192.168.0.215 user1 updating

delete from ltb2 wherec = 'CCRSFD07E'and j = 'Y15'and b >= '20230717'and d != '1'and e != '1'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 603 page no 86 n bits 248 index PRIMARY of table `testdb`.`ltb2`trx id 182396268 lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:

TRANSACTION 182396266, ACTIVE 0 sec fetching rows, thread declared inside InnoDB 1729

mysql tables inuse 1, locked 1

28 lock struct(s), heap size 3520, 2 row lock(s), undo logentries 1

MySQL thread id 59261188, OS thread handle 140464721291008, query id 3738514964 192.168.0.214 user1 updating

update ltb2 setf = '0', g = '0', is_value_date = '0', h = '0', i = '0'wherec = '22115001B'and j = 'Y4'and b >= '20230717'

*** (2) HOLDS THE LOCK(S):

RECORD LOCKS space id 603 page no 86 n bits 248 index PRIMARY of table `testdb`.`ltb2`trx id 182396266 lock_mode X locks rec but not gap

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 603 page no 86 n bits 248 index PRIMARY of table `testdb`.`ltb2`trx id 182396266 lock_mode X locks rec but not gap waiting

*** WE ROLL BACK TRANSACTION (1)

------------

以上 space id 603 page no 86 n bits 248,其中 space id 表示表空间 ID,page no 表示记录锁在表空间内的哪一页,n bits 是锁位图中的位数,而不是页面偏移量。记录的页偏移量一般以 heap no 的形式输出,但此例并未输出该信息。

基本环境信息

确认如下问题相关信息:

  • 数据库版本:Percona MySQL 5.7

  • 事务隔离级别:Read-Commited

  • 表结构和索引:

CREATE TABLE `ltb2` (

`ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',

`j` varchar(16) DEFAULT NULL COMMENT '',

`c` varchar(32) NOT NULL DEFAULT ''COMMENT '',

`b` date NOT NULL DEFAULT '2019-01-01'COMMENT '',

`f` varchar(1) NOT NULL DEFAULT ''COMMENT '',

`g` varchar(1) NOT NULL DEFAULT ''COMMENT '',

`d` varchar(1) NOT NULL DEFAULT ''COMMENT '',

`e` varchar(1) NOT NULL DEFAULT ''COMMENT '',

`h` varchar(1) NOT NULL DEFAULT ''COMMENT '',

`i` varchar(1) DEFAULT NULL COMMENT '',

`LAST_UPDATE_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',

PRIMARY KEY (`ID`),

UNIQUE KEY `uidx_1` (`b`,`c`)

) ENGINE=InnoDB AUTO_INCREMENT=270983 DEFAULT CHARSET=utf8mb4 COMMENT='';

关键信息梳理

  事务 T1
语句 delete from ltb2 where c = 'code001' and j = 'Y15' and b >= '20230717' and d != '1' and e != '1'
关联对象及记录 space id 603 page no 86 n bits 248 index PRIMARY of table testdb.ltb2
持有的锁 未知
等待的锁 lock_mode X locks rec but not gap waiting
  事务 T2
语句 update ltb2 set f = '0', g = '0', is_value_date = '0', h = '0', i = '0' where c = '22115001B' and j = 'Y4' and b >= '20230717'
关联对象及记录 space id 603 page no 86 n bits 248 index PRIMARY of table testdb.ltb2
持有的锁 lock_mode X locks rec but not gap
等待的锁 lock_mode X locks rec but not gap waiting

可以看到在主键索引上发生了死锁,但是在查询的条件中,并未使用主键列。

那为什么会在主键列出现死锁?在分析死锁根因问题前,需要先清楚 SQL 的执行情况。

2SQL 执行情况执行计划

以上两个 SQL 发现都有列 b、c 作为条件,且该列构成了索引唯一索引 uidx_1。简化 SQL 改为查询语句,并确认执行计划:

mysql> desc select* fromltb2 whereb >= '20230717'andc = 'code001';

# 部分结果

+----- -+-------------------+------+---------+

| type | possible_keys | key | Extra |

+----- -+-------------------+------+---------+

| ALL | uidx_1 | NULL | Using where |

+----- -+-------------------+------+---------+

注意:自 MySQL 5.6 开始可以直接查看 UPDATE/DELETE/INSERT 等语句的执行计划。因个人习惯、避免误操作等原因,还是习惯改为 SELECT 查看执行计划。

执行计划中可能的索引有 uidx_1(b,c),但实际并未使用该索引,而是采用全表扫描方式执行。

根据经验,由于列 b 为索引的最左列。但查询的条件为 b>= '20230717',即该条件不是等值查询。因此数据库可能只能“使用”到 b 列。为进一步确认不使用 b 列索引的原因,查询数据分布:

mysql> selectcount(1) fromltb2;

+------------+

| count(1) |

+------------+

| 4509 |

+------------+

mysql> selectcount(1) fromltb2 whereb >= '20230717';

+------------+

| count(1) |

+------------+

| 1275 |

+------------+

计算满足 b 列条件的数据占比为 1275/4509 = 28%,占比差不多达到了 1/3。此时也的确不应使用该使用索引。

难道已经是作为 MySQL 5.7 的数据库,优化器还是这么简单?

ICP 特性

带着问题,将条件设置一个更大的值(但小于该列的最大值),再次执行验证查询语句:

mysql> desc select* fromltb2 whereb >= '20990717';

# 部分结果

+----------+---------+---------+

| key_len | rows | Extra |

+----------+---------+---------+

| 3 | 64 | Using Index condition |

+----------+---------+---------+

优化器预估返回 64 行,数据占比 64/4509 = 1.4%,因此可以使用索引。但通过执行计划,从 Extra 列看到 Using index condition 提示。该提示则说明使用了索引条件下推(Index Condition Pushdown, ICP)。针对该特性,参考官方简要说明如下:

使用 Index Condition Pushdown,扫描将像这样进行:

  1. 获取下一行的索引元组(但不是完整的表行)。

  2. 测试 WHERE 条件中应用于此表的部分,并且只能使用索引列的进行检查。如果不满足条件,则继续到下一行的索引元组。

  3. 如果满足条件,则使用索引元组定位并读取整个表行。

  4. 测试适用于此表的 WHERE 条件的其余部分。根据测试结果接受或拒绝该行。

既然可以使用到 ICP 特性,进一步执行如下验证语句:

mysql> desc select* fromltb2 whereb >= '20990717'andc = 'code001';

# 部分结果

+----------+---------+---------+

| key_len | rows | Extra |

+----------+---------+---------+

| 133 | 64 | Using Index condition |

+----------+---------+---------+

发现当新增 c 列作为条件后,并且根据 key_len(索引里使用的字节数)可以判断,的确使用到了 uidx_1 索引中的 c 列。但 rows 的结果与实际返回结果差异较大(实际执行仅返回 0 行)。

更重要的是,既然具有 ICP 特性,针对原始的 SQL 为什么不能助于 ICP 特性使用到索引呢?

mysql> select * from ltb2 whereb >= '20230717'and c = 'code001'

执行计划跟踪

继续带着问题,通过 MySQL 提供的 OPTIMIZER TRACE,跟踪执行计划生成过程。命令如下:

SETOPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;

SETOPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;

-- sql-1:

select* fromltb2 whereb >= '20990717'andc = 'code001';

-- sql-2:

select* fromltb2 whereb >= '20990717';

-- sql-3

select* fromltb2 whereb >= '20230717'andc = 'code001';

SELECT* FROMINFORMATION_SCHEMA.OPTIMIZER_TRACEG

SEToptimizer_trace="enabled=off";

由于分析结果较长,截取 SQL-1 和 SQL-2 的部分结果 (rows_estimation 和 considered_execution_plans)。具体内容如下:

SQL-1select * from ltb2 whereb >= '20990717'and c = 'code001'

# 分析结果

"analyzing_range_alternatives":{

"range_scan_alternatives":[

{

"index":"uidx_1",

"ranges":[

"0xe76610 <= b"

] /* ranges */,

"index_dives_for_eq_ranges": true,

"rowid_ordered": false,

"using_mrr": false,

"index_only": false,

"rows":64,

"cost": 77.81,

"chosen": true

}

] /* range_scan alternatives */

}

"best_access_path":{

"considered access_paths":[

"rows_to_scan": 64,

"access_type":"range",

"range_details":{

"used index";"uidx 1"

} /* range_details */,

"resulting_rows": 64,

"cost": 90.61,

"chosen": true

}

] /* considered access_paths */

} /* best access_path */,

SQL-2select * from ltb2 whereb >= '20990717'

# 分析结果

"analyzing_range_alternatives":{

"range_scan_alternatives":[

{

"index":"uidx_1",

"ranges":[

"0xe76610 <= b"

] /* ranges */,

"index_dives_for_eq_ranges": true,

"rowid_ordered": false,

"using_mrr": false,

"index_only": false,

"rows":64,

"cost": 77.81,

"chosen": true

}

] /* range_scan alternatives */

}

"considered access_paths":[

{

"rows_to_scan": 64,

"access_type":"range",

"range_details":{

"used index":"uidx_1"

} /* range_details */,

"resulting_rows": 64,

"cost": 90.61,

"chosen": true

}

] /* considered access_paths */,

根据以上信息:两个 SQL 的 cost 部分是完全相同的,且在优化器分析阶段只能识别到 b 的条件。分析阶段,只能根据优化器认为可用的列来计算 cost。ICP 特性,应该是在执行阶段采用用到的特性。

同时,根据 SQL-3 的执行跟踪结果,对比全表扫描和索引扫描的 cost,截取部分结果如下:

SQL-3select * from ltb2 whereb >= '20230717'and c = 'code001';

# 全表扫描结果

"range_analysis": {

"table _scan": {

"rows": 4669,

"cost": 1018.9

} /* table_scan */,

# 索引扫描评估结果

"analyzing_range_alternatives": {

"range_scan_alternatives": [

{

"index":"uidx_1",

"ranges":[

"@xe7ce0f] <= b"

] /* ranges */,

"index dives_for_eq_ranges": true,

"rowid_ordered": false,

"using_mrr": false,

"index_only": false,

" rows": 1273,

"cost": 1528.6,

"chosen": false,

"cause":"cost"

}

] /* range scan_alternatives */,

# 最优执行计划

"best_access_path": {

"considered access_paths":[

{

"rows_to_scan": 4669,

"access_type":"scan",

"resulting_rows": 4669,

"cost": 1016.8,

"chosen": true

}

] /* considered access_paths *//* best access_path */

}

由于优化器阶段使用使用列 b,使用索引的成本高于全表扫描。那最终数据库就会选择使用全表扫描。除非应用使用 hint 强制索引:

mysql> desc select * from ltb2 FORCE INDEX (uidx_1) whereb >= '20230717'and c = 'code001';

# 部分结果

+----------+---------+---------+

| key_len | rows | Extra |

+----------+---------+---------+

| 133 | 1273 | Using Index condition |

+----------+---------+---------+

同时,根据执行计划的输出结果,rows 列应该是优化器阶段的输出,key_len/Extra 则包括了执行阶段的输出。

小结

综上所述,对于问题 SQL 和索引结构,由于列 b 为索引的最左列,且查询时的条件为 b>= '20230717'(非等值条件),数据库优化器只能“使用”到 b 列。并给予“使用”的列,评估扫码的行数和 cost。

如果优化器评估后,使用索引的成本更低,则可以使用该索引,并利用 ICP 特性进一步提高查询性能;

如果优化器评估后,使用全表扫描或的成本更低,那数据库就会选择使用全表扫描。

3SQL 优化方案

根据第 2 部分明确了问题的原因后,通过调整索引,解决最左列尾范围查询的问题即可解决该问题。具体如下:

altertableltb2 dropindexuidx_1;

altertableltb2 addindexuidx_1(c,b);

altertableltb2 addindexidx_(b);

死锁为何发生

自此,完成了 SQL 执行计划问题的分析和解决。但直接的问题是死锁,因查询语句无法使用索引,正常就应该使用全表扫描。但是全表扫描为什么会出现死锁呢?

在此,参考《故障分析 | 从 Insert 并发死锁分析 Insert 加锁源码逻辑》的经验,对死锁过程进行大胆猜想:

T1 时刻

trx-2 执行了 UPDATE,在处理行时,在 row_search_mvcc 函数中,查询到数据。获取了对应行的 LOCK_X,LOCK_REC_NOT_GAP 锁;

T2 时刻

trx-1 执行了 DELETE,在处理行时,在 row_search_mvcc 函数中,查询到数据,尝试获取行的 LOCK_X,LOCK_REC_NOT_GAP。但由于 trx-1 已经持有了该锁,因此被堵塞。并会创建一个锁(以指示锁等待);

T3 时刻

trx-2 继续执行 UPDATE 操作。由于是该操作除了在 T1 时刻的操作外,在其它位置,还需要获取锁(lock_mode X locks rec but not gap)。但由于 T2 时刻,trx-1 尝试获取该锁而被堵塞,并且也增加了一个锁。

假如此时,此处的实现机制和 INSERT 死锁案例一样,也没有先进行冲突检查。而只是看记录上是否存在锁的话,那么此时也会看到该记录上有 trx-1 事务的锁。从而导致 trx-2 第二次获取锁时,被堵塞。

死锁发生!

以上仅根据经验进行的猜想,真正的原因还需要进一步分析和验证。有兴趣的读者结合如下几个问题,进一步研究。

  1. 以上各步骤获取锁的位置,是否正确?

  2. T3 时刻,update操作在其它的什么位置再次获取了锁?

  3. T3 时刻,发起的假设是否成立?如成立,具体逻辑是什么?不成立,那正确的逻辑是什么?

  4. T3 时刻,如果假设不成立,那死锁的原因又是什么?

  5. 以上都是针对于唯一索引/主键索引的执行逻辑分析的。那结合该案例,全表扫描和索引查询的执行逻辑是否存在差异?差异的地方在哪里?

  6. 除了调整索引,还能通过什么方式避免该问题发生?



Tags:优化器   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
准线上事故之MySQL优化器索引选错
1 背景最近组里来了许多新的小伙伴,大家在一起聊聊技术,有小兄弟提到了MySQL的优化器的内部策略,想起了之前在公司出现的一个线上问题,今天借着这个机会,在这里分享下过程和结论...【详细内容】
2024-03-07  Search: 优化器  点击:(26)  评论:(0)  加入收藏
突破Pytorch核心点,优化器 !!
今儿咱们聊聊pytorch中的优化器。优化器在深度学习中的选择直接影响模型的训练效果和速度。不同的优化器适用于不同的问题,其性能的差异可能导致模型更快、更稳定地收敛,或者...【详细内容】
2024-01-05  Search: 优化器  点击:(90)  评论:(0)  加入收藏
从一个死锁问题分析优化器特性
作者:李锡超,一个爱笑的江苏苏宁银行 数据库工程师,主要负责数据库日常运维、自动化建设、DMP 平台运维。擅长 MySQL、Python、Oracle,爱好骑行、研究技术。爱可生开源社区出品...【详细内容】
2023-09-22  Search: 优化器  点击:(179)  评论:(0)  加入收藏
Python人工智能tensorflow优化器Optimizer算法汇总
这篇文章主要为大家介绍了python人工智能tensorflowtf优化器Optimizer算法汇总,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪目录 前言 tensorflo...【详细内容】
2023-06-28  Search: 优化器  点击:(220)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条