数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务的 ACID 特性
什么是 ACID: 如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性(事务的特性):
- 原子性 Atomicity:原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
- 一致性 Consistency:事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。假设用户 A 和用户 B 两者的钱加起来一共是 5000,那么不管 A 和 B 之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是 5000,这就是事务的一致性。一致性是通过 AID 特性共同实现的
- 隔离性 Isolation:数据库允许多个并发事务同时对其数据进行读写和修改,规定并发事务之间的数据互不影响,隔离性用于控制多个事务并发执行结果的可见性。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性 Durability:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
MySQL 事务如何实现 ACID
- 事务原子性的实现:
- Undo log 记录了每次数据修改的记录, 当事务失败时需要回滚, 会根据 Undo log, 如果是 log 记录的是 insert 则回滚执行 delete, 如果 log 里是 delete, 则执行 insert, 如果 log 里是 update, 则执行一次相反的 update, 所以叫 Undo log
- @link:: MySQL-05架构-日志
- 事务持久性的实现:
- Redo log(重做日志) 当数据修改时,除了修改 Buffer Pool 中的数据,还会在 Redo log 记录这次操作;Redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。
- 事务隔离性的实现:
- 这部分参考→ MySQL-03b-事务的特性和实现
- 事务 A 的写, 对于事务 B 写操作的影响: 通过「基于锁的并发控制」(LBCC), 写操作加写锁(X 锁, 即排它锁)
- 事务 A 的写, 对于事务 B 读操作的影响: 通过「多版本并发控制」(MVCC), // 读不加锁, 而是采用读视图(活跃事务数组)+ 数据版本(row trx_id) + Undo log 实现 // MVCC 解决的是 RR 下的读? // Gap 锁?
- 事务的一致性:
- 定义: 事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
- MySQL 事务一致性的实现: 一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。
- 如何实现业务数据的一致性: 数据库引擎保证的 A(原子性), I(隔离性), D(持久性), 应用层面代码逻辑正确(比如转账过程做了减扣之后, 逻辑错误导致没有执行增加)
@ref 『浅入深出』MySQL 中事务的实现 - 面向信仰编程
@ref 深入学习MySQL事务:ACID特性的实现原理 - 编程迷思 - 博客园
事务的隔离级别
➤ 什么是脏读、幻读、不可重复读:
- 脏读: 就是指当一个事务正在访问数据, 并且对数据进行了修改, 而这种修改还没有提交到数据库中, 同时另外一个事务也访问这个数据, 然后读到了这个数据
- 不可重复读:一个事务前后查询 同一行记录 两次, 两次查询到的记录不一致 (期间另外一个事务对 此行数据 进行了修改并提交);
- 幻读:一个事务内前后两次查询 (相同的 where 条件),
查询出来记录的数目不一致 (期间有其他事务进行了 del/insert),两次查询到的结果集不一致, 比如多了一行结果, 多出来的被称为”phantoms row”;
@ref: Mysql 官网对幻读行的介绍: Phantom Rows
For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
给出的例子是select * where id > 100
执行两次, 第二次查询前, 有另一个事务插入了id=101
的行, 然后第二次读, 则会在查询返回的结果集中看到 id 为 101 的新行(“幻像”)。如果我们将一组行视为一个数据项,则新的”phantom”记录将违反事务的隔离原则,即事务应该能够运行,以便它读取的数据在事务期间不会更改。
➤ 事务的隔离级别:
- 未提交读(Read Uncommitted):允许脏读, 也就是可能读取到其他事务中”未提交事务”修改的数据;
- 提交读(Read Committed):(Oracle 默认级别) 避免了脏读, 仍有”幻读”和”不可重复读”, 即一个事务中能读取到其他事务提交的数据;
- 可重复读(Repeated Read):(InnoDB 默认级别) 避免了不可重复读, 在同一个事务内的查询都是事务开始时刻一致的, 同时该级别下通过 Gap 锁机制避免了幻读;
- 串行读(Serializable):完全串行化的读,
~每次读都需要获得表级共享锁~,写锁排斥读写, 读锁排斥写 // 这里是加表级锁还是行锁? 需要根据where
条件匹配到的列是主键/唯一/非唯一/非索引几种情况具体分析
隔离级别的实现
➤ LBCC 是什么: Lock Based Concurrency Control, 基于锁的并发控制
- 读的时候加 S 锁(共享锁), 不排斥其他线程读, 但排斥其他线程的写;
- 写的时候加 x 锁(排他锁), 排斥其他线程的读和写;
- LBCC 的缺点: 只能读并发, 读写串行化(写会互斥读),这样就大大降低了数据库的读写性能
➤ MVCC 是什么: Multiversion Concurrency Control, 多版本并发控制
- MySQL 每条记录在更新的时候都会有有一个数据版本号(row trx_id), 所以一条记录可以存在多个版本, 同时每条记录在更新的时候还会记录一条 Undo log;
- MySQL 通过可见事务 id 组成的视图, 以及”版本号+Undo log” 共同实现了事务的 MVCC, 每个事务中的读操作可以看到不同的 读视图(Read View), 也叫 “快照读”
- MVCC 的特点: 读不加锁, 读与写不冲突. 读写不冲突极大增加了系统并发性能.
➤ 快照读 vs 当前读: MySQL 的读操作分为 快照读(snapshot read) 和 当前读(current read)
- 快照读: 如果执行
select
查询的时候, 首先创建读视图, 在读视图中读”当前事务的可见版本”(有可能是历史版本), 不需要加锁; - 当前读: 执行
update
,delete
,insert
, 或select ... for update
的时候, 读出来的是最新的数据版本, 需要加 X 锁.
不是所有 select
语句都是快照读, 还要看隔离级别:
- 如果在 RR/RC 级别则
select
是基于 MVCC 的快照读, 不加锁; - 如果在 Serializable 级别则是当前读, 没有 MVCC 的快照, 是通过加 S 锁进行并发控制(也就是 LBCC)
➤ 事务的四种隔离级别如何实现:
@todo: ==RC 级别执行 select,是快照读? 那么如何实现“读到另外事务的提交”?==
- Serializable 级别(解决脏读 & 幻读):
- 基于锁的并发控制(LBCC) 读加 S 锁(排斥其他写), 写加 X 锁(排查其他读写)
- 因为读写都加锁, 该级别解决了脏读, 幻读
- RC(读提交)级别(解决了脏读, 但没有解决幻读):
- 对于读, RC 是’快照读’, 读不加锁, 基于 MVCC 实现, 在每个语句执行时创建读视图;
- 对于读, RC 是’当前读”, 在相关的数据行上加’X 锁’, where 条件列不同(主键/非主键索引/非索引) X 锁影响到的行也不同, 详细见→ [[MySQL-04锁-SQL加锁分析]]
- RR(可重复读)级别: 通过 MVCC 实现, 解决幻读现象
- 快照读: MVCC, 读视图在事务开始时创建, 注意这里和 RC 级别不同, 因为是事务开始时创建, 所以整个事务过程中的读视图都一样
- 当前读: 也是通过加 X 锁实现, 不同之处在于, RR 级别为了解决幻读, 还引入了间隙锁(Gap 锁);
总结: Serializable 级别使用 LBCC 进行并发控制, RR/RC 级别使用 MVCC 进行并发控制, MVCC 即通过 “row trx_id” 和 “Undo log”组成一个读视图(Read View), RR 级别的读视图在…时创建, RC 级别的读视图在…时创建