【MySQL】事务

22 阅读5分钟

事务

事务:一组操作,要么全做,要么全不做。

事务ACID特性:

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成。

    • Undo log 保证
  • 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。

    • 持久性+原子性+隔离性来保证
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

    • MVCC、锁
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    • Redo log 保证

MySQL启动事务:

  • begin/start transaction;

    • 只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才真正启动事务;
  • start transaction with consistent snapshot;

    • 执行了命令,就会马上启动事务。

隔离性级别

隔离性问题

  • 脏读(dirty read):一个事务「读到」了另一个「未提交事务修改过的数据」
  • 不可重复读(non-repeatable read):在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样
  • 幻读(phantom read):一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样

四种隔离级

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

    • 无需事务
  • 读提交(read committed):一个事务提交之后,它做的变更才能被其他事务看到;

    • 在「每个语句执行前」都会重新生成一个 Read View
  • 可重复读(repeatable read):一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的;

    • 「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View

    • MySQL InnoDB 引擎的默认隔离级别

    • MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生

      • 快照读(普通 select),通过 MVCC 解决幻读,可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了数据也查询不出来。
      • 当前读(select ... for update 等语句),通过 next-key lock(记录锁+间隙锁),如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
  • 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

    • 通过加读写锁的方式来避免并行访问,性能很差。

Read View

四个重要的字段:

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

    • 活跃事务:启动了但还没提交的事务。
  • min_trx_id:创建 Read View 时,「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

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

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

解聚簇索引记录中的两个隐藏列

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

trx_id 三种情况

  • trx_id < min_trx_id:表示这个版本的记录是在创建 Read View 前已经提交的事务生成的

    • 该版本的记录对当前事务可见。
  • trx_id >= max_trx_id:表示这个版本的记录是在创建 Read View 后才启动的事务生成的

    • 该版本的记录对当前事务不可见。
  • min_trx_id<= trx_id < max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:

    • trx_id 在 m_ids 列表中:表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
    • trx_id 不在 m_ids列表中:表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

可重复读工作流程(快照读)

可重复读隔离级别:启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。

  • 读取记录的trx_id < min_trx_id:直接使用

  • trx_id >= max_trx_id:进入undo log版本链继续找

  • min_trx_id<= trx_id < max_trx_id:

    • 不在m_ids:直接使用
    • 在m_ids:进入undo log版本链继续找

快照读避免幻读

  • 执行第一个查询语句后,会创建一个 Read View

    • 通过 Read View 在 undo log 版本链找到事务开始时的数据

    • 事务过程中每次查询的数据都是一样的

    • 即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。

读提交工作流程(当前读)

读提交隔离级别:每次读取数据时,都会生成一个新的 Read View。

  • 读取记录的trx_id < min_trx_id:直接使用

  • trx_id >= max_trx_id:进入undo log版本链继续找

  • min_trx_id<= trx_id < max_trx_id:

    • 不在m_ids:直接使用

    • 在m_ids:进入undo log版本链继续找

当前读避免幻读

普通查询是快照读,其他都是当前读,比如 update、insert、delete

  • select ... for update 这种查询语句也是当前读,每次执行的时候都是读取最新的数据。

  • Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。

    • 例如查询(2,+∞] 时使用了next-key lock (间隙锁+记录锁)

无法避免的幻影读

  • 事务A读取,没有数据,事务B插入数据,事务A再更新得到了数据,幻影读。

  • 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

    • 尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。