1 并发事务带来的问题
当多个事务并发执行的时候就会导致事务并发问题包括脏写、脏读、不可重复读和幻读,而MVCC为了解决不可重复读和幻读。
1.1 不可重复读
同一个事务下,使用相同的查询语句,在不同时刻读到的结果不一致。
1.2 幻读
同一个事务下,两次读取一个范围的数据记录,两次读取的结果不同。
2 当前读&快照读
2.1 当前读
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
它读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他锁事务修改数据。是一种悲观锁操作。
select * from table where ? lock in share mode;共享锁
select * from table where ? for update;排它锁
insert into table values (…);排它锁
update table set ? where ?;排它锁
delete from table where ?;排它锁
2.2 快照读
快照读的实现是基于多版本并发控制,即MVCC,既然是多版本,那么快照读到的数据不一定是当前最新的数据,有可能是之前历史版本数据。
单纯的select操作,不包括上述 select ... lock in share mode, select ... for update。
3 事务隔离级别
事务隔离级别 | 脏读 | 不可重读读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
4 UndoLog
MySQL中事务一致性由UndoLog来实现的,UndoLog在事务中的主要作用就是回滚事务和多版本并发机制(MVCC)。
UndoLog记录的是逻辑日志,例如我们执行一条insert的语句,UndoLog会记录一条对应的delete语句;update和delete操作也会记录对应的语句,当数据库崩溃重启或者执行回滚事务时,可以从UndoLog读取相应的数据记录进行回滚操作。
InnoDB在数据库每行数据后面添加3个字段:
- DB_TRX_ID
用来标识本行数据最近一次修改的事务标识符我们通常称为事务id
- DB_ROLL_PTR
指向上一个版本的行记录,能够从最新版本的行记录逐级向上
- DB_ROW_ID
这个字段包含一个随着新数据行的插入操作而单调递增的行id,当由InnoDB存储引擎自动产生聚集索引时,聚集索引会包含这个id。
5 MVCC
5.1 快照
按照MySQL官方文档里对快照的解释来看的话应该是生成快照时会结合当前数据库状态定义出Read View,然后基于Read View和一定规则生成快照。快照只会在读已提交和可重复读隔离级别下执行查询语句时生成。
组成内容 | 含义 |
m_ids | 当前活跃的事务编号集合 |
min_trx_id | 最小活跃事务编号 |
max_trx_id | 定义Read View那一刻的数据库中的事务的最大id。 |
creator_trx_id | 定义Read View的事务的id。 |
在MVCC机制下判断查询类容,需要遵循以下规则:
- 如果某版本的数据的DB_TRX_ID与Read View的creator_trx_id相等,说明这个版本的数据最后由当前事务更改,故这个版本的数据对当前事务可见;
- 如果某版本的数据的DB_TRX_ID小于Read View的min_trx_id,说明这个版本的数据最后由已经提交的事务更改,故这个版本的数据对当前事务可见;
- 如果某版本的数据的DB_TRX_ID大于Read View的max_trx_id,说明最后修改这个版本的数据的事务在快照生成时还未创建,故这个版本的数据对当前事务不可见;
- 如果某版本的数据的DB_TRX_ID满足:min_trx_id <= DB_TRX_ID <= max_trx_id,且m_ids包含DB_TRX_ID,说明这个版本的数据最后由未提交的事务更改,故这个版本的数据对当前事务不可见;
- 如果某版本的数据的DB_TRX_ID满足:min_trx_id <= DB_TRX_ID <= max_trx_id,但m_ids不包含DB_TRX_ID,说明这个版本的数据最后由已经提交的事务更改,故这个版本的数据对当前事务可见。
5.2 读已提交
在读已提交事务隔离级别下,每次查询都会生成一个ReadView
- ReadView1
我们发现ReadView1不满足上面任何规则,因此此时结果就是“张石”。
- ReadView2
第1-3条规则ReadView2都不符合,可以发现目前的min_trx_id=2,max_trx_id=4,m_ids={2,3},根据规则5我们往回寻找,DB_TRX_ID=1的时候符合我们的要求,如此我们能知道ReadView2的查询结果为“张三石”。
由此可知在读已提交事务隔离级别下,无法避免不可重复读。
5.3 可重复读
在可重复读隔离级别下,只会在第一次执行查询语句时生成ReadView
因此我们发现两次查询,第二次无论时机如何,他们的结果都会根据第一次ReadView进行判断,结果就是“张石”。
由此可见可重复读隔离级别下可以避免不可重复读。
5.3.1 可重复读能避免幻读?
可重复读隔离级别可以一定程度上避免幻读,但是并不一定避免幻读。
从图中可知由于可重复读隔离级别的特性,第二次查询依然依然会复用第一个ReadView,因为没有符合规则的DB_TRX_ID,因此结果就是“张石”。
但是如果出现当前读时,可重复读隔离级别将无法避免幻读。
由图可知DB_TRX_ID=2事务,在中间执行了一次update操作因此产生了当前读,从而改变了ReadView最终使两次查询结果不一样形成幻读。