专栏首页Java识堂MySQL事务的隔离性是如何实现的?

MySQL事务的隔离性是如何实现的?

并发场景

最近做了一些分布式事务的项目,对事务的隔离性有了更深的认识,后续写文章聊分布式事务。今天就复盘一下单机事务的隔离性是如何实现的?

「隔离的本质就是控制并发」,如果SQL语句就是串行执行的。那么数据库的四大特性中就不会有隔离性这个概念了,也就不会有脏读,不可重复读,幻读等各种问题了

「对数据库的各种并发操作,只有如下四种,写写,读读,读写和写读」

写-写

事务A更新一条记录的时候,事务B能同时更新同一条记录吗?

答案肯定是不能的,不然就会造成「脏写」问题,那如何避免脏写呢?答案就是「加锁」

读-读

MySQL读操作默认情况下不会加锁,所以可以并行的读

读-写 和 写-读

「基于各种场景对并发操作容忍程度不同,MySQL就搞了个隔离性的概念」。你自己根据业务场景选择隔离级别。

√ 为会发生,×为不会发生

隔离级别

脏读

不可重复读

幻读

read uncommitted(未提交读)

read committed(提交读)

×

repeatable read(可重复读)

×

×

serializable (可串行化)

×

×

×

「所以你看,MySQL是通过锁和隔离级别对MySQL进行并发控制的」

MySQL中的锁

行级锁

InnoDB存储引擎中有如下两种类型的行级锁

  1. 「共享锁」(Shared Lock,简称S锁),在事务需要读取一条记录时,需要先获取改记录的S锁
  2. 「排他锁」(Exclusive Lock,简称X锁),在事务要改动一条记录时,需要先获取该记录的X锁

如果事务T1获取了一条记录的S锁之后,事务T2也要访问这条记录。如果事务T2想再获取这个记录的S锁,可以成功,这种情况称为锁兼容,如果事务T2想再获取这个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉

如果事务T1获取了一条记录的X锁之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务1提交,这种情况称为锁不兼容。

「多个事务可以同时读取记录,即共享锁之间不互斥,但共享锁会阻塞排他锁。排他锁之间互斥」

S锁和X锁之间的兼容关系如下

兼容性

X锁

S锁

X锁

互斥

互斥

S锁

互斥

兼容

「update,delete,insert 都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁」

那什么情况下会对读操作加锁呢?

  1. select .. lock in share mode,对读取的记录加S锁
  2. select ... for update ,对读取的记录加X锁
  3. 在事务中读取记录,对读取的记录加S锁
  4. 事务隔离级别在 SERIALIZABLE 下,对读取的记录加S锁

「InnoDB中有如下三种锁」

  1. Record Lock:对单个记录加锁
  2. Gap Lock:间隙锁,锁住记录前面的间隙,不允许插入记录
  3. Next-key Lock:同时锁住数据和数据前面的间隙,即数据和数据前面的间隙都不允许插入记录

写个Demo演示一下

CREATE TABLE `girl` (
  `id` int(11) NOT NULL,
  `name` varchar(255),
  `age` int(11),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into girl values
(1, '西施', 20),
(5, '王昭君', 23),
(8, '貂蝉', 25),
(10, '杨玉环', 26),
(12, '陈圆圆', 20);

Record Lock

「对单个记录加锁」

如把id值为8的数据加一个Record Lock,示意图如下

Record Lock也是有S锁和X锁之分的,兼容性和之前描述的一样。

SQL执行加什么样的锁受很多条件的制约,比如事务的隔离级别,执行时使用的索引(如,聚集索引,非聚集索引等),因此就不详细分析了,举几个简单的例子。

-- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主键进行等值查询
-- 对id=8的记录加S型Record Lock
select * from girl where id = 8 lock in share mode;

-- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主键进行等值查询
-- 对id=8的记录加X型Record Lock
select * from girl where id = 8 for update;

Gap Lock

「锁住记录前面的间隙,不允许插入记录」

「MySQL在可重复读隔离级别下可以通过MVCC和加锁来解决幻读问题」

当前读:加锁

快照读:MVCC

但是该如何加锁呢?因为第一次执行读取操作的时候,这些幻影记录并不存在,我们没有办法加Record Lock,此时可以通过加Gap Lock解决,即对间隙加锁。

如一个事务对id=8的记录加间隙锁,则意味着不允许别的事务在id=8的记录前面的间隙插入新记录,即id值在(5, 8)这个区间内的记录是不允许立即插入的。直到加间隙锁的事务提交后,id值在(5, 8)这个区间中的记录才可以被提交

我们来看如下一个SQL的加锁过程

-- REPEATABLE READ 利用主键进行等值查询
-- 但是主键值并不存在
-- 对id=8的聚集索引记录加Gap Lock
SELECT * FROM girl WHERE id = 7 LOCK IN SHARE MODE;

由于id=7的记录不存在,为了禁止幻读现象(避免在同一事务下执行相同的语句得到的结果集中有id=7的记录),所以在当前事务提交前我们要预防别的事务插入id=7的记录,此时在id=8的记录上加一个Gap Lock即可,即不允许别的事务插入id值在(5, 8)这个区间的新记录

「给大家提一个问题,Gap Lock只能锁定记录前面的间隙,那么最后一条记录后面的间隙该怎么锁定?」

其实mysql数据是存在页中的,每个页有2个伪记录

  1. Infimum记录,表示该页面中最小的记录
  2. upremum记录,表示该页面中最大的记录

为了防止其它事务插入id值在(12, +∞)这个区间的记录,我们可以给id=12记录所在页面的Supremum记录加上一个gap锁,此时就可以阻止其他事务插入id值在(12, +∞)这个区间的新记录

Next-key Lock

「同时锁住数据和数据前面的间隙,即数据和数据前面的间隙都不允许插入记录」所以你可以这样理解Next-key Lock=Record Lock+Gap Lock

-- REPEATABLE READ 利用主键进行范围查询
-- 对id=8的聚集索引记录加S型Record Lock
-- 对id>8的所有聚集索引记录加S型Next-key Lock(包括Supremum伪记录)
SELECT * FROM girl WHERE id >= 8 LOCK IN SHARE MODE;

因为要解决幻读的问题,所以需要禁别的事务插入id>=8的记录,所以

  • 对id=8的聚集索引记录加S型Record Lock
  • 对id>8的所有聚集索引记录加S型Next-key Lock(包括Supremum伪记录)

表级锁

「表锁也有S锁和X锁之分」

在对某个表执行select,insert,update,delete语句时,innodb存储引擎是不会为这个表添加表级别的S锁或者X锁。

在对表执行一些诸如ALTER TABLE,DROP TABLE这类的DDL语句时,会对这个表加X锁,因此其他事务对这个表执行诸如SELECT INSERT UPDATE DELETE的语句会发生阻塞

在系统变量autocommit=0,innodb_table_locks = 1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁,可以这么写

对表t加表级别的S锁

lock tables t read

对表t加表级别的X锁

lock tables t write

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

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

「如果一个事务给表加了X锁,那么」

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

「所以修改线上的表时一定要小心,因为会使大量事务阻塞」,目前有很多成熟的修改线上表的方法,不再赘述

隔离级别

读未提交:每次读取最新的记录,没有做特殊处理 串行化:事务串行执行,不会产生并发

所以我们重点关注「读已提交」「可重复读」的隔离实现!

「这两种隔离级别是通过MVCC(多版本并发控制)来实现的,本质就是MySQL通过undolog存储了多个版本的历史数据,根据规则读取某一历史版本的数据,这样就可以在无锁的情况下实现读写并行,提高数据库性能」

「那么undolog是如何存储修改前的记录?」

「对于使用InnoDB存储引擎的表来说,聚集索引记录中都包含下面2个必要的隐藏列」

「trx_id」:一个事务每次对某条聚集索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列

「roll_pointer」:每次对某条聚集索引记录进行改动时,都会把旧的版本写入undo日志中。这个隐藏列就相当于一个指针,通过他找到该记录修改前的信息

如果一个记录的name从貂蝉被依次改为王昭君,西施,会有如下的记录,多个记录构成了一个版本链

「为了判断版本链中哪个版本对当前事务是可见的,MySQL设计出了ReadView的概念」。4个重要的内容如下

「m_ids」:在生成ReadView时,当前系统中活跃的事务id列表「min_trx_id」:在生成ReadView时,当前系统中活跃的最小的事务id,也就是m_ids中的最小值「max_trx_id」:在生成ReadView时,系统应该分配给下一个事务的事务id值「creator_trx_id」:生成该ReadView的事务的事务id

当对表中的记录进行改动时,执行insert,delete,update这些语句时,才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0。

max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比如现在有事务id为1,2,3这三个事务,之后事务id为3的事务提交了,当有一个新的事务生成ReadView时,m_ids的值就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4

执行过程如下:

  1. 如果被访问版本的trx_id=creator_id,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
  2. 如果被访问版本的trx_id<min_trx_id,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
  3. 被访问版本的trx_id>=max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,该版本不可以被当前事务访问
  4. 被访问版本的trx_id是否在m_ids列表中 4.1 是,创建ReadView时,该版本还是活跃的,该版本不可以被访问。顺着版本链找下一个版本的数据,继续执行上面的步骤判断可见性,如果最后一个版本还不可见,意味着记录对当前事务完全不可见 4.2 否,创建ReadView时,生成该版本的事务已经被提交,该版本可以被访问

「好了,我们知道了版本可见性的获取规则,那么是怎么实现读已提交和可重复读的呢?」

其实很简单,就是生成ReadView的时机不同

举个例子,先建立如下表

CREATE TABLE `girl` (
  `id` int(11) NOT NULL,
  `name` varchar(255),
  `age` int(11),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Read Committed

「Read Committed(读已提交),每次读取数据前都生成一个ReadView」

下面是3个事务执行的过程,一行代表一个时间点

「先分析一下5这个时间点select的执行过程」

  1. 系统中有两个事务id分别为100,200的事务正在执行
  2. 执行select语句时生成一个ReadView,mids=[100,200],min_trx_id=100,max_trx_id=201,creator_trx_id=0(select这个事务没有执行更改操作,事务id默认为0)
  3. 最新版本的name列为西施,该版本trx_id值为100,在mids列表中,不符合可见性要求,根据roll_pointer跳到下一个版本
  4. 下一个版本的name列王昭君,该版本的trx_id值为100,也在mids列表内,因此也不符合要求,继续跳到下一个版本
  5. 下一个版本的name列为貂蝉,该版本的trx_id值为10,小于min_trx_id,因此最后返回的name值为貂蝉

「再分析一下8这个时间点select的执行过程」

  1. 系统中有一个事务id为200的事务正在执行(事务id为100的事务已经提交)
  2. 执行select语句时生成一个ReadView,mids=[200],min_trx_id=200,max_trx_id=201,creator_trx_id=0
  3. 最新版本的name列为杨玉环,该版本trx_id值为200,在mids列表中,不符合可见性要求,根据roll_pointer跳到下一个版本
  4. 下一个版本的name列为西施,该版本的trx_id值为100,小于min_trx_id,因此最后返回的name值为西施

当事务id为200的事务提交时,查询得到的name列为杨玉环。

Repeatable Read

「Repeatable Read(可重复读),在第一次读取数据时生成一个ReadView」

可重复读因为只在第一次读取数据的时候生成ReadView,所以每次读到的是相同的版本,即name值一直为貂蝉,具体的过程上面已经演示了两遍了,我这里就不重复演示了,相信你一定会自己分析了。

文章分享自微信公众号:
Java识堂

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:李立敏
原始发表时间:2021-09-07
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • MySQL事务的隔离性是如何实现的?

    最近做了一些分布式事务的项目,对事务的隔离性有了更深的认识,后续写文章聊分布式事务。今天就复盘一下单机事务的隔离性是如何实现的?

    捡田螺的小男孩
  • MySQL是怎么实现事务隔离的?

    一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它会被锁住。既然进入等待状态,那么等到这个事务自己获取到行锁要更新数据时,它读到的值又是什么呢?

    JavaEdge
  • 事务的隔离性

    事务的四大特性为原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),本篇专门说说隔离性...

    你的益达
  • MySQL事务与MVCC如何实现的隔离级别

    其实数据库章节基本上的知识点我都写过一遍了,包括这篇事务和MVCC的,但是国庆期间我翻阅资料的时候我发现之前写的还差点意思,例子举得也差点意思,那我就根据我自己...

    敖丙
  • MySQL事务(二)事务隔离的实现原理:一致性读

    今天我们来学习一下MySQL的事务隔离是如何实现的。如果你对事务以及事务隔离级别还不太了解的话,这里左转。

    lbyxiaolizi
  • MySQL是如何实现事务ACID的?

    事务:一个最小的不可再分的工作单元;一个事务通常对应一个完整的业务,例如银行账户转账业务,该业务就是一个最小的工作单元

    你呀不牛
  • MySQL是如何实现事务的ACID

    最近在面试,有被问到,MySQL的InnoDB引擎是如何实现事务的,又或者说是如何实现ACID这几个特性的,当时没有答好,所以自己总结出来,记录一下。

    纪莫
  • MySQL是如何实现事务ACID的?

    事务:一个最小的不可再分的工作单元;一个事务通常对应一个完整的业务,例如银行账户转账业务,该业务就是一个最小的工作单元

    用户1483438
  • 讲讲MySQL隔离性的实现|MVCC

    MySQL有ACID四大特性,本文着重讲解MySQL不同事务之间的隔离性的概念,以及MySQL如何实现隔离性。下面先罗列一下MySQL的四种事务隔离级别,以及不...

    白泽z
  • MySQL innoDB的事务隔离

    疑问: 那读提交和可重复读有什么区别吗? 是的,我也有这个疑问,读提交和可重复读不都是在提交后对其他事务可见。确实是这样 但是读提交在另一个事务提交后再去读取...

    袁新栋-jeff.yuan
  • MySQL的事务隔离级别

    在使用一些持久层框架时,同一个事务多次查询结果一致是因为缓存,而非可重复读的体现。

    砒霜拌辣椒
  • MySQL基础篇7 mysql的事务到底是隔离的还是不隔离的

    如果是可重复读级别. 事务T启动的时候会创建一个视图read-view. 之后事务T之星期间, 即使有其他事务修改了数据, 事务T看到的仍然跟在启动时候看到的一...

    历久尝新
  • MySQL 事务的隔离级别

    读未提交 可能会导致 脏读、不可重复读、幻读 读提交 可能会导致 不可重复读、幻读 可重读 可能会导致 幻读

    lin_zone
  • Mysql的事务的隔离级别

    mysql事务,依次为Read uncommitted(读未提交) 、Read committed(读已提交) 、Repeatable read(可重复读) 、...

    用户6182664
  • Docker是如何实现隔离的

    容器化技术在当前云计算、微服务等体系下大行其道,而 Docker 便是容器化技术的典型,对于容器化典型的技术,我们有必要弄懂它,所以这篇文章,我会来分析下 Do...

    xcbeyond
  • 几种事务的隔离级别,InnoDB如何实现?

    隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。

    江湖前辈黄药师
  • 浅谈MySQL的事务隔离级别

    推荐阅读 微服务: springboot系列教程学习 源码:Javaweb练手项目源码下载 调优:十五篇好文回顾 面试笔试:面试笔试整理系列 希望这篇文章能够阐...

    企鹅号小编
  • 谈谈MySQL的事务隔离级别

    这篇文章能够阐述清楚跟数据库相关的四个概念:事务、数据库读现象、隔离级别、锁机制  

    吴生

扫码关注腾讯云开发者

领取腾讯云代金券