[MySQL学习] Innodb锁系统(4) Insert/Delete 锁处理及死锁示例分析

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

A.INSERT

插入操作在函数btr_cur_optimistic_insert->btr_cur_ins_lock_and_undo->lock_rec_insert_check_and_lock这里进行锁的判断,我们简单的看看这个函数的流程:

1.首先先看看欲插入记录之后的数据上有没有锁,

   next_rec = page_rec_get_next_const(rec);

   next_rec_heap_no = page_rec_get_heap_no(next_rec);

   lock = lock_rec_get_first(block, next_rec_heap_no);

如果lock为空的话,对于非聚集索引,还需要更新page上的最大事务ID。

实际上这里是比较松散的检查,大并发插入的时候,可以大大的降低创建锁开销。

那么其他事务如何发现这些新插入的记录呢(重复插入同一条记录显然应该被阻塞),这里会有个判断,其他事务去看看

新插入记录的事务是否还是活跃的,如果还是活跃的,那么就为这个事务主动增加一个锁记录(所谓的隐式锁就是么有锁。。。。),这个判断是在检查是否存在冲突键的时候进行的(row_ins_duplicate_error_in_clust->row_ins_set_shared_rec_lock->lock_clust_rec_read_check_and_lock->lock_rec_convert_impl_to_expl

row_ins_set_shared_rec_lock的目的是为了向记录上加一个LOCK_REC_NOT_GAP的LOCK_S锁,也就是非GAP的记录S锁,如果发现记录上有X锁(隐式锁转换为LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP),显然是需要等待的(返回DB_LOCK_WAIT)

 

这里设置inherit为FALSE,然后返回DB_SUCCESS;

至于inherit的作用,稍后再议!


2.如果lock不为空,这意味着插入记录的下一个记录上存在锁,设置inherit为TRUE.

检查下一个记录上的锁是否和LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION相互冲突

    if (lock_rec_other_has_conflicting(

            LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,

            block, next_rec_heap_no, trx)) {


        /* Note that we may get DB_SUCCESS also here! */

        err = lock_rec_enqueue_waiting(LOCK_X | LOCK_GAP

                           | LOCK_INSERT_INTENTION,

                           block, next_rec_heap_no,

                           index, the);


如果有别的事务在下一个记录上存在显式的锁请求,并且和锁模式( LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION) 冲突,那么

这时候当前事务就需要等待。


如果别的事务持有一个GAP类型的锁以等待插入,我们认为这个锁和当前插入不冲突。

如何判定锁之间是否冲突,在上一篇博客(http://mysqllover.com/?p=425)已经介绍过,不再赘述.


当检查到存在冲突的事务,我们就将一个锁模式为LOCK_X | LOCK_GAP|LOCK_X | LOCK_GAP 加入到请求队列中(调用函数lock_rec_enqueue_waiting),这里也会负责去检查死锁。


注意在加入等待队列的时候可能会返回DB_SUCCESS,例如死锁发生,但选择另外一个事务为牺牲者。


我们上面提到变量inherit,在存在下一个记录锁时会设置为TRUE,在上层函数btr_cur_optimistic_insert,会据此进行判断:

    if (!(flags & BTR_NO_LOCKING_FLAG) && inherit) {

        lock_update_insert(block, *rec);

    }  

注意当我们执行到这部分逻辑时err为DB_SUCCESS,表示锁检查已经通过了。

BTR_NO_LOCKING_FLAG表示不做记录锁检查

对于optimistic_insert, flags值为0

对于pessimistic_insert,flags值为BTR_NO_UNDO_LOG_FLAG | BTR_NO_LOCKING_FLAG | BTR_KEEP_SYS_FLAG


因此对于乐观更新(无需修改BTREE结构),当inherit被设置为TRUE时,总会调用lock_update_insert

根据注释,lock_update_insert用于继承下一条记录的GAP锁,流程如下

1.首先获取插入的记录的heap no和下一条记录的heap no

        receiver_heap_no = rec_get_heap_no_new(rec);

        donator_heap_no = rec_get_heap_no_new(

            page_rec_get_next_low(rec, TRUE));


其中receiver_heap_no是当前记录,donator_heap_no是下一条记录


2.调用lock_rec_inherit_to_gap_if_gap_lock函数,将donator_heap_no上所有非INSERT INTENTION且非LOCK_REC_NOT_GAP的记录锁

转移给receiver_heap_no

遍历donator_heap_no上的所有记录锁,继承锁的判定条件如下:

        if (!lock_rec_get_insert_intention(lock)

            && (heap_no == PAGE_HEAP_NO_SUPREMUM

            || !lock_rec_get_rec_not_gap(lock))) {


            lock_rec_add_to_queue(LOCK_REC | LOCK_GAP

                          | lock_get_mode(lock),

                          block, heir_heap_no,

                          lock->index, lock->trx);

        }


注意这里有对SUPREMUM记录的特殊处理。

也就是说,成功插入了一条记录,其他持有该记录的下一条记录上锁的事务也会持有新插入记录上的GAP锁。


说起INSERT,就不得不提到一个有趣的死锁案例。也就是bug#43210(http://bugs.mysql.com/bug.php?id=43210)

DROP TABLE t1;

CREATE TABLE `t1` (

  `a` int(11) NOT NULL,

  `b` int(11) DEFAULT NULL,

  PRIMARY KEY (`a`),

  KEY `b` (`b`)

) ENGINE=InnoDB;

insert into t1 values (1,19),(8,12);


Session 1:

set autocommit = 0;

insert into t1 values (6,12);


Session 2:

set autocommit = 0;

insert into t1 values (6,12);  //阻塞住,同时将session1的锁转换为显示锁。等待记录上的S锁 (查找dup key)

/****

session 1上的转为显式锁:lock_mode X locks rec but not gap

session 2等待的锁:lock mode S locks rec but not gap waiting

***/

Session 3:

set autocommit = 0;

insert into t1 values (6,12);  //阻塞住,和session2 同样等待S锁,lock mode S locks rec but not gap waiting


Session 1:

ROLLBACK;


Session 2:

执行插入成功

这时候Session 2持有的锁为主键记录上的:

lock mode S locks rec but not gap

lock mode S locks gap before rec

lock_mode X locks gap before rec insert intention


Session3:

被选为牺牲者,回滚掉。


很容易重现,当session 1回滚时,session2和session3提示死锁发生。


这里的关键是当ROLLBACK时,实际上是在做一次delete操作,backtrace如下:

trx_general_rollback_for_mysql->….->row_undo->row_undo_ins->row_undo_ins_remove_clust_rec->btr_cur_optimistic_delete->lock_update_delete->lock_rec_inherit_to_gap


我们来跟踪一下创建锁的轨迹

s1的事务0x7fdfd80265b8

s2的事务0x7fdfe0007c68

s3的事务0x7fdff00213f8


s1 , type_mode=1059     //s2为s1转换隐式锁为显式锁,

s2,  type_mode=1282    //检查重复键,需要加共享锁,被s1 block住,等待S锁

s3,  type_mode=1282    // 被s1 block住,等待S锁


s1, type_mode=547       //s1回滚,删除记录,lock_update_delete锁继承,

s2, type_mode=546        //创建s锁  LOCK_GAP | LOCK_REC | LOCK_S

s3, type_mode=546        //创建s锁   LOCK_GAP | LOCK_REC | LOCK_S


s2, type_mode=2819   // LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION

s3, type_mode=2819   //  LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION


看看show engine innodb status打印的死锁信息:

insert into t1 values (6,12)

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

RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA70 lock_mode X locks gap before rec insert intention waiting

*** (2) TRANSACTION:

TRANSACTION FE3BFA6F, ACTIVE 143 sec inserting, thread declared inside InnoDB 1

mysql tables in use 1, locked 1

4 lock struct(s), heap size 1248, 2 row lock(s)

MySQL thread id 791, OS thread handle 0x7fe2d4ea1700, query id 2613 localhost root update

insert into t1 values (6,12)

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

RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA6F lock mode S locks gap before rec

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

RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA6F lock_mode X locks gap before rec insert intention waiting

*** WE ROLL BACK TRANSACTION (2)


从上面的分析,我们可以很容易理解死锁为何发生。s1插入记录,s2插入同一条记录,主键冲突,s2将s1的隐式锁转为显式锁,同时s2向队列中加入一个s锁请求;

s3同样也加入一个s锁请求;

当s1回滚后,s2和s3获得s锁,但随后s2和s3又先后请求插入意向锁,因此锁队列为:

s2(S GAP)<—s3(S GAP)<—s2(插入意向锁)<–s3(插入意向锁)   s3,s2,s3形成死锁。

B.DELETE

Innodb的delete操作实际上只是做标记删除,而不是真正的删除记录;真正的删除是由Purge线程来完成的。

DELETE操作的记录加锁,是在查找记录时完成的。这一点,我们在上一节已经提到了。

上面我们有提到,对插入一条记录做回滚时,实际上是通过undo来做delete操作。这时候有一个lock_update_insert操作,我们来看看这个函数干了什么:

1.首先获取将被移除的记录HEAP NO和下一条记录的HEAP NO

        heap_no = rec_get_heap_no_new(rec);

        next_heap_no = rec_get_heap_no_new(page

                           + rec_get_next_offs(rec,

                                       TRUE));

2.然后获取kernel mutex锁,执行:

将被删除记录上的GAP锁转移到下一条记录上:

lock_rec_inherit_to_gap(block, block, next_heap_no, heap_no);

遍历heao_no上的锁对象,满足如下条件时为下一个记录上的事务创建新的锁对象:

        if (!lock_rec_get_insert_intention(lock)

            && !((srv_locks_unsafe_for_binlog

              || lock->trx->isolation_level

              <= TRX_ISO_READ_COMMITTED)

             && lock_get_mode(lock) == LOCK_X)) {


            lock_rec_add_to_queue(LOCK_REC | LOCK_GAP

                          | lock_get_mode(lock),

                          heir_block, heir_heap_no,

                          lock->index, lock->trx);

        }

 

条件1:锁对象不是插入意向锁(INSERT INTENTION LOCK)

条件2:srv_locks_unsafe_for_binlog被设置为FALSE且隔离级别大于READ COMMITTED, 或者锁类型为LOCK_S     


和lock_update_insert类似,这里也会创建新的GAP锁对象


当完成锁表更新操作后,重置锁bit并释放等待的事务lock_rec_reset_and_release_wait(block, heap_no):

>>正在等待当前记录锁的(lock_get_wait(lock)),取消等待(lock_rec_cancel(lock))

>>已经获得当前记录锁的,重置对应bit位(lock_rec_reset_nth_bit(lock, heap_no);)


lock_update_delete主要在INSERT回滚及Purge线程中被调用到。


在查找数据时,DELETE会给记录加锁,在进行标记删除时,也会调用到锁检查函数:

聚集索引:

row_upd->row_upd_clust_step->row_upd_del_mark_clust_rec->btr_cur_del_mark_set_clust_rec->lock_clust_rec_modify_check_and_lock

这个backtrace,会从lock_clust_rec_modify_check_and_lock直接返回DB_SUCCESS,因为函数btr_cur_del_mark_set_clust_rec的参数flags总是

值为BTR_NO_LOCKING_FLAG

用户线程不做调用,但在btr_cur_upd_lock_and_undo则会继续走lock_clust_rec_modify_check_and_lock的流程。


二级索引:

row_upd->row_upd_sec_step->row_upd_sec_index_entry->btr_cur_del_mark_set_sec_rec->lock_sec_rec_modify_check_and_lock

用户线程里lock_sec_rec_modify_check_and_lock的flags参数为0,而在row_undo_mod_del_unmark_sec_and_undo_update、row_undo_mod_del_mark_or_remove_sec_low函数里则设置为BTR_NO_LOCKING_FLAG,表示不做检查。


lock_sec_rec_modify_check_and_lock用于检查是否有其他事务阻止当前修改一条二级索引记录(delete mark or delete unmark),


如果开始修改二级索引,则表示我们已经成功修改了聚集索引,因此不应该有其他事务在该记录上的隐式锁,也不应该有其他活跃事务修改了二级索引记录。该函数会调用:

    err = lock_rec_lock(TRUE, LOCK_X | LOCK_REC_NOT_GAP,

                block, heap_no, index, the);

第一个函数为TRUE,则当无需等待时,不会创建新的锁对象。


如果err返回值为DB_SUCCESS或者DB_SUCCESS_LOCKED_REC,就更新当前二级索引Page上的最大事务ID。

如果当前存在和LOCK_X|LOCK_REC_NOT_GAP相冲突的锁对象,则可能需要等待。


回到在之前博文提到的死锁,信息如下:

*** (1) TRANSACTION:

TRANSACTION 1E7D49CDD, ACTIVE 69 sec fetching rows

mysql tables in use 1, locked 1

LOCK WAIT 4 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 1

MySQL thread id 1385867, OS thread handle 0x7fcebd956700, query id 837909262 10.246.145.78 im updating

delete    from        msg    WHERE     target_id = ‘Y25oaHVwYW7mmZbmmZblpKnkvb8=’      and         gmt_modified <= ‘2012-12-14 15:07:14′

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

RECORD LOCKS space id 203 page no 475912 n bits 88 index `PRIMARY` of table `im`.`msg` trx id 1E7D49CDD lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:

TRANSACTION 1E7CE0399, ACTIVE 1222 sec fetching rows, thread declared inside InnoDB 272

mysql tables in use 1, locked 1

1346429 lock struct(s), heap size 119896504, 11973543 row lock(s), undo log entries 1

MySQL thread id 1090268, OS thread handle 0x7fcebf48c700, query id 837483530 10.246.145.78 im updating

delete    from        msg    WHERE     target_id = ‘Y25oaHVwYW7niLHkuZ3kuYU5OQ==’      and         gmt_modified <= ‘2012-12-14 14:13:28′

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

RECORD LOCKS space id 203 page no 475912 n bits 88 index `PRIMARY` of table `im`.`msg` trx id 1E7CE0399 lock_mode X

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

RECORD LOCKS space id 203 page no 1611099 n bits 88 index `PRIMARY` of table `im`.`msg` trx id 1E7CE0399 lock_mode X waiting


表结构为:

CREATE TABLE `msg` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `target_id` varchar(100) COLLATE utf8_bin NOT NULL ,

       ……

       ……

  `flag` tinyint(4) NOT NULL ,

  `gmt_create` datetime NOT NULL,

  `gmt_modified` datetime NOT NULL,

  `datablob` blob,

  `nickname` varchar(64) COLLATE utf8_bin DEFAULT NULL ,

  `source` tinyint(4) DEFAULT NULL ,

  PRIMARY KEY (`id`),

  KEY `idx_o_tid` (`target_id`,`gmt_modified`,`source`,`flag`)

) ENGINE=InnoDB 

首先我们从死锁信息里来看,发生死锁的是两个delete语句,

delete    from        offmsg_0007    WHERE     target_id = ‘Y25oaHVwYW7mmZbmmZblpKnkvb8=’      and         gmt_modified <= ‘2012-12-14 15:07:14′

delete    from        offmsg_0007    WHERE     target_id = ‘Y25oaHVwYW7niLHkuZ3kuYU5OQ==’      and         gmt_modified <= ‘2012-12-14 14:13:28′

 

我们再看看这个表上的索引,一个主键索引(target_id),一个二级索引(`target_id`,`gmt_modified`,`source`,`flag`)

根据前缀索引的原则,理论上我们应该可以通过二级索引来查找数据,从上一节的分析,我们知道,如果根据二级索引查找数据:

>>二级索引上加X 锁,记录及GAP

>>聚集索引上加记录X锁

我们再看死锁信息:

第一条SQL等待聚集索引Page 475912上的lock_mode X locks rec but not gap, 这说明该锁请求等待是走二级索引的

第二条SQL持有聚集索引Page 475912上的lock_mode X锁,等待聚集索引Page 1611099上的 lock_mode X

因此我们大致可以认为第二条SQL总是在请求聚集索引上的LOCK_ORDINARY类型的锁,简单的gdb我们可以知道走聚集索引做范围删除,锁模式值为3,也就是LOCK_X

因此,可以推测delete操作走错了索引,导致出现资源的互相占用。从而死锁;至于为什么走错索引,这就是优化器的问题了,暂不明;

C.释放锁

在事务提交或回滚时,会释放记录锁,调用函数为lock_release_off_kernel

函数的逻辑很简单,遍历trx->trx_locks。

对于记录锁,调用lock_rec_dequeue_from_page(lock)

–>从lock_sys中删除

–>检查lock所在page上的等待的锁对象是否能被grant(lock_grant),如果可以,则唤醒等待的事务。

对于表锁,调用lock_table_dequeue(lock)


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
存储 关系型数据库 MySQL
MySQL InnoDB数据存储结构
MySQL InnoDB数据存储结构
|
1月前
|
存储 缓存 关系型数据库
MySQL的varchar水真的太深了——InnoDB记录存储结构
varchar(M) 能存多少个字符,为什么提示最大16383?innodb怎么知道varchar真正有多长?记录为NULL,innodb如何处理?某个列数据占用的字节数非常多怎么办?影响每行实际可用空间的因素有哪些?本篇围绕innodb默认行格式dynamic来说说原理。
833 6
MySQL的varchar水真的太深了——InnoDB记录存储结构
|
2月前
|
存储 缓存 关系型数据库
MySQL - 存储引擎MyISAM和Innodb
MySQL - 存储引擎MyISAM和Innodb
|
13天前
|
存储 关系型数据库 MySQL
MySQL引擎对决:深入解析MyISAM和InnoDB的区别
MySQL引擎对决:深入解析MyISAM和InnoDB的区别
28 0
|
2月前
|
存储 SQL 关系型数据库
Mysql专栏 - mysql、innodb存储引擎、binlog的工作流程
Mysql专栏 - mysql、innodb存储引擎、binlog的工作流程
79 0
|
3月前
|
存储 算法 关系型数据库
MySQL相关(八)- innodb行级锁深入剖析
MySQL相关(八)- innodb行级锁深入剖析
45 0
|
3月前
|
存储 算法 关系型数据库
MySQL相关(七)- innodb 锁的介绍及使用
MySQL相关(七)- innodb 锁的介绍及使用
28 0
|
16天前
|
关系型数据库 MySQL 数据库
mysql卸载、下载、安装(window版本)
mysql卸载、下载、安装(window版本)
|
1月前
|
关系型数据库 MySQL 数据库
rds安装数据库客户端工具
安装阿里云RDS的数据库客户端涉及在本地安装对应类型(如MySQL、PostgreSQL)的客户端工具。对于MySQL,可选择MySQL Command-Line Client或图形化工具如Navicat,安装后输入RDS实例的连接参数进行连接。对于PostgreSQL,可以使用`psql`命令行工具或图形化客户端如PgAdmin。首先从阿里云控制台获取连接信息,然后按照官方文档安装客户端,最后配置客户端连接以确保遵循安全指引。
86 1
|
5天前
|
关系型数据库 MySQL 数据库
《MySQL 简易速速上手小册》第1章:MySQL 基础和安装(2024 最新版)
《MySQL 简易速速上手小册》第1章:MySQL 基础和安装(2024 最新版)
28 4