前言
这篇文章中 MySQL 的事务 都是基于 INNODB 存储引擎之上的, 而 MyISAM 存储引擎是不支持事务的.
什么是事务?
事务是指更新各种数据项的一个程序执行单元1
简单来说: 在一个事务中, 所有的数据更新操作, 要么全部执行成功, 要么全部执行失败.
例如有这样一个场景, 从我的银行卡转账1000元到你的银行卡, 那么就需要以下两个操作:
操作一: 我的银行卡账户金额减少1000元, 假设sql语句如下:
update account set amount = amount - 1000 where id = 1;
操作二: 你的银行卡账户金额增加1000元, 假设sql语句如下:
update account set amount = amount + 1000 where id = 2;
事务要求, 两个操作必须都成功执行, 如果其中一个操作执行失败, 那么执行成功的操作就需要进行回滚到原来的状态
假如我的银行卡账户有2000元, 执行操作一成功之后, 我的账户就变成1000元, 如果操作二执行失败了, 那就需要把扣我的1000元给加回去.
事务的ACID四个特性与在MySQL中的实现
四个特性分别指的是: 原子性 (Atomicity), 一致性 (Consistency), 隔离性 (Isolation) 和持久性 (Durability)
- Atomicity - 原子性 : 同一个事务的多条 SQL, 要么全部成功, 要么全部失败. 如果有其中一条 SQL 执行失败, 则执行成功的 SQL 必须回滚到原来的状态; 在 MySQL 中是通过 undo log 来实现事务的回滚的;
- Consistency - 一致性 : 事务的执行前后, 数据的一致性不会被破坏. 例如上面转账的例子, 如果我和你一开始都有2000元, 我和你的账户加起来是4000元, 在执行完转账成功之后, 我和你的账户加起来的总额必须还是4000元不变;
- Isolation - 隔离性 : 多个事务同时执行时, 事务之间互不影响, 相互隔离. 例如: 上面我在转账的过程中事务还没有提交, 我在发起一个事务去查询我的总额, 那我查到的总额应该是 2000元, 而不是 1000元; 在 MySQL 中是通过 锁 + MVCC 来实现事务之间的隔离性的;
- Durability - 持久性 : 事务执行完更新操作提交事务之后, 数据会被持久化到数据库中去; MySQL 是使用 redo log 来实现数据的持久化的;
MySQL的事务隔离级别和各个事务隔离级别对 ACID 的支持
在说 MySQL 的事务隔离级别之前, 我们先来了解两个概念, 当前读和快照读
- 当前读 : 每次读取都读取数据最新的版本, 每次读取都会在数据上加锁, 防止其他事务对数据进行修改. 在 select … for update, select … in shard mode, update, delete 都是一种快照读;
- 快照读 : 每次修改数据都会生成一个快照版本, 读取时读取当前最新的快照版本, 是基于 MVCC 实现的, 这里会有一个可见性判断, 这个我们说到 MVCC 的时候再说;
在 MySQL 中有 4 个事务隔离级别, 分别是读未提交 (Read Uncommitted), 读已提交 (Read Committed), 可重复读 (Repeatable Read), 串行化 (Serilazable)
- Read Uncommitted - 读未提交 : 当前事务可以读取到其他事务未提交的数据, 存在脏读, 不可重复读和幻读问题
- Read Committed - 读已提交 : 当前事务只能读取到其他事务已经提交的数据, 但是每次的快照读都是获取最新可见的版本; 不存在脏读问题, 但是存在不可重复读和幻读问题
- Repeatable Read - 可重复读 : 当前事务只能读取到其他事务已经提交的数据, 每次的快照读是读取第一次快照读是获取的那个快照版本; 不存在脏读和不可重复读问题, 存在幻读问题
- Serilazable - 串行化 : 事务串行执行, 事务的所有读取都是当前读. 唯一一个严格遵守 ACID 4个特性的隔离级别, 每次查询都要加锁, 性能低, 没有脏读, 不可重复度和幻读问题.
MVCC
接下来, 我们开看一下 MySQL 中 MVCC 的实现.
在 MySQL 中, 是通过隐藏字段, undo log 和 read view 来实现的
相关的隐藏字段
- DB_ROLL_PRT : 回滚指针, 记录指向上个版本数据的指针
- DB_TXR_ID : 当前最新修改的事务ID
- DB_ROW_ID : 隐藏主键, 如果表中没有主键而且没有唯一索引, INNO DB 引擎会创建一个row_id作为聚簇索引
undo log
undo log 是一个回滚日志, 在插入和修改是都很生成一个 undo log, 主要用于回滚和快照读
在插入操作的时候, 产生的 undo log 只有在当前事务回滚的时候需要, 当事务提交后, 这个 undo log 就没用了, 可以立即删除.
而在修改和删除操作的时候, 产生的 undo log 不仅在当前事务回滚的时候需要, 快照读判断可见性的时候也需要, 所以不能随便删除
只有在事务回滚和快照读可见性判断都不需要的时候, undo log 才有可能被删除, MySQL 中是有个 purge 线程来同一清除的
同一事务或不同事务, 多记录的修改, 会产生一个 undo log 链表, 用 DB_ROLL_PRT 来指向上一个版本的旧记录, 链表的第一个就是最新的旧记录, 链表最后一个就是最早的记录
read view
read view 是快照读是产生的一个读视图, 最大的作用就是用做可见性判断, Read View 中包含以下三个全局属性:
- trx_list : 记录读视图产生那一刻所有正在活跃的事务ID, 即开启当时还没有提交的事务
- up_limit_id : 记录 trx_list 中, 最小的事务ID, 事务ID生成时是递增的
- low_limit_id : 记录读视图产生那一刻还没有被使用的下一个事务ID
可见性判断逻辑
判断一 : 如果 DB_TRX_ID < up_limit_id, 则说明这个版本在生成快照的时候, 该事务已经提交了, 可以直接判断该版本对当前事务是可见的. 否则, 进入下一个判断, 判断二
判断二 : 如果 DB_TRX_ID 在 trx_list 中, 则说明这个版本在生成快照的时候, 该事务还没有提交, 可以直接判断该版本对当前事务是不可见的. 否则, 进入下一个判断, 判断三
判断三 : 如果 DB_TRX_ID >= low_limit_id, 则说明这个版本在生成快照的时候, 该事务还没有开启, 可以直接判断该版本对当前事务是不可见的. 否则, 说明该事务虽然在当前事务之后才开启的, 当是是在生成快照之前提交的, 所以可以判断出对当前事务是可见的