avatar

目录
MySQL事务隔离实验

相关背景

事务并发面临的问题

目前的数据库事务的并发面临着4个问题:丢失修改、读脏数据、不可重复读、幻影读。
现在假设有两个并发事务T1和T2

  1. 丢失修改:T1修改了某一条记录,然后T2也修改了这条记录,T1对数据的修改丢失。
  2. 读脏数据:T1修改了某一条记录但未提交,此时T2读取到了T1未提交的数据。
  3. 不可重复读:T2读取某一条记录,T1修改这条记录,T2再次读取这一条记录,发现在同一事务下数据不一致。
  4. 幻影读:T2查找某一条记录,发现没有,T1添加了T2要找的记录,T2再次查找,发现见鬼了,无缘无故多出了这条记录。

幻影读类似于不可重复读,但不可重复读强调修改,幻影读强调插入/删除

事务隔离级别

为了解决以上问题,SQL标准定义了以下事务隔离级别:

  1. 未提交读:T1未提交时T2就可以查看到T1修改的数据。
  2. 已提交读:T1提交后T2才能看到T1修改的数据。
  3. 可重复读:T1提交后T2也不能查看到T1修改的数据。
  4. 串行化:T1、T2必须严格按照先后顺序执行,即T1的所有操作在T2所有操作之前完成。

它们所能即解决的问题如下所示:

隔离级别 丢失修改 读脏数据 不可重复读 幻影读
未提交读 × × ×
已提交读 × ×
可重复读 ×
串行化

未提交读也已经解决了丢失修改的问题,如果事务T1要修改某条数据,那么T2若要修改这一条数据,则会被阻塞。阻塞方法涉及到读写锁。
在MySQL的InnoDB引擎中,默认的事务隔离级别是可重复读,但它也解决了幻影读的问题。

实验

在MySQL5.7版本,使用默认InnoDB引擎的情况下,可以分别通过以下指令来查看、修改当前会话事务隔离级别:
查看事务隔离级别:

sql
1
select @@transaction_isolation;

修改事务隔离级别:

sql
1
set session transaction isolation level __________;

横线上要填的是以上事务隔离级别的英文,分别为READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE,不区分大小写。

可以通过以下语句分别开启、回滚、提交一个事务:

sql
1
2
3
start transaction;
rollback;
commit;

接下来,暂不讨论串行化隔离级别,把事务隔离级别设置为以下,观察两个并发事务的表现。

未提交读

前面说到,未提交读只解决了丢失修改的问题,所以来试试
lost_modify.png
在T1和T2同时修改数据时,这个数据被锁住了,T1先修改,T2在T1提交之前就无法修改。此时T2发生阻塞,等待T1提交。
接下来测试以下读脏数据问题
read_uncommitted.png
T2提交修改的数据之前,T1就读取到了T2修改的数据,当T1发现异常需要回滚,那么T2此时拥有的就是一个错误的数据。

已提交读

T1先读取数据,T2修改,T1再读,发现数据没有被改变。但T2提交后,T1再去读,数据还是变了,存在不可重复读问题。
read_committed.png

可重复读

这里的测试流程和上面相似,唯一的变化就是解决了不可重复读问题,这也是MySQL5.7的默认事务隔离级别。
repeatable_read.png
同样地,它也避免了幻影读的问题
phantom_read.png

一些思考

还记得在学习关于事务的并发锁时,有读写锁这样一个概念。读锁在读取数据时给数据加上,写锁在对数据进行修改时加上。经过以上的实验,T2修改一个数据之后,确实会给数据加上一个写锁,T1要写只能等T2提交,这就解决了上述的丢失修改问题。
引起我思考的是以下这张表,当一个数据被加读锁或是写锁后,是否还能被加锁的情况:

T1已加的锁/T2可加的锁 读锁 写锁
读锁 ×
写锁 × ×

读锁又叫共享锁,因为读锁可以同时被多个并发事务加上;而写锁是独占锁,只能被一个事务独占。
显然,以上实验已经验证了写锁是互斥的,但是通过以上实验会发现,写并没有阻塞读,读也并没有阻塞写
这是什么原因呢?我查阅了相关资料,了解到了MVCC这样一种技术,它被广泛应用于已提交读和可重复读的事务隔离级别中。

MVCC

MVCC的全称是Multi-Version Concurrency Control(多版本并发控制),从表面上看上去,它主要在做一件事情:快照。所谓快照,意思是开启一个事务之前,先把需要修改或删除的数据备份一遍,如果要修改数据那就看这份备份就好了。
这样的好处就是:在读数据的时候不需要给数据加上读锁,看看上面对应的表格,去除掉读锁的行列,会发生阻塞的情况只有写-写阻塞,而读-读操作、读-写操作、写-读操作都不会发生阻塞,这提高了并发度
那么它是怎么实现的呢?
事实上,它为每一条记录加了两个隐藏字段:创建版本号删除版本号。版本号指的是对记录做出修改的事务版本号。
在进行增删查改操作时:记录的创建版本号和删除版本号会发生如下变化:

  • insert:将新记录的创建版本号修改为当前事务的版本号,删除版本号置空。
  • delete:将对应记录的删除版本号置为当前事务版本号。
  • update:将对应记录的删除版本号置为当前事务版本号,并新增一条记录,将新记录的创建版本号修改为当前事务版本号,删除版本号置空。相当于先删除再创建

查询操作是读操作,因此它不会更改版本号,但是针对不同的事务隔离级别会做出不同的反应,分类讨论一下:

  • 已提交读级别:查询这条记录中具有最新创建版本号的第一条记录。
  • 可重复读级别:查询这条记录中创建版本号不大于当前事务版本号的第一条记录。

这里有出现了一个问题:T1事务对版本号进行的修改,不会影响并发事务T2对数据的读取吗?
为了解决这个问题,引入了undo-log这样的技术。undo-log存储了这条记录未提交的状态不同的数据版本,也可以用于回滚。它是一个线性表,采用链表实现,这里不多赘述。
所以,并发事务在对事务进行读取时,读取的应该是已提交记录,并且满足上述条件的第一条记录!
同样,我们的实验也证明过如果并发事务同时对同一条数据进行写操作,会发现版本号的冲突而产生阻塞,T1对数据的修改要等到并发事务T2完成并释放锁以后才能操作。

什么时候加读锁?

如果想要强制加上读锁,可以对select操作加上for update的后缀

sql
1
select ... for update;

MVCC的设计哲学

在我看来,MVCC的设计采用了以下思想:

  • 空间换时间:新增两个字段来维护这个设计,提高并发度。
  • 乐观锁:假设没有冲突,所以先不加锁,出了冲突再说。(它们的关系依然比较暧昧)
  • Copy On Write:快照的实现并不是直接复制,而是说在进行写操作时才在undo-log中创建一个副本,减少了不必要的数据拷贝时间。

Reference

一文带你轻松搞懂事务隔离级别
从InnoDB了解MVCC
数据库事务、隔离级别和锁
数据库系统原理

文章作者: LightingX
文章链接: http://lightingx.top/blog/2019/04/14/MySQL%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E5%AE%9E%E9%AA%8C/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LightingX

评论