嘛是锁?呐,这个就是锁了。那有了锁,对应是不是就要有钥匙了,有有有,查询占领锁的那条语句直接 kill 掉就行(后面再说)。当然了数据库里面的锁可不长这个样子,这里只是让读者对锁有个现实的印象。
那为什么要有锁呢?这个问题问的好,你外出上班门要不要上锁,那肯定要啊,不然就有小偷上门关顾的风险。好了好了,扯淡就到这里了,接下来我们要正经一点了。
数据库锁设计的初衷是处理并发问题,作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
按照加锁的范围,MySQL 的锁大致可以分为全局锁,表级锁,行锁。大家是不是有联想到了什么,这不是跟住小区一样吗?小区大门有一个锁,进哪栋也有一个锁,然后你自己房子的大门也有一个锁。是不是一模一样!!!
按照锁的分类呢,实际上就两种。共享锁(Shared Lock)和排他锁(Exclusive Lock)。下面我们简称 S 锁和 X 锁。
S 锁被叫做读锁,当一个事务对对象加了 S 锁,其他事务也可以同时为一个对象加 S 锁,但不能加 X 锁。
X 锁被叫作写锁,当一个事务对对象加了 X 锁,那么其他事务就不能再给它加任何锁。
-- 加共享锁(S)
select * from table_name where ... lock in share mode
-- 加排它锁(X)
select * from table_name where ... for update
到此为止,是不是觉得没什么问题,都能理解的话我们就要加点难度了。
2. 细嗦锁
2.1. 锁粒度
我们上文说了锁大致可以分为全局锁,表级锁,行锁。下面我们就来说说他们分别应用的场景。
2.1.1. 全局锁
顾名思义就是对整个数据库实例加锁。什么情况下会用到呢?那当然是为了全库逻辑备份了,命令为 Flush tables with read lock,简称为 FTWRL。执行之后,整个库就处于只读状态,DML 和 DDL 操作就执行不了。别手欠在生产环境的业务高峰期执行,执行了也不要供出我名字。
不过一般也不会在主库上执行,毕竟执行过程业务就要停摆,在从库上执行最多就主从延迟。我们就点到为止,再深究下来就不是我们要本章要讨论的事情了。
2.1.2. 表锁
直接锁定整张表,在锁定期间,其它进程无法对该表进行写操作。通常用在DDL语句中,如DELETE TABLE,ALTER TABLE等。
特点就是开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度低。
加表锁要注意什么呢,是不是要先观察这个表有没有行锁。对对对,既然你想到了这个问题想必你有对应的处理方法了吧。
哈哈哈哈,早期就是把所有记录遍历一遍。但这样子严重影响性能,直到意向锁的出现。意向锁是表级锁,由事务在申请行锁前自动添加,用于向其他事务声明“我可能要锁某些行”,从而让表锁快速判断冲突,避免全表扫描。
2.1.3. 行锁
仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。通常用在DML语句中,如INSERT, UPDATE, DELETE等。
开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率低,但并发度高。
2.1.4. 其他锁
除了上面提到这三种常见的锁,实际上还有页表锁,自增锁,谓词锁。但是不在我们今天的讨论范围之类,有兴趣的话后面我们再单独讲讲。
2.2. 各种锁粒度下的加锁模式
2.2.1. 表锁(Table-level Lock)
2.2.1.1. 普通表锁
锁住整张表,知道就行了。
2.2.1.2. 元数据锁(Metadata Lock,MDL)
我们主要来说说元数据锁,什么是元数据呢,我们可以理解为表结构,
MySQL5.5版本引入了MDL锁(metadata lock),用于解决或者保证DDL操作与DML操作之间的一致性。例如下面的这种情形:
若没有MDL锁的保护,则事务2可以直接执行DDL操作,并且导致事务1出错,5.1版本即是如此。
5.5版本加入MDL锁就在于保护这种情况的发生,由于事务1开启了查询,那么获得了MDL锁,锁的模式为SHARED_READ,事务2要执行DDL,则需获得EXCLUSIVE锁,两者互斥,所以事务2需要等待。
InnoDB层已经有了IS、IX这样的意向锁,有同学觉得可以用来实现上述例子的并发控制。但由于MySQL是Server-Engine架构,所以MDL锁是在Server中实现。另外,MDL锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁,这是InnoDB存储引擎层不能直接实现的锁。
MDL锁的性能与并发改进
讲到这同学们会发现MDL锁的开销并不比InnoDB层的行锁要小,而且这可能是一个更为密集的并发瓶颈。MySQL 5.6和5.5版本通常通过调整如下两个参数来进行并发调优:
metadata_locks_cache_size: MDL锁的缓存大小
metadata_locks_hash_instances:通过分片来提高并发度,与InnoDB AHI类似
MySQL 5.7 MDL锁的最大改进之处在于将MDL锁的机制通过lock free算法来实现,从而提高了在多核并发下数据库的整体性能提升。
MDL锁的诊断
MySQL 5.7版本之前并没有提供一个方便的途径来查看MDL锁,github上有一名为mysql-plugin-mdl-info的项目,通过插件的方式来查看,非常有想法的实现,大赞。好在官方也意识到了这个问题,于是在MySQL 5.7中的performance_schea库下新增了一张表metadata_locks,用其来查看MDL锁那是相当的方便:
不过默认并没有打开此功能,需要手工将wait/lock/metadata/sql/mdl监控给打开:
SELECT * FROM performance_schema.setup_instruments;
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME ='global_instrumentation';
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME ='wait/lock/metadata/sql/mdl';
select * from performance_schema.metadata_locks\G;
2.2.1.3. 意向锁(Intention Lock)
上面已经说了意向锁是什么了,这里就不做重复描述了。主要说下它们之间的兼容情况。
2.2.2. 行级锁(Row-level Lock)
2.2.2.1. 记录锁(Record Lock)
记录锁其实很好理解,对表中的记录加锁,就叫做记录锁,简称行锁。
但是需要注意的是:
id 列必须是唯一索引或者主键列,否则会变成临键锁。
查询条件必须为精确匹配(=),不能是> ,< ,like 等,否则会变成临键锁。
2.2.2.2. 间隙锁(Gap Lock)
为了更好的理解间隙锁和临键锁,下面我们先来建个表插入一些数据先。
CREATE TABLE `lock_test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_value` (`value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO `lock_test` (`id`, `value`) VALUES
(1, 10),
(3, 20),
(5, 35),
(7, 40),
(10, 50);
间隙锁 是 Innodb 在 RR(可重复读) 隔离级别 下为了解决 幻读问题 时引入的锁机制。间隙锁是innodb中行锁的一种。
请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
举例来说,上面的例子有五条记录,下面的SQL:
SELECT * FROM lock_test WHERE value > 10 and value < 35 FOR UPDATE
当我们用条件检索数据,并请求共享或排他锁时,InnoDB不仅会对符合条件的value值为 10 的记录加锁,也会对value大于 10(这些记录并不存在)的“间隙”加锁。
这时锁定的区间范围是(10,20),(20,30),(30,35),注意的是间隙锁不锁定任何记录,只锁定间隙。
2.2.2.3. 临键锁(Next-key Lock)默认锁机制
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。
也可以理解为一种特殊的间隙锁。通过临建锁可以解决幻读的问题。
还是以这条 sql 来讲解,此时它锁定的区间分别是什么呢?
SELECT * FROM lock_test WHERE value > 10 and value <35 FOR UPDATE
我们要重点关注这句话加在某条记录以及这条记录前面间隙,此时锁定的区间范围为(10,20],(20,30],(30,35)。
间隙锁是临键锁的“子集”,临键锁功能更全面(既防幻读,又防当前读冲突)。临键锁是间隙锁和记录锁的组合,可以看到,临键锁区间范围的右边是闭合的,也就是说临键锁会锁定 value=20 和 value=30 的记录。
2.2.2.4. 插入意向锁(Insert Intention Lock)
允许不同事务在同一间隙进行并发插入。
3. 锁的内存结构
好好消化一下,这个就有点难
4. 锁阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这叫作堵塞。阻塞是为了确保事务可以并发且正常地运行。
在锁结构里有一个重要的属性 is_waiting,当事务 A 需要改动表里的某一个记录时,就生成了一个锁结构与这条记录相关联,因为之前没有别的事务为这条记录加锁,所以 is_waiting 属性为 false。我们把这个场景叫作获取锁成功,或加锁成功。
当事务 B 也想对这条记录进行改动时,事务 B 先去看看有没有锁结构与这条记录相关联,在发现有一个锁结构相关联之后,事务 B 也会生成一个锁结构与这条记录关联,不过锁结构的 is_waiting 为 true,这个场景称之为获取锁失败或者加锁失败。
在 innodb 引擎中,配置参数innodb_lock_wait_timeout 用来控制等待时间,默认为 50s。innodb_rollback_on_timeout 是用来设定是否等待超时时对之前的事务进行回滚操作,默认是 off,表示不回滚。
innodb_lock_wait_timeout 是动态的,可以在 mysql 运行时进行调整,innodb_rollback_on_timeout 则不行,只能在启动时配置到文件里。
下面做个实验
在生产环境使用中,建议将innodb_rollback_on_timeout 设置为ON。应用程序一定要做好事务控制,在一个事务出现异常时必须进行显式rollback
5. 查看事务加锁情况
#查看当前所有事务
select * from information_schema.innodb_trx;
# 该表记录一些锁信息
Mysql8.0 之前使用:select * from information_schema.innodb_locks;
Mysql8.0 使用:select * from performance_schema.data_locks;
# 表明每个阻塞的事务是因为获取不到哪个事务持有的锁而阻塞
Mysql8.0 之前使用:select * from information_schema.innodb_lock_waits;
Mysql8.0 使用:select * from performance_schema.data_lock_waits;
#通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
show status like'innodb_row_lock%';
/*对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数*/
案例如下:
mysql> select * from information_schema.innodb_trx\G;
*************************** 1. row ***************************
trx_id: 2190
trx_state: RUNNING
trx_started: 2025-04-08 01:39:28
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 1
trx_mysql_thread_id: 52
trx_query: NULL
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 1
trx_lock_structs: 1
trx_lock_memory_bytes: 1128
trx_rows_locked: 1
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 0
trx_is_read_only: 0
trx_autocommit_non_locking: 0
trx_schedule_weight: NULL
*************************** 2. row ***************************
trx_id: 2189
trx_state: RUNNING
trx_started: 2025-04-08 01:39:01
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 3
trx_mysql_thread_id: 53
trx_query: select * from information_schema.innodb_trx
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 1
trx_lock_structs: 3
trx_lock_memory_bytes: 1128
trx_rows_locked: 2
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 0
trx_is_read_only: 0
trx_autocommit_non_locking: 0
trx_schedule_weight: NULL
2 rows in set (0.00 sec)
mysql> select * from performance_schema.data_locks\G;
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139887271401040:56:1071:139887152011792
ENGINE_TRANSACTION_ID: 2189
THREAD_ID: 90
EVENT_ID: 22
OBJECT_SCHEMA: employees
OBJECT_NAME: lock_test
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139887152011792
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139887271400232:240:1071:139887152005808
ENGINE_TRANSACTION_ID: 2190
THREAD_ID: 89
EVENT_ID: 22
OBJECT_SCHEMA: employees
OBJECT_NAME: lock_test
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139887152005808
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139887271401040:56:9:4:3:139887152009224
ENGINE_TRANSACTION_ID: 2189
THREAD_ID: 90
EVENT_ID: 22
OBJECT_SCHEMA: employees
OBJECT_NAME: lock_test
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139887152009224
LOCK_TYPE: RECORD
LOCK_MODE: X,GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 3
*************************** 4. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 139887271401040:56:9:4:2:139887152008880
ENGINE_TRANSACTION_ID: 2189
THREAD_ID: 90
EVENT_ID: 22
OBJECT_SCHEMA: employees
OBJECT_NAME: lock_test
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139887152008880
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: GRANTED
LOCK_DATA: 1
4 rows in set (0.00 sec)
/*我们简单来分析一下
2189分别有三个锁。ix,x,GAP,GAP锁住了三条记录,id < 3,x锁住了1条记录(实际记录只有一条)
LOCK_STATUS为GRANTED表示锁已授权。LOCK_TYPE表示行锁还是表锁。
2190只有一个ix锁
*/
mysql> select * from performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
ENGINE: INNODB
D_ENGINE_LOCK_ID: 139887271400232:240:9:4:2:139887152003240
REQUESTING_ENGINE_TRANSACTION_ID: 2190
REQUESTING_THREAD_ID: 89
REQUESTING_EVENT_ID: 23
REQUESTING_OBJECT_INSTANCE_BEGIN: 139887152003240
BLOCKING_ENGINE_LOCK_ID: 139887271401040:56:9:4:2:139887152008880
BLOCKING_ENGINE_TRANSACTION_ID: 2189
BLOCKING_THREAD_ID: 90
BLOCKING_EVENT_ID: 22
BLOCKING_OBJECT_INSTANCE_BEGIN: 139887152008880
1 row in set (0.00 sec)
/*
REQUESTING为请求锁的信息,BLOCKING表示持有锁的信息
事务2190(线程89) 正在等待事务2189(线程90) 释放某个锁。
由于事务2189持有锁,事务2190被阻塞,无法继续执行。
*/
6. 死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种相互等待的现象。解决死锁最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。毫无疑问,这确实可以避免死锁问题,然而在生产环境中,这可能导致并发性能的下降,甚至任何一个事务也不能进行,这带来的问题远比死锁严重。
还有一种方式是超时,由innodb_lock_wait_timeout 用来设置超时时间,默认是 50s,这个时间显然有点久了,会导致越来越多的事务阻塞。当然了,我们也可以调小一点,比如说 1s,但是改掉这个参数之后,也会影响正常事务等待锁的时间,也就是大部分未发生死锁,但需要等待锁资源的事务在等待 1s 之后就会立马报错并返回,这显然不合理。
因此,除了超时机制,当前数据库普遍采用 wait for graph 等待图的方式来进行死锁检测。
wait-for graph
算法被启用后,会要求MySQL
收集两个信息:
锁的信息链表:目前持有每个锁的事务是谁。
事务等待链表:阻塞的事务要等待的锁是谁。
每当一个事务需要阻塞等待某个锁时,就会触发一次wait-for graph
算法,该算法会以当前事务作为起点,然后从「锁的信息链表」中找到对应中锁信息,再去根据锁的持有者(事务),在「事务等待链表」中进行查找,看看持有锁的事务是否在等待获取其他锁,如果是,则再去看看另一个持有锁的事务,是否在等待其他锁.....,经过一系列的判断后,再看看是否会出现闭环,出现的话则介入破坏。
案例理解:
上面这个算法的过程,听起来似乎有些晕乎乎的,但实际上并不难,套个例子来理解,好比目前库中有T1、T2、T3
三个事务、有X1、X2、X3
三个锁,事务与锁的关系如下:
此时当T3
事务需要阻塞等待获取X1
锁时,就会触发一次wait-for graph
算法,流程如下:
①先根据T3
要获取的X1
锁,在「锁的信息链表」中找到X1
锁的持有者T1
。
②再在「事务等待链表」中查找,看看T1
是否在等待获取其他锁,此时会得知T1
等待X2
。
③再去「锁的信息链表」中找到X2
锁的持有者T2
,再看看T2
是否在阻塞等待获取其他锁。
④再在「事务等待链表」中查找T2
,发现T2
正在等待获取X3
锁,再找X3
锁的持有者。
经过上述一系列算法过程后,最终会发现X3
锁的持有者为T3
,而本次算法又正是T3
事务触发的,此时又回到了T3
事务,也就代表着产生了“闭环”,因此也可以证明这里出现了死锁现象,所以MySQL
会强制回滚其中的一个事务,来抵达解除死锁的目的。
但出现死锁问题时,MySQL
会选择哪个事务回滚呢?之前分析过,当一个事务在执行SQL
更改数据时,都会记录在Undo-log
日志中,Undo
量越小的事务,代表它对数据的更改越少,同时回滚的代价最低,因此会选择Undo
量最小的事务回滚(如若两个事务的Undo
量相同,会选择回滚触发死锁的事务)。
同时,可以通过innodb_deadlock_detect=on|off
这个参数,来控制是否开启死锁检测机制。
注意:死锁检测机制在MySQL
后续的高版本中是默认开启的,但实际上死锁检测的开销不小,上面三个并发事务阻塞时,会对「事务等待链表、锁的信息链表」共计检索六次,那当阻塞的并发事务越来越多时,检测的效率也会呈线性增长。
实验分析
我们可以通过 show engine innodb status 语句来查看最近发生的一次死锁信息。
------------------------
LATEST DETECTED DEADLOCK
------------------------
发生死锁的时间和操作系统为当前会话分配的线程编号
2025-04-08 00:35:23 139886796564032
*** (1) TRANSACTION:
事务id active时间 当前操作为事务当前正在根据索引读取数据。
TRANSACTION 2188, ACTIVE 11 sec starting index read
此事务当前执行语句使用了1个表,为1个表上了锁
mysql tables in use 1, locked 1
3个结构锁,1个行锁
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
执行此事务的线程信息
MySQL thread id 52, OS thread handle 139887088965184, query id 720 localhost root statistics
发生死锁执行的语句
select * from employees.lock_test where id =1 for UPDATE
事务持有的锁
*** (1) HOLDS THE LOCK(S):
/*RECORD LOCKS表示持有的是行级锁。
锁位于表空间ID为9,页号为4的页面上,锁位图使用了80位
锁是在主键索引上
锁定的是employees数据库中的lock_test表
持有锁的事务ID是2188,跟上面事务id对应上
lock_mode X locks rec but not gap 这是一个排他记录锁(X锁),只锁定记录本身而不锁定间隙*/
RECORD LOCKS space id 9 page no 4 n bits 80 index PRIMARY of table `employees`.`lock_test` trx id 2188 lock_mode X locks rec but not gap
/*
被锁定的记录信息
heap no 3:堆中的记录编号为3
PHYSICAL RECORD:物理记录信息
n_fields 4:记录有4个字段
compact format:使用紧凑格式存储
info bits 0:没有特殊标志位
*/
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
/*
第一个字段(主键字段):值为3 (hex 80000003是MySQL内部整数的存储格式)
第二个字段:事务ID,值为0x000000000858(十进制2136)
第三个字段:回滚指针,值为0x820000012e011d
第四个字段:值为20 (hex 80000014)
*/
0: len 4; hex 80000003; asc ;;
1: len 6; hex 000000000858; asc X;;
2: len 7; hex 820000012e011d; asc . ;;
3: len 4; hex 80000014; asc ;;
事务z正在等待的锁
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 9 page no 4 n bits 80 index PRIMARY of table `employees`.`lock_test` trx id 2188 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000000858; asc X;;
2: len 7; hex 820000012e0110; asc . ;;
3: len 4; hex 8000000a; asc ;;
*** (2) TRANSACTION:
TRANSACTION 2187, ACTIVE 18 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 53, OS thread handle 139887090021952, query id 721 localhost root statistics
select * from employees.lock_test where id =3 for UPDATE
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 9 page no 4 n bits 80 index PRIMARY of table `employees`.`lock_test` trx id 2187 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000000858; asc X;;
2: len 7; hex 820000012e0110; asc . ;;
3: len 4; hex 8000000a; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 9 page no 4 n bits 80 index PRIMARY of table `employees`.`lock_test` trx id 2187 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 000000000858; asc X;;
2: len 7; hex 820000012e011d; asc . ;;
3: len 4; hex 80000014; asc ;;
innodb决定回滚第二个事务,也就是事务ID为2187
*** WE ROLL BACK TRANSACTION (2)
注意的是show engine innodb status 只记录最近一次死锁信息,如果死锁频繁出现,可通过全局系统变量innodb_print_all_deadlocks 设置为 ON,这样会把每个死锁发生的信息都记录在 MySQL 的错误日志里,然后就可以通过错误日志来分析更多的死锁情况。
参考文档
评论