【MySQL进阶】MySQL事务隔离与锁机制底层原理万字总结(建议收藏!!)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

【MySQL进阶】MySQL事务隔离与锁机制底层原理万字总结(建议收藏

参考资料

美团技术团队Innodb中事务隔离级别和锁的关系

数据库的锁到底锁的是什么?

阿里面试说说一致性读实现原理?

MySQL 默认隔离级别是RR为什么阿里等大厂会改成RC?

字节面试加了什么锁导致死锁的?

我的阿里二面为什么MySQL选择Repeatable Read作为默认隔离级别?

全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

MySQL 可重复读隔离级别完全解决幻读了吗?

MySQL 记录锁+间隙锁可以防止删除操作而导致的幻读吗?

MySQL 是怎么加锁的?

update 没加索引会锁全表?

【MySQL进阶】多版本并发控制——MVCC

书籍《MySQL是怎样运行的》

文章目录

一解决并发事务带来问题的两种基本方式

1并发事务带来的问题

  • 读 - 读 情况即并发事务相继读取相同的记录。

    读取操作本身不会对记录有一毛钱影响并不会引起什么问题所以允许这种情况的发生。

  • 写 - 写 情况即并发事务相继对相同的记录做出改动。

    在这种情况下会发生 脏写 的问题任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时需要让它们排队执行这个排队的过程其实是通过 锁 来实现的。
    这个所谓的 锁 其实是一个内存中的结构在事务执行前本来是没有锁的也就是说一开始是没有 锁结构 和记录进行关联的如图所示

    image-20230113205222637

    当一个事务想对这条记录做改动时首先会看看内存中有没有与这条记录关联的 锁结构 当没有的时候就会在内存中生成一个 锁结构 与之关联。比方说事务 T1 要对这条记录做改动就需要生成一个 锁结构 与之关联

    image-20230113205309485

    其实在 锁结构 里有很多信息不过为了简化理解我们现在只把两个比较重要的属性拿了出来

    • trx信息 代表这个锁结构是哪个事务生成的

    • is_waiting 代表当前事务是否在等待

      如图所示当事务 T1 改动了这条记录后就生成了一个 锁结构 与该记录关联因为之前没有别的事务为这条记录加锁所以is_waiting 属性就是 false 我们把这个场景就称之为获取锁成功或者加锁成功然后就可以继续执行操作了。

      在事务 T1 提交之前另一个事务 T2 也想对该记录做改动那么先去看看有没有 锁结构 与这条记录关联发现有一个 锁结构 与之关联后然后也生成了一个 锁结构 与这条记录关联不过 锁结构 的is_waiting 属性值为 true 表示当前事务需要等待我们把这个场景就称之为获取锁失败或者加锁失败或者没有成功的获取到锁画个图表示就是这样

      image-20230113205601554

      在事务 T1 提交之后就会把该事务生成的 锁结构 释放掉然后看看还有没有别的事务在等待获取锁发现了事务 T2 还在等待获取锁所以把事务 T2 对应的锁结构的 is_waiting 属性设置为 false 然后把该事务对应的线程唤醒让它继续执行此时事务 T2 就算获取到锁了。效果图就是这样

      image-20230113205632546

    我们总结一下后续内容中可能用到的几种说法以免大家混淆

    • 不加锁

      意思就是不需要在内存中生成对应的 锁结构 可以直接执行操作。

    • 获取锁成功或者加锁成功

      意思就是在内存中生成了对应的 锁结构 而且锁结构的 is_waiting 属性为 false 也就是事务可以继续执行操作。

    • 获取锁失败或者加锁失败或者没有获取到锁

      意思就是在内存中生成了对应的 锁结构 不过锁结构的 is_waiting 属性为 true 也就是事务需要等待不可以继续执行操作。

  • 读 - 写 或 写 - 读 情况也就是一个事务进行读取操作另一个进行改动操作。

    我们前边说过这种情况下可能发生 脏读 、 不可重复读 、 幻读 的问题。

    小贴士

    幻读问题的产生是因为某个事务读了一个范围的记录之后别的事务在该范围内插入了新记录该事务再次读取该范围的记录时可以读到新插入的记录所以幻读问题准确的说并不是因为读取和写入一条相同记录而产生的。

    SQL标准 规定不同隔离级别下可能发生的问题不一样

    • 在 READ UNCOMMITTED 隔离级别下 脏读 、 不可重复读 、 幻读 都可能发生。
    • 在 READ COMMITTED 隔离级别下 不可重复读 、 幻读 可能发生 脏读 不可以发生。
    • 在 REPEATABLE READ 隔离级别下 幻读 可能发生 脏读 和 不可重复读 不可以发生。
    • 在 SERIALIZABLE 隔离级别下上述问题都不可以发生。

2两种可选的解决方案

  • 方案一读操作利用多版本并发控制( MVCC 写操作进行 加锁

    所谓的 MVCC 我们在前一章有过详细的描述就是通过生成一个 ReadView 然后通过 ReadView 找到符合条件的记录版本(历史版本是由 undo日志 构建的其实就像是在生成 ReadView 的那个时刻做了一次时间静止(就像用相机拍了一个快照查询语句只能读到在生成 ReadView 之前已提交事务所做的更改在生成ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录读记录的历史版本和改动记录的最新版本本身并不冲突也就是采用MVCC 时 读-写 操作并不突。

    小贴士

    我们说过普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。在READ COMMITTED隔离级别下一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadViewReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改也就是避免了脏读现象;REPEATABLE READ隔离级别下一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView之后的SELECT操作都复用这个ReadView这样也就避免了不可重复读和幻读的问题。

  • 方案二读、写操作都采用 加锁 的方式

    如果我们的一些业务场景不允许读取记录的旧版本而是每次都必须去读取记录的最新版本比方在银行存款的事务中你需要先把账户的余额读出来然后将其加上本次存款的数额最后再写到数据库中。在将账户余额读取出来后就不想让别的事务再访问该余额直到本次存款事务执行完成其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行 加锁 操作这样也就意味着 读 操作和 写 操作也像 写-写 操作那样排队执行。

    小贴士

    我们说脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录如果另一个事务在写记录的时候就给这条记录加锁那么当前事务就无法继续读取该记录了所以也就不会有脏读问题的产生了。不可重复读的产生是因为当前事务先读取一条记录另外一个事务对该记录做了改动之后并提交之后当前事务再次读取时会获得不同的值如果在当前事务读取记录时就给该记录加锁那么另一个事务就无法修改该记录自然也不会发生不可重复读了。我们说幻读问题的产生是因为当前事务读取了一个范围的记录然后另外的事务向该范围内插入了新记录当前事务再次读取该范围的记录时发现了新插入的新记录我们把新插入的那些记录称之为幻影记录。采用加锁的方式解决幻读问题就有那么一丢丢麻烦了因为当前事务在第一次读取记录时那些幻影记录并不存在所以读取的时候加锁就有点尴尬 —— 因为你并不知道给谁加锁没关系这难不倒设计InnoDB的大叔的。

很明显采用 MVCC 方式的话 读-写 操作彼此并不冲突性能更高采用 加锁 方式的话 读-写 操作彼此需要排队执行影响性能。一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题但是业务在某些特殊情况下要求必须采用 加锁 的方式执行那也是没有办法的事。

3一致性读(Consistent Reads

事务利用 MVCC 进行的读取操作称之为 一致性读 或者 一致性无锁读 有的地方也称之为 快照读 。所有普通的 SELECT 语句( plain SELECT 在 READ COMMITTED 、 REPEATABLE READ 隔离级别下都算是 一致性读 比方说

SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2

一致性读 并不会对表中的任何记录做 加锁 操作其他事务可以自由的对表中的记录做改动。

4锁定读(Locking Reads

共享锁和独占锁

我们前边说过并发事务的 读-读 情况并不会引起什么问题不过对于 写-写 、 读-写 或 写-读 这些情况可能会引起一些问题需要使用 MVCC 或者 加锁 的方式来解决它们。在使用 加锁 的方式解决问题时由于既要允许 读-读 情况不受影响又要使 写-写 、 读-写 或 写-读 情况中的操作相互阻塞所以设计 MySQL 的大叔给锁分了个类

  • 共享锁 英文名 Shared Locks 简称 S锁 。在事务要读取一条记录时需要先获取该记录的 S锁 。
  • 独占锁 也常称 排他锁 英文名 Exclusive Locks 简称 X锁 。在事务要改动一条记录时需要先获取该记录的 X锁 。

假如事务 T1 首先获取了一条记录的 S锁 之后事务 T2 接着也要访问这条记录

  • 如果事务 T2 想要再获取一个记录的 S锁 那么事务 T2 也会获得该锁也就意味着事务 T1 和 T2 在该记录上同时持有 S锁 。
  • 如果事务 T2 想要再获取一个记录的 X锁 那么此操作会被阻塞直到事务 T1 提交之后将 S锁 释放掉。

如果事务 T1 首先获取了一条记录的 X锁 之后那么不管事务 T2 接着想获取该记录的 S锁 还是 X锁 都会被阻塞直到事务 T1 提交。

所以我们说 S锁 和 S锁 是兼容的 S锁 和 X锁 是不兼容的 X锁 和 X锁 也是不兼容的画个表表示一下就是这样

image-20230113213026038

锁定读的语句

我们前边说在采用 加锁 方式解决 脏读 、 不可重复读 、 幻读 这些问题时读取一条记录时需要获取一下该记录的 S锁 其实这是不严谨的有时候想在读取记录时就获取记录的 X锁 来禁止别的事务读写该记录为此设计 MySQL 的大叔提出了两种比较特殊的 SELECT 语句格式

  • 对读取的记录加 S锁

    SELECT ... LOCK IN SHARE MODE;
    

    也就是在普通的 SELECT 语句后边加 LOCK IN SHARE MODE 如果当前事务执行了该语句那么它会为读取到的记录加 S锁 这样允许别的事务继续获取这些记录的 S锁 (比方说别的事务也使用 SELECT … LOCK INSHARE MODE 语句来读取这些记录但是不能获取这些记录的 X锁 (比方说使用 SELECT … FOR UPDATE语句来读取这些记录或者直接修改这些记录。如果别的事务想要获取这些记录的 X锁 那么它们会阻塞直到当前事务提交之后将这些记录上的 S锁 释放掉。

  • 对读取的记录加 X锁

    SELECT ... FOR UPDATE;
    

    也就是在普通的 SELECT 语句后边加 FOR UPDATE 如果当前事务执行了该语句那么它会为读取到的记录加 X锁 这样既不允许别的事务获取这些记录的 S锁 (比方说别的事务使用 SELECT … LOCK IN SHAREMODE 语句来读取这些记录也不允许获取这些记录的 X锁 (比方也说使用 SELECT … FOR UPDATE 语句来读取这些记录或者直接修改这些记录。如果别的事务想要获取这些记录的 S锁 或者 X锁 那么它们会阻塞直到当前事务提交之后将这些记录上的 X锁 释放掉。

5写操作

平常所用到的 写操作 无非是 DELETEUPDATE INSERT 这三种

  • DELETE

    对一条记录做 DELETE 操作的过程其实是先在 B+ 树中定位到这条记录的位置然后获取一下这条记录的 X锁 然后再执行 delete mark 操作。我们也可以把这个定位待删除记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。

  • UPDATE

    在对一条记录做 UPDATE 操作时分为三种情况

    • 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化则先在 B+ 树中定位到这条记录的位置然后再获取一下记录的 X锁 最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。
    • 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化则先在B+ 树中定位到这条记录的位置然后获取一下记录的 X锁 将该记录彻底删除掉(就是把记录彻底移入垃圾链表最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。
    • 如果修改了该记录的键值则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作加锁操作就需要按照 DELETE 和 INSERT 的规则进行了。
  • INSERT

    一般情况下新插入一条记录的操作并不加锁设计 InnoDB 的大叔通过一种称之为 隐式锁 的东东来保护这条新插入的记录在本事务提交前不被别的事务访问更多细节我们后边看哈~

二多粒度锁

1共享锁( S锁 和 独占锁 ( X锁 的多粒度

我们前边提到的 锁 都是针对记录的也可以被称之为 行级锁 或者 行锁 对一条记录加锁影响的也只是这条记录而已我们就说这个锁的粒度比较细;其实一个事务也可以在 表 级别进行加锁自然就被称之为 表级锁 或者 表锁 对一个表加锁影响整个表中的记录我们就说这个锁的粒度比较粗。给表加的锁也可以分为 共享锁( S锁 和 独占锁 ( X锁

  • 给表加 S锁

    如果一个事务给表加了 S锁 那么

    • 别的事务可以继续获得该表的 S锁
    • 别的事务可以继续获得该表中的某些记录的 S锁
    • 别的事务不可以继续获得该表的 X锁
    • 别的事务不可以继续获得该表中的某些记录的 X锁
  • 给表加 X锁

    如果一个事务给表加了 X锁 (意味着该事务要独占这个表那么

    • 别的事务不可以继续获得该表的 S锁
    • 别的事务不可以继续获得该表中的某些记录的 S锁
    • 别的事务不可以继续获得该表的 X锁
    • 别的事务不可以继续获得该表中的某些记录的 X锁

上边看着有点啰嗦为了更好的理解这个表级别的 S锁 和 X锁 我们举一个现实生活中的例子来分析一下加锁的情况

  • 教室一般都是公用的我们可以随便选教室进去上自习。当然教室不是自家的一间教室可以容纳很多同学同时上自习每当一个人进去上自习就相当于在教室门口挂了一把 S锁 如果很多同学都进去上自习相当于教室门口挂了很多把 S锁 (类似行级别的 S锁 。
  • 有的时候教室会进行检修比方说换地板换天花板换灯管啥的这些维修项目并不能同时开展。如果教室针对某个项目进行检修就不允许别的同学来上自习也不允许其他维修项目进行此时相当于教室门口会挂一把 X锁 (类似行级别的 X锁 。

上边提到的这两种锁都是针对 教室 而言的不过有时候我们会有一些特殊的需求

  • 有领导要来参观教学楼的环境。

    校领导考虑并不想影响同学们上自习但是此时不能有教室处于维修状态所以可以在教学楼门口放置一把S锁 (类似表级别的 S锁 。此时

    • 来上自习的学生们看到教学楼门口有 S锁 可以继续进入教学楼上自习。
    • 修理工看到教学楼门口有 S锁 则先在教学楼门口等着啥时候领导走了把教学楼的 S锁 撤掉再进入教学楼维修。
  • 学校要占用教学楼进行考试。

    此时不允许教学楼中有正在上自习的教室也不允许对教室进行维修。所以可以在教学楼门口放置一把 X锁(类似表级别的 X锁 。此时

    • 来上自习的学生们看到教学楼门口有 X锁 则需要在教学楼门口等着啥时候考试结束把教学楼的 X锁 撤掉再进入教学楼上自习。
    • 修理工看到教学楼门口有 X锁 则先在教学楼门口等着啥时候考试结束把教学楼的 X锁 撤掉再进入教学楼维修。

但是这里头有两个问题

  • 如果我们想对教学楼整体上 S锁 首先需要确保教学楼中的没有正在维修的教室如果有正在维修的教室需要等到维修结束才可以对教学楼整体上 S锁 。
  • 如果我们想对教学楼整体上 X锁 首先需要确保教学楼中的没有上自习的教室以及正在维修的教室如果有上自习的教室或者正在维修的教室需要等到全部上自习的同学都上完自习离开以及维修工维修完教室离开后才可以对教学楼整体上 X锁 。

我们在对教学楼整体上锁( 表锁 时怎么知道教学楼中有没有教室已经被上锁( 行锁 了呢?依次检查每一间教室门口有没有上锁?那这效率也太慢了吧遍历是不可能遍历的这辈子也不可能遍历的于是乎设计InnoDB 的大叔们提出了一种称之为 意向锁 (英文名 Intention Locks 的东东。

2意向锁

  • 意向共享锁英文名 Intention Shared Lock 简称 IS锁 。当事务准备在某条记录上加 S锁 时需要先在表级别加一个 IS锁 。
  • 意向独占锁英文名 Intention Exclusive Lock 简称 IX锁 。当事务准备在某条记录上加 X锁 时需
    要先在表级别加一个 IX锁 。

视角回到教学楼和教室上来

  • 如果有学生到教室中上自习那么他先在整栋教学楼门口放一把 IS锁 (表级锁然后再到教室门口放一把 S锁 (行锁。
  • 如果有维修工到教室中维修那么它先在整栋教学楼门口放一把 IX锁 (表级锁然后再到教室门口放一把 X锁 (行锁。

之后

  • 如果有领导要参观教学楼也就是想在教学楼门口前放 S锁 (表锁时首先要看一下教学楼门口有没有IX锁如果有意味着有教室在维修需要等到维修结束把 IX锁 撤掉后才可以在整栋教学楼上加 S锁 。
  • 如果有考试要占用教学楼也就是想在教学楼门口前放 X锁 (表锁时首先要看一下教学楼门口有没有IS锁 或 IX锁 如果有意味着有教室在上自习或者维修需要等到学生们上完自习以及维修结束把 IS锁和 IX锁 撤掉后才可以在整栋教学楼上加 X锁 。

小贴士

学生在教学楼门口加IS锁时是不关心教学楼门口是否有IX锁的维修工在教学楼门口加IX锁时是不关心教学楼门口是否有IS锁或者其他IX锁的。IS和IX锁只是为了判断当前时间教学楼里有没有被占用的教室用的也就是在对教学楼加S锁或者X锁时才会用到。

总结一下IS、IX锁是表级锁它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁以避免用遍历的方式来查看表中有没有上锁的记录也就是说其实IS锁和IX锁是兼容的IX锁和IX锁是兼容的。我们画个表来看一下表级别的各种锁的兼容性

image-20230113220352772

三MySQL中的行锁和表锁

上边说的都算是些理论知识其实 MySQL 支持多种存储引擎不同存储引擎对锁的支持也是不一样的。当然我们重点还是讨论 InnoDB 存储引擎中的锁其他的存储引擎只是稍微提一下~

1其他存储引擎中的锁

对于 MyISAM 、 MEMORY 、 MERGE 这些存储引擎来说它们只支持表级锁而且这些引擎并不支持事务所以使用这些存储引擎的锁一般都是针对当前会话来说的。比方说在 Session 1 中对一个表执行 SELECT 操作就相当于为这个表加了一个表级别的 S锁 如果在 SELECT 操作未完成时 Session 2 中对这个表执行 UPDATE 操作相当于要获取表的 X锁 此操作会被阻塞直到 Session 1 中的 SELECT 操作完成释放掉表级别的 S锁 后Session 2 中对这个表执行 UPDATE 操作才能继续获取 X锁 然后执行具体的更新语句。

小贴士

因为使用MyISAM、MEMORY、MERGE这些存储引擎的表在同一时刻只允许一个会话对表进行写操作所以这些存储引擎实际上最好用在只读或者大部分都是读操作或者单用户的情景下。另外在MyISAM存储引擎中有一个称之为Concurrent Inserts的特性支持在对MyISAM表读取时同时插入记录这样可以提升一些插入速度。

2InnoDB存储引擎中的锁

InnoDB 存储引擎既支持表锁也支持行锁。表锁实现简单占用资源较少不过粒度很粗有时候你仅仅需要锁住几条记录但使用表锁的话相当于为表中的所有记录都加锁所以性能比较差。行锁粒度更细可以实现更精准的并发控制。

表级别的 S锁 、 X锁

在对某个表执行 SELECT 、 INSERT 、 DELETE 、 UPDATE 语句时 InnoDB 存储引擎是不会为这个表添加表级别的 S锁 或者 X锁 的。

另外在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时其他事务对这个表并发执行诸如 SELECT 、 INSERT 、 DELETE 、 UPDATE 的语句会发生阻塞同理某个事务中对某个表执行SELECT 、 INSERT 、 DELETE 、 UPDATE 语句时在其他会话中对这个表执行 DDL 语句也会发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁 (英文名 Metadata Locks 简称 MDL 东东来实现的一般情况下也不会使用 InnoDB 存储引擎自己提供的表级别的 S锁 和 X锁 。

其实这个 InnoDB 存储引擎提供的表级 S锁 或者 X锁 是相当鸡肋只会在一些特殊情况下比方说崩溃恢复过程中用到。不过我们还是可以手动获取一下的比方说在系统变量 autocommit=0innodb_table_locks =1 时手动获取 InnoDB 存储引擎提供的表 t 的 S锁 或者 X锁 可以这么写

  • LOCK TABLES t READ InnoDB 存储引擎会对表 t 加表级别的 S锁 。
  • LOCK TABLES t WRITE InnoDB 存储引擎会对表 t 加表级别的 X锁 。

不过请尽量避免在使用 InnoDB 存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句它们并不会提供什么额外的保护只是会降低并发能力而已。 InnoDB 的厉害之处还是实现了更细粒度的行锁关于表级别的 S锁 和 X锁 大家了解一下就罢了。

表级别的 IS锁 、 IX锁

当我们在对使用 InnoDB 存储引擎的表的某些记录加 S锁 之前那就需要先在表级别加一个 IS锁 当我们在对使用 InnoDB 存储引擎的表的某些记录加 X锁 之前那就需要先在表级别加一个 IX锁 。 IS锁 和 IX锁的使命只是为了后续在加表级别的 S锁 和 X锁 时判断表中是否有已经被加锁的记录以避免用遍历的方式来查看表中有没有上锁的记录。

表级别的 AUTO-INC锁

在使用 MySQL 过程中我们可以为表的某个列添加 AUTO_INCREMENT 属性之后在插入记录时可以不指定该列的值系统会自动为它赋上递增的值比方说我们有一个表

CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;

由于这个表的 id 字段声明了 AUTO_INCREMENT 也就意味着在书写插入语句时不需要为其赋值比方说这样

INSERT INTO t(c) VALUES('aa'), ('bb');

上边的插入语句并没有为 id 列显式赋值所以系统会自动为它赋上递增的值效果就是这样

image-20230113235844753

系统实现这种自动给 AUTO_INCREMENT 修饰的列递增赋值的原理主要是两个

  • 采用 AUTO-INC 锁也就是在执行插入语句时就在表级别加一个 AUTO-INC 锁然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值在该语句执行结束后再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中其他事务的插入语句都要被阻塞可以保证一个语句中分配的递增值是连续的。

    如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量比方说使用 INSERT … SELECT 、 REPLACE … SELECT 或者 LOAD DATA 这种插入语句一般是使用AUTO-INC 锁为 AUTO_INCREMENT 修饰的列生成对应的值。

    小贴士

    需要注意一下的是这个AUTO-INC锁的作用范围只是单个插入语句插入语句执行完成后这个锁就被释放了跟我们之前介绍的锁在事务结束时释放是不一样的。

  • 采用一个轻量级的锁在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后就把该轻量级锁释放掉并不需要等到整个插入语句执行完才释放锁。

    如果我们的插入语句在执行前就可以确定具体要插入多少条记录比方说我们上边举的关于表 t 的例子中在语句执行前就可以确定要插入2条记录那么一般采用轻量级锁的方式对 AUTO_INCREMENT 修饰的列进行赋值。这种方式可以避免锁定表可以提升插入性能。

    小贴士

    设计InnoDB的大叔提供了一个称之为innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值当innodb_autoinc_lock_mode值为0时一律采用AUTO-INC锁;当innodb_autoinc_lock_mode值为2时一律采用轻量级锁;当innodb_autoinc_lock_mode值为1时两种方式混着来(也就是在插入记录数量确定时采用轻量级锁不确定时使用AUTO-INC锁。不过当innodb_autoinc_lock_mode值为2时可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的在有主从复制的场景中是不安全的。

介绍行级锁前的准备

行锁 也称为 记录锁 顾名思义就是在记录上加的锁。不过设计 InnoDB 的大叔很有才一个 行锁 玩出了各种花样也就是把 行锁 分成了各种类型。换句话说即使对同一条记录加 行锁 如果类型不同起到的功效也是不同的。为了故事的顺利发展我们还是先将之前唠叨 MVCC 时用到的表抄一遍

CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;

INSERT INTO hero VALUES
(1, 'l刘备', '蜀'),
(3, 'z诸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孙权', '吴');

现在表里的数据就是这样的

image-20230114000319475

我们把 hero 表中的聚簇索引的示意图画一下

image-20230114000341962

当然我们把 B+树 的索引结构做了一个超级简化只把索引中的记录给拿了出来我们这里只是想强调聚簇索引中的记录是按照主键大小排序的并且省略掉了聚簇索引中的隐藏列(不理解索引结构的话可以去前边的文章中查看。

【MySQL进阶】深入理解B+树索引底层原理

现在准备工作做完了下边我们来看看都有哪些常用的 行锁类型 。

行级别的 Record Locks锁

我们前边提到的记录锁就是这种类型也就是仅仅把一条记录锁上我决定给这种类型的锁起一个比较不正经的名字 正经记录锁 。官方的类型名称为LOCK_REC_NOT_GAP 。比方说我们把 number 值为 8 的那条记录加一个 正经记录锁 的示意图如下

image-20230114000731568

正经记录锁 是有 S锁 和 X锁 之分的让我们分别称之为 S型正经记录锁 和 X型正经记录锁 吧(听起来有点怪怪的当一个事务获取了一条记录的 S型正经记录锁 后其他事务也可以继续获取该记录的 S型正经记录锁 但不可以继续获取 X型正经记录锁 ;当一个事务获取了一条记录的 X型正经记录锁 后其他事务既不可以继续获取该记录的 S型正经记录锁 也不可以继续获取 X型正经记录锁 ;

行级别的 Gap Locks锁

我们说 MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的解决方案有两种可以使用 MVCC 方案解决也可以采用 加锁 方案解决。但是在使用 加锁 方案解决时有个大问题就是事务在第一次执行读取操作时那些幻影记录尚不存在我们无法给这些幻影记录加上 正经记录锁 。

不过这难不倒设计 InnoDB 的大叔他们提出了一种称之为 Gap Locks 的锁官方的类型名称为 LOCK_GAP 我们也可以简称为 gap锁 。比方说我们把 number 值为 8 的那条记录加一个 gap锁 的示意图如下

image-20230114001129352

如图中为 number 值为 8 的记录加了 gap锁 意味着不允许别的事务在 number 值为 8 的记录前边的 间隙插入新记录其实就是 number 列的值 (3, 8) 这个区间的新记录是不允许立即插入的。比方说有另外一个事务再想插入一条 number 值为 4 的新记录它定位到该条新记录的下一条记录的 number 值为8而这条记录上又有一个 gap锁 所以就会阻塞插入操作直到拥有这个 gap锁 的事务提交了之后 number 列的值在区间 (3, 8) 中的新记录才可以被插入。

这个 gap锁 的提出仅仅是为了防止插入幻影记录而提出的虽然有 共享gap锁 和 独占gap锁 这样的说法但是它们起到的作用都是相同的。而且如果你对一条记录加了 gap锁 (不论是 共享gap锁 还是 独占gap锁 并不会限制其他事务对这条记录加 正经记录锁 或者继续加 gap锁 再强调一遍 gap锁 的作用仅仅是为了防止插入幻影记录的而已。

不知道大家发现了一个问题没给一条记录加了 gap锁 只是不允许其他事务往这条记录前边的间隙插入新记录那对于最后一条记录之后的间隙也就是 hero 表中 number 值为 20 的记录之后的间隙该咋办呢?也就是说给哪条记录加 gap锁 才能阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录呢?这时候应该想起我们在前边唠叨 数据页 时介绍的两条伪记录了

  • Infimum 记录表示该页面中最小的记录。
  • Supremum 记录表示该页面中最大的记录。

为了实现阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录我们可以给索引中的最后一条记录也就是number 值为 20 的那条记录所在页面的 Supremum 记录加上一个 gap锁 画个图就是这样

image-20230114001322238

这样就可以阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录。为了大家理解方便之后的索引示意图中都会把这个 Supremum 记录画出来。

行级别的 Next-Key Locks锁

有时候我们既想锁住某条记录又想阻止其他事务在该记录前边的 间隙 插入新记录所以设计 InnoDB 的大叔们就提出了一种称之为 Next-Key Locks 的锁官方的类型名称为 LOCK_ORDINARY 我们也可以简称为next-key锁 。比方说我们把 number 值为 8 的那条记录加一个 next-key锁 的示意图如下

image-20230114001411967

next-key锁 的本质就是一个 正经记录锁 和一个 gap锁 的合体它既能保护该条记录又能阻止别的事务将新记录插入被保护记录前边的 间隙 。

行级别的 Insert Intention Locks锁

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的 gap锁 ( next-key锁 也包含 gap锁 后边就不强调了如果有的话插入操作需要等待直到拥有 gap锁 的那个事务提交。但是设计 InnoDB 的大叔规定事务在等待的时候也需要在内存中生成一个 锁结构 表明有事务想在某个 间隙 中插入新记录但是现在在等待。设计 InnoDB 的大叔就把这种类型的锁命名为 Insert IntentionLocks 官方的类型名称为 LOCK_INSERT_INTENTION 我们也可以称为 插入意向锁 。

比方说我们把 number 值为 8 的那条记录加一个 插入意向锁 的示意图如下

image-20230114001522577

为了让大家彻底理解这个 插入意向锁 的功能我们还是举个例子然后画个图表示一下。比方说现在 T1 为number 值为 8 的记录加了一个 gap锁 然后 T2 和 T3 分别想向 hero 表中插入 number 值分别为 4 、 5 的两条记录所以现在为 number 值为 8 的记录加的锁的示意图就如下所示

image-20230114001546550

小贴士

我们在锁结构中又新添了一个type属性表明该锁的类型。

从图中可以看到由于 T1 持有 gap锁 所以 T2 和 T3 需要生成一个 插入意向锁 的 锁结构 并且处于等待状态。当 T1 提交后会把它获取到的锁都释放掉这样 T2 和 T3 就能获取到对应的 插入意向锁 了(本质上就是把插入意向锁对应锁结构的 is_waiting 属性改为 false T2 和 T3 之间也并不会相互阻塞它们可以同时获取到 number 值为8的 插入意向锁 然后执行插入操作。事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁( 插入意向锁 就是这么鸡肋。

行级别的 隐式锁

我们前边说一个事务在执行 INSERT 操作时如果即将插入的 间隙 已经被其他事务加了 gap锁 那么本次INSERT 操作会阻塞并且当前事务会在该间隙上加一个 插入意向锁 否则一般情况下 INSERT 操作是不加锁的。那如果一个事务首先插入了一条记录(此时并没有与该记录关联的锁结构然后另一个事务

  • 立即使用 SELECT … LOCK IN SHARE MODE 语句读取这条事务也就是在要获取这条记录的 S锁 或者使用 SELECT … FOR UPDATE 语句读取这条事务或者直接修改这条记录也就是要获取这条记录的 X锁 该咋办?

    如果允许这种情况的发生那么可能产生 脏读 问题。

  • 立即修改这条记录也就是要获取这条记录的 X锁 该咋办?

    如果允许这种情况的发生那么可能产生 脏写 问题。

这时候我们前边唠叨了很多遍的 事务id 又要起作用了。我们把聚簇索引和二级索引中的记录分开看一下

  • 情景一对于聚簇索引记录来说有一个 trx_id 隐藏列该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后该记录的 trx_id 隐藏列代表的的就是当前事务的事务id 如果其他事务此时想对该记录添加 S锁 或者 X锁 时首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务如果是的话那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构 is_waiting 属性是 false 然后自己进入等待状态(也就是为自己也创建一个锁结构 is_waiting 属性是 true 。
  • 情景二对于二级索引记录来说本身并没有 trx_id 隐藏列但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性该属性代表对该页面做改动的最大的 事务id 如果PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id 那么说明对该页面做修改的事务都已经提交了否则就需要在页面中定位到对应的二级索引记录然后回表找到它对应的聚簇索引记录然后再重复 情景一 的做法。

通过上边的叙述我们知道一个事务对新插入的记录可以不显式的加锁(生成一个锁结构但是由于事务id 这个牛逼的东东的存在相当于加了一个 隐式锁 。别的事务在对这条记录加 S锁 或者 X锁时由于 隐式锁 的存在会先帮助当前事务生成一个锁结构然后自己再生成一个锁结构后进入等待状态。

小争议MVCC机制是否彻底解决了幻读问题呢?

普通的 SELECT 语句在

  • READ UNCOMMITTED 隔离级别下不加锁直接读取记录的最新版本可能发生 脏读 、 不可重复读 和 幻读 问题。
  • READ COMMITTED 隔离级别下不加锁在每次执行普通的 SELECT 语句时都会生成一个 ReadView 这样解决了 脏读 问题但没有解决 不可重复读 和 幻读 问题。
  • REPEATABLE READ 隔离级别下不加锁只在第一次执行普通的 SELECT 语句时生成一个 ReadView 这样把 脏读 、 不可重复读 和 幻读 问题都解决了。

不过这里有一个小插曲

# 事务T1REPEATABLE READ隔离级别下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM hero WHERE number = 30;
Empty set (0.01 sec)


# 此时事务T2执行了INSERT INTO hero VALUES(30, 'g关羽', '魏'); 并提交


mysql> UPDATE hero SET country = '蜀' WHERE number = 30;
Query OK, 1 row affected (0.01 sec)
mysql> SELECT * FROM hero WHERE number = 30;
+--------+---------+---------+
| number |    name | country |
+--------+---------+---------+
|   30   |    g关羽 ||
+--------+---------+---------+
1 row in set (0.01 sec)

在 REPEATABLE READ 隔离级别下 T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView 之后 T2 向 hero 表中新插入了一条记录便提交了 ReadView 并不能阻止 T1 执行 UPDATE 或者 DELETE 语句来对改动这个新插入的记录(因为 T2 已经提交改动该记录并不会造成阻塞但是这样一来这条新记录的 trx_id 隐藏列就变成了 T1 的 事务id 之后 T1 中再使用普通的 SELECT 语句去查询这条记录时就可以看到这条记录了也就把这条记录返回给客户端了。因为这个特殊现象的存在你也可以认为 InnoDB 中的 MVCC 并不能完完全全的禁止幻读。

实际上这个问题有点四不像可以理解成幻读问题也可以理解成是不可重复读问题总之不管怎么说就是MVCC机制存在些许问题但这种情况线下一般不会发生毕竟不同事务之间都是互不相知的在一个事务中不可能会去主动修改一条“不存在”的记录。

但如若你实在不放心想要彻底杜绝任何风险的出现那就直接将事务隔离级别调整到Serializable即可。

四事务隔离机制的底层实现

对于事务隔离机制的底层实现其实在前面的章节中简单聊到过对于并发事务造成的各类问题在不同的隔离级别实际上是通过不同粒度、类型的锁以及MVCC机制来解决的也就是调整了并发事务的执行顺序从而避免了这些问题产生具体是如何做的呢?

  • RU/读未提交级别要求该隔离级别下解决脏写问题。
  • RC/读已提交级别要求该隔离级别下解决脏读问题。
  • RR/可重复读级别要求该隔离级别下解决不可重复读问题。
  • Serializable/序列化级别要求在该隔离级别下解决幻读问题。

虽然DBMS中要求在序列化级别再解决幻读问题但在MySQLRR级别中就已经解决了幻读问题因此MySQL中可以将RR级别视为最高级别而Serializable级别几乎用不到因为序列化级别中解决的问题在RR级别中基本上已经解决了再将MySQL调到Serializable级别反而会降低性能。

当然RR级别下有些极端的情况依旧会出现幻读问题但线上100%不会出现先来看看各大隔离级别在MySQL中是如何实现的。

1RU(Read Uncommitted读未提交级别的实现

对于RU级别而言从它名字上就可以看出来该隔离级别下一个事务可以读到其他事务未提交的数据但同时要求解决脏写(更新覆盖问题那思考一下该怎么满足这个需求呢?先来看看不加锁的情况

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

-- ----------- 请按照标出的序号阅读代码 --------------

-- ①开启一个事务T1
begin;

-- ③修改 ID=1 的姓名为 竹子
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;

-- ⑥提交T1
commit;
-- ②开启另一个事务T2
begin;

-- ④这里可以读取到T1中还未提交的 竹子 记录
SELECT * FROM `zz_users` WHERE user_id = 1;

-- ⑤T2中再次修改姓名为 黑熊
UPDATE `zz_users` SET user_name = "黑熊" WHERE user_id = 1;

-- ⑦提交T2
commit;

假设上述两个事务并发执行时都不加锁T2自然可以读取到T1修改后但还未提交的数据但当T2再次修改ID=1的数据后两个事务一起提交此时就会出现T2覆盖T1的问题这也就是脏写问题而这个问题是不允许存在的所以需要解决咋解决呢?

写操作加排他锁读操作不加锁

还是上述的例子当写操作加上排他锁后T1在修改数据时当T2再次尝试修改相同的数据也要获取排他锁因此T1、T2两个事务的写操作会相互排斥T2就需要阻塞等待。但因为读操作不会加锁因此当T2尝试读取这条数据时自然可以读到数据。

因为写-写会排斥但写-读不会排斥因此也满足了RU级别的要求即可以读到未提交的数据但是不允许出现脏写问题。

最终经过这一系列的讲解后能够得知MySQL-RU级别的实现原理即写操作加排他锁读操作不加锁

2RC(Read Committed读已提交级别的实现

理解了RU级别的实现后再来看看RCRC级别要求解决脏读问题也就是一个事务中不允许读另一个事务还未提交的数据咋实现呢?

写操作加排他锁读操作加共享锁

这样一想似乎好像没问题还是以之前的例子来说因为T1在修改数据所以会对ID=1的数据加上排他锁此时T2想要获取共享锁读数据时T1的排他锁就会排斥T2因此T2需要等到T1事务结束后才能读数据。

因为T2需要等待T1结束后才能读既然T1都结束了那也就代表着T1事务要么回滚了T2读上一个事务提交的数据;要么T1提交了T2T1提交的数据总之T2读到的数据绝对是提交过的数据。

这种方式的确能解决脏读问题但似乎也会将所有并发事务串行化会导致MySQL整体性能下降因此MySQL引入了一种技术在每次select查询数据时都会生成一个ReadView快照然后依据这个快照去选择一个可读的数据版本。

因此对于RC级别的底层实现对于写操作会加排他锁而读操作会使用MVCC机制。

但由于每次select时都会生成ReadView快照此时就会出现下述问题

-- ①T1事务中先读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

-- ②T2事务中修改 ID=1 的姓名为 竹子 并提交
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
commit;

-- ③T1事务中再读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 竹子      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+
复制代码

此时观察这个案例明明是在一个事务中查询同一条数据结果两次查询的结果并不一致这也是所谓的不可重复读的问题。

3RR(Repeatable Read可重复读级别的实现

RC级别中虽然解决了脏读问题但依旧存在不可重复读问题而RR级别中就是要确保一个事务中的多次读取结果一致即解决不可重复读问题咋解决呢?两种方案

  • ①查询时对目标数据加上临键锁即读操作执行时不允许其他事务改动数据。
  • MVCC机制的优化版一个事务中只生成一次ReadView快照。

相较于第一种方案第二种方案显然性能会更好因为第一种方案不允许读-写、写-读事务共存而第二种方案则支持读写事务并行执行咋做到的呢?其实也比较简单

写操作加排他锁对读操作依旧采用MVCC机制但RR级别中一个事务中只有首次select会生成ReadView快照。

-- ①T1事务中先读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

-- ②T2事务中修改 ID=1 的姓名为 竹子 并提交
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
commit;

-- ③T1事务中再读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 竹子      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+
复制代码

还是以这个场景为例在RC级别中会对于T1事务的每次SELECT都生成快照因此当T1第二次查询时生成的快照中就能看到T2修改后提交的数据。但在RR级别中只有首次SELECT会生成快照当第二次SELECT操作出现时依旧会基于第一次生成的快照查询所以就能确保同一个事务中每次看到的数据都是相同的。

也正是由于RR级别中一个事务仅有首次select会生成快照所以不仅仅解决了不可重复读问题还解决了幻读问题举个例子

-- 先查询一次用户表看看整张表的数据
SELECT * FROM `zz_users`;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      || 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      || 4321     | 2022-09-16 07:42:21 |
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+

-- ①T1事务中先查询所有 ID>=4 的用户信息
SELECT * FROM `zz_users` WHERE user_id >= 4;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+
-- ②T1事务中再将所有 ID>=4 的用户密码重置为 1111
UPDATE `zz_users` SET password = "1111" WHERE user_id >= 4;

-- ③T2事务中插入一条 ID=6 的用户数据
INSERT INTO `zz_users` VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33");
-- ④提交事务T2
commit;

-- ⑤T1事务中再次查询所有 ID>=4 的用户信息
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       4 | 猫熊      || 1111     | 2022-09-27 17:22:59 |
|       6 | 棕熊      || 7777     | 2022-10-02 16:21:33 |
|       9 | 黑竹      || 1111     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+
复制代码

此时会发现明明T1中已经将所有ID>=4的用户密码重置为1111了结果改完再次查询会发现表中依旧存在一条ID>=4的数据棕熊而且密码未被重置这似乎产生了幻觉一样。

如果是RC级别因为每次select都会生成快照因此会出现这个幻读问题但RR级别中因为只有首次查询会生成ReadView快照因此上述案例放在RR级别的MySQLT1看不到T2新增的数据因此MySQL-RR级别也解决了幻读问题。

4Serializable序列化级别的实现

前面已经将RU、RC、RR三个级别的实现原理弄懂了最后再来看看最高的Serializable级别在这个级别中要求解决所有可能会因并发事务引发的问题那怎么做呢?比较简单

所有写操作加临键锁(具备互斥特性所有读操作加共享锁。

由于所有写操作在执行时都会获取临键锁所以写-写、读-写、写-读这类并发场景都会互斥而由于读操作加的是共享锁因此在Serializable级别中只有读-读场景可以并发执行。

五事务与锁机制原理篇总结

在本章中结合事务、锁、MVCC机制三者的知识点彻底理清楚了MySQL不同隔离级别下的实现最后做个简单的小总结

  • RU级别读操作不加锁写操作加排他锁。
  • RC级别读操作使用MVCC机制每次SELECT生成快照写操作加排他锁。
  • RR级别读操作使用MVCC机制首次SELECT生成快照写操作加临键锁。
  • 序列化级别读操作加共享锁写操作加临键锁。
级别/场景读-读读-写/写-读写-写
RU级别并行执行并行执行串行执行
RC级别并行执行并行执行串行执行
RR级别并行执行并行执行串行执行
序列化级别并行执行串行执行串行执行
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: mysql