MySQL事务隔离级别详解
MySQL InnoDB对事物隔离级别的支持:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read Uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read Committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 对InnoDB不可能 |
串行化(Serializable) | 不可能 | 不可能 | 不可能 |
InnoDB支持的四个隔离级别和SQL92定义的完全一致,隔离级别越高,事务的并发度就越低。唯一的区别就在于,InnoDB在RR的级别就解决了幻读的问题。
也就是说,不需要使用串行化的 隔离级别去解决所有问题,既保证了数据的一致性,又支持较高的并发度。这个也就是InnoDB默认使用RR作为事务隔离级别的原因。
如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?总体上来说,有两大类解决方案。
第一中,既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。
如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们大多数应用都是读多写少的,这样会极大地影响操作数据的效率。
所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致,那么我们可以在修改数据之前给它建立一个备份或者叫做快照
,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制Multi Version Concurrency Control(MVCC)
这也是今天我们讨论的重点。
MVCC的原则:
1、一个事务能看到的数据版本:(1)第一次查询之前已经提交的事务的修改;(2)本事务的修改。
2、一个事务不能看见的数据版本:(1)在本事务第一次查询之后创建的事务(事务ID比我的事务ID大);(2)活跃的(未提交的)事务的修改。
基于这两个原则,我们可以总结出MVCC的效果:可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的。
所以我们才把这个叫快照,不管别的事务做任何增删改查的操作,它只能看到第一次查询时看到的数据版本
。
那么问题来了:这个快照是怎么实现的呢?会不会占用额外的存储空间?
InnoDB的事务都是有编号
的,而且会不断递增
。InnoDB为每行记录都实现了两个隐藏字段
:
DB_TRX_ID
,6字节:事务ID,数据是在哪个事务插入或者修改为新数据的,就记录当前事务ID。
DB_ROLL_PTR
,7字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID,没有修改或删除的时候为空)
(1)第一个事务
,初始化数据(检查初始数据):
此时的数据,创建版本是当前事务ID(假设事务编号是1),删除版本为空:
(2)第二个事务
,执行一次查询,查出两条原始数据,假设第二个事务的ID为2:
(3)第三个事务
,插入一条数据:
此时的数据,多了一条tom,它的创建版本号是当前事务编号,假设为3:
(4)第二个事务
执行第二次查询:
根据MVCC的查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)
。
也就是不能查到在我的事务开始之后插入的数据,tom的创建ID大于2,所以还是只能查到两条数据。
(5)第四个事务
,删除数据,删除了id = 2 huihui的这条记录:
此时的数据,huihui的删除版本被记录为当前事务ID,假设为4,其他数据不变:
(6)第二个事务
中,执行第三次查询:
根据MVCC的查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)
。
也就是,可以查出在我事务开始之后删除的数据,所以huihui依然可以查出来,所以还是这两行数据。
(7)第五个事务
,执行更新操作,假设事务ID是5:
此时的数据,更新数据的时候,旧数据的删除版本被记录为当前事务ID为5(undo),产生了一条新数据,创建ID为当前事务ID 5:
(8)第二个事务
执行第四次查询:
根据MVCC的查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)
。
因为更新后的数据penyuyan创建版本大于2,代表是在事务之后增加的,查不出来。而旧数据qingshan的删除版本大于2,代表是在事务之后删除的,可以查出来。
通过该实例我们能看到,通过版本号的控制,无论其他事务是插入、修改、删除,第一个事务查询到的数据都没有变化。这就是MVCC的效果。当然,这是一个简化的模型。
假设一条数据修改了3次,两次提交了一次未提交。每次修改之后都有开启一个事务去查询,那么事务2、4、6查到的数据是不一样的:
InnoDB中,一条数据的旧版本,是存放在哪里的呢?undo log
。因为修改了多次,这些undo log会形成一个链条,叫做undo log链,现在undo log里面有liudehua、wuyanzu、penyuyan。
所以前面我们说的DB_ROLL_PTR,它其实就是指向undo log链的指针。
第二个问题,事务2、4、6最后再查一次,它们去undo log链找数据的时候,拿到的数据是不一样的,在这个undo log链里面,一个事务怎么判断哪个版本的数据是它应该读取的呢?
回想一下MVCC规则:
1、一个事务能看到的数据版本:(1)第一次查询之前已经提交的事务的修改;(2)本事务的修改。
2、一个事务不能看见的数据版本:(1)在本事务第一次查询之后创建的事务(事务ID比我的事务ID大);(2)活跃的(未提交的)事务的修改。
所以,我们必须要有一个数据结构,把本事务ID、活跃事务ID、当前系统最大事务ID
存起来,这样才能实现判断。这个数据结构就叫Read View(可见性视图),每个事务都维护一个自己的Read View。
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务ID列表。
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的ID值。
creator_trx_id:表示生成该ReadView的事务的事务ID。
有了这个数据结构之后,事务判断可见性的规则是这样的:
0、从数据的最早版本开始判断(undo log)。
1、数据版本的trx_id = creator_trx_id,本事务修改,可以访问。
2、数据版本的trx_id < min_trx_id(未提交事务的最小ID),说明这个版本在生成ReadView已经提交,可以访问。
3、数据版本的trx_id > max_trx_id(下一个事务ID),这个版本是生成ReadView之后才开启的事务建立的,不能访问。
4、数据版本的trx_id 在min_trx_id和max_trx_id之间,看看是否在m_ids中,如果在,不可以;如果不在,可以。
5、如果当前版本不可见,就找undo log链中的下一个版本。
注意:RR中ReadView是事务第一次查询的时候建立的。RC的ReadView是事务每次查询的时候建立的。
Oracle、Postgres等等其他数据库都有MVCC的实现。
在InnoDB中,MVCC和锁是协同使用的,这两种方案并不是互斥的。