隔离级别

事务并发执行遇到的问题

在MySQL的事务中,有隔离性的特性,理论上,当MySQL同时处理多个事务时,只允许一个事务对某数据进行访问,其他事务应该进行排队。这样就完全保证了隔离性(隔离级别:序列化)

问题:对性能影响很大,当并发量高的时候,大家都在排队等待操作数据,导致长时间无法执行后续操作。

考虑:能否牺牲一部分隔离性来换取更高的性能?

我们先从隔离级别要解决的问题开始说起:

脏写

如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写

实例:在下图中,有一条number=1的数据,会话B将其name更新为关羽,会话A将其name更新为张飞,会话A提交后,会话B回滚,那么会话A对数据的修改也不复存在。

image

脏读

如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读

示例:会话B将number=1的数据name修改为关羽后,虽然未提交,但是却被会话A都到了,此时会话B回滚数据,那么会话A读取的就是失效数据image

不可重复读

如果一个事务总是能读到了其他事务已经提交的最新数据,那就意味着发生了不可重复读

幻读

如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读

隔离级别

首先我们先对上述问题的严重性进行排序

脏写 > 脏读 > 不可重复读 > 幻读

我们需要舍弃一些隔离性来换取性能,所以就设立了一些隔离级别,隔离级别越低,更严重的问题就越可能发生。一共设置了4个隔离级别:

  • READ UNCOMMITTED:未提交读。

  • READ COMMITTED:已提交读。

  • REPEATABLE READ:可重复读。

  • SERIALIZABLE:可串行化。

隔离级别

脏读

不可重复读

幻读

READ UNCOMMITTED

可能

可能

可能

READ COMMITTED

不可能

可能

可能

REPEATABLE READ

不可能

不可能

可能

SERIALIZABLE

不可能

不可能

不可能

MVCC原理

MVCC就是通过生成一个ReadView,然后通过ReadView找到符合条件的记录版本(历史版本是由undo日志构建的),其实就像是在生成ReadView的那个时刻做了一次时间静止(就像用相机拍了一个快照),查询语句只能读到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。

版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列):

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。

  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

比方说我们的表hero现在只包含一条记录:

image

当这条记录被多个事务修改 ,当前的版本链会类似于这样:

image

ReadView

对于隔离级别读未提交,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

对于使用可串行化隔离级别的事务来说,InnoDB规定使用加锁的方式来访问记录。

对于使用读已提交可重复读隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。因此在事务中引入了ReadView的概念。

ReadView中有一些比较重要的参数:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。

  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。

  • creator_trx_id:表示生成该ReadView的事务的事务id

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

  • 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。

  • 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

MySQL中,READ COMMITTEDREPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

  • READ COMMITTED —— 每次读取数据前都生成一个ReadView

  • REPEATABLE READ —— 在第一次读取数据时生成一个ReadView