事务与MVCC
前言
关于事务,是一个很重要的知识点,大家在面试中也会被经常问到这个问题;
数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,**锁的应用最终导致不同事务的隔离级别 **;在上一篇文章中我们说到了数据库锁的一部分知识,知道了InnoDB是支持行锁的,但是走行锁是基于索引的;
这里我们会说一下和锁紧密相关的事务;
希望本文对大家有所帮助;
引入
本文参考文章:数据库的两大神器
事务和MVCC
数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,**锁的应用最终导致不同事务的隔离级别 **;
关于事务,大家也是比较熟悉的,在这里我们再来唠叨一下:
说到事务,就不得不提它的特性以及隔离级别了;
特性
事务具有四个特性:原子性、一致性、隔离性、持久性。这四个属性通常被称为ACID属性。
- 原子性(Atomicity) :事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
对于以上的四个特性,我们来拿经典的转账的例子来说明;
有A和B两个人,现在A需要往B的账户上转钱,一般的操作是这样:
- A账户需要读取账户余额(500);
- A需要给B转账100元,所以需要从A的账户上扣除100元(500 - 100);
- 把减去的结果写回A账户(400);
- B账户需要读取账户余额(500);
- 对B账户进行加的操作(500 + 100);
- 把结果写回B账户(600);
以上是转账的操作步骤,我们来说明一下事务的四大特性:
原子性
以上的六步操作要么全部执行,要么全部不执行。不管执行到那一步出现了问题,就需要执行回滚操作;
一致性
在转账之前,A和B的账户加一起500 + 500 = 1000 元,在转账之后A和B的账户加起来是400 + 600 = 1000。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态,需要保持一致性;
隔离性
在A向B转账的过程中,只要所处事务还没有提交,其他事务查询A或者B账户的时候,两个账户的金额都不会发生变化;
如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱;
持久性
一旦转账成功,事务提交,所做的修改就会永久的保存;
参考文章:https://www.hollischuang.com/archives/898
隔离级别
我们对于事务的隔离级别也是很清楚的,分为四种:
- Read uncommitted:未提交读
- 最低级别,会出现脏读、不可重复读、幻读。
- Read committed:已提交读
- 避免脏读,会出现不可重复读和幻读。
- Repeatable read:可重复读
- 避免脏读和不可重复读,会出现幻读(在MySQL实现的Repeatable read配合gap锁不会出现幻读!)。
- Serializable :串行化
- 避免脏读、不可重复读、幻读。
脏读
在Read uncommitted隔离级别下会出现脏读,我们先来看一下脏读;
脏读:一个事务读取到另一个事务未提交的数据的情况被称为脏读。
举例说明:
还是拿转账的例子作为说明。A向B转账,A执行了转账语句,但A还没有提交事务,B读取数据,发现自己账户钱变多了!B跟A说,我已经收到钱了。A回滚事务【rollback】,等B再查看账户的钱时,发现钱并没有多。
分析:
出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据。
解决(Read committed):
从上面的分析也能看出来,解决的方式就是把锁释放的位置放到事务提交之后 。这样的话,在事务还未提交之前,其他的事务对该数据是无法进行操作的,这也是Read committed
避免脏读的做法;
不可重复读
Read committed
虽然避免了脏读但是会出现不可重复读;
不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改 ;
举例说明:
事务A在读取一条数据,得到结果a,事务B把这条数据改成了b并提交了事务,这个时候事务A再次去读取这条数据,得到的结果是b。这样就发生了不可重复读;
分析:
Read committed
采用的是语句级别的快照!每次读取的都是当前最新的版本!
解决:
Repeatable read
避免不可重复读是事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。
这里涉及到了快照一词,我们需要说一下这个东西:
MVCC
MVCC(Multi-Version Concurrency Control):多版本并发控制 。通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本。一句话总结就是 同一份数据临时保留多版本的一种方式,进而实现并发控制 ;
快照有两个级别:
- 语句级
- 针对于
Read committed
隔离级别 - 事务级别
- 针对于
Repeatable read
隔离级别
InnoDB MVCC实现分析
InnoDB 的 MVCC, 是通过在每行记录后面保存两个隐藏的列来实现的, 这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值, 而是系统版本号 (可以理解为事务的 ID),每次开始一个新的事务,系统版本号就会自动递增;当删除一条数据的时候,该数据的删除时间列就会存上当前事务的版本号 ;事务开始时刻的系统版本号会作为事务的 ID;
下面看一下在 REPEATABLE READ 隔离级别下, MVCC 具体是如何操作的;
例子
首先创建一个表:
1CREATE TABLE `mvcc` (
2 `id` bigint(20) NOT NULL AUTO_INCREMENT,
3 `username` varchar(255) NOT NULL,
4 PRIMARY KEY (`id`)
5) ENGINE=InnoDB CHARSET=utf8;
假设系统版本号从1开始;
INSERT
InnoDB 为新插入的每一行保存当前系统版本号作为版本号,上面我们假设系统版本号从1开始;
1start transaction;
2INSERT INTO `mvcc`(username) VALUES ('tom'),('joey'),('James');
3commit;
得到如下结果(后面两列是隐藏的,通过查询语句看不到):
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | undefined |
2 | joey | 1 | undefined |
3 | james | 1 | undefined |
SELECT
InnoDB会根据以下两个条件检查每条记录:
- InnoDB只会查找版本早于当前事务版本的数据行(创建时间系统版本号小于或等于当前事务版本号),这样可以确保事务读取到的数据要么是本次事务开始之前就已经存在的,要么是当前事务本身做的修改;
- 行的删除版本要么是未定义,要么大于当前事务的版本号,这样确保了事务读取到的行,在事务开始之前未被删除;
以上两个条件同时满足的情况下,才能作为结果返回;
DELETE
InnoDB 会为删除的每一行保存当前系统的版本号 (事务的 ID) 作为删除标识;
具体例子:
第二个事务,系统版本号为2;
1start transaction;
2select * from mvcc; //step 1
3select * from mvcc; //step 2
4commit;
情况一
第三个事务,系统版本号为3;
1start transaction;
2INSERT INTO `mvcc`(username) VALUES ('yang');
3commit;
当我们执行step 1刚完毕,这个时候第三个事务往表中插入了一条数据,这个时候表中的数据如下:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | undefined |
2 | joey | 1 | undefined |
3 | james | 1 | undefined |
4 | yang | 3 | undefined |
然后step 2执行了,得到如下结果:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | undefined |
2 | joey | 1 | undefined |
3 | james | 1 | undefined |
大家可能会感到迷惑,第三个事务不是往里面插入了一条数据吗,怎么查不到。这个时候我们来说一下原因:
- id = 4是由事务三(系统版本为3)创建的,该数据的创建时间(事务ID)为3;
- 第二个事务的系统版本号是2,大家要记得我们上面说的查询的两个条件;
- InnoDB只会查找创建时间(事务ID)小于或等于当前事务的数据行;
- 查找删除时间(事务ID)列大于当前系统版本号的数据行;
- id = 4的数据的创建时间(事务ID)明显大于第二个事务的系统版本号,而且删除时间也是未定义的,所以第三个事务插入的数据未被检索;
情况二
第四个事务,系统版本为4:
1start transaction;
2delete from mvcc where id=1;
3commit;
当第二个事务执行了step 1,这个时候第三个事务的插入也执行完毕了,接着事务四开始执行,此时数据库的数据如下:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | 4 |
2 | joey | 1 | undefined |
3 | james | 1 | undefined |
4 | yang | 3 | undefined |
上面可以看出,当执行DELETE操作的时候,删除时间(事务ID)列会存上当前事务的系统版本号;
然后step 2执行了,得到如下结果:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | 4 |
2 | joey | 1 | undefined |
3 | james | 1 | undefined |
具体原因我就不说了(SELECT查询的两个条件);
UPDATE
InnoDB 执行 UPDATE,实际上是新插入的一行数据 ,并保存其创建时间(事务ID)为当前事务的系统版本号,同时保存当前事务系统版本号到需要UPDATE的行的删除时间(事务ID) ;
情况三
第五个事务,系统版本号为5:
1start transaction;
2update mvcc set name='jack' where id = 3;
3commit;
当执行完step 1,第三个的插入和第四个事务的删除都执行完毕并且提交,又有一个用户执行了第五个事务的更新操作,这个时候,数据库数据如下:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | 4 |
2 | joey | 1 | undefined |
3 | james | 1 | 5 |
4 | yang | 3 | undefined |
3 | jack | 5 | undefined |
然后我们执行step 2得到如下数据:
id | username | 创建时间 (事务 ID) | 删除时间 (事务 ID) |
---|---|---|---|
1 | tom | 1 | 4 |
2 | joey | 1 | undefined |
3 | james | 1 | 5 |
以上几种情况可以看出,不管咋样,查出的数据都是和第一次查询的数据一致,尽管其他事务做了各种修改操作,但是没有影响到第二个事务中的查询操作;
通过以上对MVCC的介绍,我想大家也明白了Repeatable read
避免不可重复读的方式;
参考文章:https://blog.csdn.net/whoamiyang/article/details/51901888
幻读
幻读:是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致 (幻读是事务非独立执行时发生的一种现象);
举例说明:
例如事务A对一个表中符合条件的一些数据做了从a修改为b的操作,这时事务B又对这个表中插入了符合A修改条件的一行数据项,而这个数据项的数值还是为a并且提交给数据库。而操作事务A的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务B中添加的,就好像产生幻觉一样,这就是发生了幻读。
解决:
但在MySQL实现的Repeatable read配合间隙锁不会出现幻读;
使用间隙锁锁住符合条件的部分,不允许插入符合条件的数据。
间隙锁
间隙锁:当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(间隙锁只会在Repeatable read
隔离级别下使用)。
InnoDB使用间隙锁的目的有两个:
- 为了防止幻读
- 满足恢复和复制的需要
- MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读 ;
总结
本文介绍了MySQL数据锁以及事务的一些知识点,下面我们来总结一下;
事务的四大特性:
- 原子性(Atomicity :事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
对于事务的隔离级别也是很清楚的,分为四种:
- Read uncommitted:未提交读
- 最低级别,会出现脏读、不可重复读、幻读。
- Read committed:已提交读
- 避免脏读,会出现不可重复读和幻读。
- Repeatable read:可重复读
- 避免脏读和不可重复读,会出现幻读(在MySQL实现的Repeatable read配合gap锁不会出现幻读!)。
- Serializable :串行化
- 避免脏读、不可重复读、幻读。
MVCC(Multi-Version Concurrency Control):多版本并发控制 ,一句话总结就是 同一份数据临时保留多版本的一种方式,进而实现并发控制 (上面也简单的演示了InnoDB MVCC的实现);
MVCC能够实现读写不阻塞 ;
快照有两个级别:
- 语句级
- 针对于
Read committed
隔离级别 - 事务级别
- 针对于
Repeatable read
隔离级别
Repeatable read
避免不可重复读是事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。
最后
本文简单的说了一下事务一块的东西,有问题的话还望大家指教,本人一定抱着虚心学习的态度。
大家共同学习,一起进步!