当前位置: 代码迷 >> 综合 >> MySQL 实战 45 讲笔记 | 事务隔离和 MVCC
  详细解决方案

MySQL 实战 45 讲笔记 | 事务隔离和 MVCC

热度:71   发布时间:2024-01-04 21:09:49.0

这篇文章,总结了《MySQL实战45讲》中的第 3 篇、第 8 篇 和 第 20 篇,因为这几篇文章的内容联系比较紧密,所以放在一起做了总结。

事务隔离级别

SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

InnoDB 默认的隔离级别是可重复读,即一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。本文的讨论,也是在可重复读隔离级别下。

回滚日志

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。也就是说语句更新会生成 undo log(回滚日志)。而这些回滚日志,会当没有事务再需要用到这些回滚日志时被删除,即当系统里没有比这个回滚日志更早的 read-view 的时候。

因此,尽量不要使用长事务,因为长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

我们应该总是使用 set autocommit=1, 通过显式语句的方式来启动事务,避免长事务的发生。

对于一个需要频繁使用事务的业务,可以使用 commit work and chain 来减少语句的交互次数。在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务。

MVCC

每一行数据都有多个版本(row),每个版本都有自己的事务 ID(row trx_id)。

InnoDB 为每一个事务构造了一个数组,保存这个事务的启动瞬间,当前活跃的(未提交)所有事务 ID。数组中最小值记为低水位,最大值+1 位高水位。

一个事务,能看到某一行的数据哪个版本,是根据这个事务数组来决定的:

按照数据版本从高到低依次查找,只能看到 trx_id<低水位的版本,以及 trx_id 在[低水位, 高水位)中且不在事务数组中的版本。

引用《MySQL实战45讲》的内容来解释一下,“一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

版本未提交,不可见;

版本已提交,但是是在视图创建后提交的,不可见;

版本已提交,而且是在视图创建前提交的,可见。”

这里需要注意一点的是,更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。而且,除了 update 语句之外,select 语句如果加锁也是当前读,例如 lock in share mode 或 for update。

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

表结构不支持可重复读 ,因为表结构没有对应的行数据,也没有 row trx_id,因此只能遵循当前读的逻辑。

幻读

幻读,指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。这里需要注意的是,幻读只在“当前读”才会出现,专指“新插入的行”。

产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。

间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。

间隙锁和 next-key lock 的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。例如,间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

由于间隙锁是在可重复读隔离级别下才会生效的,所以如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,需要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。

其实到这里,要深刻理解幻读,还需要理解 InnoDB 的加锁规则,我会在后面的文章中进行总结。

  相关解决方案