工程师揭2PL锁灾难 押注SSI与单线程分区破解并发瓶颈

[Chinese] Chapter 10: Transaction part 2 — Scott Shi

Scott Shi

高并发系统应优先通过分区与队列设计消除资源争抢,因两阶段锁极易引发死锁与吞吐崩溃,而可串行化快照隔离在低争用场景下性能可较传统锁提升百倍。

AI-assisted summary of [Chinese] Chapter 10: Transaction part 2 — original source. Check the original for full context.

Source published:

Key ideas

  • 快照隔离(Snapshot Isolation)无法防御丢失更新(Lost Update)与写偏差(Write Skew),必须引入可串行化(Serializability)才能杜绝此类异常。 (5:00)
  • 实现可串行化的三条主要路径为两阶段锁(2PL)、单线程串行执行与可串行化快照隔离(SSI);其中 SSI 在低冲突场景下的吞吐量可比传统 2PL 高出 1 到 2 个数量级。 (20:00)
  • 单线程串行执行(如 Redis)要求操作极小、执行时间可预测,且全量数据必须纯内存驻留,以杜绝磁盘 I/O 导致的队列停滞。 (25:00)
  • 分布式系统的三大核心难题——全序广播(Total Order Broadcast)、分布式共识(Consensus)和线性一致 Compare-and-Set,在数学与逻辑底层是完全等价的同一个问题。 (65:00)
  • 线性一致性(Linearizability)关注的是分布式复制中单对象的新鲜度保证,而可串行化(Serializability)关注的是多对象事务的隔离性,二者概念完全正交。 (73:30)

Detailed notes

## Background 本内容由 Scott Shi 于 2021 年 1 月 5 日发布在 YouTube 平台,属于其分布式系统与数据库系列讨论课第 10 章的第二部分。视频简介中附带了配套讲解幻灯片的 Google Docs 链接供学员参考阅读。

## TL;DR 本期内容深入剖析了数据库从单机弱隔离级别向严格可串行化(Serializability)演进的技术脉络与工程代价。讲者详细对比了两阶段锁(2PL)、单线程串行执行与可串行化快照隔离(SSI)在应对丢失更新、写偏差与幻读时的权衡取舍,并结合自己在工业界亲历的生产死锁事故警示复杂锁设计的危险性。随后话题自然过渡至分布式领域,系统辨析了分布式线性一致性(Linearizability)与事务可串行化的本质差异,并指出了共识算法、全序广播与线性 CAS 之间深邃的理论等价性。

## Atmosphere & style 整场讲座充盈着硬核、务实且极富现场感的技术研讨氛围,讲者操着纯正流利的工业界技术黑话,将深奥的分布式与数据库理论讲得生动透彻。从吐槽 48 小时锁死记录的“金字塔时代封印”,到复盘长达一个月的乐观锁活死锁惨剧,叙述兼具自嘲与工程警醒;而在现场推导反例出现破绽时坦率反思、直面边界条件的真实感,更让人仿佛置身于硅谷资深工程师领衔的高阶内部架构复盘会。

## Bulletin - [05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=300s) 快照隔离(Snapshot Isolation)无法防御丢失更新(Lost Update)与写偏差(Write Skew),必须引入可串行化(Serializability)才能杜绝此类异常。 - [20:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1200s) 实现可串行化的三条主要路径为两阶段锁(2PL)、单线程串行执行与可串行化快照隔离(SSI);其中 SSI 在低冲突场景下的吞吐量可比传统 2PL 高出 1 到 2 个数量级。 - [25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1500s) 单线程串行执行(如 Redis)要求操作极小、执行时间可预测,且全量数据必须纯内存驻留,以杜绝磁盘 I/O 导致的队列停滞。 - [01:05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3900s) 分布式系统的三大核心难题——全序广播(Total Order Broadcast)、分布式共识(Consensus)和线性一致 Compare-and-Set,在数学与逻辑底层是完全等价的同一个问题。 - [01:13:30](https://www.youtube.com/watch?v=crJJovKcuqw&t=4410s) 线性一致性(Linearizability)关注的是分布式复制中单对象的新鲜度保证,而可串行化(Serializability)关注的是多对象事务的隔离性,二者概念完全正交。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=0s) 现代系统设计中 ACID 的 Consistency 偏向应用层业务契约,与分布式系统讨论的副本一致性有本质区别。 - [05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=300s) 丢失更新(Lost Update)发生时并发线程读取的都是真实提交的版本,因此快照隔离本身无法防御该并发写入异常。 - [10:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=600s) CAS 在快照隔离下会因为事务视图始终一致而隐蔽失效,无法感知外部并发修改;CRDT 则因严格的代数结合律与墓碑开销无法作为通用事务解决方案。 - [15:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=900s) 写偏差(Write Skew)发生在事务基于先前的查询判定前置条件后修改不同对象,导致单行锁完全无能为力。 - [25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1500s) 单线程串行架构严禁交互式事务(Interactive Transaction),所有操作必须打包成存储过程一次性提交以防整个引擎被阻塞。 - [28:48](https://www.youtube.com/watch?v=crJJovKcuqw&t=1728s) 两阶段锁(2PL)作为严格串行化的标准实现,通过读写互斥带来了严重的性能断崖并容易引发死锁。 - [32:48](https://www.youtube.com/watch?v=crJJovKcuqw&t=1968s) 谓词锁(Predicate Lock)由于判定逻辑过于复杂难以落地,工业界主流采用索引区间锁(Index-Range Lock)来解决幻读与写偏差。 - [36:31](https://www.youtube.com/watch?v=crJJovKcuqw&t=2191s) 复杂的锁机制会带来沉重的长期维护负担与排查噩梦,优秀的架构设计应通过请求排队与异步解耦规避并发资源争抢。 - [50:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3000s) 可串行化快照隔离(SSI)基于乐观并发控制,将整个事务判定的裁判逻辑压缩在 Commit 的时间节点上,实现先到为胜、后到 Abort。 - [55:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3300s) PostgreSQL 和 FoundationDB 原生支持成熟的 SSI 机制,而 MySQL 与 MongoDB 并不支持原生的可串行化快照隔离。 - [01:00:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3600s) 分布式一致性问题的根本源泉在于数据副本复制(Replication)与物理世界的不可靠时钟、进程暂停及无界网络延迟。 - [01:05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3900s) 全序广播、分布式共识与线性一致 CAS 具有等价性,攻克其中任意一个即可解决其他两个问题。 - [01:10:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=4200s) 线性一致性(Linearizability)提供全局时钟下的最新性保证(Recency Guarantee),满足该特性的系统对于外部客户端表现得如同单机副本。 - [01:20:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=4800s) AWS SQS 的 SLA 仅为 99.9%,通过消息不可见超时(Visibility Timeout)和显式调用 Delete 机制来防止任务消费失败丢失。 - [01:25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=5100s) 在支付等高可靠系统中,采用基于分区(Partition)的单 Worker 独占消费模式可以从架构根源上规避错综复杂的并发冲突。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=0s) ACID 的现实认知与弱隔离级别回顾

在正式切入本期主题前,讲者先串联了上周讨论过的事务(Transaction)核心概念,并规划了后续的进度安排。事务作为单机乃至分布式系统的基石,常被冠以 ACID 的名号,但细究之下,ACID 在现代系统设计中已经是一个略显过时的框架: - **Atomicity(原子性)**:实质上关注的是可中止性(Abortability),即事务在遭遇错误或故障时能否安全中止并回滚,不留下半成品数据。 - **Durability(持久性)**:主要是保障数据落盘(Disk)以及通过预写日志(Write-Ahead Log / WAL)来实现。WAL 将复杂的写操作在逻辑上收敛为一个确定的点(Point)——在该点提交之前,持久性不提供任何保证;一旦越过该点,系统便承诺数据不会丢失。 - **Consistency(一致性)**:在 ACID 中它其实偏向于应用层或客户端的契约,与分布式系统讨论的共识(Consensus)与数据副本一致性有着本质区别。

随后,话题聚焦于隔离性(Isolation)。不同隔离级别(Isolation Level)用于解决单对象或多对象并发读写时的异常现象。最基础的如防范半写入(Half-written data,例如一个 10KB 大小的 JSON 写入到一半发生崩溃),而一旦进入并发读写,问题便复杂起来: - **Read Uncommitted(读未提交)**:允许事务读到尚未提交的脏数据。一个典型的反面模式是邮件系统反范式化(Denormalization)设计:将邮件列表和未读数(Unread Email Count)拆为两个字段存储。如果允许读未提交,用户就会读到数量与列表对不上的数据不一致状态。 - **Dirty Read 与 Dirty Write(脏读与脏写)**:脏写尤其致命,会导致不同事务的写操作混杂在一起,例如一个房产 Listing 的所属人和发票(Invoice)分别写入了两个不同买家的信息,造成极其离奇的业务错误。值得一提的是,像 DynamoDB 这样的 NoSQL 数据库在默认情况下并不提供防范此类异常的高隔离级别,而是依托 Quorum 多数派投票机制来决定采纳哪份数据。

[05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=300s) 快照隔离(Snapshot Isolation)与丢失更新(Lost Update)

越过读未提交和读已提交后,系统常采用快照隔离(Snapshot Isolation,借助多版本并发控制 MVCC 实现)。在 MVCC 下,记录就像一条持续追加的历史日志,系统通过事务 ID 判断哪些版本的数据对当前事务可见(Visible),哪些不可见。这对于耗时极长(几十分钟甚至半小时)的分析型只读查询(Analytic Read)非常关键,它将长达数十分钟的时间跨度在逻辑上压缩进了一个一致的时间切片(例如严格定格在事务 2 提交之后、事务 3 提交之前),从而避免读到诸如“银行转账中途凭空蒸发 100 块钱”的多对象读取不一致。然而,快照隔离并非万灵药。

讲者以自己研究生时期与同学开发“MOBA 游戏英雄推荐系统(Game Recommendation Machine)”的真实踩坑经历,说明了并发写所面临的新挑战:当时为了给选人算法喂数据,系统需要爬取 DotA / LoL 约 50 万场对局的 Replay 数据(单份约 20KB)。在多节点(8 个 Node)、多线程并发解析写入计数器(Counter)时,经典的**丢失更新(Lost Update)**发生了: - 线程 A 爬完数据,读取计数器当前值为 42; - 线程 B 同时爬完,也读到计数器为 42; - 线程 A 将其递增为 43 并写回; - 线程 B 同样递增为 43 写入,直接覆盖了线程 A 的更新。

此问题根本无法被快照隔离防御,因为并发线程并没有读到未提交数据,快照隔离读出来的 42 就是当时已提交的真实版本(甚至是陈旧数据 Stale Data)。

针对丢失更新,行业内常见的应对手段各有利弊: 1. **数据库原生原子操作(Atomic Write / Increment)**:交由底层执行原子加 1,最优雅省心。 2. **悲观显式加锁(Locking)**:极其强大但也极其危险。讲者提到了自己在 Salesforce 时的真实案例:当时一位极其资深的性能工程师排查故障,抓到一个事务把某张核心数据表锁了整整 48 个小时。“从计算机科学的角度来看,这感觉简直就像从金字塔时代、公元前就把这行记录封印住了,直到今天都没解冻,成了千古谜团。”更糟糕的是,如果这些逻辑写在存储过程(Stored Procedure)里,不仅缺乏完善的 Metrics 监控和链路追踪(Logging),还没有健全的 Code Review 机制,排查是谁触发了这桩长达 48 小时的锁灾难简直就是工程师的噩梦。

```mermaid flowchart TD accTitle: 并发计数器丢失更新问题 accDescr: 展示两个并发线程同时读取并写入计数器时导致一次累加丢失的过程。

subgraph Timeline ["并发执行时间线"] direction TB subgraph ThreadA ["线程 A (爬虫 Node 1)"] A1["读取 Counter = 42"] A2["本地计算 42 + 1 = 43"] A3["写入 Counter = 43"] end

subgraph ThreadB ["线程 B (并发爬虫)"] B1["读取 Counter = 42"] B2["本地计算 42 + 1 = 43"] B3["写入 Counter = 43 (覆盖更新)"] end end

A1 --> A2 --> A3 B1 --> B2 --> B3 A3 -.->|"丢失一次递增计数"| B3 ```

[10:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=600s) CAS 的快照失效陷阱与无冲突复制数据类型(CRDT)

如果数据库不支持自动检测丢失更新(Automatically Detect Lost Updates),程序员往往会退回到客户端层面的 **CAS(Compare-and-Set)**。然而,CAS 遇到快照隔离会出现隐蔽失效:因为快照隔离在设计上就会保证事务内部看到一致的视图,在同一个事务生命周期内反复读取,读到的始终是最初快照下的 42;无论外界如何推进,该事务无法感知到底层数据已被并发修改,导致 CAS 判定出现逻辑偏差。

另一种前沿方案是 **CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)**,广泛应用于像 Google Docs 这种多人协同编辑系统(Collaborative Editing)。但深入剖析后会发现,CRDT 具有相当严苛的局限性: - **内存与存储开销极高**:在协同文档中,文本中的每一个字符(Character)本质上都被建模为一个 CRDT 实体。为了支持多人随时并发打字、撤销与删改,系统不仅要记录每次插入和属性变更,甚至连删除(Deletion)本身都必须保留墓碑标记(Tombstone),导致内存使用量迅速膨胀。 - **数据模型高度受限**:CRDT 只适用于具有结合律和交换律(Commutative Operations)的特定数学模型,例如满足 $A + B = B + A$ 或 $A + B + C = C + B + A$。常见能被 CRDT 良好支持的仅有 Counter、无序集合(Unordered Set)等极少数类型。 - **Counter 的底层向量实现**:为了实现一个支持增减的无冲突计数器,系统往往需要维护两个基于节点 ID 的向量(Vector):Addition Array 与 Remove Array。节点 1 累加则在自己的 Addition 槽位加 1,节点 2 递减则在自己的 Remove 槽位记 1,各节点独立写入互不冲突。最终结果通过全量求和并计算两者的差值导出: $$\text{Total} = \sum \text{Addition Array} - \sum \text{Remove Array}$$

这种严格的数学约束证明了 CRDT 无法作为通用的事务冲突解决方案。

[15:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=900s) 写偏差(Write Skew)与幻读(Phantom Read)

讲者随后引入了更复杂的并发异常——**写偏差(Write Skew)与幻象写入(Phantoms)**。为了具象化该场景,讲者给出了医院值班医生系统的经典案例:

```mermaid flowchart LR accTitle: 医生值班系统写偏差示意 accDescr: 展示两个并发事务同时检查并退班,最终导致值班人数归零的安全约束违背。

subgraph InitialState ["初始状态"] D["Alice: On Call<br/>Bob: On Call<br/>Carol: Off Call"] end

subgraph ConcurrentTx ["并发写操作 (Read-Modify-Write)"] T1["事务 1 (Alice 申请休假):<br/>1. 检查 On Call 数量 (读出 2,满足 >= 2)<br/>2. 将 Alice 置为 Off Call<br/>3. Commit"] T2["事务 2 (Bob 申请休假):<br/>1. 检查 On Call 数量 (读出 2,满足 >= 2)<br/>2. 将 Bob 置为 Off Call<br/>3. Commit"] end

subgraph FinalState ["破坏安全约束 (Safety Broken)"] FAIL["所有人均离岗 (On Call = 0)"] end

InitialState --> T1 InitialState --> T2 T1 --> FAIL T2 --> FAIL ```

系统硬性要求“必须保证至少有一名医生处于值班(On Call)状态”。此时 Alice 和 Bob 都在值班: 1. Alice 提交退班请求,事务先查询在岗人数(返回 2,大于等于 2),认为自己退出后仍有人兜底,于是将自己的状态改为 Off Call; 2. 恰在同一时刻,Bob 也在手机端申请退班,同样读出当前在岗人数为 2,遂将自己的状态也改为 Off Call; 3. 两个事务各自顺利提交。结果是值班室空无一人,系统的 Safety 承诺被彻底打破(Safety Broken)。

这种异常的棘手之处在于: - **不是脏写**:双方读取的都是已提交的数据。 - **不是丢失更新**:Alice 修改的是 Alice 这行记录,Bob 修改的是 Bob 这行记录,两者更新的是完全不同的对象(Distinct Objects)。 - **单行锁无能为力**:只对 Alice 或 Bob 这一行记录加锁完全阻挡不了对方事务的推进。 - **模式本质**:属于典型的“读取-修改-写入(Read-Modify-Write)”。事务先带着某种业务前置假设(Expectation)进行查询,然而由于并发写入,在执行写入时,当初读出来的判定前置条件已经被外部并发进程悄然破坏。

类似的问题在工业界随处可见: - **会议室预订系统(Meeting Room Booking)**:查询会议室在某时间段为空,两个用户同时发起预订。 - **多人在线网游(Multiplayer Game)**:两个玩家操控各自的角色,同时尝试拾取并占有地图上的同一处稀有资源。 - **唯一性约束与抢注**:注册系统查询用户名或域名未被占用,随后两名用户并发申请注册。 - **防双花(Double Spending)**:检查用户钱包余额或信用额度足够,然后并发扣款。虽然在打车软件(如连续叫 10 辆 Uber)等业务中更倾向于采用幂等键(Idempotent Key)或接受透支为负债来处理,但在强一致性金融场景下依旧严峻。

[20:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1200s) 最强隔离级别:可串行化(Serializability)与单线程串行执行

面对写偏差与幻读,现存隔离级别已全部失效,唯一的终极手段是采用最强隔离级别——**可串行化(Serializability)**。 所谓可串行化,就是虽然系统底层有成百上千个事务在并发执行,但其最终落库的业务结果必须与“这些事务一个接一个排队串行执行(Executed one by one)”的效果完全等价。这就好比感恩节或新春大抢购(Black Friday Rush),店门外人山人海、摩肩接踵涌入,但从收银台的视角看,所有人必须规范地排成单列纵队逐一结账。

在数据库历史上,实现可串行化主要有三条技术路线: 1. **两阶段锁(Two-Phase Locking, 2PL)**:从 1970 年代起统治了数据库领域数十年,属于重型悲观机制。 2. **实际串行化执行(Actual Serial Execution)**:完全摒弃并发争抢,采用单线程模型顺序跑完写入(如 Redis、VoltDB)。 3. **可串行化快照隔离(Serializable Snapshot Isolation, SSI)**:近十几年(约 2006~2008 年提出)崭露头角的乐观并发控制(OCC)算法,已被 PostgreSQL 和 FoundationDB 等系统采纳,在具备出色一致性的同时兼顾了高并发读写性能。

深入到实际串行化执行模式(Actual Serial Execution)时,系统往往在一个 Single Thread 内消化所有写操作: - **排队进门机制**:就像抢购进店前设立的安检通道,不管外部并发调用多高,在到达内核写逻辑前一律在单一队列(Single Queue)中排队,彻底消除线程上下文切换(Context Switch)和资源踩踏。 - **禁止交互式事务(Interactive Transaction)**:这是单线程执行模式的绝对死穴。在商场排队安检时,绝不允许有人走到队伍最前面突然说“我忘带钱包了,你们都在这等我回去拿完钱包再放行”。如果在单线程数据库里开一个交互式事务,等待网络另一端的客户端想清楚了再发下一步 SQL,整个数据库的写引擎就会被活活卡死。因此,该模式必须要求客户端将所有操作打包成一次性提交或存储过程执行。

[25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1500s) 交互式事务的弊端与单线程内存模型的苛刻要求

在交互式事务(Interactive Transaction)的模式下,客户端通常会经历一套漫长的流程:先从数据库读取数据,在应用层进行复杂的业务运算,甚至还要调用外部系统拉取数据进行多方核对,确认无误后才把数据写回数据库。这种模式会导致事务长时间处于开启(open)状态。这就像你去超市排队买单,眼看马上就轮到你进门结账了,你却突然跟收银员说“我要回家拿个钱包”,让后面排队的所有人原地傻等,整个系统的吞吐量和时间效率瞬间跌至冰谷。为了解决这一痛点,现代高性能单线程架构(如 VoltDB 或 Redis)彻底摒弃了交互式事务,转而要求所有操作必须以存储过程(Stored Procedure)的形式提交——事务一次性打包送入系统,一气呵成地执行完毕,任何后续逻辑都必须拆分为独立的新事务。

如果系统的写吞吐量(Write Throughput)超越了单线程所能承载的物理极限,架构上就必须进行分区(Partition)。在每一个独立的分区内部,依然严格维持“单线程写入”(Single-thread write),但读取(Read)则完全放开,允许几十、上百甚至上千个线程并发读取。这意味着当单机写性能无法横向扩展时,必须基于写入特征对数据进行精细分区。同时,单线程事务模型对操作本身提出了近乎苛刻的要求:

```mermaid flowchart TD accTitle: 单线程事务架构的核心前提 accDescr: 展示保障单线程高性能执行的四个关键技术约束:存储过程、小而快的操作、内存驻留以及分区扩展。 subgraph Engine["单线程执行引擎"] direction TB SP["存储过程批量提交<br/>(拒绝交互式等待)"] SF["事务必须小而快<br/>(杜绝长尾 SLA 阻塞)"] RAM["数据纯内存驻留<br/>(微秒级访问时延)"] end subgraph Scaling["高吞吐扩展方案"] PART["基于 Key 的数据分区<br/>(单分区单线程写,多线程自由读)"] end SP --> SF SF --> RAM RAM -.->|写吞吐超限时| PART ```

- **事务必须小而快(Small and Fast)**:正如排队结账时前面突然插进来一个四五百磅的大胖子,走路慢吞吞,一个人霸占了正常人 10 倍、50 倍甚至 100 倍的时间,后方所有人的 SLA 都会被拖垮几百毫秒。在计算机并发世界里,慢事务与快事务的性能差距往往不是几倍,而是成千上万倍,一个慢操作就能让整个队列的延迟暴增好几秒。 - **数据集必须完全驻留内存(In-Memory)**:内存与磁盘的访问延迟差距悬殊。从旧金山开车到萨克拉门托(不到 100 英里),好比访问内存的时间开销;而访问磁盘的延迟,就如同“从旧金山直接飞到火星”。单线程架构一旦发生磁盘 I/O,整个系统就会彻底卡死,因此像 Redis 这样的系统必须要求全量数据集 fit into memory。 - **单事务指标可预测(Predictable)**:单次写入的性能必须足够可控,即 "small, fast enough and predictable"。这种理念在 Redis(2014-2015 年前后)等现代数据库中得到广泛应用。在底层演进动辄停滞五六十年的数据库领域,这类单线程内存架构属于过去十年里极为前沿的技术突破。

---

[28:48](https://www.youtube.com/watch?v=crJJovKcuqw&t=1728s) 两阶段锁(2PL)机制与其沉重的性能代价

从学术教材和历史统治地位来看,提到事务隔离级别(Isolation Level),核心往往绕不开两阶段锁(Two-Phase Locking, 2PL)。两阶段锁也是实现严格可串行化(Strict Serializability)的标准机制,其核心将锁划分为共享的读锁(Non-exclusive / Read Lock)与排他的写锁(Exclusive / Write Lock)。多个读操作可以并发持有同一个对象的读锁,但一旦有事务要执行写入,就必须获取排他锁——该对象上不仅不能有其他写锁,甚至连任何未释放的读锁都不能存在。如果允许读写互不阻塞,系统就会退化为快照隔离(Snapshot Isolation),进而再度引发写偏差(Write Skew)或幻读问题。

然而,2PL 极高的安全保证背后是极其沉重的性能惩罚(Performance Penalty)。如果某个持有排他写锁的事务由于业务逻辑或系统故障拖延了 48 个小时,那么在这整整两天里,没有任何其他事务能够读取该行数据,该数据对于外部服务和客户而言无异于处于完全“死掉”(dead)的状态。更为严重的是,排他锁与等待机制极易触发死锁(Deadlock)。在操作系统原理中,死锁发生的四个必要条件(互斥、占有且等待、不可抢占、循环等待)在数据库两阶段锁中完全适用。数据库必须内置死锁检测机制,并在死锁成环时强行中止(Abort)其中一个事务,在多个竞争者中判定出 Winner 和 Loser。相比快照隔离等机制,2PL 的吞吐量和响应时间可能会出现数个数量级(几十倍甚至上百倍)的断崖式下跌。

---

[32:48](https://www.youtube.com/watch?v=crJJovKcuqw&t=1968s) 谓词锁(Predicate Lock)的理想与索引区间锁(Index-Range Lock)的工程折衷

在现实业务中,2PL 面临着一个经典的死穴:如何锁定一个“尚不存在”的对象?例如在会议室预订系统中,Scott 准备预订今晚 7 点到 8 点的会议室 A。在预订记录表(Appointment Table)中,这条数据根本还没有被插入,因此没有任何实体行(Materialized Row)可以挂上排他锁。如果此时另一个用户 Brad 也尝试在同一时段预订该会议室,由于读不到现成的行,传统的行级锁彻底失效。若为了防止冲突而对整张表加排他锁,系统的并发性能将彻底崩溃。

```mermaid flowchart LR accTitle: 谓词锁与索引区间锁的冲突控制对比 accDescr: 对比谓词锁只锁定逻辑条件与索引区间锁扩大范围锁定索引 B-tree 叶子节点的工程实现。 subgraph Predicate["谓词锁(理论完美,工程难以实现)"] direction TB P_Cond["锁定逻辑条件:<br/>Room='A' & Time=[19:00, 20:00]"] P_Row["仅约束目标时间段的虚幻数据<br/>无实体行,元数据开销与检测极其高昂"] end subgraph IndexRange["索引区间锁(工业界主流折衷方案)"] direction TB I_Index["锁定包含时间的索引页/索引范围<br/>Key: Room_A_19:00~20:00"] I_Broader["锁定了更宽泛的区间范围<br/>可能连带阻塞无害写入,但开销低且易实现"] end Predicate -.->|工程落地演化| IndexRange ```

为了解决这种幻读和写偏差问题,理论上提出了**谓词锁(Predicate Lock / 条件锁)**。谓词锁不针对具体存在的行,而是锁定一个查询条件(例如:`WHERE room = 'A' AND time BETWEEN 19:00 AND 20:00`)。在事务执行期间,任何其他事务若试图写入或修改符合该谓词条件的数据,都会被强行拦截。然而在工程实践中,由于谓词条件的判定开销过大、条件匹配极其繁琐,纯粹的谓词锁性能极差,几乎没有主流数据库能高效实现它。

工业界最终采取的折衷方案是**索引区间锁(Index-Range Lock)**。以经典的医生值班问题(Doctor Problem)为例,原本的谓词锁只需精确锁定“明天晚上值班且 `on_call = true`”的医生;而采用索引区间锁时,数据库直接在“明天晚上的值班时间段”这一索引项或索引区间上加锁。这样一来,虽然锁定的判定范围被稍微放大了(可能会把整个时间段甚至整个医生排班表的一整片区间全部锁死,连带阻止了部分原本无害的写入),但它能够直接复用现有的 B-Tree 索引结构。这种方案在铁路订票、会议室抢占等高并发竞态系统中被各大成熟数据库普遍采用。

---

[36:31](https://www.youtube.com/watch?v=crJJovKcuqw&t=2191s) 工程哲学:宁写简单平庸的代码,也绝不轻易碰复杂的锁

从资深工程师的视角来看,数据库提供的悲观锁、乐观锁以及复杂的区间锁功能固然强大,但在真实的工业级系统设计中,第一准则是**“尽一切努力不要用到它”**。编写复杂锁逻辑的工程师在写下代码的那一刻,头脑里或许对各种 Race Condition、边界条件(Edge Cases)推演得无比透彻,甚至写了上百行注释来阐释锁状态的流转。然而,代码写完后,后续负责维护系统的往往是刚进公司的新人(New Guys)或者其他交接者。在长期的维护周期中,后人根本不敢动这些满是锁机制的代码,任何细微的逻辑改动都可能引发线上不可预测的死锁或并发 Bug。

一个负责任的工程师应当追求“Simple is always best”。很多时候宁可牺牲微小的性能或写一个看起来稍显平庸但极易理解的算法,也强过构筑一个精巧却脆弱的高复杂度架构。以 12306 火车票抢票系统为例,面对海量并发,最优雅的解法绝不是在数据库底层大搞行级或区间级的复杂竞争修改,而是将请求彻底解耦: - 前端购票请求进来后,直接作为不可变行(Append-only Row)快速插入队列; - 用户的请求瞬间排队完成,不需要就地更新座位库存或争夺互斥锁; - 后台 Worker 进程以单线程或可控批量的形式,从排队列表中拉取数据并在后台离线分配座位。

一旦引入单机索引区间锁就已经极其烧脑,如果系统演化为分布式系统(Distributed System),再叠加上多副本复制(Replication)与网络分区,各种死锁与状态不一致的“鬼故事”(Ghost Stories)就会层出不穷。

---

[40:40](https://www.youtube.com/watch?v=crJJovKcuqw&t=2440s) Salesforce 生产事故复盘:一次自研 Airflow 的乐观锁死循环

为了具象化锁机制失控带来的灾难,讲者分享了自己在 Salesforce 亲历的一起真实生产事故。当时团队自研了一套类似于 Apache Airflow 的任务调度引擎,整套数据处理链路如下:

```mermaid flowchart TD accTitle: Salesforce 生产环境乐观锁活死锁事故流程 accDescr: 展示前端重复发送两个请求如何触发乐观锁互斥 Abort 并在后台定时重试中陷入长达一个月的死循环。 subgraph Client["客户端异常行为"] FE["前端未知 Bug 重复发包"] --> R1["Dataflow Request 1"] FE --> R2["Dataflow Request 2"] end subgraph Nodes["集群处理节点"] R1 --> N1["Node A 转化为 Instance 1"] R2 --> N2["Node B 转化为 Instance 2"] end subgraph DB["底层列式分析数据库"] N1 & N2 --> Collision{"冲突检测引擎<br/>(发现两实例同时写同一目标 Dataset)"} Collision -->|谁先谁后未知| AbortBoth["双方均触发 Abort<br/>(两个任务双双失败中断)"] end subgraph Background["后台重试机制"] AbortBoth --> Timer["静默 5~10 分钟"] Timer --> N1 & N2 end ```

用户提交一段包含 DAG(有向无环图)的 JSON 请求(Dataflow Request),后端解析该 JSON 并在集群节点上转换为具体的执行实例(Dataflow Instance),最终运算并将数据写入自研的底层列式分析数据库(Analytic Database)。

由于单个分析任务往往涉及庞大的数据集转换,执行一次耗时十多分钟、半小时甚至数天,如果两个实例同时向同一个目标 Dataset 写入,就会导致数据文件彻底损坏(Corruption)。为了保障数据一致性,架构设计上采用了乐观并发控制(Optimistic Locking): 1. 实例初始化并准备落盘时,查询数据库校验是否有其他并发实例正试图写入相同的 Dataset; 2. 如果检测到写入冲突(Write Collision),由于系统无法从逻辑上裁定到底谁先谁后,设计团队采取了“宁可错杀”的保守策略——**双方同时中止(Abort Both)**; 3. 系统设置了一个后台守护进程(Background Process),每隔 5 到 10 分钟巡检一次,如果发现这批未被执行完毕的 Request,就会重新唤醒实例并再次执行。

理论上,由于该服务面向 2B 企业级大客户,并发冲突概率极低,这套方案看似坚不可摧。然而不久后,系统里出现了一个诡异的极端案例:某企业客户配置了一个将数据集转化为 Dashboard 的分析任务,但这个任务在长达一个月的时间里**从未成功执行过哪怕一次**,一直死死卡在队列中。

讲者翻遍了 Splunk 和 Datadog 过去 30 天的系统日志,经过极其痛苦的排查,终于揪出了元凶: - 该客户的前端页面存在一个未知的防抖失效 Bug,用户每次点击提交,前端都会同时发出两模一样的重复网络请求(Duplicate Requests); - 这两个请求被分别路由到了 Node A 与 Node B,各自生成了 Instance 1 与 Instance 2; - 由于两个实例处理速度完全一致,它们几乎在同一微秒准备写回同一个 Dataset,乐观锁机制随即精准触发碰撞,将两个实例全部 Abort; - 5 到 10 分钟后,后台守护进程重新拉起这两个未完成的请求,两者再次以完全同步的节奏并发执行、再度碰撞、双双 Abort; - 如此往复,每 10 分钟触发一次 Race Condition 并 Abort,整整死循环了一个月。

最终团队通过在接入层引入请求去重(Deduplication)机制才化解了危机。这起事故充分证明,并发控制不仅会让排查成本呈指数级上升,而且对于新入职的工程师(New Grad)而言,理解复杂的实例转化、冲突检测与日志链路简直是一场“极其痛苦的 Onboarding 噩梦”。

---

[48:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=2880s) 悲观与乐观的极端:从 Redis 单线程到可串行化快照隔离(SSI)

回顾事务隔离的演进,悲观锁与乐观锁代表了两种截然相反的设计哲学: - **悲观锁(Pessimistic Locking)**:秉持“总有人想害朕”的防御心态,在执行任何读写操作前,先把资源死死锁住,直到整个事务彻底提交或回滚才释放。两阶段锁(2PL)是在行级和区间级贯彻这一思想;而 Redis 的单线程串行执行(Serialized Execution),则更是将悲观锁发挥到了极致——一个事务直接独占整台机器的整个数据库实例,彻底杜绝任何并发冲突的物理可能。然而,如果业务场景中 99.99% 的写入根本没有资源争抢,悲观锁带来的加锁排队与上下文切换开销将严重拉低吞吐量。 - **乐观锁(Optimistic Locking)与 SSI**:秉持“天下太平”的乐观预期,事务执行期间各跑各的,互不加锁阻塞;直到事务准备提交的最后一刻,系统才对是否存在冲突进行统一核验。学术界与工业界最现代化的方案便是**可串行化快照隔离(Serializable Snapshot Isolation, SSI)**。SSI 在保留快照隔离高并发、读写互不阻塞优势的同时,通过追踪事务间的读写依赖来在提交时精确检测写偏差,一旦发现不可串行化的异常拓扑才选择性地 Abort 冲突事务。

归根结底,如果一个系统的数据库在实际运行中频繁遭遇锁冲突或频繁被乐观回滚,往往说明系统的底层数据模型(Data Model)或数据流转链路(Data Flow)在顶层设计上就已经出现了严重偏差,应当优先从业务切分与异步化架构层面寻找出路,而非一味依赖更为复杂的锁机制。

[50:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3000s) 乐观并发控制与可串行化快照隔离(SSI)的诞生

在应用程序架构层面,开发者应当尽量减少对同一个资源的争抢。从概率角度来看,现代系统中大部分业务场景下对同一资源的冲突概率非常低,往往只有千分之一、万分之一甚至十万分之一。如果一味采用悲观锁(Pessimistic Locking),系统的吞吐量和性能就会大打折扣。因此,业界提出了可串行化快照隔离(Serializable Snapshot Isolation, 简称 SSI),其底层本质是一种乐观并发控制(Optimistic Concurrency Control, OCC)。乐观锁的核心逻辑非常直观:“我写的时候是不加锁的,直到写完准备提交时,我再检查有没有别人修改了我读写过的数据;如果有人改动过且对方已经提交(committed),那我就主动中止(abort)自己的操作并进行重试。”由此可见,选择悲观锁还是乐观锁取决于资源的争用率(contention rate):如果事务耗时长、事务范围大、并发争用非常激烈,就必须老老实实用悲观锁;反之,若在数据模型设计之初就已经规避了冲突,争用率极低,那么使用乐观并发控制就能带来极大的性能收益。

讲师以之前讨论过的医生值班(On-call)案例来类比生产环境中的数据流碰撞。在之前的工程实践中,由于不同物理机器的时钟存在偏差、无法绝对比对先后顺序,一旦发现两个并发请求同时尝试修改同一个 on-call 数据集发生冲突,粗暴的做法是“两个请求全部 abort 掉”。而 SSI 算法在此基础上做了显著优化——它不仅能检测到冲突,还能判定出一个“赢家(winner)”和一个“输家(loser)”,赢家顺利提交,只有输家需要 abort。具体到医生值班的场景:事务 42(Transaction 42)想把自己从 on-call 列表中撤销(从 true 改为 false),在它提交时,系统检查发现并没有其他并发事务与之争抢资源,它的假设条件(“系统中至少有两人值班”)依然成立,因此事务 42 顺利提交。与此同时,事务 43 也发起了同样的下班请求,它在提交时检测到事务 42 正在争夺并已成功修改了相同资源。既然事务 42 抢先一步提交,打破了事务 43 读取时的前置假设,事务 43 便被判定为争夺失败的输家,必须 abort 并等待之后重试。

- **悲观锁适用场景**:单资源争用率高(High Contention Rate)、大事务、长事务。 - **乐观锁适用场景**:争用概率极低(如千分之一至十万分之一)、读多写少、模型已做去冲突设计。 - **SSI 的优化目标**:告别“双双中止”,在并发冲突中挑选出 Winner 予以提交,仅让 Loser 中止并重试。

```mermaid flowchart TD accTitle: SSI 事务提交判定流程 accDescr: 展示两个并发事务竞争同一资源时,基于提交时间点判定胜者提交、败者中止的过程。

subgraph T42["事务 42 (先到达 Commit)"] A["读取数据 (至少 2 人在班)"] --> B["执行修改 (Alice -> false)"] B --> C{"检测是否有冲突事务已提交?"} C -- "否 (无冲突提交)" --> D["Commit 成功 (Winner)"] end

subgraph T43["事务 43 (后到达 Commit)"] E["读取数据 (基于旧快照)"] --> F["执行修改 (Bob -> false)"] F --> G{"检测是否有冲突事务已提交?"} G -- "是 (事务 42 已提交)" --> H["Abort 并重试 (Loser)"] end

D -. "更新资源版本并通知".-> G ```

[55:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3300s) 将事务压缩为一个时间点与数据库的 SSI 实现

有人在讨论中提问:“事务的开始时间(Start Time)对于最终谁能提交重要吗?”讲师明确回答:“不 matter,完全不重要。”无论事务 43 是在事务 42 之前还是之后开始执行,哪怕事务 43 先发起并先完成了一堆读写操作,只要事务 42 先一步进入提交阶段并顺利完成,数据库就会在那个瞬间确立事务 42 的胜者地位。SSI 的精妙之处,就在于把跨越很长区间的事务“压缩成了一个时间点”——即执行 Commit 的绝对瞬间。在单机数据库底层,处理事务提交的核心逻辑往往是由单个线程(或单点排队逻辑)有序处理的,因而在 Commit 这个具体的时间节点上绝不可能发生冲突。谁的时间点排在前面,谁就是 Winner;排在后面的事务一旦检测到重叠的数据范围(Key-range)已被先到者改写并提交,就只能全盘 abort。

在工程实现上,SSI 依赖于追踪事务的读写依赖关系。例如事务 43 在访问某个 Key-range 时会留下记录,当事务 42 把值从 true 改写为 false 时,底层机制能够捕捉到该资源已被其他并发事务所关注;当事务 42 抢先提交完成,事务 43 随后尝试 Commit 时就会被明确通知冲突失败,必须 abort。讲师指出,虽然学术界给出了这种逻辑,但工业级实现的手段各有差异。Serializable Snapshot Isolation 是一项相对较新的技术成果,最早于 2008 至 2009 年左右由相关学术论文(如 Michael Cahill 等人)提出,真正落地到工业级开源产品中不过数年时间。

从数据库生态来看,PostgreSQL 在较新版本中对 Serializable Snapshot Isolation 提供了极其成熟的原生支持;苹果开源的分布式键值库 FoundationDB 也采用了类似的高性能并发控制机制。相比之下,MySQL 的特性相对保守稳健,并没有实现真正的 SSI(其可串行化仍主要依赖两阶段锁 2PL 实现),而 MongoDB 等常见文档型数据库同样缺乏这一特性。讲师总结道,只要搞清楚乐观锁的本质——“先尝试做,提交时比拼时间点,先到为胜,后到 abort”,就能明白为何它在单资源争夺率低的系统中拥有统治级的性能优势,而一旦争用率暴增,大量的冲突与连续 abort 重试也会导致其效率严重下滑。

- **时间点压缩原理**:事务虽然包含开始时间、大量操作与结束时间,但 SSI 将整个事务的决定性隔离裁判压缩在“Commit”这一个时间点上。 - **数据库支持现状**:PostgreSQL 与 FoundationDB 均支持 SSI;而 MySQL 与 MongoDB 并未原生支持可串行化快照隔离。 - **学术与工业背景**:该理论发端于 2008-2009 年的学术论文,在主流开源关系型数据库中属于近年来最前沿的隔离级别实现之一。

[01:00:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3600s) 从单机事务迈向分布式:一致性(Consistency)与共识(Consensus)的源泉

随着单机事务章节的告一段落,讨论正式进入全书最核心也最复杂的主题——分布式系统中的一致性(Consistency)与共识(Consensus)。讲师特别强调,这门课第九章讨论的“一致性”,与前一章 ACID 事务中的“C”有着天壤之别,切忌混为一谈。ACID 里的 Consistency 本质上是“业务逻辑(Client Logic / Business Requirement)的有效性”,例如“系统中必须至少保留一个值班医生”这一规则,纯粹是客户端业务提出的约束;如果系统里所有医生全都离线(所有标志位全为 false),从数据库的技术视角看数据写入完全合法、挑不出任何毛病,只是违反了业务规则而已。

相反,分布式系统中所探讨的 Consistency(如线性一致性 Linearizability),其核心源泉在于**数据复制(Replication)**。只要系统存在主从复制(Master-Slave)或者多副本机制(如 DynamoDB、Cassandra 的副本集群),不同节点之间必然存在多份数据拷贝。在异步复制(Asynchronous Replication)的架构下,Slave 节点的数据落后于 Master 是物理常态,任何一个快照时间点上,不同的副本之间都必然存在数据差异(Discrepancy)。尽管在没有新写入时系统能够保证“最终一致(Eventually Consistent)”,但在数据流转过程中,副本间的不一致是不可避免的。理论上可以通过全同步复制(Synchronous Replication)来消除差异,但整个系统的可用性与性能将会“一塌糊涂”。因此,副本之间的数据分歧,正是催生分布式一致性与共识算法(Consensus)的根本来源。讲师由衷感叹,共识算法是数据库与分布式系统领域皇冠上的明珠,最为复杂艰深;分布式领域的泰斗级先驱 Leslie Lamport 正是凭借在分布式共识与一致性方面的奠基性贡献荣获了图灵奖。

- **ACID 一致性 vs 分布式一致性**:前者是应用层的业务约束有效性(Business Invariant);后者是多副本(Replicas)之间的数据等价与线性一致性(Data Discrepancy)。 - **矛盾的根源**:异步复制带来了不可避免的时间差与副本偏离;全同步复制虽能解决不一致,却以摧毁系统性能为代价。 - **学术里程碑**:分布式一致性与共识算法构成了现代大规模系统的理论基石,Leslie Lamport 因该领域的突破性理论荣膺图灵奖。

[01:05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3900s) 单机 ACID 抽象与分布式三大核心问题的等价性

在分布式环境下解决共识问题之所以极度困难,是因为不可控的真实物理世界故障层出不穷。回顾分布式系统的基础故障模型:不同服务器的物理时钟可能会发生漂移(Clock Drift),严重时两台机器能相差几十分钟;进程随时可能遭遇挂起(Process Pause),尤其是 Java 等语言在经历 Stop-the-world 垃圾回收时,进程暂停几分钟甚至长达十分钟并不罕见;更有甚者,网络延迟是无上界的(Unbounded Network Delay),数据包可能延迟几分钟、几小时甚至彻底丢失。面对如此险恶的运行环境,如果不对单节点做坚实的前提假设,分布式系统的设计将寸步难行。幸运的是,单机数据库通过 ACID 事务提供了极其强大的抽象保障——崩溃恢复交给底层日志(要么全写要么全不写),并发隔离交给并发控制,持久性交给存储引擎。讲师感叹道:“作为一个程序员,如果你要设计一个很严谨的系统,很多时候优先考虑 SQL”,因为传统关系型数据库的事务抽象替后端工程师消除了最底层的灾难,而很多 NoSQL 数据库甚至连最基本的读已提交(Read Committed)都无法提供,留下一堆难以排查的边界缺陷。

在扫清了单机抽象之后,讲师引申出了分布式系统中最震撼的理论之美:**问题的等价性转化**。这与计算理论中的 NP 完全问题(NP-Complete)完全一致——无论是旅行商问题(Traveling Salesperson Problem)还是布尔可满足性问题(如 3-SAT),计算理论证明了数十个看似迥异的 NP 问题在多项式时间内可以互相归约,攻克其中任意一个,剩下的所有问题便迎刃而解。分布式系统同样存在这种美妙的对称性:

> **全序广播(Total Order Broadcast) = 分布式共识(Consensus) = 线性一致比较并交换(Compare-and-Set / Linearizability)**

这三个在工程表面看起来各异的技术挑战,在数学与逻辑底层是完全等价的同一个问题。只要通过某种算法(如 Paxos、Raft)解决了共识,就能天然实现全序广播与线性一致的原子条件写入;基于这一底层支柱,业界才得以构建出像 ZooKeeper 这样提供分布式锁与集群成员服务(Membership Services)的核心中间件,以及两阶段提交(Two-Phase Commit, 2PC)和因果一致性顺序(Causal Ordering)等高级特性。

- **分布式现实缺陷**:不可靠时钟(偏差可达数十分钟)、垃圾回收 Stop-the-world 引发的长暂停(几分钟至十分钟)、无上界的网络延迟与丢包。 - **单机 ACID 的庇护**:SQL 事务的“原子性、隔离性、持久性”是一层极高价值的工程抽象,屏蔽了节点内部最复杂的单机容错细节。 - **三大问题的等价闭环**: 1. 全序广播(Total Order Broadcast) 2. 分布式共识(Consensus) 3. 线性一致 Compare-and-Set(Linearizable CAS) - **两阶段区分警示**:后续探讨的分布式事务两阶段提交(2PC, Two-Phase Commit)与并发控制的两阶段锁(2PL, Two-Phase Locking)是完全不同的两个概念。

[01:10:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=4200s) Linearizability 的核心定义与世界杯比分案例

讲座进入分布式系统一致性中最关键的特性之一:线性一致性(Linearizability)。在深入线性一致性之后,后续还会依次探讨全序广播(Total Order Broadcast)、共识机制(Consensus,包括 ZooKeeper 和 Membership Service 的应用)以及分布式事务(Distributed Transaction)的本质与代价。事务虽然提供了极强的安全性保证,但其开销极为高昂。

```mermaid flowchart TD accTitle: Linearizability Violation Example accDescr: Leader node replicates asynchronously to followers, causing Bob to see stale data after Alice saw the latest score. subgraph Storage["Database Cluster"] L["Primary (Leader)<br/>Write: 德国 1:0 阿根廷"] F1["Follower 1<br/>Updated: 1:0"] F2["Follower 2<br/>Stale: 0:0"] L -->|"Sync"| F1 L -.->|"Async Delay"| F2 end Alice["Alice"] -->|"1. Read (1:0)"| F1 Alice -.->|"2. Tell Bob: 德国赢了"| Bob["Bob"] Bob -->|"3. Read (0:0)"| F2 ```

在讨论 Linearizability 时,通常假设存在一个主库(Primary/Leader)和若干从库(Secondary/Follower),暂不考虑数据分片(Partitioning),仅聚焦于复制(Replication)。Linearizability 的本质直觉在于:虽然底层由主从多个副本构成,但对于外部客户端而言,整个数据库看起来就像只有一个单机副本在提供服务。它提供了一种强烈的“最新性保证(Recency Guarantee)”:写操作一旦 Commit,后续的所有读取必须能够读到该写入或更新的值,绝不能出现读出旧数据或时间倒退。

为了具象化违反 Linearizability 的场景,主讲人以 2014 年世界杯决赛(德国对阵阿根廷)为例进行了推演: - 比赛进球后,主库写入了“德国 1:0 阿根廷”; - 主库向两个 Follower 进行异步复制。Follower 1 及时同步了该数据,而 Follower 2 由于网络等原因尚未同步; - Alice 查询 Follower 1,看到了“1:0”的比分,并兴奋地告诉 Bob:“德国队已经赢了!”; - Bob 随即刷新网页查询比分,但他的读请求被路由到了尚未同步的 Follower 2,看到的仍然是旧比分“0:0”; - 从整个外部系统的视角看,系统先返回了最新结果“1:0”,随后的读取却倒退回了“0:0”,这就直接违反了 Linearizability。

[01:13:30](https://www.youtube.com/watch?v=crJJovKcuqw&t=4410s) Linearizability 与 Serializability 的区别及反例辨析

主讲人强调,Linearizability 和 Serializability 是两个完全不同维度、经常被混淆的概念: 1. **Linearizability(线性一致性)**:是关于分布式数据复制(Replication)的一致性保证,关注的是单对象、单操作在全局绝对时间轴上的“新鲜度(Recency)”。它并不解决多对象事务间的并发冲突,例如著名的医生值班(Write Skew / 写偏斜)问题。即便系统满足 Linearizability,如果缺乏适当的多对象控制,依然无法保证事务的 Serializability。 2. **Serializability(可串行化)**:是事务隔离性的最高等级,关注的是多操作、多对象的执行顺序,确保所有并发事务的执行效果等价于某种串行执行的顺序,但它本身不带物理时间维度的线性约束。

主讲人随后尝试现场构造一个“满足 Serializability 但违反 Linearizability”的反例,例如在快照隔离(Snapshot Isolation)或可串行化快照隔离(SSI)下,事务由于基于历史快照读(Stale Data)可能导致的时序现象。但在推演“两个读事务分别在 Transaction 2 和 Transaction 3 提交后发起并返回”的图示时,主讲人敏锐地发现自己现场随手画的例子存在漏洞:因为并发请求后期的延迟可能被解释为网络延迟(Network Delay),从而并没有真正破坏 Linearizability 的理论定义。

主讲人坦诚地打住并反思:“刚才这个例子是不对的,我得反省一下哪块不对……但这块结论是对的,如果你是 Snapshot Isolation / SSI,你是满足 Serializability 的,但不一定满足 Linearizability;你可以满足 Linearizability,但可能不满足 Serializability,这两个概念不等价。”由于该反例的边界情况(Counter Case)非常微妙且复杂,主讲人决定在下周的讨论课上重新梳理出严谨完备的对比示例。此外,主讲人建议大家在课后除了复习 Slide,还要提前阅读 Raft 算法论文,因为共识机制的复杂度极高,提前预习才能跟上下周的节奏。

[01:20:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=4800s) Q&A:DynamoDB 隔离级别与 SQS 消息处理机制

进入答疑环节后,现场学员提出了两个具体问题:

**问题一:DynamoDB 对 Read Committed 与 SSI 的支持** - 学员提问:DynamoDB 是否支持 Read Committed?为何某些资料提及它支持 SSI?Read Committed 是否是 SSI 的必要非充分条件? - 主讲人解释:早期的 Dynamo 论文非常极简(采用最终一致性、Sloppy Quorum 和 Hinted Handoff),且最初使用的是 Berkeley DB 存储引擎。后来 AWS 对 DynamoDB 的底层引擎进行了重构,并且它属于闭源商业系统,官方后续在不同配置下引入了更强的一致性支持(例如强一致读、事务支持等)。在单机事务理论上,如果一个系统能支持 SSI,那它在逻辑层面上肯定能覆盖 Read Committed 的要求。主讲人补充道,曾与同事 Rose 讨论过他们内部的 Payment 系统就是基于 DynamoDB 构建的,为了满足支付场景,往往会关闭 Sloppy Quorum,启用强一致性(甚至类似 Linearizability 的强保证)。

**问题二:AWS SQS 消费失败与高可用(SLA)** - 学员提问:在周二的 Mock Interview 中提到,如果 Worker 从 SQS 消费了一个任务传给后端服务,若该处理过程挂了(Failed),队列如何处理? - 主讲人分享了 SQS 的底层逻辑与 SLA 特性: - SQS 本身的设计非常独特,其服务可用性协议(SLA)只有“三个九”(99.9%),这意味着理论上每年可能允许大约 8 小时不可用,使用时需要特别谨慎; - 在 SQS 中,Worker 获取任务后,该任务会进入“不可见(Visibility Timeout)”状态,**只有在 Worker 显式调用 Delete 请求后,任务才会被真正从队列中移除**; - 如果 Worker 奔溃或处理失败,超时后该请求会重新变为可见,供 Worker Pool 中的其他 Worker 重新抢占(Claim)并重试,从而防止任务丢失。

[01:25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=5100s) Uber 支付架构思路与 SSI 的性能优势

针对复杂任务队列的处理,主讲人拓展分享了 Uber 早期在 Engineering Blog(约 2016 年)中介绍的支付系统架构思路: - 与其在应用层处理错综复杂的并发 Claim 与锁冲突,Uber 采用了一种简单直观的静态分片方案:将支付业务哈希切分成 $N$ 个 Partition(例如 1,000 到 10,000 个分区); - 每个 Partition 在同一时刻由调度系统(如 Kubernetes)保证最多只有一个活跃的 Worker 在消费; - 一旦某个 Worker 宕机,通过调度框架拉起一个新 Worker 顶替。这种单 Worker 独占分区的模式从根本上消除了并发冲突(Collision)。

在课程收尾阶段,有学员询问“SSI”的含义。主讲人解释道: - **SSI** 代表 **Serializable Snapshot Isolation(可串行化快照隔离)**,是基于乐观并发控制(Optimistic Locking)实现的无锁/低冲突隔离技术; - 该算法在学术界提出约十年左右(由 Michael J. Cahill 等人在 2008 年前后提出并于 2009 年发表),对工业界数据库(如 PostgreSQL)产生了深远影响; - 论文性能测试数据显示,相较于传统的两阶段锁(Two-Phase Locking, 2PL),SSI 在读多写少的并发场景下性能可提升 100 倍以上(快 2 到 3 个数量级)。虽然传统 2PL 实验可能由于 Strict 2PL 的限制而表现偏保守,但 SSI 相比 2PL 拥有至少一个数量级的吞吐优势是公认的事实; - 当然,SSI 的表现也高度取决于数据访问模式(Data Access Pattern):如果所有并发事务都在剧烈争抢修改同一个数据对象,乐观检测必然频繁失败回滚,SSI 的表现也会大打折扣。

解答完毕后,主讲人宣布本周课程结束,并与大家相约下周二继续深入讨论。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=300s) 从计算机科学的角度来看,这感觉简直就像从金字塔时代、公元前就把这行记录封印住了,直到今天都没解冻,成了千古谜团。 — Scott Shi

> [25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1500s) small, fast enough and predictable — Scott Shi

> [25:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=1500s) 从旧金山直接飞到火星 — Scott Shi

> [36:31](https://www.youtube.com/watch?v=crJJovKcuqw&t=2191s) 尽一切努力不要用到它 — Scott Shi

> [36:31](https://www.youtube.com/watch?v=crJJovKcuqw&t=2191s) Simple is always best — Scott Shi

> [50:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3000s) 我写的时候是不加锁的,直到写完准备提交时,我再检查有没有别人修改了我读写过的数据;如果有人改动过且对方已经提交(committed),那我就主动中止(abort)自己的操作并进行重试。 — Scott Shi

> [55:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3300s) 不 matter,完全不重要。 — Scott Shi

> [01:05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3900s) 作为一个程序员,如果你要设计一个很严谨的系统,很多时候优先考虑 SQL — Scott Shi

> [01:05:00](https://www.youtube.com/watch?v=crJJovKcuqw&t=3900s) 全序广播(Total Order Broadcast) = 分布式共识(Consensus) = 线性一致比较并交换(Compare-and-Set / Linearizability) — Scott Shi

> [01:13:30](https://www.youtube.com/watch?v=crJJovKcuqw&t=4410s) 刚才这个例子是不对的,我得反省一下哪块不对……但这块结论是对的,如果你是 Snapshot Isolation / SSI,你是满足 Serializability 的,但不一定满足 Linearizability;你可以满足 Linearizability,但可能不满足 Serializability,这两个概念不等价。 — Scott Shi

## Mentioned Scott Shi, Salesforce, DynamoDB, Google Docs, Redis, VoltDB, 12306, Apache Airflow, Splunk, Datadog, PostgreSQL, FoundationDB, MySQL, MongoDB, Leslie Lamport, Michael J. Cahill, ZooKeeper, AWS SQS, Uber, Kubernetes, Berkeley DB, DotA, LoL

## Video description The doc could be found below: https://docs.google.com/presentation/d/1bo4SVBEVWgWPmagMuA2jsIpEco9hfGZ3sNZk25g4RKg/edit?usp=sharing

[Source](https://www.youtube.com/watch?v=crJJovKcuqw)

中文简报

工程师揭示2PL锁隐患:押注SSI与单线程分区破解并发瓶颈 高并发系统应优先通过分区与队列设计消除资源争用,两阶段锁极易引发死锁与吞吐崩溃,而可串行化快照隔离在低争用场景下性能可比传统锁提升百倍。 本期内容深入剖析了数据库从单机弱隔离级别向严格可串行化(Serializability)演进的技术脉络与工程代价。讲者详细对比了两阶段锁(2PL)、单线程串行执行与可串行化快照隔离(SSI)在应对丢失更新、写偏差与幻读时的权衡取舍,并结合工业界亲历的生产死锁事故警示复杂锁设计的风险。随后话题自然过渡至分布式领域,系统辨析了分布式线性一致性(Linearizability)与事务可串行化的本质差异,并指出了共识算法、全序广播与线性 CAS 之间深层的理论等价性。 快照隔离(Snapshot Isolation)无法防御丢失更新(Lost Update)与写偏差(Write Skew),必须引入可串行化(Serializability)才能杜绝此类异常。 (5:00) 实现可串行化的三条主要路径为两阶段锁(2PL)、单线程串行执行与可串行化快照隔离(SSI);其中 SSI 在低冲突场景下的吞吐量可比传统 2PL 高出 1 到 2 个数量级。 (20:00) 单线程串行执行(如 Redis)要求操作极小、执行时间可预测,且全量数据必须纯内存驻留,以杜绝磁盘 I/O 导致的队列停滞。 (25:00) 分布式系统的三大核心难题——全序广播(Total Order Broadcast)、分布式共识(Consensus)和线性一致 Compare-and-Set,在数学与逻辑底层是完全等价的同一个问题。 (65:00) 线性一致性(Linearizability)关注的是分布式复制中单对象的新鲜度保证,而可串行化(Serializability)关注的是多对象事务的隔离性,二者概念完全正交。 (73:30)

Reading summary

讲者 Scott Shi 阐释了事务隔离性从弱隔离级别到可串行化的演进逻辑与设计取舍,指出面对并发写异常应审慎评估复杂锁机制的工程代价,并系统对比了单线程串行、可串行化快照隔离(SSI)及分布式线性一致性之间的本质差异。 并发写异常与快照隔离的局限 (5:00) 快照隔离(依托 MVCC)虽能将长达数十分钟的分析型只读查询定格在一致时间切片内,却无法防御“读取-修改-写入”模式下的并发写异常。讲者以多线程爬取并解析 50 万场对局数据的计数器累加为例,说明快照隔离无法阻断丢失更新;在此场景下,客户端 CAS 也会因快照的一致视图而隐蔽失效,而 CRDT 方案因墓碑标记导致内存膨胀且严格受限于交换律数学模型,难以作为通用解法。 面对更复杂的写偏差与幻读(如医生排班系统中两名医生并发申请退班,各自读取到在岗人数充足后提交,最终导致在岗人数归零),由于事务修改的是不同对象,传统的行级锁完全无能为力。此类异常的本质在于并发写入破坏了业务判断的前置条件,现存的弱隔离级别在此类场景下均告失效。 悲观可串行化代价与单线程串行方案 (20:00) 可串行化是解决写偏差与幻读的终极手段,其传统实现主要依赖两阶段锁(2PL)。2PL 区分共享读锁与排他写锁,但可能导致吞吐量断崖式下跌并极易引发死锁;针对“尚不存在”的数据幻读,理论上完美的谓词锁因判定开销过大难以在工程中落地,工业界普遍折衷采用扩大锁定范围但可复用 B-Tree 的索引区间锁。 作为替代路径,单线程串行执行(如 Redis、VoltDB)通过单一队列排队消化写入,彻底消除了上下文切换与锁冲突。然而,该模式对架构要求极高:必须彻底禁止交互式事务,要求事务打包为存储过程提交,操作必须小、快且可预测,全量数据需纯内存驻留,写吞吐超限时则需依托基于 Key 的精细分区。 现代 SSI 机制与架构去锁哲学 (50:00) 在单资源争用率极低(如千分之一至十万分之一)的读多写少场景中,基于乐观并发控制的可串行化快照隔离(SSI)展现出显著优势。SSI 将长事务的隔离裁判压缩在 Commit 瞬间,通过读写依赖追踪选出 Winner 提交并仅让 Loser 中止重试。引用 Cahill 等人的论文数据,SSI 吞吐量相比 2PL 拥有至少一个数量级以上的公认优势,已获 PostgreSQL 与 FoundationDB 原生支持;但在高争用场景下,频繁冲突回滚会导致其效率大幅下滑。 讲者结合在 Salesforce 亲历的任务调度事故指出,因前端防抖失效重复发包,导致乐观锁冲突检测反复将两个实例双双 Abort、陷入长达一个月的定时死循环。工业级系统的第一准则是尽量避免编写复杂的锁逻辑,应优先通过追加不可变记录(如抢票排队队列)或静态独占分区(如 Uber 支付架构)在数据流层消除并发争抢。 单机 ACID 与分布式线性一致性辨析 (60:00) 单机 ACID 中的一致性偏向应用层业务有效性,而分布式系统的一致性则直接源于多副本数据复制中的异步延迟与副本分歧。面对不可靠时钟、GC 长暂停及无界网络延迟等物理现实,单机 ACID 构筑了底层容错抽象;在此基石之上,分布式理论存在三大问题的等价闭环:全序广播 = 分布式共识 = 线性一致比较并交换(CAS)。 分布式领域的线性一致性关注单对象、单操作在物理绝对时间轴上的“最新性保证”(如世界杯进球后,异步复制延迟导致不同用户读到新旧不一的比分即违背该保证)。它与关注多操作、多对象执行等价串行拓扑的可串行化属于正交维度,两者互不包含且不能等同。 讲者明确指出 SSI 满足可串行化却未必满足线性一致性,二者概念不等价,但现场构造边界反例时因网络延迟推演出现漏洞,留待后续课程梳理严谨对比示例;同时建议深入分布式共识前需预习 Raft 等核心算法。

Source quotes

[5:00] Scott Shi: 从计算机科学的角度来看,这感觉简直就像从金字塔时代、公元前就把这行记录封印住了,直到今天都没解冻,成了千古谜团。 [25:00] Scott Shi: small, fast enough and predictable [25:00] Scott Shi: 从旧金山直接飞到火星 [36:31] Scott Shi: 尽一切努力不要用到它 [36:31] Scott Shi: Simple is always best [50:00] Scott Shi: 我写的时候是不加锁的,直到写完准备提交时,我再检查有没有别人修改了我读写过的数据;如果有人改动过且对方已经提交(committed),那我就主动中止(abort)自己的操作并进行重试。 [55:00] Scott Shi: 不 matter,完全不重要。 [65:00] Scott Shi: 作为一个程序员,如果你要设计一个很严谨的系统,很多时候优先考虑 SQL [65:00] Scott Shi: 全序广播(Total Order Broadcast) = 分布式共识(Consensus) = 线性一致比较并交换(Compare-and-Set / Linearizability) [73:30] Scott Shi: 刚才这个例子是不对的,我得反省一下哪块不对……但这块结论是对的,如果你是 Snapshot Isolation / SSI,你是满足 Serializability 的,但不一定满足 Linearizability;你可以满足 Linearizability,但可能不满足 Serializability,这两个概念不等价。

Original transcript

[0:00] 我们今天是讲Transaction, 因为之前上一周也讲Transaction, 所以讲过一部分了, 所以Transaction之后可能还会有一点时间讲Consistency and Consensus,嗯然后对我们来就是看这个,就是看的consistency和consensus还能讲多少然后应该没有其他的吧xada5OK,所以这个Transaction这块的话,我们上周是简单讲了一下一部分,就是什么是Transaction然后什么是 ACID 为什么 ACID 是个过时的概念是吧为什么 ACID 是个过时的概念然后 ACID 中这个复杂度又在哪里然后比如说 Atomic 的话其实是为了 About Aboutability 是吧然后 Durability 的话相对来说就是你要把那个data存到disk上,然后你可能还有个read and hide log,这样的话就是把那个transaction变成一个点,一个点之后的话就是全部你的duerability就guarantee一个点之前的话,一个point之前的话,你的duerability是没有guarantee的然后 consistency 更多的是一个 client 的要求然后这块的这个ACID里面的consistency和这个distributed system里面的consistency是不一样的就是它那块的话这个我们我们之下来讲的就是跟下一张讲的consistency是不一样的这块的话更多是单个node的consistency,然后它更多是一个client的需求。然后剩下的话大部分时间都讲这个isolation,Isolation有不同的Isolation Level然后就说讲了一个 single object 和 multiple object 的这个 consistency 和 isolation然后最简单的就是比如说你这个data呢,就是数量呢,你可能这个data是一个10k的一个json是吧,那怎么样guarantee你这个如果一个transaction没有committed的话,你data不会有half written data是吧,这个是怎么样guarantee的。然后讲了一下transaction,然后我们就讲这个isolation了。isolation有多个isolation level,第一个常见的isolation level就是 read uncommitted。就比如说你在写的时候,就是我们常见的一个denormalization,就是把这个email和unread email就是分成两个,然后你读的时候,如果能读到那个uncommitted data的话,你会看到一种inconsistency,data inconsistency。然后除了这个read uncommitted的话,就是可能还有一些这个就是这个是read uncommitted就是DirtyRide,DirtyRide的话就导致你这个listing的和invoice是散了不同的人的,就造成各种很奇怪的结果。然后这个不管是DirtyRide和DirtyRide,这个DynamoDB都是不提供的。这个isolation level,就是dirty read和dirty read的DenimalDB也是不提供的,所以你DenimalDB的话,很多时候就是多数决定你到底,就是多数,majority决定你到底有什么data然后我们讲完DirtyRead和DirtyRead以后和ReadCommitted以后了讲了一个叫Snapshot Isolation,Snapshot Isolation呢是跟DirtyRead和DirtyRead是不一样的,就是你没有读到Uncommitted Data,你其实这个Snapshot Isolation你是没有读到Uncommitted Data,你读都是Committed Data但是你在读多个object的时候你跨越了transaction你比如说你在读account1的时候你是在这个transaction发生之前你在读account二的时候,你是在这个transaction发生之后你就造成了一种inconsistent,就是账户里平卖无故的少了100块钱这个是你其实是没有violet的这个right read committed的,但是你读的时候是跨越了多个transaction的。所以这也是一个就是导致你data inconsistency 的一个原因。然后解决StampShotTranslation就可能需要一个MultiVersionConcurrencyController就是你自己控制光下的话,就相当于是一个,有点像一个log是吧,每个record都是一大堆log,然后你根据你的,你读的这个transaction,你知道哪些transaction的data是visible的,哪些transaction的data是invisible的。那你这样的话就是能guarantee你读的时候正好你的这个读的action,你本来读的时候可能跨越时间很长我们知道这个这个sicle read,有的read是这个analytic read有analytic read,它快速时间是非常非常长的,有时候甚至高达几十分钟,甚至半个小时都可能就是你因为你读的data很多嘛在这个sicle里面所以你这种情况下Analytic RAID的时候如果你把你的整个RAID压缩到一个时间点,压缩到snapshot isolation的话,相当于你把整个read,压缩到一个transaction的时间点,就是你有transaction1,transaction2,transaction3,但你进行read的时候,你其实在transaction2和transaction之间进行读的话,那你这个读出data是非常consistent的。就是这个叫snapshot isolation所以这个也是很重要的一点,就是我们在做这个transaction和这个Consistency的时候一定要想好它到底是个时间段还是时间渐然后时间段的话一毫秒的时间段你可以很轻松的把它扩展 [5:00] 一分钟是吧因为你各种各样的原因可能这个时间段本来是一毫秒supposed to be 1毫秒然后就变成一分钟了那一分钟就能发生很多事情了所以这个snapshot isolation这个读呢就是相当于把之前是一个一毫秒的读变成了一个时间点的读一个atomic的读是吧那你不管这个读花了多久的时间但是从你的角度来看它就是在transaction2发生之后在transaction3发生之前读的所以它读的data就比较consistent了再有第二层的isolation 来问,snapshot isolation再有第二层的isolation 来问以后,你还是可能会出现各种各样的问题这个是我在研究生的时候做的项目我和林士尔他们在研究生的时候做过项目这个是我一个真实的例子,当时做了一个我记得之前跟大家讲过,game recommendation machine说白了就是你玩即时战略游戏的时候你比如说玩LuaLu或者是玩那个Dota的时候我当时玩Dota,我现在玩Dota然后你现在的话是根据你之前选的英雄然后recommend 你这个best match 英雄然后我们那块的话就是用Machine Learning来recommend的更好的一个英雄然后我当时是crawled了好多datacrawled了好多replay data大概crawled了50万个500,000 replay data然后crawled那个data的时候就有一个原因就是我当时写的就是写counter就大概initialize了大概八个nodes然后有个counter然后每个replay data大概是20k我记得还是比较近了20k然后partition也比较简单就是摸反正这个多少个counter我可以自己来写吗然后但是我发现他每一个里面他这个count这个add的时候是有问题的就是exactly这个问题因为我每个node为了让它最大效率是吧我其实是在每一个node里面每个thread里面是每个node run的时候是multi-thread run所以multi-thread run的时候就出现这个问题就是我开始我读了一个data我读了那个现在的counter是42是吧然后我开始 crowd data不是,我是先 crowd data然后读了 counter 42然后把它写成 43然后把这个 data写进去然后下一个的话就被覆盖掉了因为同样的是在这个同时的时候在做这个同时的时候同时有一个 data是 42然后写到43然后就被覆盖掉了Exactly the same thing就是这个这个的话,这个 isolation level是不能用之前的任何一个Isolation Level解决的因为你这个里面首先不是Dirty Read, Dirty Read,没有任何Uncommitted Data Exposed其次不是Snapshot Isolation你其实是那个你用Snapshot Isolation也没用的因为你用Snapshot Isolation你这块读还是42你不可能说用Snapshot Isolation就解决了因为你snapshot isolation 读的可能是steal data所以用snapshot isolation是没有用的所以这个是一个新的这个isolation level是比之前的要求都要高一点所以解决这个有多种方式一种就是你有的database是专门解决这个的它专门就提供一个atomic的writeincremental的solution这是最好的如果没有这个的话你可以做lockinglocking的话就是locking还是这样的就是这个coding要非常非常小心因为这个有很多horror story在这个locking上我像之前跟大家讲过Southforce做这个locking的时候我们有一次performance engineer一个非常senior的performance engineer说他找见了一个locking就是一个transaction lock了database system长达48个小时48个小时我举过一个类比的概念就感觉从公元前那个从金字塔时代然后这个Database road就被lock住了到现在还没有解冻就这种感觉从金字塔时代从Computer Sense这种感觉就从金字塔时代这个 road就被lock了到现在还没有解冻就变成千年封存千年的秘密的感觉所以这个locking是可以做很多powerful式的事情的但是非常非常的难写然后其次的话现在写这种Logging的话如果你写是Post Stop Procedure的话如果写是Stop Procedure的话它不管是Logging它首先缺乏Logging就是你很难去Log一些Data然后缺乏Matrix现在的话不管你做什么就是Server端比如Transaction的话你有Logging有Matrix然后还可以做CodeReview是吧但是你用Stop Procedure写的话你很多过去20年之间的程序员非常好的management system都没有没有logging没有很好的code review没有matrix所以你其实这块危险度是很高的所以就是为什么我们早不见这个到底lock 48小时是哪一个transaction我们不知道是哪一段code trigger对于这个performance engineer很头疼不知道是哪一个code trigger了然后lock了这个database 48个小时所以这个 [10:00] 所以这个stop procedure是如果你自己写的话你要非常非常小心尽量避免这个stop procedure然后的话有一些database是支持automically,就是automatically detect loss updates的。它是有些database支持的,但是你只是要看database,并且有时候database promise的东西不一定它真正实践了,也不一定真正实践对了。然后Compared Set的话是一种common的一种,非常common的一种,就是用程序员解决,程序员角度解决这个的,就是假设你不支持自动detecting,然后不支持automic write,然后你可能只能做这个Compared Set了。但是Compared Set要非常小心snapshot and isolation,因为你snapshot and isolation by design,你读的data就是steal的data。所以你读的时候是42,别人读的时候也是42,但42是真的42,因为你读的时候就在Transaction2发生了之后,Transaction3发生之前所以你读的42别人读的42你写进去的时候43别人写进去也是43那你在compare的时候你在进行读的时候在你这个transaction内读的时候你再读还是42,因为你这个整个transaction是在transaction3 commit之前嘛,所以这个comparsight这种情况下是不work的。然后最后的话就是这个CRDT的,这个是CRDT是一个比较新的一个Data Model和这个这个就是解决方式啊,就是专门负责这个concurrent right, non-conflicting concurrent right 的。然后CRDT常见的使用的地方呢,就比如说这个,Dock Elaboration 是吧 Google Doc这种地方它就是为Google Doc这种这种Google Doc设计的就是这种设计的,然后CRDT我那天看了一下,它其实支持的Operation也比较有趣啊,我看了一下Viki,我觉得支持Operation也比较有趣啊,它其实是有它Limitation的,CRDT是有Limitation的,首先CRDT的这个memory是Cost是比较高的,因为你比如说你用Google Doc用CRDT来实践的话,CRDT实践的话,它每个corrector都是一个CRDT的一个operation你对于每一个corrector,就是google doc里面,任何一个corrector都是CRDT的operation,你不仅要记录它的写,还要记录它的更改,甚至要记录它deletedelete都要记录进去,然后因为你需要你不知道多少人在同时modify进去,所以delete要记录进去所以他其实每个operator呢就是一个CRDT的一个Anti,就是Antity,然后CRTT支持的DataModel也是比较有限的,我给大家举个例子,就比如说你做一个counter是吧你做一个counter你这个counter可以加可以减它实际上是什么呢?它实际上是根据你这个 concurrency model 做了一个很大的 vector 比如说你有可能会有四台机器同时访问这个 counter那么你就相当于是有两个array,一个是增加的array,叫addition array,一个叫就是minus array,就是remove array然后你每次加1的时候,你counter加1的时候,你会在你对应的node上加1。然后你每次简易的时候会在你对应的node上简易,所以说最后当你统计这个counter到底有多少的时候,你会说node1上的addition,就是node1上面addition有多少,然后node2上的addition有多少,然后把这个全部sum起来,就是所有的不同的request,roll到node1上的,这个这个所有的这个request的总和加的总和因为这个里面是没有conflict每个node的话是不会有这种conflict然后你再把这个这个下面这个vector呢是node1上的deletion和node2的deletion然后node3deletion全部加起来然后这两个值的差值就是你最后的counter,所以你奇怪的看一下,其实这个是就是它可以非常没有conflict free的这个replication,这是比较直观的。但是另一点也可以看出这个data model是非常restrictive的。我看了一下CRDT支持的Data Model也就那么几个,就是Counter,然后On Ordered Set,然后Communitive Data Operations一般是比较支持的,什么就是Communitive?就是你A和B进行操作和B对A进行操作,这个换序Community 是没有影响的,就是A+B=B+A,或者是A+B+C=C+B+A,你知道吗?就是我记得大家讲,学代数的时候肯定学过这个概念啊,就是你换的这个order顺序是不影响的,这种必须满足这个才可以。然后,所以它这个CRDT支持的这个model和这个计算是非常有限的。好,我们今天就是我们上周讲的,就是上周讲了这么多,然后今天呢讲一个相对来说更复杂一点的,也是这个里面就是这个database transaction 里面 [15:00] 复杂一点的叫这个Ride Screw和Rest School和 Phantoms我在总结了这么多以后,总结上周的话,大家有什么问题吗?我们来看看有没有什么问题,额外的问题然后如果没有的话我们就开始看这个例子好,没有,我们就看这个例子好了,然后这个scenario也相对比较简单,其实我觉得是比较database中常见的scenario,就是你在这个database里面有两个啊,Doctor同事在on call 是吧,啊,就Alice和Bob,然后这时候呢,在在当前我们这个transaction发生之前,我们是Alice,Bob都是oncall的,然后Carol是不oncall的,然后这个要求是在必须保证至少有一个人是on call的就是这些人里面至少有一个人是uncal的,因为这个doctor嘛从Alice的角度来看他看一下有没有doctor在oncall,然后他会返回处,因为确实有doctor在oncall,然后并且oncode的数量大于2的,所以它可以把自己的oncode的数量呢,就是把自己变成不oncode,因为它觉得自己走了以后还是有人oncode的。然后从Bob的角度来看他看有没有人在oncall,然后他读出来以后呢,也是有人oncall的,然后oncall的数量也是大于2的,然后他把自己变成不oncall,然后这两个operation同时进行commit的时候呢,这个就导致了什么呢,就没有人oncall了,这个promise就被broken了这个safety就被broken了一般来说,如果你就是就是这种 consistency broken 你都可以叫 safety broken 了吗?就是就是 safety broken 了那么首先这个不属于DeltateWrite因为你没有卡没有cover这个 uncommitted data你同时也不是一个lost update,因为你这两个transaction update是不同的object你像那个加1加1加1那个是同一个object update,但你这个情况下update是不同的object谁说这种情况下呢就不是很明显它到底是一个什么样的conflict也不是很明显,到底是一个什么的,就是race condition所以说这个呢,但是这个呢是非常常见的一种情况,我们之后会看到各种各样可能的情况,比如说你是uniqueness,比如说你uniqueness的时候可能会发生这个情况,是吧?我要注册一个域名,那我开始看这个域名不存在,没有人抢注过,我注册了,然后同时呢别人也在做同样,exactly same thing,那这样的话这也是一种这个rescule和这个phantom rights,phantoms是吧?然后这块的话,即使有single object的update,single object lock也不能帮助,因为你这个其实是多个object。你就是你对于你比如说你假设只lock Alice的这个oncall的话你是没有任何帮助的因为你这个Bob还是可以修改它的oncall的所以你SingleObject的这个lock是没有任何帮助的然后有的话有的时候有些database已经帮你支持一些东西啊,比如说你可以Full-Restrict,Uniqueness啊,是database从这个角度是可以帮助的,但是因为它不是一个single object,所以即使用这个database这个uniqueness也是帮助不了的。然后所以这个的话这个是一个非常新的问题,就是这个问题是一个非常新的问题,就是之前我们没有讨论过的这个不属于我们讨论过任何一个范畴。这个问题呢,它的关键是什么?就是你其实是一个pattern是什么呢?是RAID。ModifiedRight想一下read 我先读有没有人按号 然后modify 然后再write 就是read,modify,write的一个model然后read,modify,write 它的问题产生的原因是什么呢当你在read之前,你对你read的data有一种expectation,有一个expectation,但是因为有两个concurrent process同时在写,所以你之前read的那个data的expectation呢,被破坏你ray的 expectation 就是你在你写的时候对你读的那个data的 expectation 被broke了,所以是造成这个问题的源泉是一个risk condition好,我们可以看到更多example,比如说我们之前讨论过的这个Google Calendar里面Meeting Room,是吧,我们在Google Calendar里面那张讨论了大量时间,怎么样设计Google calendar,然后这里面这个,嗯这个是这个,我记得当时那个,当时我没有讨论一个怎么样是一个正确的solution,我没有花太多时间,但是看了这个以后可能大概有个概念什么样的正确的设计是。然后这块的话,比如说meeting room的时候,你就是看到一个conflicting meeting room 是吧你看到一个conflicting meeting room 的话你就book 它 但是同时和很多人在做同一个事情然后你做multiplayer game的时候,比如说前面有一个resource,是吧,你control一个corrector前面有个resource,你去claim那个resource的时候,你需要去做一些lock是吧,需要做一些lock, [20:00] 把那个Resource Lock 保证这个Resource 是你Claim的但是如果同时有很多人做这个事情的话,你可能会有同样的问题。然后Claim username是一样的,然后double spending,我们就上一周,这周二讨论这个double spending也是一样的问题。你看了一下你有这个credit,然后你去claim 就是去把这钱花掉然后结果发现你到时候已经没有这个credit,也是一样的问题是吧,但是我们这个周二的时候是没有focus on double spending,因为这个和business model有关。就是对于这个每个individual account来说,这个double spending毕竟是一个counterpiece,你不可能一下来刷了10个uber trip是吧,然后发现credit信息不够了。这个可能性不大,然后我们其次的话解决DoubleSpending是用这个Adempotent的Key来解决的,是吧?所以DoubleSpending是可以解决的,但是有的时候你如果你的account credit card真的是没有钱的话,那其实也没办法是吧,也只能变成一个欠款好好,所以为了解决以上的问题呢,现在来说只能用做什么呢?就是只能用一个方法就是,就是 Strongest isolation level, Serial ability.SerializabilitySerializability,Serializability 什么呢?就是尽管你有很多的write,有很多transaction executed in parallel,然后我们最后run的结果呢,就感觉这些transaction好像在executed,就是一个一个被executed,举个形象的例子就是比如说,有一个新年大抢购的时候是吧,万圣节大抢购或者是感恩节抢购的时候有很多人去超市抢同一个东西商店去抢同一个东西然后尽管是从外面看的角度是非常杂叙无章的同时在几百个人在同时在抢进商店但是从商店的角度来看他们仿佛是一个一个进来的样子一个一个进来的样子我觉得这个也是当大家讲到这个transaction的时候,这个isolation的时候,大家默认的就是serialized transaction但是这个其实是最高 level 的一个 strongest level of isolation好这个实践方法有多种从大部分时间从1970年到现在大部分时间都是用two-phase locking来实现的,我们会看一下这个two-phase locking统治了serialization,大概几十年我们看它是怎么实现的。然后其次的话也可以用非常简单的方法就是Actually Serial Execution,就是你只有一个Single Side的执行你就可以就是一个actual serialization execution 也可以执行然后最后的话最新的话有一些optimistic concurrency control比如说SSI 是吧 Serializable snapshot isolation也是一种新的一个非常非常新的一个时间方法我记得这个paper好像是06年还是什么 其实就十几年20年不到 非常新的一个时间方法现在的话只有几个少数的几个database 时间了比如说 Postgres,比如FoundationDB,FoundationDB好像是在它技术上建的,但是这个是这个其实Performance相对来说是最好的。那我们看一下为什么就是这个Serializability看起来是最好的一个isolation,至少是对于developer maintain来说是最less confusing isolation,那为什么不用呢?最后一个问题是我们为什么在很多情况下不打算用这个,就是这个,这个,这个,这个,这个,这个,这个是最 strongest isolation level,我们要讨论一下这个问题。好,我们先看一下最简单的scenario,就是你实际上只有一个single thread进行write,你比如说这个的话就是RedisVoltDB来做的你只有一个single side进行write,就是你任何情况下你只有一个single side进行write,不是一个single process,是single side进行write因为你是 single thread 进行 thread 相当于是你当进行写的时候你已经变成了,我们还是举刚才的例子几百个人冲进商店但你真正去写的时候,当你真正进商店的时候,你已经变成一个 thread 了,说白了就是他在入门之前就给你一个 queue你进去的时候你就在这个 queue 里面拍好了,你很难进去那个 thread 里面但是你到这个stread里面的时候,你就变成一个single thread了,你就写了,就不会有这个concurcy,不会有这个践踏的时间,不会有这种同时抢同一个资源的isolation level被broke的时间因为它是single side 所以必须要做一些优化 是吧?我们就还是接着拿几百个人冲上商店的例子假设我们几百人冲上商店 然后你进商店其实是有安检的过程 是吧?那么你就不可能有一个人说 我突然忘带钱包了 那我回去拿个钱包 这个位置站着 是吧?这个位置站着 然后回去拿了钱包以后我再进去那我之后的人不许等我回去拿钱包,这是不可以的,是吧?但是实际上在database中,这个是有可能发生的,因为这个叫interactive transaction,就是说,你这个transaction可能是open的一个transaction,然后你需要去 [25:00] 我先去从database拿一些data,然后进行一些运算,甚至我要从其他地方拿一些data进行核对,然后核对以后,然后再把data写进去,这是Interactive Transaction。这个transaction open很久,那就有点像你已经快排到你了,快排到你进商店了,然后你说你要回去拿一个钱包,然后整个后卖人全部卡住了那这个时间效率很低的是吧,假设进商店的这个throughput的时间效率很低的。所以你不需没有这个interactive transaction,你就得用stall procedure,就是说你其实这个整个transaction是一下执行的,执行完了以后就,如果你有其他的想,其他的话,你会变成另一个transaction来执行。然后其次的话,如果这个 data 的 rise throughput 如果大于你 single thread 能 handle 的数量毕竟是有限的,如果你 rise throughput 大于 single thread 能 handle 的话,那你就必须做 partition 了。但是在每一个 partition 里面,都有一个 single thread write 就是一个 partition 里面只有一个 thread 在 write 但是也可以有多个 thread 去 readread是不限制的,你可以有多个slide read,可能有几十个上百个上千个slide read,无所谓,但你write的时候只有一个slide write,所以这样的话是如果你有这个WriteSupport的Skill不过来的话,你可能要根据你的Write来进行partition 是吧然后再其次的话这个transaction必须是small and fast,就像排队一样,你如果前面有个大胖子是吧,然后占了一个人10倍的时间,50倍的时间,甚至100倍的时间,因为他可能比如说走得很慢是吧,比如说400磅600磅大胖子走得很慢的话,那你这个整个后面的人全部这个SLA要增加一个几百毫秒,并且comparisonce这个差距更大一点,就是comparisonce里面,你这个数量差距可能不像人和人差距那么小是吧,它这个可能几千倍差距,上万倍差距都可能,你可能有个transaction特别特别慢,那你导致整个后买的所有的SLA都要加好几,好几,好几,耗金秒,那其实这个performance就很糟糕了然后再其次的话这个也是还是和single transaction有关,你这个dataset必须要fit into memory,我们之前讲过一个类比是吧,在memory写的话就有点感觉就是从这到sacramento大概100多miles,不到100miles。从San Francisco到San Cremantle不到100mL如果你要写到这个Disk上的话就感觉从San Francisco到火星的感觉这个数量级就是从San Francisco到火星的感觉就不知道有多少mile了所以如果你要写到disk的话你这是一个single transaction你这个Single transaction就很慢,你这个Performance就很糟糕了,所以你这个Actio dataset必须要fit into memory,这也是为什么这个In-memory database,比如Redis会考虑用它。然后最后的话read throughput必须非常小其实这些全部在讲一件事你任何一个 single 的一个 transaction 一个 single 的 write 的 performance 应该是可控的 就是 predictable 并且是 fast enoughsmall, fast enough and predictable.不管是其中任何一条,其实都是这个的。如果你这个write throughput是大于你single partition能处理的话,你应该把它变成不同的partition来处理它。这就是这个,这也是非常新的,Redis 14年,15年那样子,这也是非常新的一个,就是这个Algoism,也是近几年。从Database的角度来讲,10年都属于非常新的,从Database的角度来讲,这个东西其实五六十年变化的很小,所以这10年新的科技其实是非常非常新的一种科技了,就是你很多时候使用的非常小型,就是非常新的一种科技。然后我们考虑到就是从历史上占统治意义的这个two-phase locking,two-phase locking呢,我觉得如果你上过任何一门database的话,就是这个isolation level或者是这个transaction的话,他们讲的都是two-phase locking这个是这个textbook里面常讲的这个什么呢?就是它分了两个锁,一个是 non-exclusive lock,就是当你 read 一个 object 的时候,你是可以和别人 share 这个 object 的这个 lock 的,因为你这个 read lock 是non-exclusive就是block,你可以同时多个svide read同一个object,但是当你进行当你进行WriteObject的时候,你就是一个exclusiveLockexclusive lock就是你不能有任何锁不仅是write的锁不能在这个object上你也不能有read的锁在这个object上因为你有read的锁在这个object上的话就像刚才那个问题就变成snapshot isolation了就是你read如果不block write,read不block read的话,就是snapshot isolation,你可能还会出现刚才我们讨论的这个Phantom,这个reads clue的问题,reads clue或者phantom object的问题。所以这个to face locking呢非常简单,如果你有多个就是对一个object来说如果你有多个rate说你是可以接受的但是如果你有个write说你必须把所有的rate说 [30:00] 这个readlock的transaction 必须全部finish以后然后你这个readsword才能claim这个object然后你这个lock就变成exclusivelock别人不可以访问了,然后什么时候可以在read这个object呢?必须别人说的这个read就是你这个write finished了以后 然后别人才可以再去read这个object 然后才能再去write这个object所以说你就看到如果你有一个transaction就是单公里接下来就48个小时的话你整个这个database其实对于server来说对于customer来说你这个roll这个Diabase这个肉就是相当于这个object相当于dead是吧,就根本没办法读到它所以为什么这个很长的一个transaction会有一个potential issue然后还有一种情况是什么呢?就是你可能会到deadlog,deadlog就是我讲那个operating system的时候常讲deadlog是吧?你有四个deadlog条件,mutual,exclusive,hold and wait,是吧?然后还有两个条件,我忘了什么了,Mutual Exclusive Hold and Waitnon preemptive 是吧 你不能去强行的把这资源给上仓了还有circulardependency什么circular什么有一个环嘛就是downlog一样的这块也可能会发生downlog这块也会发生deadlock,就是一样的也是会发生deadlock,如果你发生deadlock的话,你必须能database能detect deadlock,是吧?然后就必须about其中一个,就是相当于是你多个人在compete的时候,你总是有一个winner,总是有个loser。所以这个two-phase locking是历史上,它是safety最高的,就是two-phase locking的safety是,肯定是最高的,有时候也叫strict,我记得也叫strict是有了的strict serialization,它这个 two-phase lock-in的这个guarantee是最高的,但是它performance是最差的,是吧?如果你有一个transaction lasting for 48 hours,没有任何人能读这个data然后先把说to-face locking和read/write lock是同一回事吧嗯好像应该是同一回事吧,我没有见其他地方用read, write, lock的Read-Write-Lock 没有提供利用这个Read-Write-Lock 一般你说这个Read-Write-Lock 这边是Two-Face-Login的这个ReadReadLock因为这个2-phase lock-in,它这个performance penalty 太高了,这个performance penalty 非常非常高,就是你跟其他的可能会差好几个数量级,可能不是一个数量级,不是10倍,可能差100倍这种感觉。所以它很多时候就有一种predictive lock,什么意思呢?你希望是就是你lock呢是lock一个potentialpotential object就是说我们刚才举的例子是吧,我们刚才举的例子是,就是,我们刚才举的例子就是医生的例子你还是有实体去lock的但是假设一下说你是比如说假设一下是Claim一个Book是吧,你有一个典藏版的一个增藏版的一个Book然后你去claim book,然后看你有一个side说,哎有没有人claim这个book然后你这个ownership就有一个ownership table,你在这个里面是没有一个material的东西去lock它的或者是我举个刚才我们讲过的例子这样context 话也不是太大去Claim Meeting Room你要claim今天晚上7点到8点的meeting room,你可能有个meeting room appointment table,是吧?你说7点到8点,meeting room A是属于Scott的,所以这个比如说这个比如说另一个brand过来去claim这个七点到八点的meeting room,就不能claim了。但是当我进行这个appointment这个table读的时候,是没有一个东西我能去去reference他做lock的,因为这个appointment还没有写进去但是我是可以把整个table进行lock了,整个table进行lock,但是这个performance会很糟糕嘛我也可以对其中的indexing,就是我们这块要讨论的,就是说你没有一个具体的materialized,没有一个实力化的东西实力化的肉让你去进行lock所以这种情况下你更多的时候是一种就是一个predict lock,就是一个你说的一个condition lock,就是你说的一个条件锁,我不知道怎么中文怎么翻译,predict lock就是条件锁,就是满足这个条件的row全部必须被锁下来,这是理想最好的条件,比如说条件说什么,meeting room A 7-8 必须被锁下来,我在写的时候不能有人访问 meeting room A 7-8 这个 row 在 appointment table 里面,但是这个 row 是不存在的,我在写的时候这个 row 是不存在的,但是我对它设置了一个条件,满足这个条件的肉。然后我写完以后,别人在写的时候就不会出现这种,不会出现我们之前说的这个Rise-Crew的问题。条件所的实践呢,就是它实践会有很麻烦的地方它是很难去实践的,实践了以后 performance [35:00] 不太好所以它就产生了一个什么呢?就是产生了一个则中就是条件说Pretty Clock会相对比较难写所以它产生一个折衷那我们这话说,那这个Pretic Lock对应的Docker Problem是什么呢?就是说我doctor problem是什么的就是我对于这个shift里面我对于所有on call的doctor进行lock我对于这个shift 就是明天晚上的shift就是oncall shift 我对于所有oncall doctor为处进行lock然后这个的话就满足条件了 是吧就这样的话就不可能会出现这种不可能会出现这种Race Condition了但是这个实践非常困难,我没办法对这个实践进行,我取一个责任什么呢?我对这个index进行log。什么意思我对明天晚上值班的医生进行lock我对明天晚上值班的时间段进行lock这样的话这里面可能就有多个一声是吧因为我lock的index上我modify的时候可能有很多true可能有很多false如果你是false的话你就没办法更成true了因为我整个time range被我lock住了但是我是触的话,我感想放弃就不会出现我们之前讨论的问题。它lock是比刚才的PrettyClock更严谨一点,就是更多一点。PrettyGlock就说,On Call为True,然后明天晚上lock,就是两个lock,你lock了只lock了两个人在我们现在的话就是明天晚上lock那其实lock的话可能整个lock了整个 entire doctor table这个肯定是能guarantee你不会出现我们之前说的rescool的问题但你lock的row也更多一点,你lock的criteria更多一点这个的话是实践的很多database都支持的所以这个叫index-ringedlock,比如说明天晚上oncode进行lock这也是Database中订票系统,Compete系统也是很常见的一个东西。然后我们可以是,就是,从工程的角度来讲,从engineering的角度来讲,你当然是可以用database这个非常复杂的lock,不管是optimistic,lock,passimity,lock,Pessimistic Log当然是可以实践了,但是我觉得从工程的角度实践来讲,因为我之前实践过一个Optimistic Log我觉得更重要的一个东西是最好你不要用它,为什么不要用它?因为它这个你写完了以后你可能是非常clear这个概念什么的,你optimistic lock很清楚,pessimistic lock很清楚,你中间什么样的risk condition很方便但是你这个系统不一定以后是你维护首先这个系统以后不一定是你维护其次之后写的人不一定知道怎么样 maintain不一定知道should the risk condition这个也是这个也是那个,如果在这个业界当中足够走,毕竟写过这个lock的话,也会知道,有很多坑你是不敢碰的,就是你看到它会写很多comments,然后注视很多age cases,然后risk conditions,但是你很多时候如果涉及到lock的话,你是不敢碰的,这个东西是不敢碰,不敢改的,所以说最好的情况下,就是我们可以参考一下,就是大家可以参考一下那个12306那个,记得我们之前讲过一个12306设计的那个,也是相当于是个booking system,就是,嗯,火车票booking system看一下那一张,然后我记得我当时proposed的一个changes是,我记得是没有用到这么复杂的lock的,因为我proposed的是你不进行修改,你其实你相当于是上来就进一个排队是吧,排队就是insert一个role,然后后面所有的处理assignment都是后面的所有的assignment都是background进行处理的在那个在那个Google 传里面有过去的recording,所以这个lock你知道当然很好,但是你应该尽一切努力不要用到它,尽一切努力不要用到这个这么复杂lock,因为这个从这个公司效能就是从那个,如果你从公司的角度来讲的话,当你用这个lock的时候,你冒冒的风险就是没有人能懂,就是很多时候没有人懂,这个这个其实,嗯,其实就是就是就是我觉得experience development,就是经常会看到一个问题就是不止一个地方看过是吧就是写的时候写程序的时候那我本来写了100行程序写了一个非常复杂的algorithm解决这个问题了但我觉得好像之后maintain的时候别人看不懂是吧来的new guys 看不懂或者是之后maintain的人看不懂,可能出问题的概率更大,那我干脆还是写一个简单点的algorithm,但是可能效能稍微低一点,或者是他这个可能会有其他地方稍微低一点,但是简单的话就更好maintain,那作为一个负责的程序员,那其实写一个简单的algorithm。是最好的,就是有时候可能会牺牲一点其他的,但是simple is always best.这个index range log 有时候写完以后会很复杂特别是这是一个single node的index range log 如果你变成一个distribute system 还会更复杂一点你变成一个distribute system,然后这个data可能 [40:00] 然后再加一个replication这个复杂度一下就上去很多了你就会看到各种鬼故事看到各种鬼故事好,我们就是在这个讲这个PreticLock和这个IndexRangeLock这块有什么问题呢?这个相对来说是我觉得可能很多人之前都没有接触到这个故事,就是接触到这个概念也是two-phase locking和这块有什么问题大家有问题的话可以直接问好好好这话有说之前有什么鬼故事啊?谢谢大家我讲一个就是optimistic locking的一个事吧我就是之前时间的Optimacy Lock是这样子的,是一个三段覆盖的一个architecture我当时实践的有点像 Airflow,就是自己实践了我们公司自己实践了 Airflow然后你会Send一个Request你会算了一个 JSON payload,然后这个 JSON payload 里面生存了一个DAG然后生存了一个DHG,然后我们会pause JSON payload,然后在后端呢生成一个analytic table,就是column based table,我们自己就是Salesforce自己写了,他其实买了一个startup,自己写了一个column analytic database然后呢我这块呢就是做了一个这个就是这块其实这个Data Model还是非常复杂的因为你需要去Q一个request我记得叫Dataflow Request 然后你会你根据你的Request 要把你的Request 转发成Dataflow Instance然后当时request 是你可能有conquer的同时几个人在做queue request,因为user的话你可能会不停的click这个request 是吧?然后当时的话我们当时做了optimistic locking什么意思呢?就是我做这个request,我会conver成instance但我convert成instance的时候,我这个比如说我举个例子,你nodeA上有一个request,接收到一个request,然后把requestA,nodeA上有个request1,要把request1convert成一个instance你nodeB上接到request2要把request2converged成instance然后这两个request的instance是不能同时run的,因为它这个同时run的话你会写进同一个dataset,你这个dataset就cropted了,所以这两个instance在initialize以后我们当时写了一个就是optimistic locking我们会写完以后就是initializecreate了以后我们去database读一下会看有没有同样的 instance 写到同一个 dataset我们注意到这个 dataset 写是个 JSON 是吧?这个 JSON 是 DAJ 嘛所以你这个 input dataset 和 output dataset 其实都是 user define所以我们会把你说要写的datasites,然后读出来,然后去那个其他database里面看有没有其他database instance,写到那个datasites,会不会有corruption,会不会有clation,或者叫clation,就是一个clation嘛,如果有的话,你就把这两个instance因为你这两个instance是不知道谁先谁后的所以我们当时的设计是什么呢?就是我不知道怎么样foss另一个人的Bot,所以当时设计是两个人同设Bot,两个人同事的Bot,就是你相当于是我们再回顾一下这个问题,这个问题很复杂你这个request呢是send的一个json,你是读一个json,这个json是个daj,你是读一大堆data site,然后进行一大堆transformation,然后写进一大堆data site。然后然后这个request呢,你可能来自于不同的user,一个公司不同的user,是吧,request,然后你两个request会去不同的node会去不同的node,所以node1可能会有这个request,node2会有个request,然后他们两个之间是同时进行的,所以他们检测的时候,你是一开始是没有检测到config的,因为它可能是完全不同的request。但是他进行写的时候当你这个 instance 进行准备去写的时候发现他们同时写到同一个 data set同时写到同一个 data set,那这 data set 就可能就是你想同时写到同一个 file,这个 file 就没法用了是吧?所以必须有一个 instance share bot,中间有很多 concurrency 必须有一个 instance bot,我们当时设计什么呢?就是同时 share bot,然后呢过五分钟后,我们也看这个 request,这两个 request 还没有被 serve,那么我们再重新 kill 这个 instance,让它重新执行。这个理论上来说是非常一个real的instance, 因为不是customer facing, 因为是to be facing的。 按理来说是一个非常real的case。但是我当时发现有一个客户,因为Selford不是全部服务2B的吗?有一个客户的request就从来没有被执行过就是他的request从来没有被convert成instance然后我就看这怎么回事儿,我就发现 [45:00] 当时Dick saw Splunk Dog的log有很多看了过去30天的log,然后这个log就花了很久时间去debug,就看这个log有什么回事。然后我就发现这个用户不管写多少个request不管写多少个request,假设写一个request,写两个request,他每次写的时候他本来是想做一个request的但是他每次都会传到两个request他每次算的都是两个request 你知道吧 他不管进行什么要求,它每次都是两个request,两个request就变成两个instance,因为你想这个sign and forget 执行,并且analytic request 其实时间是非常久的,执行就要十几分钟半个小时。甚至几天都有可能,因为几天还是有可能的,否则也不会有48小时这个transaction,所以它进行一个request的时候,internally convert 成两个request,然后这两个request一定会deadlog,然后代log以后,然后就全部about,然后再过10分钟以后,有一个background process,然后再同时进行process这两个request,然后再代log,然后再about,所以就是这样子。所以最后我们还是没有查出来是什么问题,我highly expected是个前端的问题,是个前端的duplicate request,我highly expected一次前端的问题,但是我们当时没有查错问题。但是我们这块的话就后来做一个Ddupe,就是如果你这个request是同一个事情的话,我就把你的request 继续不成一个式了 就变成一个request了谢谢大家所以就是你可以看到这个复杂度是非常非常高的,就是,对,就解决这个问题了。但是,您可以想一下,从用户的角度来讲,他每次send的东西,他那个request从来没有被served,就一直在cue在那里是吧我想把我的dataset transform成一个dashbot,我的request永远刻在那里,从来没有被执行过,那它就非常frustrated从我们的角度来讲 我们就不停的看到 risk condition 不停看到 risk condition 每10分钟一个 risk condition 持续了一个月 你可以想一下每10分钟一个Race Condition 持续了一个月,那其实就很奇怪嘛,那这个是最有情况下来的就是需要很多debug这个knowledge从另一方面,你如果想,如果是一个New Grad,或者是一个新加入team的人的话,这个 onboarding是非常痛苦的,你首先得知道整个这个 optimistic lock 是怎么实现的,然后得知道这个 request 是怎么样 convert 成 instance 的,然后你要知道为什么会有 conflict 是吧,为什么会有 conflict 的,因为你同时写入两个request写到同一个dataset里面会有writeconfig然后你要看出来这个request为什么会一个request convert 成两个dataflow request并且这些东西你还得不停的推导然后得从log里面找出蛛丝马迹,这个其实非常复杂的好,我们最后再讲一个就是serialized snapshot isolation,这个是最新的也是optimistic locking,这个就是optimistic locking,然后我们这块的话,我们会之前讨论了很多optimistic locking和passimistic lockingPassimistic Location就是在你进行任何写的时候,你已经把你的resource log 做了。只有当你把你的写完成的时候,你才会release你的这个resource所以这是"pessimistic locking"所以pessimistic locking基本上是guarantee 是什么呢?就是你任何一个时候只有一个人有你的 exclusive lock 在这个 database 上在这个肉上,所以你是不可能有collision的但是这个什么呢,就是如果你的这个clision很少你写的时候其实99.99%的时候都没有人跟你竞争的话,那你这个每次做pessimistic locking这个其实performance的效率是很低的,所以two-face locking就是pessimistic locking然后像Redis 用Serialized Execution 相当于是 Pessimistic 到 极致啊 就是那个什么呢悲观到极致了,相当于是每个transaction整个把整个database log 住你每个 transaction 把 database log 做整个 database 属于你这个 transaction那相当于是这个悲观到极致嘛所以这个就是像悲观到极致但我们之前不管是two-faced locking是在每个肉上的这个悲观锁还是这个Redis Serialized Execution 是整个database上的悲观所 都是一个悲观所但是悲观所有一个问题是如果你的collision很小 大部分时候你database设计你的collision都很小很小如果你这个设计的时候,你这个依旋就很大,那本身可能这个Data Model设计就有问题,这个Data Flow存在,就是这个Data Model设计就有问题是吧我们大部分时候是尽量地做到 [50:00] 从application level从application level你就要减少这个减少这个同一个资源的抢夺的所以说大部分时候因为现在大部分设计都是从一个资源抢夺从这个概率角度上很小可能只有千分之一万分之一甚至十万分之一的概率那么pessimistic locking performance就很糟糕因此呢就产生了一个叫serializable snapshot isolation是一种optimistic lockingoptimistic locking什么意思呢就是我写的时候是不锁的我写完以后我看有没有人modify我写的东西如果有人modify我写东西然后我about我的operation有人modify我写东西并且这个人已经committed那我就about我自己的transaction所以这是optimistic login所以简单来讲的话如果你的resource contention rate非常high就是你很多人在抢同一个资源你的所有人抢同一个资源的这个概率非常高那你要pessimistic login这种的话可能比如说你的transaction比较大或者是你这个transaction时间比较长然后或者是这个resource的competition rate确实比较高contention rate确实比较高那你需要做pessimistic locking如果对应呢其实你这个compensation rate比较低或者是你设计data model你之前已经想过这了那你就可以尝试用optimistic然后 performance会更好然后这块举了一个例子举了一个也是这个oncode的例子我们可以看一下就是这个对于transaction42来说这个其实和我之前讲的这个dataset是同一个东西这个dataset就是这个dataflow requestdataflow instance然后dataset的collision是同一个东西我们这里面,只不过这里面dataset就是这个on called doctor roledataset就是on called doctor role然后你同时有两个request想去change这个on called这个dataset然后结果就发现collision了,那必须有一个人about只不过我们当时做的是两个都about因为这个为什么两个都about因为你没法判断先后瞬间就是你没法判断先后瞬间,不是说clock是有这个clock也是有这个,就是不同机器上的clock是没有对比性的嘛然后你是没法判断先后顺序的所以我们当时都两个about可能有其他方法但是毕竟当时实践的时候是这么实践的没有做太多优化所以当时都两个都about这块 Serialized Snapshot Acceleration是做了一些优化就是你找出一个winner然后winner不about然后loserabout是这么做的那它具体怎么做呢就是具体怎么做呢就是这个transitinner不About,然后LoserAbout,是这么做的。那他具体怎么做呢?就是这个Transaction42呢,先去说是这个想去Modify这个OnCoff,是吧?然后他进行Committed,然后他Committed的时候,他会发现他要去增夺的这个资源,就是他要去Update的这个资源,没有人跟他进行Competition,就没有人跟他进行增多contention那所以他就committed掉了但是他写的时候呢他同时呢在这个在另一个table里面他会写说我进行了我这个transaction呢我进行了data的修改根据刚才那个也根据刚才那个multi-version的concurrency control我进行data进行修改我把这个alice shift从true变成了false从true变成了false然后我进行了修改是吧然后当他另一个写的时候Transaction43写的时候他会说我同时读这个虽然这个Alice的oncall还是true但是他同时被42被修改了是吧他同时被Transaction42修改了所以当他进行把这个自己的oncall shift从true变成false的时候他会看这个42因为42有跟他增夺资源42有跟他增夺资源因为42不管是在他之前因为42在他之前嘛所以42有跟他增夺资源,因为42在他之前嘛,所以42有跟他争夺资源。所以他在进行Commit的时候会看42有没有Commit,因为42已经Committed了,他进行这个Request,42已经Committed了。所以这个资源争夺,他已经变成Loser了,那他变成Loser就必须About,然后可以之后Retry。所以这个用形象化的例子来讲是什么呢就是相当于是你先做什么事情比如说两个人进行增夺资源然后做某些事情就这种情况下两个doctor同时想不想oncall然后他会先读然后同时想把自己选择off call然后当这个transaction42进行要改说我要off call了我不想oncall了那会看跟他自作争夺同一个资源的这个transaction43有没有committed因为他没有committed所以对于42来说他的assumption没有变assumption是什么呢同时至少有两个人在oncallassumption没有变因为他assumption没有变所以他可以committed所以他就committed了但是43呢因为他知道42的存在他当进行committed的时候他会看assumption变了没有然后他看到他想committed这个时间是在42之后的 [55:00] 去about然后retry这个实践的另一个原因就是说整个transaction其实压缩到一个点了所以整个42的transaction是在这个时间点上的在这个时间点上 这辈子这个时间点上所以43如果发生在这个时间点之前那就是变成43 committed,42没有 committed但是43发生在这个时间点之后所以就变成42 committed,43没有 committed这个其实也是把整个transaction压收到一个时间点的办法,它只不过是我先做任何我想做的事情,我把它做完了以后我check一下跟我有conflicting的transaction有没有committed?然后如果没有committed我committed然后这个里面我看有人问说,start time 的 transaction matters 吗?不 matter。不 matterStart time,我们43可以在42之后43可以在42之前就是假设我们把这个换一下43在42之前然后一样的到42写的时候42到他写的时候他也会committed,就是他也会看一下,看到43跟他抢夺资源那43跟他抢夺之前,但是43并没有committed,所以他可以committed。然后43就不能committed就是说Transaction 是一个时间段,你有 transaction 开始时间,做下了一大堆 operation,你有 commit 时间。但是这个里面 Serialized Isolation 就把整个 transaction 变成了一个时间点,就是 commit 的这个时间点。你在 commit 的时候,你 database 是只有一个 thread 来处理 commit 的,所以在这块是不可能有 collision 的。所以这样的话,当你进行多个transaction对同一个资源进行competing的时候你有一个时间点,你就看这个具体的Commit的时间点,谁在先谁就是Winnet,谁在后全部得About,所有的Optimistic Locking的Transaction全部得About这是不同的地方所以如果你是这个optimacy lock你有很多collision的话你有很多transaction在compete同一个resource的话你其实效率很低但是大部分时候如果都没有 collision 的话,那你其实效率会很高,你会比这个 passimity悲观锁会效率高很多谢谢大家对,所以这个里面其实就是把所有的commit transaction变成了一个时间点把所有的transaction变成了一个时间点,然后 detect 有没有其他时间点,有没有其他transaction跟你compete result,然后根据你时间点的前后来决定谁是winner,谁是loser这也是这块的话也是给了一个更具体的例子这个例子这个例子应该是就是怎么样 inform,就是43进行 commit的时候怎么样 inform 42了他这块的话就说他inform 42 什么呢?就是你看这个情况就是43线committed的,那怎么inform 42 呢?就是他写的时候他会说,K-range 1234 从事被43访问,然后他把这个,当他把这个true变成false的时候,就是他会通知另一个transaction,我也在写这个资源是吧,我也在写这个资源。所以当这个43Committed的时候,43, sorry,所以当这个42 committed的时候,43会说他争夺失败了,所以他必须要bot it这块实践的话应该还是有多种实践方法,可能只是一种实践方法。但是这个Basic Concept和逻辑就是,你就是先claim想做的任何事情然后你去做,但你commit的时候看有没有人跟你有conflict如果有config,他有没有committed那committed的话你必须要bot,如果他没有committed,你先committed,然后他之后必须要bot就是它这个逻辑,这个是在database level实现的,这个是Postgres也是支持这个serialized snapshot isolation,然后这个foundation db,Apple的这个foundation db也是支持这个的。嗯相对来说也是比较一个新的概念了好,这块有没有什么问题啊,就是大家知道这个概念显得,并且从这个概念角度来说,这个performance是最高的,MySQL好像没有,MySQL好像是没有这个serialized snapshot isolation,MySQL它的feature相对比较少一点,但是相对比较robust一点。好像是没有这个snapshot isolation,serialized snapshot isolation,你可以搜一下,我知道postquise是有的,但MySQL还是没有的。谢谢大家MongoDB应该是没有的然后这块大家只要搞清楚一个high level的概念,因为首先这个概念是比较新的,我记得09年还有这个第一篇paper的,然后后面实践其实没多少年然后是比较 [60:00] 新的一个概念就是optimistic locking 但是它这个本质就是搞清楚这个本质就是两个人同时进行compete,然后先的人会变成winner,之后的人必须要bot它跟passive mystic跟悲观所不同的就是悲观所是在你compete之前就给你限制死了所以它这个,悲观说,compete之前就限制死了,所以这个,你就没有compete的这个概念是吧,所以悲观说的话,它那个performance会比较糟糕,因为每次到lock的话,你lock如果是row level的可能还好一点,但如果是column level的如果是Database Level,或者Table Level的话, performance就比较糟糕。但是Optimistic Locking的话,其实是,如果你的增强同一个资源,对于Single Resource来说,Single Resources来说你这个增多率比较低的话你Performance会很好但如果你这个Contention Rate比较高的话那这个Performance就比较糟糕了好,这块有没有什么问题?如果没有的话,我们就去下一张,就是Consensus and Consistency.谢谢大家谢谢大家好,没有的话我们就去下一招优优独播剧场——YoYo Television Series Exclusive好,这也是我今天讲的最后一讲了,就是这个周四这个课讲的最后一章了,就是Consistency和Consensus,第九章。这一张和上一张不同呢这个是distribute system的这张的来源就是在你做replication的时候,因为你有replication所以你不同的database会有不同的copy data,你这个database会有不同的copy data,是吧?你master data是肯定是一般来说是比slav,如果是asynchronized replication的话,你是会比slav要浅,就是会有一些discrepancy的在任何一个时间段都会有discriminacy,如果你没有data写的话,没有data read的话,eventually会consistent,是吧?insynchronized replication eventually will consist,基本上在任何一个时间点,任何一个snap chart的时间点,你都有discrepancy的,那这个discrepancy就是你就是这个consistency和consensus问题的源泉replication 是这个 consistency 和 consensus 这个问题的源泉Consensus 相对来说也是这个 database 里面最复杂的一块,然后我们知道LandPod也是去年还是前年拿到Turing WordTuring Award 就是Turing奖也是LandPod 就是在Distribute System里面的Consensus和Consistence里面贡献最多的然后这个Consistency和刚才我们之前讲的Transaction consistency是不一样的这块Consistent很多时候更像Linearability这种Consistency所以把它是不同的database之间data的就是不一致性不同的database之间,你可能是master slave之间的data的不一致性,很多时候都是master slave之间的data不一致性。如果你是DenamoDB或Cassandra的话,是不同的replica之间这个database的不一致性但是刚才的那个consistency是logic的就是client logic的这个consistency 比如说这个比如说什么呢就是同时必须有一个doctor on call,你是一个client business request的一个requirement,是吧?我从database的角度来讲,所有人都是false, perfectly fine,是吧?虽然都是法子,perfectly fine,但是你从这个requirement, business requirement 来讲,如果没有人 on call 的话就不ok 了,所以它纯粹是个 business requirement,就是 consistency 很多时候是 client request在这个database的角度, consistency是client requirement,但distribute system里面这个consistency是不同copy之间这个data的discrepancy,不同copy之间data的discrepancy,所以这个database的consistency它的源泉它的问题的源泉是client requirement是吧,那我如果说的Data Doctor都是off call,那从Data Base的角度来讲没有任何问题啊就是就是它这on-core是false吗没有任何问题啊可能确实有一个valley requirement是on-core而false但是这个distribute system 里面这个 consistency 它问题来源是replication是replication 理论上来说你可以保证所有的replication是synchronized replication 但你这个database performance 就一塌糊涂了所以这个Asynchronized Replication的话就会导致这个Distributed System的Consistency issue好好所以说这个,然后我们讲这个consensus的时候,就是因为database可以发生任何事情嘛,那我们必须要去tolerate各种faults, [65:00] 我昨天最早的时候就是5月份的时候讲过说可能的force是吧比如说你clock可以drift,你clock一台机器的clock和另一台机器的clock差几十分钟然后你这个process 可以pause 当你做stop the word garbage collection 的时候 你的process 可以pause 一个小时 长达一个小时 以后一个小时不常见,但是长达几分钟是有的,十分钟是有的 process plus,几分钟到十分钟是偶尔会出现的。然后你的network可以delay,Network Delay是unbounded的,Network可以delay几分钟几个小时,甚至整个package有lost的,所以Network Delay是可以unbounded的。然后,所以当我们用这个design consistency的时候,我们必须有一些assumption,你必须对single node有一些assumption,你对single node的assumption负责化这个问题就太复杂了,我们知道single node database有可能是crash的问题,是吧?同时访问dataspace的问题啊,然后你这个storage device可能不是perfectly reliable 的问题啊。所以我们必须说我们这个single node在这个Distribute System里面,Consistency和Consensus对于它是Single node必须满足 ACID 就是这块 ACID 就是必须满足这个这个这个这个这个Database 提供的Transaction Guarantee当你用这个databizity one transaction的时候你至少能guarantee就是从这个理论上来说,这个crush就database帮你handle了,你就不需要考虑single nodecrush的问题了。就是Single node crash的问题了,就是你要么就check in,要么就committed,要么就all or nothing,就不会有说多个object进行write,然后只有一部分写进去,不会有这个问题了。然后你同时也不用考虑这个Single node里面有isolation的问题,就是多个人同时访问的话,isolation也帮你处理掉了。然后你也不用考虑这个Durability的问题,所以就为什么说这个作为一个程序员,如果你要设计一个很严谨的程序,serious problem,谢谢大家你很多时候优先考虑SQL,这个SQL其实帮你解决了很多问题像NoSQL的isolation连read committed都没有保证是吧read committed都没有把握到 它可能会出现各种counter cases所以Debug说头疼很大所以优先考虑SQL,因为这个SQL会处理很多问题谢谢大家然后其次的话当你用到transaction的时候,当你用到这个SQL transaction的时候,你就其实就是有一种abstract,当你用了这个abstract的时候,很多最头头的问题就帮你解决了。比如说这个atomic isolation durability 这种最头疼的问题所以从现在来说最终的问题帮你解决了所以说这块的话我们就讲了为什么这一张有这张的存在,以及为什么要Single node transaction,它去解决程序员很多最复杂的问题。对程序员来说,Backend programmer 里面最复杂的问题是可以 rely on transaction 的 abstraction 来解决的。以及这个distribute system的consistency和single transaction的consistency的区别这块是讨论了一些这些不同如果我们看这个里面的话这个这一章会cover一些比如说distributed transaction,就是Two-Face Commit。注意这块是Two-Face Commit和Two-Face Locking不一样的,我们只会讨论一下是不是Two-Face Commit。然后这个consensus呢和这个consistency呢其实是多个问题相互转化的就是你的Total Order Broadcast等于Consensus等于Compared Set Lineability我再重复一下,我们之后会讲它们各个是什么,但是这个里面有点像这个P和NP的问题,我们讲的就是如果算过计算理论的话,NP其实是有33个问题,但是这33个问题就全部能转化成同一个问题,就是NP问题。不管是 travel salesmanTravel Salesman Problem 还是三段论是吧?其实它可以转化成同一个问题,就是NP问题,这块是一致的Total order broadcast 就是讲话上同一就是concensus problem也能转化成这个Compared Set Linearability Problem所以这是同一个问题只要你解决了其中一个问题其他问题可以迎刃而解就跟NP问题是一样的,我们现在不能证明P等于NP,但是NP的17个常见的NP问题其实是同一个问题你只要证明其中一个问题,剩下的32个问题可以引刃而解所以这三个问题就是Total Order Broadcast, Consensus, Comparison Linearability,同一个问题然后基于这个问题我们有了这个Locking 比如说Rulekeeper 实现了Consensus然后它入Kipper可以提供一些membership services,相当于是这个Consensus application的应用有很多能解决的问题。然后还有一些比如说timestamp ordering,casual ordering也是在这个distribute system里面常见的问题 [70:00] Thank you. So we will talk about linearability, because linearability is the best to understand. And then we'll talk about linearity will talk about total order broadcasts, and then the consensus. Consensus and its use is Rulekeeper and Membership Service And what is distributed transaction? And why we should not be able to distribute the transaction? What is cost? What is the cost? It's a very good thing. Transaction is very strong. Safety will guarantee. What is cost? Okay, so we will talk about linear ability. We have a linear ability. Linearability 概念是什么? We say distribution system in the distribution system. you will have a primary database, and we don't consider partition. Partition is from the original概念. and the single database is the same. We don't call it partition. to just call it replication You have primary database There are two secondary databases in this situation. So linearity is what? When you have a database, you have primary and secondary, but primary and secondary are the same. Predemate and secondary are still alive. But when you're in the database, you feel like it's only one database that serves you. Just use a database for you. So when you were writing a job, When you're committed, you'll expect you to read the letter after reading the letter. This is very simple. You write is you read. You write it after you can read the letter. You read the book is your first writing, not the old writing. And you read the值, you can read the first one and the second one. This is not going to exist. In this situation, we discussed the first one. You first read the new and the new and the new. You can say that in the "Degrees杯" is 16 years. You don't need it. 2014, 2014, in the world of the world, when we were in the world of Argentina, we were in the world of Argentina. Then we changed it. "德国 1:0 Argentina" Is it right? "Devue Ebeneer" and "Argentin" And then you have "Devue Ebeneer" and "Argentin" and "Argentin" and "Argentin" and "Argentin" and "Argentin" and "Argentin" and "Leader" is written. And then you have "Asynchronize" and "Replicate" and "Follower" you know for the first time So Alice said, "I got 1/0 a good time." Then he told Bob, Bob said, "Is it?德国 already won?" and then, "I'll see." Bob read the following 2. Bob read the following 2. This data is not replicated. So, no. Don't mail This website is not new. If you don't have a database, so many people will not be a leader or a follower, but a single database. You will find the time of the回溯. You first read 1:0 and then the other read becomes 0:0. Actually, it's a way to say that it is违反 Linearability. So linear ability is the recent guarantee. You read the first thing. If you don't have the first thing, you don't have the first thing. You don't have the first thing. You don't have the first thing. You don't have the first thing. You don't have the first thing. 好啊 This one looks like a simple thing, right? But there are some very complicated places. Linearability is very simple, but there are some very complicated places. And then we'll look at different examples. First, Linearability and Serialization is not the same. We discussed this serialization. Linearability is what is the result of replication. The result of replication is the result of replication. Just you have a copy of data, every copy of data is inconsistent. So this is replication of the reason. Linearability, if we can do this transaction, it's linearability and serialization is different. Linearability is different from the serialization. It's guaranteed. You're going to say that you're So linear ability, it's not to prevent the dual object. So, for example, the right school is the doctor. from Docker's question, if it is completely full of Linearability But it can't be a good transaction. uh, to manage serializability, For example, doctor's question. It is possible to be able to read and write the book. Your doctor is the oncology. but it can't be a serialized ability. The assumption can be broken So, to the right school, right school is not a good person. [75:00] Gasco是显然不满足Serializability的但是它可以满足Linearability所以这些Linearability和Serializability不一样你是 Linearability 不代表你 Serializability啊另一方面来说你可以是serializability,但你不是lineability,什么呢?比如说你这个snapshot isolationsnapshot isolation 你snapshot isolation 你就是by define 你就读的不是最新的 你读的是still data是吧,你snapshot isolation,你读就是steal data,你在读的transaction2发生之后,Transaction3发生之前的Data有可能有这种什么情况呢?就是说,我们举个例子,你有一个read request你有一个read request,我们先把这张弄回去你有一个Transaction2然后有一个transaction3然后呢有个transaction4是吧这是一个常见例子是吧然后你这块呢有一个read request1所以你读的是Transaction 2之后的事情,但你是读不到Transaction 3的。然后呢 相应的在这块呢有一个ReaderRequest2Rainer request2,然后在这返回了所以呢你会看到什么呢如果从Linear Billet的角度来看,你会先看到Transaction 3 committed了,就是德国已经赢了世界杯了,然后再看到Transaction 2。所以它这个是这个Linearability是没有guarantee的所以所有这种snapshot isolation的lineability是没有guarantee的我们再看一下有Transaction 2 Commit Time,Transaction 3 Commit Time,Transaction 4 Commit Time,所以这是每一个时间点。你有一个read transaction 是在transaction2之后的,所以你只能读到transaction2的东西,你读不到transaction3你有第二个 transaction, rate transaction是Transaction3,你是能读到Transaction3的,所以当你这个Raid的Transaction完成的时候你transaction 3就是德国已经赢了世界杯已经返回了在你Transaction 1稍后返回的是吧,那你这时候就说没有返回是吧,所以这个Linearability应该是没有把揍的好像不对啊,我想象好像不对啊这个例子是不对的,Linearability这块这个例子是不对的,因为这个有可能是有可能是什么呢?就是你这个在这个database读的点是在这儿然后这个database主的点是在这儿所以说后面全部都是Network Delay,所以这个其实理论上来说是不broke linear ability 的这个理论还是不broke linear ability的我得再想一下这个Counter Case,这个例子应该是不对的这个例子应该是不对的,这块这个什么东西满足Linearability,然后不满足这个什么东西满足Linearability,但不满足Serializability,这块是非常tricky的刚才例子是不对的,结论是对的,如果你是snapshot isolation,你是不满足lineability的但刚才的例子是不对的刚才那例子是不对的我再想一下这个例子或者再想一下例子,刚才例子是不对,结论是对的,就是最后的结论是什么呢?就是如果你是 snapshot isolation, serialized snapshot isolation,你是满足seriability的,但是你不满足lineability但是你可以满足Linearability,但你不满足Serialability,这两个概念是不等价的。这个结论是对的,例子是不对的。好 我这块今天到这为止吧然后这个Linearability这个例子也是比较复杂的就是我们到时候我明天我下周的时候下周也是最后一个这个周四的这个就是讨论了,我们再把这个例子再写的更细一点刚才举的这个例子是不对的 我得反省一下哪块不对啊 我得想想哪块不对啊但是这个例子是不对的,因为我记得我之前思考的时候想过这个问题,但是这个例子是不对的,我得想一下哪块不对嗯所以这个Linearability assumption其实比较简单,但是它中间例子Counter case是比较多的,我到时候再想一下大家有什么额外的问题吗?我们要讨论一下然后这张的话我还是建议大家提前看一下,复杂度是比较高,然后中间这个很容易理解错,就是Linearability这块很多东西容易理解错,然后 consensus 的话也很多理解错然后除了看这个slides以外,我建议大家读一下这个RAFT的algorithm,也是比较复杂的一部分,我建议大家读一下然后这样的话我下周讲的时候会相对来说比较轻松的理解一下还有大概十几个slides吧 但是相对来说复杂度比较高 [80:00] 有什么问题吗有什么问题我们可以讨论一下谢谢大家哦,Scott,我有两个问题一个是好像刚刚来晚了,然后听到你说DynamoDB是不support那个Recommitted 是吧?哦 对但是他有一些文章说他还能support这个SSI那所以这个就是偷换概念嘛他有些文章说他support SSI也可能不是偷换概念,他有可能提供,因为这个毕竟DenimalDB他最早的那个paper是非常简单的,是不提供这个的,但是他之后的话可能会有一些优化来去support SSI哦,那在我的认识里面,recommitted是不是SSI的必要补充分条件?对,它有可能的,因为它这个DenableDB,我们说的时候,recommitted是SingleDB的Database EngineDynamo之前是换过一次Data Base Engine 我记得他那个原来的paper写的是用Berkeley的那个Berkeley 的那个 Database Engine 后来换过一次 Database Engine所以他现在你用的Denimal 可能和他最早的Paper的Design的DenimalDB的那个Engine不一样了那安置不一样以后他可能改下configuration并且这个Dynamo里面是有不同的support是吧我记得Denimo像我们那天我那天和Rose讲Rose说他那个payment是用Denimo实践的然后他用了Denom的一个guarantee叫叫什么来着我想想用了一个Denum的guarantee叫好像就是一个Linearity的guarantee好像就是一个另一边,你想我们payment是用denomal时间的嘛,它可能会进行一些优化,然后他比如说他减去这个sloppy coral嘛,减去这个hinted handoff啊,他可能进行一个优化,然后然后可能会它的这个assumption会更强一点就是它这个guarantee会更强一点但是Denimal的话因为它现在是个closed source所以说我们也不知道它具体用了什么这样子,如果他能够实现SSI的话,他应该可以实现recommitted是吧?他实际上是肯定是有点 committed的。哦,对对。那第二个问题就是,就周二,周二你那个Move interview的时候,我当时还在工作,所以有一些可能没有听得很清楚。就是,当时你问了一个问题就是这个如果就是他就是Worker从SQS里面,就Worker从EventQ里面拿到了拿到了一个job 然后去pass到SPS或者是Band service. If this process failed,像那个Q要怎么处理呢?哦,那个SQS的那个Q非常有趣,我也是第一次知道,SQS它的那个availability很低啊,只有三个九,就是表示一年就要当八个小时。但是你的Q的Request是不被Serve 除非你是Explicitly marked it deleted也就是说如果你这个process在任何一个情况下fill掉的话你这个request还是会还是在Q里面的就其他人可以去claim the request哦,ok,I see.所以这个周粉在那当时设计其实是用了这个SQS的一个错证确实其他地方可能会没有这个实现这个Q的话我感觉他这个logic也虽然不是很复杂吧但也不是很简单所以他所以导致他这个SLA很低嘛只有三个九那一年当八个小时的话你使用的时候就非常小型嗯, I see.在worker A在处理第一个Event的时候,那worker B是可以处理第二个Event吧?对,他可以用一个worker pool,就是你worker过来claim最新的这个request,按顺序来claim这个request哦哦,那时候,这个request就不会被其他worker接受的。对 他这块可能还是有一些claim的操作吧 应该是有一个claim的操作我没有用过SQS 但是我不知道Claim操作自己怎么是 [85:00] 如果你就是觉得这个时间比较复杂的话我其实觉得最简单也是Uber的payment的时间Uber payment其实也这么早时间了我记得06年读Uber engineering blog 16年就有个 engineering blocker 这么时间你把这个所有的payment变成就是N个partition,1000个partition,1万个partition,然后每个partition有一个worker,最多有一个worker活着就可以了然后如果这个worker down掉的话 你再用比如说用Kubernetes啊就OK 使用另一个新的worker起来 那这样的话你就没有clisher了好的好的谢谢谢谢观看谢谢大家好,我们还没有其他的问题讨论一下这块SSS是什么? Serialized Snapshot Isolation,就是我们刚才讲的那个optimistic locking的那个 Isolation,就是那个 Serialized Snapshot IsolationSSI这个概念非常紧密,因为它从Pipper发布就十年时间我记得看那个performance的话这话也给一个数据他当时做研究的时候SSI和这个two-phase locking进行对比SSI是比two-phase locking快100多倍两个数量级到三个数量级之间但是跟他这个后面有很多paper写出他这个two-phase locking可能做的有点太restrict了是吧Strick的这个 serialization所以它这个它这个太strict了,所以这个SSR improvement 来会比它好100多倍实际上可能没有100倍到1000倍之间但是它这个performance比至少高一个数量级是肯定的也跟这个Data Access Pattern相关嘛,你要是所有的都有collision的话就是所有的Transaction对于同一个Object进行Compete的话,SSX肯定会比较惨的。也跟这个Data Access Pattern相关好,没有问题的话,今天就到这吧,非常感谢,我们下周二见。谢谢大家谢谢大家