Mysql 事务隔离 MVCC

64 阅读5分钟

事务隔离

以Innodb引擎为引子

什么是事务隔离?事务隔离其实就是事务与事务之间相互“隔离”嘛,一个事务的执行不应该影响其他事务的执行。

ACID(原子性、一致性、隔离性、持久性)防止多个事务并发执行时相互干扰

  • 原子性:保证事务中的所有操作要么全部成功,要么全部失败回滚
  • 一致性:数据库从一个一直的状态转移到另一个一直的状态,不破坏数据完整性约束
  • 隔离型:事务中间结果不可见,每个事务就像单线程运行,并发互不影响
  • 持久性:事务一旦提交,对其数据库的修改是永久性的,哪怕数据库宕机数据也不会丢失。

事务隔离级别

  • 读未提交(read uncommitted):允许读取未提交的数据。事务可以读取到其他事务尚未提交的修改。
  • 读已提交(read committed):只允许读取提交的数据。事务只能读取其他事务已提交的修改。
  • 可重复读(repeatable read):同一事务内多次读取结果一致,即使其他事务已经修改并提交的数据。
  • 串行化(serializable):所有事务串行执行,单线程

事务并发问题

  • 脏读(Dirty Read)
    • A、B两个用户,数据X;
    • A开启事务读取X数据,将X数据变成为X+1(未提交);
    • B开启事务读取数据X,读取到的是X+1这个数据;
    • 此时,A发生错误并回滚了,此时X+1变成X;
    • 这个时候就会出现问题,B读到的是X+1,但是数据现在为X;
    • 所以B读取的数据是错误的;
    • 这就是脏读
  • 不可重复读(Non-Repeatable-Read)
    • A、B两个用户,数据X;
    • A开启事务读取X数据;
    • B开启事务读取X数据;
    • A将X更新成为X+1并且提交结束事务(commit);
    • 此时B又读取数据,此时X变成X+1;
    • B开启事务第一次获取数据为X,第二次读取变成X+1,前后不一致;
    • 在一个事务期间,多次读取一个数据前后不一致的情况;
    • 这个就是不可重复度
  • 幻读(Phantom Read)
    • A、B两个用户,数据条数为X;
    • A开启事务读取表中有X条数据;
    • 此时,B开启事务插入了Y条数据并提交结事务(commit);
    • 然后,A又再一次去读取数据条数,此时发现变成了X+Y条;
    • 出现了问题:一开始我读取到的X条数据,再次读取发现变成X+Y条;
    • 这个就是幻读
总结
并发问题本质涉及行是否提交出现场景
脏读读到了别人还没提交的数据一行❌未提交最危险
不可重复读同一行多次读结果不一致一行✅已提交比较常见
幻读满足条件的行数变了多行✅已提交插入/删除触发

事务隔离和三大问题

前边说的事务隔离级别就是为了解决三大问题。

  • 读未提交 隔离级别下,存在脏读、不可重复读、幻读;
  • 读提交 隔离级别下,解决了脏读,存在不可从复读、幻读;
  • 可重复读 隔离界别下,解决了脏读、可重复度,存在幻读;
  • 串行化 隔离级别下,脏读、可重复读、幻读问题
隔离级别是否可能出现的问题
读未提交(READ UNCOMMITTED)✅脏读 ✅不可重复读 ✅幻读
读提交(READ COMMITTED)❌脏读 ✅不可重复读 ✅幻读
可重复读(REPEATABLE READ)❌脏读 ❌不可重复读 ✅幻读(基本避免)
串行化(SERIALIZABLE)❌脏读 ❌不可重复读 ❌幻读

其实innodb默认的隔离级别为可重复度,但是依旧存在幻读的问题。他使用了**mvcc(多版本并发控制)和next-key-lock来规避幻读的问题。

MVCC(多版本并发控制)

核心组成
  • 隐藏字段 (row结构中的隐藏字段)
  • Undo Log(撤销日志)
  • Read View(读取试图)
隐藏字段
  • DB_TRX_ID:最后一次修改这条记录的事务ID
  • DB_ROLL_PRT:回滚指针,指向Undo Log日志(旧版本数据)
  • DB_ROW_ID:唯一自增ID(内部使用)
Undo Log(撤销日志)
  • 回滚时用于还原旧值
  • 读取操作读取“历史快照”
  • DB_ROLL_PRT 将新的记录和旧的记录串联起来
Read View(读取视图,快照读)

每个事务在第一次使用快照读(普通的SELECT)时,Innodb会生成一个 Read View

  • m_ids[]:当前系统中活跃的事务(未提交的事务)ID列表
  • min_trx_id:最小未提交的事务ID
  • max_trx_id:创建Read View时当前数据库中对应给下一个事务的ID值(全局事务中最大事务ID+1)
  • creator_trx_id:指创建该Read View的事务的事务ID
    • roll_pointer:指向每一个旧版本记录的指针,这样就形成了版本链。

mvcc的核心原理:版本可见性规则,事务在读取数据的时,Innodb会判断记录是否对当前事务可见
之前在介绍row(行)结构的时候提到的一个字段为trx_id这个就是当前记录,最后一次修改当前记录的事务ID。

  • 如果当前trx_id小于min_trx_id的情况下(min_trx_id > trx_id),可见
  • 如果当前trx_id大于max_trx_id的情况下(trx_id > max_trx_id),不可见
  • 如果trx_id在min_trx_id和max_trx_id之间
    • trx_id在m_ids[]中 不可见
    • trx_id不在m_ids[]中 可见
  • 如果当前trx_id和当前事务判断可见性的时候如果都是不可见的情况,将会沿着版本链去找。

当前读其实可以解决幻读的问题,这个后边涉及到锁的时候再详细讲讲。