文章目录

  • 一、前言
  • 二、MVCC 多版本并发控制
  • 2.1 MVCC实现的宏观效果:什么能读取到,什么不能读取到(底层由readview一致性视图支持)
  • 2.2 MVCC的底层支持,InnoDB为每个表提供了三个隐藏的字段以及事务id和删除版本号的使用
  • 2.2.1 行记录三个隐藏字段的结构
  • 2.2.2 行记录三个隐藏字段的应用
  • 2.3 MVCC底层原理是read view,一致性视图
  • 2.3.1 readview一致性视图的结构
  • 2.3.2 readview一致性视图的应用
  • 2.4 MVCC的局限
  • 三、LBCC 基于锁的并发控制
  • 3.1 从表锁到行锁
  • 3.1.1 从表锁到行锁
  • 3.1.2 表锁
  • 3.1.2 行锁
  • 3.2 四种基本锁(读共享锁+写独占锁+意向读共享锁+意向写独占锁)
  • 3.2.1 读共享锁
  • 3.2.2 写独占锁
  • 3.2.3 意向读共享锁(表锁) + 意向写独占锁(表锁)
  • 3.2.4 两个行锁锁住的实际是一行记录的索引
  • 3.3 三种高级锁
  • 3.3.1 记录锁 lock record (锁住命中的行记录)
  • 3.3.2 间隙锁 gap record (锁住命中的范围)
  • 3.3.3 临键锁 nextkey lock
  • 3.4 四种隔离级别,哪种mvcc,哪种加锁
  • 四、尾声


一、前言

事务的目的是要实现 读一致性 ,有两种方法:加锁(基于锁的并发控制 LBCC) 和 多版本并发控制MVCC,加锁就是四种隔离级别对应的锁。

基于锁的并发控制 LBCC:读取数据之前,对其加锁,阻止其他事务对数据进行修改;
多版本并发控制 MVCC: 生成一个数据请求时间点的一致性快照Snapshot,并用这个快照提供一定级别(语句级或事务级)的一致性读取。

二、MVCC 多版本并发控制

2.1 MVCC实现的宏观效果:什么能读取到,什么不能读取到(底层由readview一致性视图支持)

MVCC 多版本并发控制的效果:建立起一个快照,同一个事务无论查多少次都是同样的数据。

一个事务能看到的数据版本:
(1)在本次事务,第一次查询之前已经提交的事务的修改(事务ID比当前事务ID小,当前事务就可以看到)
(2)本次事务的修改(当前事务自己的修改,自己当然能看到);

一个事务不能看到的数据版本:
(1)在本次事务,第一次查询之后提交的事务的修改(事务ID比当前事务ID大,当前事务无法看到)
(2)活跃的事务的修改 / 未提交的事务的修改(未提交的事务的修改,自然没有被记录下来,自然看不到);

MVCC步骤如下:第一次查询就确定了快照版本,
能读取到,快照建立后创建的事务已提交的修改,不能读取到,快照建立后创建的事务
能读取到,当前自己事务的修改(包括未提交的),不能读取到,其他事务未提交的修改;

2.2 MVCC的底层支持,InnoDB为每个表提供了三个隐藏的字段以及事务id和删除版本号的使用

2.2.1 行记录三个隐藏字段的结构

InnoDB为每个表提供了三个隐藏的字段,只要是innodb存储引擎的表,每个行记录都有这三个字段。

rowid 行标志:这个字段用户索引,任何表都有且仅有一个聚集索引,在innodb存储引擎中,索引即数据,数据即索引,聚集索引是一个 B+树,表示 表数据的物理存储方式。那由什么来充当这个聚集索引呢?有主键,就用主键索引作为聚集索引,没有主键,就用唯一索引作为聚集索引,若没有主键,没有唯一索引,或者说没有任何索引,就用隐藏的rowid作为聚集索引。

trxid: 事务id,这个字段用于事务隔离级别的MVCC, insert或update会更新行记录的这个版本号。
rollPtr: 回滚指针,删除版本号,delete会更新这个行记录的版本号。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据

2.2.2 行记录三个隐藏字段的应用

现在,让我们通过一个图来理解事务id和删除版本号

navicat打开一个会话,Transaction 1 插入两条数据,

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_02


Transaction 2 表示第一次查询就确定了快照版本,

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_03

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_04

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_05


执行顺序为,

Transaction 1 插入两条数据

Transaction 2 查询

Transaction 3 新插入的第三条数据

Transaction 2 查询

Transaction 4 删除第二条数据

Transaction 2 查询

Transaction 5 修改第一条数据

Transaction 2 查询

Tranaction 2 第一次查询确定了快照版本之后,后面的无论 Transaction 3 新插入的第三条数据、 Transaction 4 删除第二条数据、Transaction 5 修改第一条数据,都不会影响查询结果,因为查找已经建立了,只能看到Transaction 1最初插入的两条数据。

2.3 MVCC底层原理是read view,一致性视图

MVCC底层原理是read view,一致性视图,这个read view决定当前事务中,哪两种可以访问,按两种不可访问。

对于MVCC,不同的表类型生成 read view 的时机有不同,

RR repeat read 可重复读: read view 是在事务第一次查询的时候建立(之后 read view 就不会变了);
RC read commit 已提交读: read view 是在事务每次查询的时候建立(每次查询都重新生成一个 read view );

所以说,只有repeat read利用mvcc解决了幻读问题(update delete insert都无法影响到)。

2.3.1 readview一致性视图的结构

read view(一致性试图)结构,四个字段,一个readview里面有m_ids,存放多个事务id

m_ids是一个列表,可以理解为一个数组,里面存放的是 未提交的事务id
min_trx_id 表示在 m_ids 数组里面最小的未提交的事务id
max_trx_id 表示在 m_ids 数组里面最大的未提交的事务id,也就是系统要分配的下一个事务的id
creator_trx_id 表示生成这个readview的事务的事务id

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_06

2.3.2 readview一致性视图的应用

对于某个事务trx_id,如果这个trx_id=creator_trx_id,表示这个readview就是这个trx_id创建的,自然可以访问这个trx_id

对于某个事务trx_id,如果这个trx_id<min_trx_id,表示这个trx_id 比最小的未提交的事务id还小,表示这个事务id是已提交的,自然可以访问这个trx_id

对于某个事务trx_id,如果这个trx_id>max_trx_id,表示这个trx_id 比最大的未提交的事务id还大,表示这个事务id是未提交的,自然不可以访问这个trx_id

对于某个事务trx_id,如果这个max_trx_id > trx_id>min_trx_id,表示这个 trx_id 比在这个范围内,如果在 m_ids ,表示未提交,不可访问,如果不在 m_ids ,表示已提交,可以访问

mysql事务隔离 mvcc mysql事务隔离怎么实现的_共享锁_07


第一条,本事务,create view 第四个

第二条,前面是的事务,create view 第二个

第三条,后面的事务,create view 第三个

第四条,活跃(未提交)就不可以读取,create 第一个

2.4 MVCC的局限

MVCC(Multi-Version Concurrent Control):多版本并发控制,只作用于RC和RR隔离级别。

repeat read特点:只保存第一次查询的视图;
repeat read优点:解决了幻读问题(第一次生成视图之后,不受 update/insert/delete 影响);
repeat read缺点:不能查询到最新的实时数据。

MVCC的局限:对于RR repeat read 可重复读,只能保存第一次查询的视图,不能查询到最新的实时数据,要想实时查询到最新的实时数据,只能 LBCC ,就是加锁。

三、LBCC 基于锁的并发控制

从锁到事务的关系(锁 -> 读一致性 -> 事务隔离 -> 并发数据安全):锁是读一致性的一种实现方案(另一种实现方案是MVCC),读一致性能够实现事务隔离(实现了事务隔离就是提供了四种不同事务隔离级别),事务隔离级别是事务的一大特性(保证并发下数据的安全性)。

通过数据库资源的不同粒度的划分,来阐述隔离性不同级别的实现。隔离级别的底层实现方式就是:数据库锁的不同级别导致资源互斥粒度的不同,依次是:表级锁,行级锁,读写分离锁,不加锁。

3.1 从表锁到行锁

3.1.1 从表锁到行锁

从表锁到行锁:MyISAM仅仅支持表锁,InnoDB支持表锁和行锁,行锁可以在保证数据读一致性的同时,带来更高的并发性能,这是InnoDB的进步。

表锁和行锁的区别:表锁粒度大,行锁效率高,行锁冲突概率小,行锁并发性能强,所以行锁优于表锁。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_08

3.1.2 表锁

表锁就是直接锁表

lock tables 表名 read;

lock tables 表名 write;

unlock tables;

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据库_09

3.1.2 行锁

下面都是行级锁,因为只有行级锁才是InnoDB的精髓,在保证 读一致性 下实现了比较高的效率。

InnoDB行锁的分类(8类):

基本锁:读共享锁、写独占锁、意向锁

锁的算法(在什么情况下锁定什么范围):记录锁、间隙锁、临键锁

不重要:插入意向锁、自增锁、

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_10


mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_11

3.2 四种基本锁(读共享锁+写独占锁+意向读共享锁+意向写独占锁)

3.2.1 读共享锁

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_12


目的:自己事务读的时候保证多次读取,实现读到一致性。

读共享锁加锁之后, select * from tablename where id = 1 lock in share mode
(1) 其他事务:不能修改,但是可以读
(2) 本事务:可以读,但是自己也不能修改
(3) 自己事务加上读共享锁之后,其他事务可以在同一行都加上 读共享锁 ,反正大家(加了读锁的事务、美甲读锁的事务)都不能修改, 但是其他事务不能在该行记录上加 写独占锁。

读共享锁释放: commit/rollback

和其他行级锁相同,如果 navicat 会话连接断开,锁释放。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_java_13

3.2.2 写独占锁

目的:自己加上了别人不能再加上读共享锁、也不能再加上写独占锁。别的事务无法读这个行记录,更无法写这个行记录,别的事务执行select或者udpate/insert/delete,等待50s,无法成功,就会失败。

自己加上了写独占锁之后,insert / update / delete / select … for udpate
(1 )当前事务既然可写,也可读。
(2) 其他事务无法读这个行记录,更无法写这个行记录
(3) 自己加上了别人不能再加上读共享锁、也不能再加上写独占锁

释放锁:
commit/rollback
和其他行级锁相同,如果 navicat 会话连接断开,锁释放。

什么时候加写独占锁?
1、写insert update delete,默认加上了写独占锁,只有当前事务可读可写(但是select默认不加上写锁,也默认不加上读锁)。
2、对于select,写成 select … for udpate,就加上了写独占锁。写成 select … lock in share mode,就加上了读共享锁。

3.2.3 意向读共享锁(表锁) + 意向写独占锁(表锁)

问题:这两个表锁(意向共享锁、意向排他锁)存在的意义是什么?

一个事务给一行数据加上锁(读共享锁、写独占锁),如果另一个事务想来加锁,必须全表扫描,这样效率太慢。所以,Mysql设计

如果一个事务给一行数据,加上一个读共享锁(select * from tablename where id =1 lock in share mode ),那么先给这个表加上一个意向共享锁。
如果一个事务给一行数据,加上一个写独占锁( insert / update / delete / select … for udpate ),那么先给这个表加上一个意向独占锁。
这样设计的理由:一个事务可以给一张表加表锁的前提是:没有任何一个事务给这张表的任何一个记录加行锁。即 如果现在事务B想来给mysql的 tablename 加锁,mysql会给这个表加对应的 意向锁表锁,但是如果 此时 事务A 已经给这个表的某行记录 加上了 行锁,这个 意向锁表锁 就无法加上去,事务B这个行锁也就无法加上去。

这样一来,每个事务加行锁之前,只要看一下这个表上有没有意向读共享锁和意向写排他锁,只要有,自己就不加锁了,从来提高判断效率。

所以,这两个表锁(意向共享锁、意向排他锁)大大的提高了不同事务给表加行锁的效率,这就是这两个表锁存在的最大意义。

意向锁由数据引擎自己维护,程序员无法手动操作意向锁,与程序员无关,是mysql加快LBCC的一种方式。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_14

3.2.4 两个行锁锁住的实际是一行记录的索引

读共享锁,一个事务给一行数据加锁(读共享锁),另一个事务还可以给这行数据加锁(读共享锁,但是不能加写独占锁);
写排他锁,一个事务给一行数据加锁(写排他锁),另一个事务不可以给这行数据加锁(既不能加读共享锁,也不能加写独占锁)。

底层是怎么实现的?所谓的共享锁和排他锁锁住的是一行数据的什么东西呢?实际上,锁住的是索引,普通索引也行,唯一索引也行、主键索引也行。

在INNODB_LOCKS

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_15

在一张表中,如果没有设置索引,就会有InnoDB中隐藏列rowid作为聚集索引,此时,会锁住整个表(这就是上文中隐藏字段的第一个字段的 rowid 的使用)。

对于程序员给表加上行锁(读共享锁 写独占锁),
如果存在索引,锁住的索引,如果where条件中有索引列,仅锁住where条件命中的这一行或多行数据;如果where条件中没有索引列 或者 没有根本就没有where子句,会锁住整个表(连插入都插入不进去了 解决幻读问题)。
如果不存在索引,锁住的隐藏的rowid,此时会锁住整个表(连插入都插入不进去了 解决幻读问题)。

3.3 三种高级锁

什么情况下锁住什么范围?先理解记录record、间隙gap(不包含记录)、临键(左开右闭 gap+record)三个概念

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_16

对于索引列,无论是整数还是字符,都可以排序。

3.3.1 记录锁 lock record (锁住命中的行记录)

对于唯一性索引(唯一索引、主键索引),如果where条件命中数据行,仅仅锁住单个记录,此时使用记录锁,包括行读共享锁、行写排他锁。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据_17

3.3.2 间隙锁 gap record (锁住命中的范围)

如果where条件没有命中数据库中的数据行,即where条件在数据库中找不到数据,比如:

先用where id>4 and id<7加锁,第二个事务还可以用where id=6加锁,不冲突,但是如果insert id=5,会失败。

如果用where id >20 会锁住 (10,正无穷大) ,如果insert id = 11 ,会失败。

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_18

间隙锁和间隙锁本身不冲突,因为间隙锁的设计是专门用来阻塞插入的,也只有在InnoDB的可重复读隔离级别中才会有间隙锁,在Innodb的未提交读、已提交读没有间隙锁,这就是为什么使用可重复可以避免幻读错误。

3.3.3 临键锁 nextkey lock

临键锁和间隙锁唯二区别是锁住的空间是右边闭区间,还有就是因为临键锁是间隙锁和记录锁的合并。所以,临键锁和临键锁是冲突的,如果第二个事务加锁到记录锁上面,不能成功,for update 写锁是独占的,如果第二个事务加锁…

如果在第一个事务

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据库_19

3.4 四种隔离级别,哪种mvcc,哪种加锁

mvcc只有在RC RR中有,

在read uncommited啥都没有,没有锁,没有mvcc,

在rc里面,有mvcc,有锁,只有记录锁,没有间隙锁,所以无法解决幻读问题

在rr里面,有mvcc,有锁,有记录锁,有间隙锁,可以解决幻读问题

在serializable,只有锁,没有mvcc

mysql事务隔离 mvcc mysql事务隔离怎么实现的_mysql事务隔离 mvcc_20

mysql事务隔离 mvcc mysql事务隔离怎么实现的_数据库_21

四、尾声

事务隔离性的两种实现方式,要么使用 MVCC (Multi-Version Concurrency Control) 多版本并发控制,要么使用 LBCC(Lock-Base Concurrency Control)基于锁的并发控制,至于到底使用的哪种,就看程序员的sql语句是这样写的。

天天打码,天天进步!!