数据库事务 AID->C 概念梳理
关于事务的详细解释, 可以参考 《凤凰架构》 这本书, 书中从单体应用到分布式系统详细解释了事务的概念
一、事务的目标
确保系统中所有的数据全部符合期望, 相关联的数据不会产生矛盾, 即 数据状态的一致性 Consistency
关于 一致性 Consistency, 又能分为以下两点:
- 内部一致性 (本地事务): 一个服务仅使用一个数据源, DB 内部的事务
- 外部一致性 (架构问题): 分布式、微服务、多数据 DB 实例
二、达成目标 (一致性 Consistency) 的手段
数据库 ARIES 理论: 基于语义的恢复和隔离算法
- 原子性 Atomic: 在同一个事务中, 事务保证对多数据修改, 要么同时成功要么同时失败
- 隔离性 Isolation: 在不同的事务中, 事务保证了各业务正在读写的数据相互独立, 不会被彼此影响
- 持久性 Durability: 事务应该保证所有成功被提交的数据修改, 都能够被正确的持久化, 不会丢失数据
原子性 Atomic + 持久性 Durability 的挑战: 数据写入磁盘的操作不是原子性的, 存在多种状态, 写入中、未写入、写入成功、失败
上述三点 AID
是用来达成目标 C
的手段, 平时在太多博客上看到很多人直接拿 ACID
来分析, 感觉这样并不是很准确, 后面我们可以养成习惯, 使用 AID->C
三、Commit Logging
以顺序追加文件的形式, 将数据写入到磁盘:
- 当日志记录全部安全落盘, 在日志中写入 “Commit Record”
- 当 DB 发现 “Commit Record” 后, 会根据日志上的信息修改数据
- 数据全部修改完毕后, 在日志中写入 “End Record” 来完成持久化
Commit Logging 是存在一定缺陷的, 它对数据的修改发生在 Commit 之后, 容易造成空闲 IO 浪费, 并且消耗内存和缓存过多, 代表的 DB 有阿里的 OceanBase
四、WAL (Write Ahead Logging)
从名字可以看出来, WAL 表示 “在日志前写入”, 即 WAL 能够允许事务在提交之前写入变动数据到存储。 按照事务提交的时间点, 将写入变动数据又分为了 Force 和 Steal 两种情况:
- Force: 事务提交之后, 变动数据必须同时完成写入, 否则是 No Force
- Steal: 事务提交之前, 允许数据提前写入, 否则是 No Steal
在实现 WAL 时, 绕不开下面两个 Log:
- 回滚日志 UndoLog: 变动数据写入磁盘之前, 需要先记录到 UndoLog, 注明修改的数据 (擦除事务提交前, 已经提前写的)
- 重做日志 RedoLog: 用于崩溃恢复时, 重演数据变动的日志 (完成崩溃时未来得及写完的)
WAL 允许 Force 也允许 Steal, 我们可以使用各种组合来实现 WAL (2x2=4), 其中 No Force + Steal 的性能最好, 但是对应的实现也是最复杂的
五、隔离性 Isolation
SQL 标准定义了四种事务隔离级别, 每一种级别都会影响数据库的并发性和一致性: 读未提交、读已提交、可重复读、可串行化。 隔离性的实现原理: 通过对事务的读写操作应用不同类型的锁(读锁, 写锁, 范围锁), 来达到不同级别的事务隔离性
隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 | 案例 |
---|---|---|---|---|---|
读未提交 (Read Uncommitted) | 允许 | 允许 | 允许 | 仅仅对事务涉及到的数据加了写锁。可能会发生事务 T1 读到了事务 T2 未提交的数据 | 发生幻读(缺乏范围锁) + 不可重复读(缺乏贯穿 T1 的读锁) + 脏读(T1 读到了 T2 未提交的事务, 缺乏读锁) |
读已提交 (Read Committed) | 禁止 | 允许 | 允许 | 对事务涉及到的数据一直持有写锁, 读操作结束之后就释放读锁。在事务 T1 执行的时候, 两次相同的查询得到的查询结果可能不同(没加读锁, 数据被事务 T2 修改了) | 发生幻读(缺乏范围锁) + 不可重复读(缺乏贯穿 T1 的读锁) |
可重复读 (Repeatable Read) | 禁止 | 禁止 | 允许 | 对事务涉及到的数据加上读锁和写锁, 未加范围锁。可能造成 T1 事务中两个完全相同的范围查询得到了不同的结果集 | 发生幻读(缺乏范围锁) |
可串行化 (Serializable) | 禁止 | 禁止 | 禁止 | 对所有事务的读写数据全部加上读锁、写锁、范围锁 | 能够保证绝对一致性 |