MySQL 事务

事务广泛应用于订单系统、银行系统等多种场景

A给B转账500,需要做以下几件事:

  1. 检查 A 的账户余额 > 500
  2. A 扣 500
  3. B 加 500

正常流程走下来,A扣500,B 加 500,皆大欢喜

如果 A 扣了后,系统出故障了,A 损失 500,而 B 也没有收到本该属于他的 500

以上的案例中,隐藏着一个前提条件:A 扣钱和 B 加钱,要么同时成功,要么同时失败

事务的需求就在于此

事务是什么?

与其给事务定义,不如说一说事务 ACID 四个特性

  1. A(atomicity) 一个事务的执行被视为一个不可分割的最小单元。事务里面的操作,要么全部成功执行,要么全部失败回滚,不可以只执行其中的一部分
  2. C(consistency)一个事务的执行不应该破坏数据库的完整性约束。如果上述例子中第 2 个操作执行后系统崩溃,保证 A 和 B 的金钱总计是不会变的
  3. I(isolation) 通常来说,事务之间的行为不应该互相影响。而实际上事务相互影响的程度受到隔离级别的影响
  4. D(durability) 事务提交之后,需要持久化到磁盘。即使系统崩溃,提交的数据也不应该丢失

事务的四种隔离级别

可以认为是事务的 “自私” 程度,它定义了事务之间的可见性

1.READ UNCOMMITTED(未提交读)

事务 A 对数据做的修改,即使没有提交,事务 B 也可见,这种问题叫脏读

隔离程度较低,实际运用中会引起很多问题,因此一般不常用

2.READ COMMITTED(提交读)

不会脏读,但会不可重复读,事务 A 对数据做的修改,提交之后会对事务 B 可见

事务 B 开启时读到数据 1,接下来事务 A 开启,把这个数据改成 2,提交

B 再次读取这个数据,会读到最新的数据

这是许多数据库的默认隔离级别

3.REPEATABLE READ(可重复读)

不会不可重复读,但会幻读

事务 A 修改,提交后,对于先于事务 A 开启的事务是不可见的

事务 B 开启时读到数据 1,接下来事务 A 开启,把这个数据改成 2,提交

B 再次读取这个数据,仍然只能读到 1

幻读意思是,某事务读取某个范围内的值的时候,另外一个事务在这个范围内插入了新记录,之前的事务再次读取这个范围的值,会读取到新插入的数据

Mysql 默认隔离级别是 RR,而 innoDB 引擎间隙锁解决了幻读的问题

4.SERIALIZABLE(可串行化)

强制要求所有事物串行执行,在这种隔离级别下,读取的每行数据都加锁,会导致大量的锁征用问题,性能最差

理解四种隔离级别

事务 A 和事务 B 先后开启,并对数据 1 进行多次更新

四个小人在不同的时刻开启事务,可能看到数据 1 的哪些值呢?

  • 第一个

可能读到 1-20 之间的任何一个

因为未提交读的隔离级别下,其他事务对数据的修改也是对当前事务可见的

  • 第二个

可能读到 1,10 和 20,他只能读到其他事务已经提交了的数据

  • 第三个

读到的数据去决于自身事务开启的时间点

在事务开启时,读到的是多少,那么在事务提交之前读到的值就是多少

  • 第四个

只有在 A end 到 B start 之间开启,才有可能读到数据,而在事务 A 和事务 B 执行的期间是读不到数据的

因为第四小人读数据是需要加锁的,事务 A 和 B 执行期间,会占用数据的写锁,导致第四个小人等待锁

不同隔离级别所面对的问题

隔离级别越高,所带来的资源消耗也就越大 (锁),因此它的并发性能越低

准确的说,在可串行化的隔离级别下,是没有并发的

MySql 中的事务

mysql 事务的实现基于存储引擎。不同存储引擎对事务的支持程度不一样

默认存储引擎innoDB,默认隔离级别 RR,并在 RR 隔离级别下更进一步

MVCC 解决不可重复读问题,间隙锁(并发控制)解决幻读问题

因此 innoDB 的 RR 隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能

隔离性通过锁实现,原子性、一致性和持久性通过事务日志实现

事务日志,redo 和 undo log

redo log

innoDB 事务日志通过 redo log 和 InnoDB Log Buffer 实现

事务开启时,事务中的操作,先写入 Log Buffer,事务提交前,需要先刷盘持久化,这就是 Write-Ahead Logging

事务提交后,Buffer Pool 中映射的数据文件才会慢慢刷盘

如果数据库崩溃或宕机,系统重启进行恢复时,就可以根据 redo log 中记录的日志,把数据库恢复到崩溃前的一个状态

未完成的事务,可以继续提交,也可以选择回滚,基于恢复策略而定

Redo Log 顺序追加,改善 IO 性能

所有的事务共享 redo log 存储空间

1
2
3
4
5
记录 1:<trx1, insert...>
记录 2:<trx2, delete...>
记录 3:<trx3, update...>
记录 4:<trx1, update...>
记录 5:<trx3, insert...>

undo log

主要为事务的回滚服务

事务执行过程中,除记录 redo log,还会记录一定量的 undo log

undo log 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undo log 进行回滚操作

单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作

undo+redo 事务的简化过程

假设有 2 个数值,分别为 A 和 B, 值为 1,2

  1. start transaction;
  2. 记录 A=1 到 undo log;
  3. update A = 3;
  4. 记录 A=3 到 redo log;
  5. 记录 B=2 到 undo log;
  6. update B = 4;
  7. 记录 B = 4 到 redo log;
  8. 将 redo log 刷新到磁盘
  9. commit

1-8 任意一步系统宕机,事务未提交,盘上数据无影响

8-9 间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时 redo log 已经持久化

9 之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据 redo log 把数据刷回磁盘

所以,redo log 保障持久性和一致性,undo log 保障了事务的原子性