[MySQL学习] Innodb锁系统(3)关键结构体及函数

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介:

1.锁对象的定义:

关键结构体:

UNIV_INTERN lock_sys_t* lock_sys = NULL;

lock_sys是一个全局变量,用于控制整个Innodb锁系统的全部锁结构,其对应的结构体为lock_sys_t,该结构体只包含两个成员:

struct lock_sys_struct{

    hash_table_t* rec_hash;

    ulint rec_num;

};

从函数lock_rec_create可以很容易看出这两个变量的作用:

quoted code:

    HASH_INSERT(lock_t, hash, lock_sys->rec_hash,

            lock_rec_fold(space, page_no), lock);

    lock_sys->rec_num++;


每次新建一个锁对象,都要插入到lock_sys->rec_hash中,这里会根据space id 和page no来计算对应的哈希桶,然后再将锁对象插入到其中,并递增lock_sys->rec_num。

注意只有记录锁会存在lock_sys->rec_hash中,表锁是不会存这里,只会插入到事务trx->trx_locks链表和对应表对象的table->locks中,并且都是加到链表的尾部(lock_table_create)。


每个锁对象的类型为lock_t,在lock0priv.h中定义,描述如下:

trx* trx

持有该锁对象的事务

UT_LIST_NODE_T(lock_t) trx_locks

在事务锁链表trx->trx_locks中对应的节点

ulint type_mode

锁类型,在前文已有介绍

hash_node_t hash

在lock_sys->rec_hash对应哈希桶中的下一个节点

dict_index_t* index;

锁对应的索引

union {


lock_table_t tab_lock;

表锁节点

lock_rec_t rec_lock;

记录锁,lock_rec_t包含成员:

space id

page no

n_bits //锁bitmap的比特位数,值的大小为:

(1+(page_dir_get_n_heap(page) + 64)/8))*8

在锁对象结构体后面会分配对应的bitmap内存。

} un_member


2.锁系统接口函数

a.加锁函数

记录锁

lock_rec_bitmap_reset,根据记录的heap no(在一个page上是唯一的)设置锁对象的bitmap相应位为1.

表锁:

lock_table函数用于加表级别锁


b.lock_rec_lock

参数:

@1,impl ,当设置为true时,如果无需等待,则不设置锁。这里假定调用者已经设置了一个隐式锁

@2,mode,  加锁模式

@3,block,记录所在的文件块

@4,heap_no,记录在Page中的堆号,可以标示其在Page内的物理地址

@5,index,记录所在的索引

@6,thr


在函数lock_clust_rec_modify_check_and_lock和lock_sec_rec_modify_check_and_lock中调用时被设置为TRUE

在函数lock_clust_rec_read_check_and_lock和lock_sec_rec_read_check_and_lock中调用时被设置为FALSE


该函数是底层的对记录加锁函数,这里实现了快速和慢速加锁方式,加锁代码逻辑都是在kernel_mutex下进行的。

>>首先调用lock_rec_lock_fast


|–>先看该block上是否有记录锁(lock = lock_rec_get_first_on_page(block);)

没有的话,且impl为false时候,直接创建锁对象(lock_rec_create),返回成功


|–>如果该page上有超过1个锁对象(lock_rec_get_next_on_page(lock)不为空),则返回失败,走SLOW逻辑


|–>如果当前page上第一个锁锁对应的事务不是当前事务(lock->trx != trx)、或者lock->type_mode != (mode | LOCK_REC)、或者lock_rec_get_n_bits(lock) <= heap_no时,满足这三个条件时,返回失败,走SLOW逻辑


|–>当逻辑走到这里时,且impl=false时,设置记录锁lock_rec_set_nth_bit(lock, heap_no),返回成功

>>如果快速加锁失败,则调用lock_rec_lock_slow

|–>首先判断lock_rec_has_expl(mode, block, heap_no, trx)

检查该事务是否已经在该记录拥有了对应的记录锁或者更强级别的锁,如果有的话,就无需继续建锁,直接从lock_rec_lock_slow返回。

判断时依次读取该记录对应heap no的锁对象,做如下判断:

lock->trx == trx       //锁对象的事务为当前事务

&& lock_mode_stronger_or_eq(lock_get_mode(lock),precise_mode & LOCK_MODE_MASK)  //比较锁强度是否大于当前申请锁

&& !lock_get_wait(lock)  //该锁对象无需等待,即type_mode不是LOCK_WAIT

&&(!lock_rec_get_rec_not_gap(lock)                      //锁模式不是record lock(LOCK_REC_NOT_GAP)

           ||(precise_mode & LOCK_REC_NOT_GAP)          // 或者当前请求锁模式是record lock

           || heap_no == PAGE_HEAP_NO_SUPREMUM)         // 或者请求锁的heap no 为PAGE_HEAP_NO_SUPREMUM


&&(!lock_rec_get_gap(lock)                              //锁模式不是LOCK_GAP

        || (precise_mode & LOCK_GAP)                    //或者当前请求锁模式为LOCK_GAP

        || heap_no == PAGE_HEAP_NO_SUPREMUM)      //或者heap no为PAGE_HEAP_NO_SUPREMUM


&& (!lock_rec_get_insert_intention(lock))   //锁模式不是插入意向锁(LOCK_INSERT_INTENTION)


其中lock_mode_stronger_or_eq的判断矩阵如下:

      IS IX S X AI
IS +   –    –  –   –
IX +  +   –  –   –
S   +   –   +  –  –
X  +   +  + +  +
AI  –   –   –  –   +

当同时满足如上条件时,认为该事务已经拥有了一个等同或更强的记录锁。

|–>当需要创建新的锁对象时,继续检查是否有相冲突的锁

lock_rec_other_has_conflicting(mode, block, heap_no, trx)

同样是遍历该heap_no上的锁,然后调用函数lock_rec_has_to_wait,根据heap_no是否为PAGE_HEAP_NO_SUPREMUM,传递的第四个参数不同(TRUE or FALSE)


lock_rec_has_to_wait函数用于检查当前请求的锁是否需要等待其他锁,参数如下:

@1,trx,新申请锁的事务

@2,type_mode, 新申请锁的锁模式

@3,lock2,对比的锁对象

@4,lock_is_on_supremum,新申请锁是否是在supremum记录上;当为true时,实际上我们申请的就是一种GAP类型的锁。

判断逻辑如下:

  |–>if 

  trx != lock2->trx

  && !lock_mode_compatible(LOCK_MODE_MASK & type_mode,lock_get_mode(lock2))

 首先判断锁是否是相容的,显然,如果锁相容,则肯定没有冲突,直接跳出if的逻辑,返回FALSE。

 锁模式相容性判断通过函数lock_mode_compatible来判断

      IS IX S X AI
IS  +  +  + –   +
IX  + +   – –   +
S    +  –   + –   –
X    –  –   –   –  –
AI  + +   –  –  –

 注意对于行记录,只加S或者X锁,对于表锁,一般加IS活IX锁。只有当执行LOCK TABLES语句时才可能对加S/X级别的表锁  

   |–> (lock_is_on_supremum || (type_mode & LOCK_GAP)) && !(type_mode & LOCK_INSERT_INTENTION)

   如果锁不是插入意向锁并且 (在supremum记录上或者请求锁类型为LOCK_GAP)

   没有LOCK_INSERT_INTENTION标记的GAP类型的锁无需等待任何事务。这是因为不同的用户可以也难怪有冲突的GAP类型锁

   返回FALSE     


   |–>!(type_mode & LOCK_INSERT_INTENTION) && lock_rec_get_gap(lock2) 

   请求的锁类型不是LOCK_INSERT_INTENTION 并且锁对象lock2类型为LOCK_GAP

   LOCK_ORDINARY 或者 LOCK_REC_NOT_GAP类型的记录锁无需等待GAP类型锁

   返回FALSE   


   |–>(type_mode & LOCK_GAP) && lock_rec_get_rec_not_gap(lock2)

   请求的锁类型为LOCK_GAP且锁对象lock2的类型为record lock(LOCK_REC_NOT_GAP)

   在GAP上的锁无需等待LOCK_REC_NOT_GAP类型的锁

   返回FALSE


   |–>lock_rec_get_insert_intention(lock2)

   锁对象Lock2上锁类型为LOCK_INSERT_INTENTION,则无需等待

   没有必要等待一个即将被移除的插入意向锁。由于我们的规则允许在GAP上的冲突锁。这消除了由于一个next-key锁等待插入意向锁

   造成的假的死锁现象;当赋予了插入意向锁后,插入会在等待next-key锁时发生死锁。(来自注释,不太了解)

   同样的插入意向锁之间不互相影响

   返回FALSE


   |–>返回TRUE

|–> 返回FALSE

|–>如果存在相互冲突的锁(lock_rec_other_has_conflicting返回TRUE),那么就需要将请求的锁设置为需要等待

lock_rec_enqueue_waiting(mode, block, heap_no,index, trx)

如果其他事务有一个non-gap的冲突锁请求已经在队列中,并且当前事务在记录上没有更强的锁,这里就需要等待。

函数lock_rec_enqueue_waiting用于将等待锁入队列,同时这里还会负责检查死锁

   |–>创建锁对象lock_rec_create,模式为type_mode | LOCK_WAIT

   |–>检查是否有死锁发生lock_deadlock_occurs(lock, trx),如果发生死锁的话,则重置新建的锁对象,并返回DB_DEADLOCK

       下文单独阐述如何进行死锁检测

   |–>如果死锁发生了,但另外一个事务被选作牺牲者,则表明我们已经拥有了对应的记录锁(trx->wait_lock == NULL) ,直接返回加锁成功

   |–>设置相关变量,例如事务trx->que_state为TRX_QUE_LOCK_WAIT,并返回DB_LOCK_WAIT


|–>如果事务本身既不拥有更强等级锁,也没有相冲突的事务锁,且impl为false(显式锁),则将锁加入到队列中

lock_rec_add_to_queue(LOCK_REC | mode, block,

   |–>如果记录是supremum记录,需要重置GAP位,因为所有在supremum记录上的锁会被自动当做GAP类型的锁

        type_mode = type_mode & ~(LOCK_GAP | LOCK_REC_NOT_GAP);

   |–>查看该记录或者GAP上是否有等待的锁(lock_get_wait(lock) && (lock_rec_get_nth_bit(lock, heap_no))

        如果有的话,则创建锁对象(lock_rec_create(type_mode, block, heap_no, index, trx),并返回; 

   |–>如果当前请求锁模式不是LOCK_WAIT:

       |–>在该Page上查看当前事务是否有类似的记录锁对象(lock_rec_find_similar_on_page,同一个page,事务相同,type_mode相同,且锁对象的bitmap大小大于当前的heap no)

            如果有的话,则重用该结构,设置对应bit位(lock_rec_set_nth_bit(lock, heap_no))

       |–>如果找不到相似的锁对象时,再调用lock_rec_create创建锁。


  

b.死锁检测流程

lock_deadlock_occurs

该函数是死锁检测接口函数,检查一个新的锁请求是否会产生死锁。

参数:

@1,lock,请求的锁对象

@2,锁请求所对应的事务

返回TRUE表示发生死锁且当前事务被选为牺牲者,返回FALSE表示没有死锁,或者发生死锁,但当前事务未被选成

注意死锁检测也是在持有kernel_mutex的情况下进行的。

流程如下:

|–>遍历事务列表(trx_sys->trx_list),将所有事务的deadlock_mark设置为0

|–>调用死锁检测函数

ret = lock_deadlock_recursive(trx, trx, lock, &cost, 0);该函数用于递归调用检测,算法为深度优先遍历(5.6有所改进)

参数描述:

@1,start,递归检测开始的事务

@2,trx,正在等待锁的事务

@3,wait_lock,正在等待被grant的锁对象

@4,cost

@5,depth,递归检测深度


流程如下:

1.当前事务trx->deadlock_mark=1,表明已经穷尽了从当前事务开始的子树,直接return 0;

2.*cost = *cost + 1;

3.如果wait_lock锁类型为LOCK_REC,则查询哈希锁表,找到当前page上所有不等同当前wait_lock且设置了和wait_lock对应

heap no的bit位的锁对象,如果没有这样的锁对象,则设置lock = NULL.

否则如果wait_lock锁类型为LOCK_TABLE,设置lock = wait_lock;

4.查看锁队列中,在wait_lock之前的锁

5.for (;;) //for循环

1)wait_lock为表锁时,设置lock为当前表锁的前一个锁对象

2)当lock = NULL时,设置trx->deadlock_mark = 1,返回FALSE

3)检查wait_lock是否需要等待lock(lock_has_to_wait(wait_lock, lock))

–>如果lock->trx = start,表明发现了一个死锁。打印死锁信息

通过函数trx_weight_ge(wait_lock->trx, start)选择牺牲者

>>@1,更新非事务表的事务总是权重更大

>>@2,通过trx->undo_no+trx->trx_locks的长度做简单计算来衡量事务的权重

权重小的被选作牺牲者。

>>@3,如果非当前事务(start)被选做牺牲者,则调用lock_cancel_waiting_and_release(wait_lock)将另外一个事务回滚。

–>如果depth>LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK(默认200),或者*cost>LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK(默认1000000)

则认为搜索路径太长,返回LOCK_EXCEED_MAX_DEPTH

–>如果lock->trx->que_state被标记为等待(TRX_QUE_LOCK_WAIT),则说明前面已经有一个lock->trx请求的锁的事务,递归调用:

ret = lock_deadlock_recursive(

                    start, lock_trx,

                    lock_trx->wait_lock, cost, depth + 1);


当ret为true时,返回ret,否则继续下面的逻辑


4)当heap_no != ULINT_UNDEFINED时,继续在当前Page上查找下一条锁记录。

#end if


综上:

两个关键参数:

一个是start,用于表示最开始的事务,也就是当前事务,这个参数是一直不变的。

另外一个是wait_lock,这个锁正在被另外一个事务等待(也就是第二个参数trx所代表的事务)


初始状态:start = trx , wait_lock为当前事务请求的锁


然后依次遍历当前page上持有wait_lock所等待的锁对象(在同一个heap no上,     且在wait_lock之前入队列的锁对象)

对于每个这样的锁对象,再深度递归遍历它也在等待的事务。

当发现存在一个这样的锁对象,其lock->trx= start时,表示发现了一个环,这说明检测到了一个死锁。

死锁检测深度太大,也会被认为出现了死锁,这主要是权衡递归检测的开销。(太大的递归深度可能导致栈溢出)

如果没有这样的锁对象,直接返回FALSE。


c.lock_rec_convert_impl_to_expl

该函数用于将隐式锁转换为显式锁

该函数在lock_clust_rec_read_check_and_lock和lock_sec_rec_read_check_and_lock中可能被调用,前者总是会调用到,

后者则在满足page上的最大事务id为活跃事务并且记录不是supremum记录时。


1.首先判断,如果是聚集索引,则调用lock_clust_rec_some_has_impl,查看记录上的事务id,判断该事务是否还是活跃的,如果活跃的,表明在该记录上的修改或者插入事务是活跃的,认为这个记录

上存在隐式锁;

如果是二级索引,则调用lock_sec_rec_some_has_impl_off_kernel来检查

1)二级索引page上的最大事务id是非活跃事务并且没有打开recovery(崩溃恢复时的事务id可能不是正确的),这时候肯定没有隐式锁,返回NULL

2)检查Page上的最大事务ID的有效性;如果事务ID不正常,表明该Page已经损坏了,这里打印page,并返回NULL(避免crash)

3)调用row_vers_impl_x_locked_off_kernel找到在该记录上持有隐式锁的事务(即最近修改或插入了该记录的事务id),具体分析见http://mysqllover.com/?p=136

 

2.如果存在持有隐式锁的事务:

如果持有隐式锁的事务没有对该记录的显式锁(lock_rec_has_expl(LOCK_X | LOCK_REC_NOT_GAP, block,heap_no, impl_trx)),则

为其建立显式锁,并入队列:

lock_rec_add_to_queue(

                LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP,

                block, heap_no, index, impl_trx);


锁类型为NOT-GAP类型的记录X锁。


所以说隐式锁并不是真正的锁,而是根据记录在记录或page上的事务id来推导出来的,当需要请求该记录上的锁时,需要先判断

是否有活跃的事务更新或插入了该记录,如果有的话,则为该事务创建一个显式锁并加入到锁哈希中。


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1天前
|
存储 缓存 关系型数据库
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
MySQL的存储引擎是其核心组件之一,负责数据的存储、索引和检索。不同的存储引擎具有不同的功能和特性,可以根据业务需求 选择合适的引擎。本文详细介绍了MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案。
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
|
2天前
|
SQL 关系型数据库 MySQL
【MySQL基础篇】盘点MySQL常用四大类函数
本文介绍了MySQL中的四大类常用函数:字符串函数、数值函数、日期函数和流程函数。
【MySQL基础篇】盘点MySQL常用四大类函数
|
6天前
|
存储 关系型数据库 MySQL
MySQL存储引擎详述:InnoDB为何胜出?
MySQL 是最流行的开源关系型数据库之一,其存储引擎设计是其高效灵活的关键。InnoDB 作为默认存储引擎,支持事务、行级锁和外键约束,适用于高并发读写和数据完整性要求高的场景;而 MyISAM 不支持事务,适合读密集且对事务要求不高的应用。根据不同需求选择合适的存储引擎至关重要,官方推荐大多数场景使用 InnoDB。
45 7
|
15天前
|
存储 关系型数据库 MySQL
Mysql索引:深入理解InnoDb聚集索引与MyisAm非聚集索引
通过本文的介绍,希望您能深入理解InnoDB聚集索引与MyISAM非聚集索引的概念、结构和应用场景,从而在实际工作中灵活运用这些知识,优化数据库性能。
78 7
|
18天前
|
SQL 关系型数据库 MySQL
MySQL常见函数第二期,你都用过哪些呢 ?
本期介绍了20个常用的MySQL函数,涵盖日期处理(如CURDATE()、DATE_FORMAT())、数学运算(如ABS()、ROUND())、统计分析(如COUNT()、SUM())等,帮助提高SQL查询效率和数据处理能力。希望对大家的学习有所帮助。
59 7
|
3天前
|
存储 Oracle 关系型数据库
数据库传奇:MySQL创世之父的两千金My、Maria
《数据库传奇:MySQL创世之父的两千金My、Maria》介绍了MySQL的发展历程及其分支MariaDB。MySQL由Michael Widenius等人于1994年创建,现归Oracle所有,广泛应用于阿里巴巴、腾讯等企业。2009年,Widenius因担心Oracle收购影响MySQL的开源性,创建了MariaDB,提供额外功能和改进。维基百科、Google等已逐步替换为MariaDB,以确保更好的性能和社区支持。掌握MariaDB作为备用方案,对未来发展至关重要。
13 3
|
3天前
|
安全 关系型数据库 MySQL
MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!
《MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!》介绍了MySQL中的三种关键日志:二进制日志(Binary Log)、重做日志(Redo Log)和撤销日志(Undo Log)。这些日志确保了数据库的ACID特性,即原子性、一致性、隔离性和持久性。Redo Log记录数据页的物理修改,保证事务持久性;Undo Log记录事务的逆操作,支持回滚和多版本并发控制(MVCC)。文章还详细对比了InnoDB和MyISAM存储引擎在事务支持、锁定机制、并发性等方面的差异,强调了InnoDB在高并发和事务处理中的优势。通过这些机制,MySQL能够在事务执行、崩溃和恢复过程中保持
19 3
|
3天前
|
SQL 关系型数据库 MySQL
数据库灾难应对:MySQL误删除数据的救赎之道,技巧get起来!之binlog
《数据库灾难应对:MySQL误删除数据的救赎之道,技巧get起来!之binlog》介绍了如何利用MySQL的二进制日志(Binlog)恢复误删除的数据。主要内容包括: 1. **启用二进制日志**:在`my.cnf`中配置`log-bin`并重启MySQL服务。 2. **查看二进制日志文件**:使用`SHOW VARIABLES LIKE &#39;log_%&#39;;`和`SHOW MASTER STATUS;`命令获取当前日志文件及位置。 3. **创建数据备份**:确保在恢复前已有备份,以防意外。 4. **导出二进制日志为SQL语句**:使用`mysqlbinlog`
22 2
|
17天前
|
关系型数据库 MySQL 数据库
Python处理数据库:MySQL与SQLite详解 | python小知识
本文详细介绍了如何使用Python操作MySQL和SQLite数据库,包括安装必要的库、连接数据库、执行增删改查等基本操作,适合初学者快速上手。
117 15
|
10天前
|
SQL 关系型数据库 MySQL
数据库数据恢复—Mysql数据库表记录丢失的数据恢复方案
Mysql数据库故障: Mysql数据库表记录丢失。 Mysql数据库故障表现: 1、Mysql数据库表中无任何数据或只有部分数据。 2、客户端无法查询到完整的信息。