事务隔离
以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和当前事务判断可见性的时候如果都是不可见的情况,将会沿着版本链去找。
当前读其实可以解决幻读的问题,这个后边涉及到锁的时候再详细讲讲。