当前位置: 首页 > news >正文

MySQL的锁(InnoDB)【学习笔记】

MySQL的锁(InnoDB)

知识来源:

  • 《MySQL是怎样运行的》--- 小孩子4919
  • MySQL 是怎么加锁的? | 小林coding

MySQL实现事务的不同隔离级别,是通过MVCC和锁。锁分为不同的类型,和对应的记录、表还有变量关联。当有事务获取到一把锁后,其他的事务根据锁的类型,选择等待或者直接再次获取到锁。

加锁的本质

  • 一开始内存中本没有锁

  • 当有事务T1想对记录改动时,就会查看内存中有无和记录关联的锁结构

  • 没有的话就创造一个锁结构与之关联(成功加锁),记录事务信息、并且将is_waiting设置为false,意为不阻塞。

  • 后面事务T2,想修改这条记录,发现已经有锁结构与之相关联,就会再创造一个锁结构,记录事务信息,并且将is_waiting设置为true,意为被阻塞(加锁失败)

  • 等到事务T1,修改完记录,就会查看内存中是否有其他锁结构与该记录关联,如果有,就唤醒那些阻塞的事务。被唤醒的事务T2会将锁结构的is_waiting设为false(重新加锁)

  • TIPS:当然不是每个修改记录的语句都会创造一个锁结构,当满足一些条件之后,那些修改记录的语句就会塞到一个锁结构里面,后面【锁的结构】的一堆比特位就是用来存储的。

锁的分类

  • 表级锁

    • 元数据锁

    • 表级读写锁(S/X),实现对表的锁定

      • S表示读锁,也叫共享锁,一个事务占有了一张表的S锁,其他事务可以占有这张表的S锁,但是获取这张表的X锁时会被阻塞

      • X表示写锁,也叫独占锁、排他锁,一个事务占有了一张表的X锁,其他事务获取这张表的S/X锁时会被阻塞

    • AUTO-INC锁,用于给AUTO_INCREMENT修饰的字段自增时加锁,避免字段重复。

    • 表级读写意向锁(IS/IX),在获取一张表中记录的S/X锁前,会先对这张表加上对应的IS/IX意向锁。

      • IS表示读意向锁,IX表示写意向锁。IS-IS、IS-IX、IX-IX都互相兼容。

      • 因为在对表加上S/X锁前,表中的记录的S/X锁不能被占有(SS互相兼容属于特例)。如果没有意向锁,则需要遍历整张表来查看是否能加上表锁。

      • 比如:表中的记录1被加上了S锁、记录1000被加上了X锁,这时我想对这张表加上S锁,会先查看记录1加上了S锁,那么继续遍历记录2、3、4...,直到遍历到记录1000才发现被加上了X锁,这时就会被阻塞。

      • 但是我如果在加上加上行级锁,提前加上对应的意向锁。还是上面的例子,我加上表的S锁时,发现这张表有IS和IX锁,就知道表中有记录被加上了S、X锁,那么就立刻被阻塞。

  • 行级锁

    • 记录读写锁(I/X),Record Lock,锁定莫一条记录。

    • 间隙锁,Gap Lock,加在某一条记录上,锁定这条记录前的一个区间。比如:有记录1(id为20)、记录2(id为30),对记录2加上间隙锁,此时id为(10,20)就被锁定了,就不能插入id为11~19的记录了(id为10、20本就存在,插入就会报错)。

    • Next-Key读写锁,记录读写锁和间隙锁的结合版,还是上面的例子,加在记录2上,就会锁定id为(10,20],注意此时插入id为20的记录就不是报错,而是先阻塞了。

    • 插入意向锁,如果有事务对一个区间加上了间隙锁(当然Next-Key锁包含间隙锁),那么另一个事务的对该区间的插入操作就会被阻塞,这时会对加上间隙锁的记录再加上一个插入意向锁。就是一个标记,不能阻止其他事务对这条记录加上任何类型的锁。

    • 隐式锁,当事务1在插入操作时(当然插入的区间不能加间隙锁),此时首先不会对任何记录加上任何锁

      • 如果此时事务2使用select ... lock in share mode/select ... for update查看那条聚簇索引记录,由于事务1插入了那条记录,那条记录的trx_id则为事务1,事务2就会查看这个trx_id对应的事务1是否依然活跃,如果是,则会为事务1和自己生成都生成一个锁结构,并且自己阻塞起来。否则,自己加上对应的S/X锁

      • 如果事务2查看的是二级索引记录,那么就首先查看记录所在页的PAGE_MAX_TRX_ID属性,观察这个值是否小于系统最小的活跃事务id。如果是,则自己加上对应的S/X锁,否则,回表找到对应的聚簇索引记录再做判断。

锁的结构

  • 在某些情况下,对多条记录的锁结构是可以放到同一个锁结构中的。需要同时满足下面的四个条件:

    • 同一个事务加锁

    • 被加锁的记录在同一个页面中

    • 加锁的类型一致

    • 等待状态一致()

  • 锁所在的事务信息,一个指向事务结构的指针

  • 索引信息,指向索引信息的指针

  • 表锁/行锁信息

    • 表锁:表信息+其他信息

    • 行锁:

      • Space ID,记录所在表空间id

      • Page Number,记录所在页号

      • n_bits,表示末尾一堆比特位的数量,大于等于页中记录数量

        • n_bits=(1+((n_recs + LOCK_PAGE_BITMAP_MARGIN)/8))*8

          • n_recs当前页面记录中heap_no最大值的大小(每在表中生成一条记录,就会将当前heap_no的值分配给新记录,heap_no就加1,初始值为2,0和1分别分配给了Infimum记录和Supermum记录)

          • LOCK_PAGE_BITMAP_MARGIN默认值64

      • 一堆比特位:类似一张bitmap,用来记录这锁作用于哪条记录,比特位映射记录的heap_no

  • type_mode,表示锁的类型,32位,分成lock_mode、lock_type和rec_lock_type三部分

    • lock_mode,锁模式

      • LOCK_IS,共享意向锁

      • LOCK_IX,独占意向锁

      • LCOK_S,共享锁

      • LOCK_X,排他锁

      • LOCK_AUTO_INC,AUTO_INC锁

    • lock_type,锁类型

      • LCOK_TABLE,表级锁

      • LCOK_REC,行级锁

    • rec_lock_type,当lock_type为行级锁时,该部位有效,表示行锁的具体类型

      • LOCK_ORDINARY,Next-Key锁

      • LOCK_GAP,间隙锁

      • LOCK_REC_NOT_GAP,记录锁

      • LOCK_INSERT_INTENTION,插入意向锁

    • LOCK_WAIT,独占整个type_mode的第9位,1表示成功加上锁,0表示等待其他事务释放锁

语句执行底层

  • select语句

    • select ... lock in share mode,底层会加上S型锁(具体是记录锁还是Next-Key锁需要具体分析,下面的S/X锁都是如此)

    • select ... for update,底层加上X锁

  • delete语句

    • 首先对需要删除的记录尝试加上X锁(失败则阻塞),然后执行delete mark操作

  • update语句

    • 如果未修改主键字段

      • 前后记录的字段的值占用空间没有丝毫变化,则首先获取记录的X锁,然后执行原地更新操作

      • 否则,获取该记录的X锁,然后彻底删除这条记录,最后根据原纪录和修改字段执行insert语句

    • 修改主键字段

      • 首先对该记录执行delete操作,然后根据原纪录和修改字段执行insert语句

  • insert语句

    • 首先判断新纪录要插入的位置有没有加锁(可能是记录锁、间隙锁、Next-Key锁),如果加了,就给加锁的记录再加上插入意向锁,然后阻塞

    • 如果没有被阻塞,则会直接插入记录,对该记录加上隐式锁

语句加锁分析

首先明确锁和MVCC是用来实现事务隔离级别,用来解决事务并发问题的。事务的并发问题有脏写、脏读、不可重复读、幻读。事务的隔离级别分为读未提交、读已提交、可重复读、串行化。MVCC看这篇MySQL的MVCC【学习笔记】-CSDN博客

区分两个读的而概念:

  • 一致性读(Consistent Read),也叫快照读,读取时生成Read View

  • 锁定读(Lock Read),也叫当前读,读取前会尝试获取锁

普通的select语句
  • 读未提交,直接读最新记录

  • 读已提交,在事务每次执行普通的select语句时都会生成一个Read View

  • 可重复读,事务第一次执行普通的select语句时生成一个Read View,可以避免绝大部分的幻读,但是在特殊情况下依旧会有幻读发生。

  • 串行化

    • 系统变量autocommit为0时,禁止自动提交,普通的select语句会转化成select ... lock in share mode,加锁情况和可重复读一样。

    • 系统变量autocommit为1时,开启自动提交,每次执行普通的select语句会生成一个MVCC

锁定读

包含以下四种语句:

  • select ... lock in share mode,主动给记录加行级共享锁

  • select ... for update,主动给记录加行级排他锁

  • update ...,首先会查对应的记录,加上行级排他锁,然后再进行更新操作

    • 更新聚簇索引记录,且每个字段前后占用空间不变,则加上记录锁,原地更新

    • 更新聚簇索引记录,并且更新非主键字段,

  • delete ...,首先会查对应的记录,加上行级排他锁,然后再进行删除操作

下面就以select ... lock in share mode作为案例,演示流程:

普通情况
  • 读未提交和读已提交,属于同一种情况

    • 首先确定索引扫描区间,找到第一条符合条件的记录,作为当前记录

    • 为当前索引(二级索引或者聚簇索引的记录加记录锁

    • 如果是二级索引的话,会多一些步骤

      • 判断是否符合索引下推的条件,如果符合,则提前通过索引的其他字段筛选掉一部分记录(二级索引)

        • 例如:联合索引(name,age),查询条件为where name like 'zhang%' and age=10,此时根据最左匹配原则,联合索引只用到了name这个字段筛选记录,但是如果开启了索引下推(ICP),那么就会把二级索引记录,再根据age字段,进一步筛选,减少回表次数(!!!只能用于select语句,update、delete的隐式查询无效!!!)

      • 则进行回表操作,否则就选择下一条记录作为当前记录,从第一步开始。

      • 当然还会判断当前记录是否为扫描区间的边界,如果是,则直接向server层返回查询完毕,此时该记录的锁不会被释放(不论什么事务隔离级别)。

      • 回表操作,将记录(聚簇索引加上记录锁

    • 当前记录(聚簇索引是否符合边界条件,符合的话,则释放掉当前记录的锁,否则执行下一步

    • 记录(聚簇索引返回给server层,判断是否满足where的其他条件,满足的话则返回给客户端,不释放锁,否则释放掉锁。

    • 获取下一条记录,作为当前记录(二级索引或者聚簇索引,从头开始

  • 可重复读和串行化,属于同一种情况

    • 和上述类似,只不过

      • 记录上加Next-Key锁,而不是记录锁。

      • 并且记录加上锁后就不会被释放了。

  • 下面举几个例子:

    • select * from hero where number>1 and number <=15 and country = '魏' lock in share mode;

      • 读未提交和读已提交

      • 可重复读和串行化

    • select * from hero force index(idx_name) where name > 'c曹操' and name <= 'x荀彧' and country != '吴' lock in share mode;

      • 读未提交和读已提交

      • 可重复读和串行化

    • Update hero set name = 'cao曹操' where number > 1 and number <=15 and country = '魏';

      • 读未提交和读已提交

        • 加锁过程不是锁定读,而是半一致性读(后面会讲)

      • 可重复读和串行化

        • 采用锁定读的方式加锁

    • delete语句和select ... for update类似,只不过需要给对应的二级索引也都加上隐式锁,效果和X记录锁类似,update在二级索引加的也是隐式锁

      • 读未提交读已提交

        • 会使用半一致性读方式来加锁,而不是锁定读

      • 可重复读和串行化

        • 采用锁定读的方式加锁。

特殊情况
  • 首先明确精确匹配和唯一性匹配的概念

    • 精确匹配:扫描区间的左右开区间都是同一个值,例如:[1,1]、[(1,1),(1,1)]

    • 唯一性搜索(unique search):可以确定扫描区间中有且仅有一条记录,需要满足以下条件

      • 首先是精确匹配

      • 并且索引需要是聚簇索引或者唯一二级索引(搜索条件的值不能等于NULL)

      • 如果索引包含多个列,生成扫描区间的时候,每个列都要用到

  • 精确匹配的情况

    • select * from hero where name='c曹操' for update;

      • 读未提交和读已提交,不会为不符合区间的第一条数据加锁

      • 可重复读和串行化,为不符合区间的第一条数据加间隙锁而不是Next-Key锁

    • select * from hero where name='g关羽' for update;

      • 读未提交和读已提交

        • 不会加任何锁

      • 可重复读和串行化

        • 会加上间隙锁

  • 不是精确匹配的情况

    • select * from hero where name>'d' and name <'1' for update;

      • 可重复读和串行化

        • 会为下一条记录加上Next-Key锁

    • select * from hero where number>=8 for update;

      • 可重复读和串行化

        • 会为第一条记录符合区间的记录加上记录锁,而不是Next-Key锁

  • 唯一性搜索的情况

    • select * from hero where number=8 for update;

      • 无论什么隔离级别,都加上X记录锁(不会是Next-Key锁)

  • 从右往左扫描的情况(一般都是从左往右)

    • select * from hero force index(idx_name) where name>'c曹操' and name<= 'x荀彧' and country != '吴' order by name desc for update;

      • 可重复读和串行化

        • 会为合法区间外的后一条记录,加上间隙锁

半一致性读

读未提交读已提交隔离级别在执行update语句时会使用半一致性读(Semi-Consistent Read)。介于一致性读和锁定读之间的读取方式。

  • 当update语句读到被其他事务加上X锁的记录时,就会将该记录的最新版本读出来。

  • 判断该记录是否满足where的匹配条件,如果不满足,就会跳过这条记录,读取下一条记录。

  • 如果满足,就会对该记录加锁。

  • 例如(隔离级别为读已提交):

    • 事务1执行:select * from hero where number=8 for update;

    • 事务2执行:update hero set name='cao曹操' where number>=8 and number <20 and country!='魏';

      • 依次读取number为8、15、20的记录

      • 如果是锁定读的话,就会阻塞在读取number为8的记录,但此时是本一致性读,

      • 存储引擎就会先将记录返回给server层,判断不符合country!='魏的条件,直接掠过这条记录,server层让存储引擎继续读取后续记录

insert语句
  • 正常情况前面已经说明了

  • 但是插入过程如果报错了,那么就会报错,在报错前还会有一些操作:

    • 遇到重复键(主键或者唯一键冲突)

      • 读未提交、读已提交,会给重复键冲突的记录加上S型记录锁

      • 可重复读、串行化,会给重复键冲突的记录加上S型Next-Key锁

      • 如果insert语句是,insert ... on duplicate key ...,则上述S型都变成X型

    • 外键检查

      • 如果外键能在对应的表找到,那么无论什么隔离级别,都会给外键的表对应的记录加上S型记录所。

      • 如果找不到

        • 读未提交、读已提交,不加锁

        • 可重复读、串行化,外键的表对应的记录加上间隙锁(外键对应的下一条记录,原本有2、4两条记录,外键为3,那么给4加上间隙锁)

查看事务加锁情况

  • MySQL5.7

    • show engine innodb status\G;

      • TRACSACTION段落存在

    • select * from information_schema.innodb_trx;

      • 该表存储了 lnnoDB存储引擎当前正在执行的事务信息,包括事务 id(如果没有为该事务分配唯一的事务id,则会输出该事务对应的内存结构的指针)、事务状态(比如事务是正在运行还是在等待获取某个锁、事务正在执行的语句、事务是何时开启的)

    • select * from information_schema.innodb_locks;

      • 记录锁的信息,但是只有在以下两种情况才会记录:

        • 如果一个事务想要获取某个锁但未获取到, 则记录该锁信息

        • 如果一个事务获取到了某个锁,但是这个锁阻塞了别的事务,则记录该锁信息.

    • select * from information_schema.innodb_lock_waits;

      • 表明每个阻塞的事务是因为获取不到哪个事务持有的锁而阻塞

  • MySQL8.0

    • select * from performance_schema.data_locks;

      • LOCK_TYPE:锁的类型(RECORD行级锁,TABLE表级锁)

      • LOCK_MODE:锁的模式

        • X,REC_NOT_GAP ---X型记录锁

        • X,GAP ---X型间隙锁

        • X ---X型Next-Key锁

死锁

  • 两个事务互相获取对方的锁就会发生死锁。

  • MySQL自己会自动将事务id较小的那个事务回滚,解除死锁。

  • 可以通过show engine innodb status\G;查看死锁日志

    • LATEST DETECTED DEADLOCK 段落(最近一次死锁日志)

    • innodb_print_all_deadlocks设置为on,可以查看所有死锁日志

相关文章:

  • win11报错 ‘wmic‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件 的解决方案
  • NestJS + Kafka 秒杀系统完整实践总结
  • 在 Ubuntu 24.04 系统上安装和管理 Nginx
  • SDRAM介绍和时序
  • 列出es查询match、term、wildcard、prefix、fuzzy、range、query_string、text、missing的区别及用法
  • 数据可视化 —— 饼图
  • 人工智能时代的网络安全威胁
  • EN18031测试,EN18031认证,EN18031报告解读
  • [Jupyter Notebook]:Jupyter Notebook 安装教程(代码编辑器)
  • C# 高级编程:Linq
  • uniapp跳转和获取参数方式
  • 基于javaweb的SpringBoot新闻发布系统设计与实现(源码+文档+部署讲解)
  • ultralytics-YOLO模型在windows环境部署
  • C++学习:六个月从基础到就业——模板编程:模板特化
  • css响应式布局设置子元素高度和宽度一样
  • 华为 MRAG:多模态检索增强生成技术论文阅读
  • 深度学习涉及的数学与计算机知识总结
  • [论文阅读]Practical Poisoning Attacks against Retrieval-Augmented Generation
  • 如何打包一个QT 程序
  • SmolVLM2: The Smollest Video Model Ever(六)
  • 幸福航空五一前三天航班取消:客服称目前是锁舱状态,无法确认何时恢复
  • 因高颜值走红的女通缉犯出狱后当主播自称“改邪归正”,账号已被封
  • VR数字沉浸体验又添新节目,泰坦尼克号驶进文旅元宇宙
  • 上海经信委:将推动整车企业转型,加强智能驾驶大模型等创新应用
  • 最高法知识产权法庭:6年来新收涉外案件年均增长23.2%
  • 中国经济“第一省会”广州,从传统商贸中心到直播电商第一城