MySQL的事务实现逻辑是位于引擎层的,并且不是所有的引擎都支持事务的,下面的说明都是以InnoDB引擎为基准。
首先我们要知道 事务的隔离性解决的是并发事务出现的问题。
先简单介绍一下MySQL的四种事务隔离级别:
- 序列化读 (Serializable):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的『世界』。所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
- 可重复读(Repeatable read,MySQL默认模式):如果一个事务成功执行并且添加了新数据,这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在有新数据的时候会突破隔离,对已存在的数据仍然具有隔离。 举个例子,如果事务A运行”SELECT count(1) from person” ,然后事务B在 person表 加入一条新数据并提交,当事务A再运行一次 count(1)结果不会是一样的。 这叫幻读(phantom read)。
- 读取已提交(Read committed):可重复读+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(或删除)并提交,事务A再次读取数据D时数据的变化(或删除)两次读取的结果是不一样的。 这也叫不可重复读(non-repeatable read)。
- 读取未提交(Read uncommitted):最低级别的隔离,是读取已提交+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(但并未提交,事务B仍在运行中),事务A再次读取数据D时,数据是被修改后的结果。如果事务B回滚,那么事务A第二次读取的数据D是无意义的,因为那是事务B所做的从未发生的修改(已经回滚了嘛)。 这叫脏读(dirty read)。
除了序列化读是完全没问题的,其他三个隔离级别在并发下都可能出现问题:
隔离级别(+:允许出现,-:不允许出现) | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | + | + | + |
提交读 | - | + | + |
可重复读 | - | - | + |
序列化读 | - | - | - |
标准SQL事务隔离级别实现原理
标准SQL事务隔离级别的实现是依赖锁的,我们来看下具体是怎么实现的:
可以看到,在只使用锁来实现隔离级别的控制的时候,需要频繁的加锁解锁,而且很容易发生读写的冲突(例如在RC级别下,事务A更新了数据行1,事务B则在事务A提交前读取数据行1都要等待事务A提交并释放锁)。
为了不加锁解决读写冲突的问题,MySQL引入了MVCC机制
InnoDB事务隔离级别实现原理
我们先来看下InnoDB的事务具体是怎么实现的:
可以看到,InnoDB通过 MVCC 很好的解决了读写冲突的问题,而且提前一个级别就解决了标准级别下会出现的幻读和不可重复读问题,一定程度上大大提升数据库的并发能力。
我们看到InnoDB的事务隔离级别下用到了 当前读 、 快照读、 间隙锁 ,他们是什么意思呢:
当前读
读取的是最新版本,像UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE(共享锁)、SELECT ... FOR UPDATE(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读
读取的是快照版本,也就是历史版本,不加锁的SELECT操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是未提交读和序列化读级别,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而序列化读则会对表加锁。
间隙锁
锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。
MVCC能解决了幻读问题?
实际上MVCC并不能解决幻读问题。如以下的例子:
begin;
#假设users表为空,下面查出来的数据为空
select * from users; #没有加锁
#此时另一个事务提交了,且插入了一条id=1的数据
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,所以更新成功,并生成一个更新的快照
select * from users; #读快照,查出来id为1的一条记录,因为MVCC可以查到当前事务生成的快照
commit;
可以看到前后查出来的数据行不一致,发生了幻读。所以说只有MVCC是不能解决幻读问题的,解决幻读问题靠的是间隙锁。如下:
begin;
#假设users表为空,下面查出来的数据为空
select * from users lock in share mode; #加上共享锁
#此时另一个事务B想提交且插入了一条id=1的数据,由于有间隙锁,所以要等待
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,由于不存在数据,不进行更新
select * from users; #读快照,查出来的数据为空
commit;
#事务B提交成功并插入数据
注意,RR级别下想解决幻读问题,需要我们显式加锁,不然查询的时候还是不会加锁的。