Mysql的事务隔离
起源
当多个事务同时执行时,可能会产生脏读、幻读、不可重复读等。为了解决这些问题,所以有了隔离级别。
脏读:事务还没有提交,就被其他事务读到修改值;
幻读:比如你执行update t set field1=newValue where field2=xxxx时,应该是满足field2=xxxx的行都被修改了,结果发现有一行没被改,因为这时候有个事务在插入了一行field2=xxxx field1=otherValue;
不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
不可重复读的重点是修改 :
同样的条件,你读取过的数据, 再次读取出来发现值不一样了
幻读的重点在于新增或者删除
同样的条件 , 第 1 次和第 2 次读出来的记录数不一样
mysql支持多种隔离级别,默认是可重复度
读未提交
其实就是没有隔离。未提交读隔离级别是最低的隔离级别,该级别不涉及到任何数据库锁,会产生脏读的情况,脏读是指如果一个事务的更改还未提交,其他数据库的会话就读了该更改数据。
读提交
读提交是oracle的默认隔离级别,所以如果做数据迁移,需要注意隔离级别要保持一致。当一个事务所作修改还没提交时,这些修改对其他事务是不可见的。
可重复读
Mysql的默认隔离级别。可重复读指的是在一个事务开启后,可能会对某一个变量重复读,那么多次读写的值是不变的。
串行化
最严格的隔离级别,用读写锁实现,一个事务对某些行进行读写会阻塞其他事务的读写。
事务隔离的实现
想知道事务隔离是如何实现的,先要了解视图(ReadView)的概念。
ReadView是某一个时间点,事务执行状态的一个快照(snapshot),可以用来判断事务的可见性。ReadView的基本结构如下:
ReadView {
creator_trx_id
low_limit_id
up_limit_id
ids
...
}
creator_trx_id 创建这个ReadView的事务ID,也就是本事务ID
low_limit_id 所有事务ID大于或等于low_limit_id对当前事务都不可见
up_limit_id 所有事务ID严格小于up_limit_id的事务对当前事务可见
ids 未提交的事务ID列表
可见性的判断
事务通过当前事务(或SQL语句,取决于隔离级别)的ReadView来判断一个事务id的操作是否对当前事务可见。
IsVisible(trx_id) if (trx_id == creator_trx_id) // 当前事务 return true; else if (trx_id < up_limit_id) // ReadView创建时, 事务已提交 return true; else if (trx_id >= low_limit_id) // ReadView创建时,事务还未被创建 return false; else if (trx_id is in m_ids) // ReadView创建时,事务正在执行,但未提交 return false else // ReadView创建时, 事务已提交 return true;
以可重复读为例,了解一下隔离实现。在mysql中,update操作都会记录一条回滚操作。DB记录好最新的值,然后通过回滚操作,得到某个视图想要的版本的信息。
比如数据从1被改为2,3,4。那么回滚日志里就会记录下如下信息。
当前值是4,其实在记录4这条信息的时候,还会记录不同回滚日志,不同的时刻启动的事务会有不同的read view就被以日志形式记录起来了。同一个信息在系统中可以存在多个版本,这就是多版本并发控制(MVCC)。(其实我暂时也不太懂mvcc,后续继续学习在补充吧。)这时候又有一个事务将4改成5,但是和read view A, B, C是不冲突的。
可见,在可重复读的情况下,读不加锁,只有写才加锁,读写互不阻塞,并发度相对于可串行化级别要高,但会有Write Skew异常。
事务在开始时创建一个ReadView,当读一条记录时,会遍历版本链表,通过当前事务的ReadView判断可见性,找到第一个对当前事务可见的版本,读这个版本。
那么回滚日志什么时候删除呢,总不能一直存着吧。当不再用到这个视图时会被删除。系统会判断是否还需要这条回滚日志的。
什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的read view的时候。
比如可重复读,因为我的事务还没结束,所以我的readview是不会更新的,readview结构体也不会变化,所以别人始终不可见,别人也就是怎么读都是一个值,我怎么读别人也是一个值(我也只是一个事务啊,事务id没有变化)。但是read-commited,由于每句sql开始都会创建一个readview,所以,事务执行期间,一旦有其他的事务A提交了A的修改的行,等我下一句sql开始时就能感知到了(虽然我的事务id没有变,但是我的read view刷新了,结构体里的数据变了),不用等到下次事务创建时才能见到。