# 事务
事务会保证一组数据库操作要么全都成功,要么全部失败。事务的支持是在引擎实现的。MyISAM 引擎不支持事务,InnoDB 引擎支持事务。
特性
- 原子性 Atomicity
- 一致性 Consistency
- 隔离性 Isolation
- 持久性 Durability
# 隔离性
在多事务同时执行时,可能会出现脏读、不可重复读和幻读的问题:
- 脏读:事务 B 在执行中读取到了事务 A 修改过但未提交的数据;
- 不可重复读:事务 B 前后读取两次,在读第二次前事务 A 对数据进行了修改,导致事务 B 第二次读取时记录内容不一致;
- 幻读:事务 B 前后读取两次,在第二次读取前事务 A 增加或删除了记录,导致事务 B 第二次读取时获得的记录数量不一致;
# 隔离级别
MySQL 给出了四个隔离级别,通过隔离级别来解决上面的问题:
- 读未提交:事务未提交时其所作更改就对其他事务可见;
直接返回记录最新值,不使用阅读视图。 - 读提交:事务提交后所做更改对其他事务可见;
阅读视图在每条 SQL 开始执行时创建。 - 可重复读:事务在执行过程中可见的数据与其启动时一致,未提交前所作更改其他事务不可见;
阅读视图在事务启动时创建,在整个事务期间使用该视图。 - 串行化:对同一记录根据需求加读锁和写锁,当锁冲突时需要等待前一事务执行完成后才能执行;
使用加锁的形式避免冲突。
MySQL 会在读数据采用一致性读,即利用一致性阅读视图来实现。但在更新数据时,会采用当前读的方法来保证更新数据生效。
对于当前读来说, SELECT 语句可以加上 lock in share mode
或 for update
变成当前读。其中第一个是读锁(共享锁),第二个是写锁(排它锁)。
# 并发版本控制
具体通过多版本并发控制 (MVCC) 来实现,不同时刻启动的阅读视图 (read-view).
在 MySQL 中,每条更新记录会同时记录一条回滚操作到 undo log 中,记录中的最新值可以通过回滚来回到之前的状态。例如一串操作:1 → 2, 2 → 3, 3 → 4,对应的 undo log 是:2 → 1, 3 → 2, 4 → 3.
假设在第一个更新创建了 read-view A, 在所有操作完成后创建了 read-view B. 对于 read-view A 来说,假如需要回到最原始的值 1,需要依次回滚所有操作;假如有新事务将当前值 4 修改为 5,这个操作对 read-view A,B 所在的事务不冲突。
示例:
一条记录 r,当前值为 1.
事务 A | 事务 B | 读未提交 | 读提交 | 可重复读 | 串行化 |
---|---|---|---|---|---|
R(r) | A: 1 | A: 1 | A: 1 | A: 1 | |
R(r) | B: 1 | B: 1 | B: 1 | B: 1 | |
W(r): 2 | |||||
R(r) | A: 2 | A: 1 | A: 1 | A: 1 | |
Commit | |||||
R(r) | A: 2 | A: 2 | A: 1 | A: 1 | |
Commit | |||||
R(r) | A: 2 | A: 2 | A: 2 | A: 2 |
特别的
- 不同数据库的默认隔离级别不同,Oracle 的默认隔离级别是读提交,而 MySQL 的默认隔离级别是可重复读。在使用由 Oracle 数据库迁移到 MySQL 数据库的应用时需要修改隔离级别。启动时设置启动参数
transaction-isolation
为READ-COMMITTED
.
使用语句SHOW VARIABLES LIKE 'transaction-isolation';
查看当前的隔离级别。 - 系统会在判断没有比当前 undo log 更早的 read-view 存在时删除该日志,所有不建议使用长事务。
长事务会导致在该事务提交前所有回滚记录都必须保留,占用很大存储空间。同时它也会占有锁资源。
一致性读视图
阅读视图是基于整个库的。其基本逻辑是:
- 对于每一个事物,拥有一个 transaction id, 它按申请顺序严格递增,在事务开始时从事务系统申请。
- 每行记录有多个版本,每次事务更新时创建新版本。记录 row trx_id,内容为事务的 transaction id. 旧版本会被保留,新版本可以直接提取到信息。行旧记录版本不是物理存在,而是当前版本根据 undo log 计算得来的。
- 事务数组
InnoDB 为每个事务创建数组来保存事务启动瞬间当前活跃的所有事务的 ID (启动但未提交)- ID 最小值记为低水位;ID 最大值加 1 作为高水位;
- 数组和高水位共同组成一致性阅读视图。
- 数据版本可见性规则:以可重复读隔离级别为例:
所有事务: [已提交事务 [未提交事务集合] 未开始事务] ^ ↑ ^ 低水位 当前事务 高水位 活跃事务数组: [低水位,……,高水位+1]
1
2
3
4
5
6
7- 当 row trx_id 落在已提交事务,说明当前版本是已提交的事务或当前事务自己生成的。该数据可见;
- 当 row trx_id 落在未开始事务,说明当前版本是将来事务生成的。数据不可见;
- 当 row trx_id 落在未提交事务集合,则对比事务数组(因为序列会标记事务开始时已经提交事务为低水位,其后都标记为未提交或未开始。但这里面的事务有可能会在后续提交):
- 若 row trx_id 在数组中,表示当前版本是还未提交事务生成。数据不可见;
- 若 row trx_id 不在数组中,表示当前版本是已提交的事务生成的。该数据可见。
总结下来:一个记录的版本对于一致性阅读视图来说有:
- 事务自己更新该记录,可见;
- 版本未提交,不可见;
- 版本已提交,在创建阅读视图后提交,不可见;
- 版本已提交,在创建阅读视图前提交,可见。
# 启动方式
- 显示启动。
使用
begin transaction
或者start transaction
来启动,提交语句是commit
, 回滚语句是rollback
. 这种启动方式会在它之后第一个操作表的语句开启事务。如果想立即启动一个事务,使用start transaction with consistent snapshot
.
在读提交级别下,start transaction with consistent snapshot
等同于begin transaction
, 因为它每个语句都会创建一个一致性阅读视图。 - 设置关闭线程自动提交。
使用
set autocommit=0
来关闭自动提交。当就算只执行一个 SELECT 语句时也会开启一个事务。这个事务只有当主动 commit 或 rollback,或者断开连接时才结束。特别地
为避免有些客户端框架在连接成功后自动执行
set autocommit=0
的命令造成长连接带来的长事务,推荐使用set autocommit=1
并用显示的方式启动事务。
# 提交并开启下一个事务
在显示使用事务的时候,可以使用 commit workk and chain
命令来提交当前事务并开启下一个事务。这样可以减少执行一次 begin
语句的交互开销,也可以知道每个语句是否在事务中。
查找长事务
使用命令来查找时间超过 60 秒的事务:
SELECT *
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(timediff(now(),trx_started))>60;
2
3