Skip to main content

参考:隔离级别、MVCC 与时钟

一份关于隔离级别、MVCC 与时钟的参考——snapshot isolation 下的 write skew、叠加在共识之上的跨 shard 提交、TrueTime 与混合逻辑时钟。

它是什么

并发的事务会触碰同一份数据,隔离(isolation) 就是关于一个事务能看到其他事务的多少内容的承诺。更强的隔离隐藏更多的异常,也需要更多的协调。MVCC(多版本并发控制)是标准的实现方式:不覆盖一行,而是保留多个带时间戳的版本,所以读者读到的是一个旧的、一致的版本,永远不会阻塞写者。在分布式数据库里,“哪个版本是当前的”取决于时钟,这就是混合逻辑时钟和 TrueTime 出场的地方。

Important

核心要点: 隔离是一个阶梯,不是一个开关,而且大多数数据库的默认级别弱于 serializable。MVCC 让读者和写者互不阻塞地运行;在分布式系统里,它还需要可比较的时间戳,这是一个时钟问题,靠有界不确定性的时钟来解决。

何时需要它

“用什么隔离级别,可能发生什么异常?“是一个标准的数据库追问,而”怎么在多个 shard 之间得到一致的快照?“是它的分布式版本。点出 write skew(写偏斜) 是资深的信号。

Note

面试信号: 大多数候选人知道脏读和不可重复读。描述 write skew(两个事务各自读到某个条件,各自写入的东西使对方的读失效),并指出 snapshot isolation 不能防止它,就把有经验的人和只会背书的人区分开了。

隔离阶梯

级别防止仍然允许典型默认值
Read uncommitted什么也不防脏读(看到未提交的数据)少见
Read committed脏读不可重复读(一个事务里两次读之间某行变了)PostgreSQL、Oracle
Repeatable read / snapshot isolation脏读和不可重复读;每个事务看到一个固定的快照Write skew 和一些幻读MySQL(InnoDB),以及 PostgreSQL 的”repeatable read”
Serializable以上全部:结果等价于某种一次一个的顺序无(代价是中止或阻塞)需要主动开启

各个数据库的术语不尽相同(PostgreSQL 的 repeatable read 就是 snapshot isolation;MySQL 的细节有别),所以要说出保证本身,而不只是名字。

算例:write skew。 一家医院要求至少有一位医生值班。Alice 和 Bob 都在值班。每人执行:读取值班医生的数量(2),看到至少是 2,于是把自己标记为下班。在 snapshot isolation 下,两个事务看到同一个快照(count = 2),谁也没写对方的行,所以两个都提交,结果是没有医生值班。Serializable 隔离(或者对读过的行显式加锁、或一个约束)可以防止它。模式是:一个事务基于一些数据做决定,而另一个并发事务在不写同样的行的情况下改变了这些数据。

MVCC

每次写入创建该行的一个新版本,并打上事务的时间戳,而不是原地更新。读者按它的时间戳读取:对每个 key,取不晚于它的最新版本。

属性效果
读者永不阻塞写者,写者也永不阻塞读者长报表不会卡住写入
一致的快照是免费的一个事务在它所有的读里看到同一个时间点
写者只在真正的写-写冲突上冲突对同一行的并发写入:一个等待或中止
旧版本会累积一个后台进程(vacuum、垃圾回收)移除没有任何事务还能看到的版本

为什么在分布式数据库里用 MVCC 而不用锁? 在单机上,读锁很便宜。跨机器时,每把锁都是一次网络对话,每个读写冲突都是一次跨节点的停顿。MVCC 把”100 个 range 的一致快照”变成”在时间戳 T 读每个 range”,读者和写者之间不需要任何协调。

跨 shard 的事务

写入多个 shard 的事务需要一个原子的决定。一种生产模式是在共识之上叠加两阶段提交:

  1. 第一次写入创建一个临时版本(write intent),并在同一个 shard 上创建一个 PENDING 的事务记录。
  2. 对其他 shard 的写入创建指回事务记录的 intent。每个 intent 本身都是经共识提交的。
  3. 提交就是一次写入:把事务记录翻转成 COMMITTED。在那个瞬间,事务被持久地、原子地决定了。
  4. 客户端得到确认;intent 被异步地重写为普通版本。遇到 intent 的读者会检查事务记录并就地解析它。

因为协议状态(intent、投票、决定)全都存放在复制的 shard 里,任何一台机器的死亡都不会让系统陷入不确定。这条记录就是这个事务。

时钟:让时间戳可比较

“不晚于 T 的最新版本”,只有当时间戳在各机器之间可比较时才有意义,而完全同步的时钟是不存在的(NTP 把时钟保持在大约几毫秒内,而不是微秒)。

方式思路代价
TrueTime(Spanner)硬件(GPS 和原子钟)把时钟不确定性限定在几毫秒;系统在提交时等待不确定性过去,让时间戳顺序与真实时间的顺序一致需要特殊硬件;每次提交都要付出等待
混合逻辑时钟(HLC)一个结合了物理时间和逻辑计数器的时间戳,加上一个不确定性窗口;读到落在其不确定性窗口内的版本的读会以更高的时间戳重启不需要特殊硬件;只在真正接近时才付出代价
时间戳 oracle一个服务发放递增的时间戳一个单一瓶颈,但简单

同一个不变量,付出方式不同:TrueTime 在每次提交时付出,HLC 只在两个事件靠得很近时付出。

Follower 读。 一个 range 的 leader 会定期发布一个 closed timestamp:承诺不会再有任何新写入落在 T 或之前。之后任何 follower 都可以在本地服务不晚于 T 的读。这让远程区域的读成为快速且略微过期的,而且是按契约如此,这是复制延迟的一种有纪律的版本。

Warning

生产实践: 在 serializable 隔离下,冲突会以客户端可见的重试(中止)形式出现。应用必须捕获可重试的错误并重新运行事务,因此事务必须是幂等的。更高的隔离不是免费的;它把成本从异常转移到了重试上。

如何选择

需求选择原因
默认的 OLTP,容忍某些异常Read committed 或 snapshot isolation开销低
跨多行的不变量(至少一人值班、不能透支)Serializable,或锁住读过的行,或一个约束防止 write skew
不阻塞写入的长报表MVCC 快照读读者永不阻塞
跨 shard 的一致快照带时间戳的 MVCC 读在 T 时刻读每个 shard
跨 shard 的原子写入叠加在共识之上的 2PC没有哪个协调者的死亡会让任何人陷入僵局
远程区域的读,可接受过期在 closed timestamp 上做 follower 读本地读,过期有界

Caution

常见错误: 以为”repeatable read”或”snapshot”就等于 serializable。它们防止了人们熟悉的那些异常,却仍然允许 write skew。如果一个正确性不变量跨越多行,要说出你需要的级别,以及你怎么强制它。

相关