2021-04-24

数据库事务的原子性与隔离级别

Views: 2829 | Add Comments

数据库事务的原子性并不仅仅是指写操作, 还包括读操作. 对于写操作, 众所周知, 原子性是指操作序列中的所有操作要么全部成功, 要么全部失败. 但多次读操作未必是指要么全部读到要么全部读不到. 如果不是 Snapshot Isolation, 那么两次读操作, 可能有其中一次读到旧值, 另一次读到新值. 但是, 读操作的原子性要求必须满足因果性. 当读到新值之后, 就说明事务中的所有写操作都已完成, 显然, 后续的读操作必须读到新值, 不能读到旧值.

Snapshot Isolation 有一个最大的问题, 那便是在创建快照的时候(可能隐式创建), 如何处理 pending 状态的资源, 如果不处理, 那么同一个事务的两个资源, 在快照中可能处于不同的状态(一个已提交, 另一个未提交), 也显然违反了原子性.

对于隔离级别来说, 只有 Serializable 才是符合原子性的. Read Committed 违反了原子性, 因为大部分的数据库实现, 只判断资源的状态是否为 committed, 并没有向上追查资源所关联的事务对象(commit point)的状态, 所以会出现先读到新值, 再读到旧值的情况(见后面的例子). 这种情况违反了因果关系, 也违反了一致性.

在 Read Committed 和 Repeatable Read 之间, 应该加入一级叫 Linearizable Read, 符合线性一致性, 符合因果关系(Causality), 符合原子性.

Linearizable Read 虽然不是 repeatable 的, 但却是 read committed 的, 同时, 还具有了 Repeatable Read 所不具有的线性一致性(因果性).

考虑这样的例子:

Alice 有两个银行账户 A 和 B, A 账户有 1000 元, B 账户有 0 元. Alice 将 A 账户的 100 元转到 B 账户里, 数据库在一个事务中修改 A 和 B, 将 A 修改为 900, 将 B 修改为 100.

转账操作后, Alice 分两次先后查询 A 和 B 两个账户的余额.

由于 Alice 查询两个账户余额的操作, 是两次独立的操作, 而且是先后进行的, 所以, 系统无法针对两次操作创建一个快照, 所以出现"不一致"的问题(也即违反数据约束 - A B 总和不等于 1000). 但是, 不一致性的问题不是笼统的一种, 而是两种. 用表格表示, 即:

A B 个人资产总和
不一致1 900 0 900
不一致2 1000 100 1100

针对这两种问题, Read Committed 理论认为这是可接受的, 因为 Read Committed 不保证 Repeatable Read.

但是, 我认为, 这里的问题与隔离级别无关, 因为两次读操作是独立的, 并不在一个事务中.

在第一种场景中, 先读出 A 为 900, 基于因果关系, 事务必然已经提交, 注意, 不像某些错误的观念, 我们认为整个事务(当然同时包含 A 和 B)已经提交, 而不是仅限于 A. 既然事务已经提交, 那么基于因果关系推理可知, B 也已经被修改, 因此, 这时我们再去读 B, 绝对不允许返回 0. 否则, 就违反了事务的原子性保证.

在第二种场景中, 先读出 A 为 1000, 所以, 我们无法下结论事务是否已经提交, 所以, 无论读出 B 为 0 还是 100, 都能接受的.

所以, 针对这个问题, 我觉得不应该用事务隔离级别来解释, 而应该用因果关系来解释.

相关资料:

Related posts:

  1. Paxos和Raft读优化 – Quorum Read 和 Read Index
  2. 流式布局的原理和代码实现
  3. CocoaUI 的 CSS 样式应用算法说明和源码解析
  4. 在Linux进行IO的正确姿势
  5. 经典的 TCP socket 读取报文错误
Posted by ideawu at 2021-04-24 10:25:29

Leave a Comment