Skip to content

4、事务

事务隔离级别是怎么实现的?

开启事务:

  • begin/start transaction:不会立即开启,执行增删查改操作的 SQL 语句时才会真正启动
  • start transaction with consistent snapshot:立刻启动事务

事务有哪些特性?

事务是由 MySQL 的引擎来实现的,如 nnoDB,而 MyISAM 引擎就不支持事务。

事务 4 个特性:

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,失败了会回到原始状态。
  • 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性是通过 redo log(重做日志)来保证的;
  • 原子性是通过 undo log(回滚日志)来保证的;
  • 隔离性是通过 MVCC(多版本并发控制)或锁机制来保证的;
  • 一致性则是通过持久性 + 原子性 + 隔离性来保证;(AID 是手段,C 是目的)

并行事务会引发什么问题?

  • 脏写:事务 A 和 B 同时更新一条数据,A 先更新为 a,B 接着更新为 b, 由于 sql 标准不允许不加写锁,所以 mysql 不存在和这个问题;
  • 脏读:A 事务「读到」了未提交事务 B 「修改过的数据」,因为如果 B 回滚了,那么 A 读到的是过期数据。
  • 不可重复读:一个事务多次读取同一个数据,前后两次读到的数据不一样;
  • 幻读:当同一个查询在不同的时间产生不同的结果集时,比如数据值发生变化,数据量发生变化(增/删)。

事务的隔离级别有哪些?

  • 读未提交(read uncommitted:指一个事务还没提交时,它做的变更就能被其他事务看到;

    • 克服了脏写
    • 直接读取最新数据就可以了
  • 读提交(read committed:一个事务提交之后,它做的变更才能被其他事务看到;

    • 克服了脏读
    • 每次语句执行前生成一个 ReadView,如果期间其他事务提交了修改,第二次就可能读到不一样的数据
  • 可重复读(repeatable read:一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别

    • 克服了不可重复读,可以很大程度避免幻读(但不是测底避免)
    • 每次事务启动时生成一个 ReadView,整个事务期间都用这个 ReadView
  • 串行化(serializable:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

    • 克服了幻读
    • 加读写锁避免并行访问
  • 隔离级别越高,性能效率就越低

  • 快照读:普通 select 语句,通过 MVCC 方式解决了幻读

  • 当前读:select ... for update、update、insert、delete 等语句,通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读,其他事务在当前事务没有提交前无法插入数据。

Read View

Read View 有四个重要的字段:

  • m_ids:创建 Read View 时,当前数据库中「活跃事务」(未提交)的事务 id 列表

  • min_trx_id:指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

  • max_trx_id:这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;

  • creator_trx_id:指的是创建该 Read View 的事务的事务 id

InnoDB 存储引擎的聚簇索引记录中有两个隐藏列

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

在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 值在 Read View 的 min_trx_idmax_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_id m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
    • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见
    • 源码中是用的二分查找,因为是有序的,且相对来说活跃事务比较少,所以二分查找效率足够,用hash需要调整结构等可能更麻烦

Read View 失效案例

表中有 id 1~4 的记录

  • 事务 A 执行查询 id = 5 的记录,无记录
  • 事务 B 开启并插入一条 id = 5 的记录,并且提交了事务。
  • 事务 A 执行查询 id = 5 的记录,无记录,因为这个时候这条记录的 trx_id 是 B 的,大于 A 的 trx_id
  • 事务 A 更新 id = 5 这条记录,更新成功,因为 update 是当前读,会读到最新的数据
    • 如果这里是删除,不符合幻读的定义,因为下一次查询还是查询不到这条记录
  • 事务 A 执行查询 id = 5 的记录,看到记录了(这个时候这条记录的 trx_id 是 A 的)
    • 如果 A 这里直接当前读,不用上面的更新也会看到 id=5 的记录,也会发生幻读
  • 因为 A 执行的快照读不会加锁,所以 B 能插入,如果是当前读,会加间隙锁,B 就无法插入,完全避免幻读。

并且如果主从同步,两边事务不共用,主库写事务没有提交,但是从库可能已经同步了 binlog,导致类似幻读的问题(因为不算是同一张表了,所以不是直接的幻读)

正在精进