Mysql Innodb死锁情况分析与归纳

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
日志服务 SLS,月写入数据量 50GB 1个月
简介: Mysql Innodb死锁情况分析与归纳

案例描述
     在定时脚本运行过程中,发现当备份表格的sql语句与删除该表部分数据的sql语句同时运行时,mysql会检测出死锁,并打印出日志。

     两个sql语句如下:
     (1)insert into backup_table select * from source_table

     (2)DELETE FROM source_table WHERE Id>5 AND titleWeight<32768 AND joinTime<'$daysago_1week'

     teamUser表的表结构如下:

     PRIMARY KEY  (`uid`,`Id`),

     KEY `k_id_titleWeight_score` (`Id`,`titleWeight`,`score`),

     ENGINE=InnoDB

两语句对source_table表的使用情况如下:

    死锁日志打印出的时间点表明,语句(1)运行过程中,当语句(2)开始运行时,发生了死锁。

     当mysql检测出死锁时,除了查看mysql的日志,还可以通过show engine innodb status 语句在mysql客户端中查看最近一次的死锁记录。

     得到的死锁记录如下:



 

     根据死锁记录的结果,可以看出确实是这两个语句发生了死锁,且锁冲突发生在主键索引上。那么,为什么两个sql语句会存在锁冲突呢?冲突为什么会在主键索引上呢?语句(2)得到了主键索引锁,为什么还会再次申请锁呢?

锁冲突分析
2.1 innodb的事务与行锁机制
     MySQL的事务支持不是绑定在MySQL服务器本身,而是与存储引擎相关,MyISAM不支持事务、采用的是表级锁,而InnoDB支持ACID事务、行级锁、并发。MySQL默认的行为是在每条SQL语句执行后执行一个COMMIT语句,从而有效的将每条语句作为一个单独的事务来处理。

2.2 两语句加锁情况
     在innodb默认的事务隔离级别下,普通的SELECT是不需要加行锁的,但LOCK IN SHARE MODE、FOR UPDATE及高串行化级别中的SELECT都要加锁。有一个例外,此案例中,语句(1)insert into teamUser_20110121 select * from teamUser会对表teamUser_20110121(ENGINE= MyISAM)加表锁,并对teamUser表所有行的主键索引(即聚簇索引)加共享锁。默认对其使用主键索引。

     而语句(2)DELETE FROM teamUser WHERE teamId=$teamId AND titleWeight<32768 AND joinTime<'$daysago_1week'为删除操作,会对选中行的主键索引加排他锁。由于此语句还使用了非聚簇索引KEY `k_teamid_titleWeight_score` (`teamId`,`titleWeight`,`score`)的前缀索引,于是,还会对相关行的此非聚簇索引加排他锁。

2.3 锁冲突的产生
     由于共享锁与排他锁是互斥的,当一方拥有了某行记录的排他锁后,另一方就不能其拥有共享锁,同样,一方拥有了其共享锁后,另一方也无法得到其排他锁。所以,当语句(1)、(2)同时运行时,相当于两个事务会同时申请某相同记录行的锁资源,于是会产生锁冲突。由于两个事务都会申请主键索引,锁冲突只会发生在主键索引上。

     常常看到一句话:在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的。那就说明,单个SQL组成的事务锁是一次获得的。而此案例中,语句(2)已经得到了主键索引的排他锁,为什么还会申请主键索引的排他锁呢?同理,语句(1)已经获得了主键索引的共享锁,为什么还会申请主键索引的共享锁呢?

     死锁记录中,事务一等待锁的page no与事务二持有锁的page no相同,均为218436,这又代表什么呢?

     我们的猜想是,innodb存储引擎中获得行锁是逐行获得的,并不是一次获得的。下面来证明。

死锁产生过程分析
     要想知道innodb加锁的过程,唯一的方式就是运行mysql的debug版本,从gdb的输出中找到结果。根据gdb的结果得到,单个SQL组成的事务,从宏观上来看,锁是在这个语句上一次获得的,但从底层实现上来看,是逐个记录行查询,得到符合条件的记录即对该行记录的索引加锁。

     Gdb结果演示如下:

? (gdb) b lock_rec_lock

? Breakpoint 1 at 0x867120: file lock/lock0lock.c, line 2070.

? (gdb) c

? Continuing.

? [Switching to Thread 1168550240 (LWP 5540)]

? Breakpoint 1, lock_rec_lock (impl=0, mode=5, rec=0x2aedbe01c1 "789\200", index=0x2aada734b8, thr=0x2aada74c18) at lock/lock0lock.c:2070

? 2070    {

? Current language:  auto; currently c

? (gdb) c

? Continuing.

? Breakpoint 1, lock_rec_lock (impl=0, mode=1029, rec=0x2aedbc80ba "\200", index=0x2aada730b8, thr=0x2aada74c18) at lock/lock0lock.c:2070

? 2070    {

? (gdb) c

? Continuing.

? Breakpoint 1, lock_rec_lock (impl=0, mode=5, rec=0x2aedbe01cf "789\200", index=0x2aada734b8, thr=0x2aada74c18) at lock/lock0lock.c:2070

? 2070    {

? (gdb) c

? Continuing.

(说明:"789\200"为非聚簇索引,"\200"为主键索引)

     Gdb结果显示,语句(1)(2)加锁的获取记录为多行,即逐行获得锁,这样就解释了语句(2)获得了主键索引锁还再次申请主键索引锁的情况。

     由于语句(1)使用了主键索引,而语句(2)使用了非聚簇索引,两个事务获得记录行的顺序不同,而加锁的过程是边查边加、逐行获得,于是,就会出现如下情况:

     于是,两个事务分别拥有部分锁并等待被对方持有的锁,出现这种资源循环等待的情况,即死锁。此案例中被检测时候的锁冲突就发现在page no为218436和218103的锁上。

     InnoDB 会自动检测一个事务的死锁并回滚一个或多个事务来防止死锁。Innodb会选择代价比较小的事务回滚,此次事务(1)解锁并回滚,语句(2)继续运行直至事务结束。

innodb死锁形式归纳
     死锁产生的四要素:互斥条件:一个资源每次只能被一个进程使用;请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

     Innodb检测死锁有两种情况,一种是满足循环等待条件,还有另一种策略:锁结构超过mysql配置中设置的最大数量或锁的遍历深度超过设置的最大深度时,innodb也会判断为死锁(这是提高性能方面的考虑,避免事务一次占用太多的资源)。这里,我们只考虑满足死锁四要素的情况。

     死锁的形式是多样的,但分析到innodb加锁情况的最底层,因循环等待条件而产生的死锁只有可能是四种形式:两张表两行记录交叉申请互斥锁、同一张表则存在主键索引锁冲突、主键索引锁与非聚簇索引锁冲突、锁升级导致的锁等待队列阻塞。

     以下首先介绍innodb聚簇索引与非聚簇索引的数据存储形式,再以事例的方式解释这四种死锁情况。

4.1聚簇索引与非聚簇索引介绍
     聚簇索引即主键索引,是一种对磁盘上实际数据重新组织以按指定的一个或多个列的值排序,聚簇索引的索引页面指针指向数据页面。非聚簇索引(即第二主键索引)不重新组织表中的数据,索引顺序与数据物理排列顺序无关。索引通常是通过B-Tree数据结构来描述,那么,聚簇索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点,通常是一个指针指向对应的数据块。

     而innodb在非聚簇索引叶子节点包含了主键值作为指针。(这样是为了减少在移动行或数据分页时索引的维护工作。)其结构图如下:

 


      当使用非聚簇索引时,会根据得到的主键值遍历聚簇索引,得到相应的记录。

4.2四种死锁情况
     在InnoDB中,使用行锁机制,于是,锁通常是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。

     即将分享的四种死锁的锁冲突分别是:不同表的相同记录行索引锁冲突、主键索引锁冲突、主键索引锁与非聚簇索引锁冲突、锁升级造成锁队列阻塞。

不同表的相同记录行锁冲突
     案例:两个表、两行记录,交叉获得和申请互斥锁

条件:

A、 两事务分别操作两个表、相同表的同一行记录

B、 申请的锁互斥

C、 申请的顺序不一致

主键索引锁冲突
案例:本文案例,产生冲突在主键索引锁上

条件:

A、 两sql语句即两事务操作同一个表、使用不同索引

B、 申请的锁互斥

C、 操作多行记录

D、 查找到记录的顺序不一致

主键索引锁与非聚簇索引锁冲突
案例:同一行记录,两事务使用不同的索引进行更新操作

此案例涉及TSK_TASK表,该表相关字段及索引如下:

ID:主键;

MON_TIME:监测时间;

STATUS_ID:任务状态;

索引:KEY_TSKTASK_MONTIME2 (STATUS_ID, MON_TIME)。

条件:

A、 两事务使用不同索引

B、 申请的锁互斥

C、 操作同一行记录

     当执行update、delete操作时,会修改表中的数据信息。由于innodb存储引擎中索引的数据存储结构,会根据修改语句使用的索引以及修改信息的不同执行不同的加锁顺序。当使用索引进行查找并修改记录时,会首先加使用的索引锁,然后,如果修改了主键信息,会加主键索引锁和所有非聚簇索引锁,修改了非聚簇索引列值会加该种非聚簇索引锁。

     此案例中,事务一使用非聚簇索引查找并修改主键值,事务二使用主键索引查找并修改主键值,加锁顺序不同,导致同时运行时产生资源循环等待。

锁升级造成锁队列阻塞
案例:同一行记录,事务内进行锁升级,与另一等待锁发送锁队列阻塞,导致死锁


条件:

A、 两事务操作同一行记录

B、 一事务对某一记录先申请共享锁,再升级为排他锁

C、 另一事务在过程中申请这一记录的排他锁

避免死锁的方法
     InnoDB给MySQL提供了具有提交,回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在SELECT语句提供非锁定读。这些特色增加了多用户部署和性能。

     但其行锁的机制也带来了产生死锁的风险,这就需要在应用程序设计时避免死锁的发生。以单个SQL语句组成的隐式事务来说,建议的避免死锁的方法如下:

     1.如果使用insert…select语句备份表格且数据量较大,在单独的时间点操作,避免与其他sql语句争夺资源,或使用select into outfile加上load data infile代替 insert…select,这样不仅快,而且不会要求锁定

     2. 一个锁定记录集的事务,其操作结果集应尽量简短,以免一次占用太多资源,与其他事务处理的记录冲突。

     3.更新或者删除表格数据,sql语句的where条件都是主键或都是索引,避免两种情况交叉,造成死锁。对于where子句较复杂的情况,将其单独通过sql得到后,再在更新语句中使用。

     4. sql语句的嵌套表格不要太多,能拆分就拆分,避免占有资源同时等待资源,导致与其他事务冲突。

     5. 对定点运行脚本的情况,避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操作数据量比较大的语句。

     6.应用程序中增加对死锁的判断,如果事务意外结束,重新运行该事务,减少对功能的影响。


110621 16:36:35 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 22 seconds
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 565984, signal count 499776
Mutex spin waits 0, rounds 34679668, OS waits 453120
RW-shared spins 200843, OS waits 72185; RW-excl spins 348501, OS waits 18768
------------------------
LATEST DETECTED DEADLOCK
------------------------
110618  3:04:31
*** (1) TRANSACTION:
TRANSACTION 0 41582946, ACTIVE 1 sec, process no 13236, OS thread id 1177626976 starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 368, 1 row lock(s)
MySQL thread id 2774, query id 242753807 10.10.221.41 rat Searching rows for update
update item set cell = cell + 10000 where role_id = 11683 and backpack_type = 1 and cell >= 31 and cell <= 60
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 198 page no 2424 n bits 864 index `INDEX_RD_BE_CELL` of table `webgame`.`item` trx id 0 41582946 lock_mode X waiting
Record lock, heap no 687 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
0: len 4; hex 00002da3; asc   - ;; 1: len 1; hex 01; asc  ;; 2: len 4; hex 0000002a; asc    *;; 3: len 4; hex 0006a828; asc    (;;

*** (2) TRANSACTION:
TRANSACTION 0 41582947, ACTIVE 1 sec, process no 13236, OS thread id 1175230816 updating or deleting, thread declared inside InnoDB 499
mysql tables in use 1, locked 1
9 lock struct(s), heap size 3024, 47 row lock(s), undo log entries 7
MySQL thread id 2775, query id 242753808 10.10.221.41 rat Updating
update item set add_time='2011-06-16 15:47:42', agility=0, backpack_type=1, caption_addon=0, cell=31, contain_items=null, count=1, cur_wear=0, gem_pack=null, had_given=0, has_identify=0, intellectuality=0, intellectuality_addon=0, item_pack=null, item_template_id=72019, limit_day=0, modify_time='2011-06-18 03:04:30', physique=0, physique_addon=0, role_id=11683, socket_count=0, star_rating=0, strength=0, strength_addon=0, version=31, wear_up_limit=0 where id=436264 and version=30
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 198 page no 2424 n bits 864 index `INDEX_RD_BE_CELL` of table `webgame`.`item` trx id 0 41582947 lock_mode X

以上的日志是发生了索引死锁,通过修改实现方式,去掉SQL"update item set cell = cell + 10000 where role_id = 11683 and backpack_type = 1 and cell >= 31 and cell <= 60
"的应用,解决问题

LATEST DETECTED DEADLOCK
------------------------
110617 21:50:00
*** (1) TRANSACTION:
TRANSACTION 0 39006017, ACTIVE 0 sec, process no 13236, OS thread id 1174698336 starting index read
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1216, 3 row lock(s), undo log entries 3
MySQL thread id 2414, query id 225896768 10.10.221.41 rat Updating
update city set last_update_time='2011-06-17 21:50:00', modify_time='2011-06-17 21:50:00', nontradable_food=16605.08057590968, nontradable_l=20150.229607626545, nontradable_stone=21358.9563609526, nontradable_wood=20714.37318622977, prosperity=8042.103040336108, tradable_food=2695.4569870030477, tradable_l=2170.594222515836, tradable_stone=1743.0695105690684, tradable_wood=1793.4135316803108, version=270 where id=2056 and version=269
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 189 page no 206 n bits 96 index `PRIMARY` of table `webgame`.`city` trx id 0 39006017 lock_mode X locks rec but not gap waiting
Record lock,

*** (2) TRANSACTION:
TRANSACTION 0 39006027, ACTIVE 0 sec, process no 13236, OS thread id 1199458656 starting index read, thread declared inside InnoDB 102
mysql tables in use 2, locked 2
1498 lock struct(s), heap size 145392, 11403 row lock(s)
MySQL thread id 2431, query id 225896693 10.10.221.40 rat Sending data
INSERT INTO ranking_city (role_id, city_id, city_name, role_name, alliance_name, map_id, state_id, x, y, population, add_time)
 SELECT
   c.role_id, c.id, c.name, r.name, IFNULL(r.alliance_name, ""), c.map_id, c.state_id, c.x, c.y, c.building_population, NOW()
 FROM
   city c LEFT JOIN role r ON c.role_id = r.id
 WHERE
   c.role_id <> 0
 ORDER BY
   c.building_population DESC, c.add_time ASC
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 189 page no 206 n bits 96 index `PRIMARY` of table `webgame`.`city` trx id 0 39006027 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

以上的日志是发生了主键索引死锁,修改SQL语句:

原本为:INSERT INTO ranking_city (role_id, city_id, city_name, role_name, alliance_name, map_id, state_id, x, y, population, add_time)
 SELECT
   c.role_id, c.id, c.name, r.name, IFNULL(r.alliance_name, ""), c.map_id, c.state_id, c.x, c.y, c.building_population, NOW()
 FROM
   city c LEFT JOIN role r ON c.role_id = r.id
 WHERE
   c.role_id <> 0
 ORDER BY
   c.building_population DESC, c.add_time ASC
修改成如下,

Drop table if exists `ranking_city`;

create table ranking_city(id int(11) unsigned not null auto_increment,primary key(id)) ENGINE MEMORY (select c.role_id,c.id as city_id,name as city_name,c.map_id,c.state_id,c.x,c.y,c.building_population as population,now() as add_time) from city c order by c.building_population desc);

不仅效率上从原本的13秒提高到0.01秒,而且不会再发生锁现象。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
2天前
|
存储 缓存 关系型数据库
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
MySQL的存储引擎是其核心组件之一,负责数据的存储、索引和检索。不同的存储引擎具有不同的功能和特性,可以根据业务需求 选择合适的引擎。本文详细介绍了MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案。
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
|
7天前
|
存储 关系型数据库 MySQL
MySQL存储引擎详述:InnoDB为何胜出?
MySQL 是最流行的开源关系型数据库之一,其存储引擎设计是其高效灵活的关键。InnoDB 作为默认存储引擎,支持事务、行级锁和外键约束,适用于高并发读写和数据完整性要求高的场景;而 MyISAM 不支持事务,适合读密集且对事务要求不高的应用。根据不同需求选择合适的存储引擎至关重要,官方推荐大多数场景使用 InnoDB。
48 7
|
16天前
|
存储 关系型数据库 MySQL
从新手到高手:彻底掌握MySQL表死锁
通过本文的介绍,希望你能深入理解MySQL表死锁的概念、原因、检测方法及解决方案,并在实际开发中灵活应用这些知识,提升系统的稳定性和性能。
106 9
|
16天前
|
存储 关系型数据库 MySQL
Mysql索引:深入理解InnoDb聚集索引与MyisAm非聚集索引
通过本文的介绍,希望您能深入理解InnoDB聚集索引与MyISAM非聚集索引的概念、结构和应用场景,从而在实际工作中灵活运用这些知识,优化数据库性能。
83 7
|
23天前
|
存储 关系型数据库 MySQL
MySQL引擎InnoDB和MyISAM的区别?
InnoDB是MySQL默认的事务型存储引擎,支持事务、行级锁、MVCC、在线热备份等特性,主索引为聚簇索引,适用于高并发、高可靠性的场景。MyISAM设计简单,支持压缩表、空间索引,但不支持事务和行级锁,适合读多写少、不要求事务的场景。
52 9
|
1月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
本文介绍了MySQL InnoDB存储引擎中的数据文件和重做日志文件。数据文件包括`.ibd`和`ibdata`文件,用于存放InnoDB数据和索引。重做日志文件(redo log)确保数据的可靠性和事务的持久性,其大小和路径可由相关参数配置。文章还提供了视频讲解和示例代码。
152 11
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
|
1月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的表空间
InnoDB是MySQL默认的存储引擎,主要由存储结构、内存结构和线程结构组成。其存储结构分为逻辑和物理两部分,逻辑存储结构包括表空间、段、区和页。表空间是InnoDB逻辑结构的最高层,所有数据都存放在其中。默认情况下,InnoDB有一个共享表空间ibdata1,用于存放撤销信息、系统事务信息等。启用参数`innodb_file_per_table`后,每张表的数据可以单独存放在一个表空间内,但撤销信息等仍存放在共享表空间中。
|
1月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的段、区和页
MySQL的InnoDB存储引擎逻辑存储结构与Oracle相似,包括表空间、段、区和页。表空间由段和页组成,段包括数据段、索引段等。区是1MB的连续空间,页是16KB的最小物理存储单位。InnoDB是面向行的存储引擎,每个页最多可存放7992行记录。
|
1月前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL的InnoDB存储引擎
InnoDB是MySQL的默认存储引擎,广泛应用于互联网公司。它支持事务、行级锁、外键和高效处理大量数据。InnoDB的主要特性包括解决不可重复读和幻读问题、高并发度、B+树索引等。其存储结构分为逻辑和物理两部分,内存结构类似Oracle的SGA和PGA,线程结构包括主线程、I/O线程和其他辅助线程。
【赵渝强老师】MySQL的InnoDB存储引擎
|
6月前
|
存储 关系型数据库 MySQL
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)
MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)