一、引言
脏读、不可重复读和幻读是数据库中由于并发访问导致的数据读取问题。当多个事务同时进行时可以通过修改数据库事务的隔离级别来处理这三个问题。
二、问题解释
1、脏读(读取未提交的数据)
脏读又称无效数据的读出,是指在数据库访问中,事务 A 对一个值做修改,事务 B 读取这个值,但是由于某种原因事务 A 回滚撤销了对这个值得修改,这就导致事务 B 读取到的值是无效数据。
2、不可重复读(前后数据多次读取,结果集内容不一致)
不可重复读即当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做了修改操作,之后事务 A 为了数据校验继续按照之前的查询条件得到的结果集与前一次查询不同,导致不可重复读取原始数据。
3、幻读(前后数据多次读取,结果集数量不一致)
幻读是指当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做新增操作,之后事务 A 继续按照之前的查询条件得到的结果集平白无故多了几条数据,好像出现了幻觉一样。
三、事务隔离
在并发条件下会出现上述问题,如何着手解决他们保证我们程序运行的正确性是非常重要的。数据库提供了 Read uncommitted 、Read committed 、Repeatable read 、Serializable 四种事务隔离级别来解决脏读、幻读和不可重复读问题,同时容易想到,可以通过加锁的方式实现事务隔离。
在数据库的增删改查操作中,insert 、delete 、update 都会加排他锁,排它锁会阻止其他事务对其加锁的数据加任何类型的锁。而 select 只有显示声明才会加锁。
-
Read uncommitted
读未提交,说的是一个事务可以读取到另一个事务未提交的数据修改。
读若不显式声明是不加锁的,可以直接读取到另一个事务对数据的操作,没有避免脏读、不可重复读、幻读。
-
Read committed
读已提交,说的是一个事务只能读取到另一个事务已经提交的数据修改。
很明显,这种隔离级别避免了脏读,但是可能会出现不可重复读、幻读。
-
Repeatable read
可重复读,保证了同一事务下多次读取相同的数据返回的结果集是一样的。
这种隔离级别解决了脏读和不可重复读问题,但是扔有可能出现幻读。
-
Serializable
串行化,对同一数据的读写全加锁,即对同一数据的读写全是互斥了,数据可靠行很强,但是并发性能不忍直视。
这种隔离级别虽然解决了上述三个问题,但是牺牲了性能。
总结如下表: √ 代表可能出现,× 代表不会出现。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable read | × | × | √ |
Serializable | × | × | × |
四、MySQL 事务隔离级别的实现
在 MySQL 中只有 InnoDB 存储引擎支持事务,但是在日常使用 MySQL 时我们好像没有怎么关心过上述三个问题啊...
原因很简单,MySQL 默认 Repeatable read 隔离级别,使用了 MVCC 技术,并且解决了幻读问题。
MVCC
MVCC 全名多版本并发控制,使用它可以保证 InnoDB 存储引擎下读操作的一致性。使用 MVCC 可以查询被另一个事务修改的行数据,并且可以查看这些行被更新之前的数据,值得注意的是使用 MVCC 增加了多事务的并发性能,但是并没有解决幻读问题。
1、原理
MVCC 是通过保存数据在某个时间点的快照来实现的。也就是说在同一个事务的生命周期中,数据的快照始终是相同的;而在多个事务中,由于事务的时间点很可能不相同,数据的快照也不尽相同。
2、实现细节
- 每行数据都存在一个版本,每次数据更新时都更新该版本。
- 修改时Copy出当前版本随意修改,各个事务之间互不干扰。
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)。
通过上面特点我们可以看出,MVCC 其实就是类似乐观锁的一种实现。
3、InnoDB 中 MVCC 实现
在 InnoDB 中为每行增加两个隐藏的字段,分别是该行数据创建时的版本号和删除时的版本号,这里的版本号是系统版本号(可以简单理解为事务的 ID),每开始一个新的事务,系统版本号就自动递增,作为事务的 ID 。通常这两个版本号分别叫做创建时间和删除时间。
InnoDB 会根据下面两个条件检查每行记录:
- 只会查找版本早于当前事务版本的数据行(行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- 行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。
总结:
- SELECT
读取创建版本号小于或等于当前事务版本号,并且删除版本号为空或大于当前事务版本号的记录。如此可以保证在事务在读取之前记录是存在的。- INSERT
将当前事务的版本号保存至插入行的创建版本号。- UPDATE
新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号。- DELETE
将当前事务的版本号保存至行的删除版本号。