Scott Shi警示快照隔离难防更新丢失 跨系统一致性须靠业务幂等

[Chinese] Chapter 9: transaction part 1 — Scott Shi

Scott Shi

快照隔离因并发旧读缺陷无法阻止更新丢失,而高成本的XA分布式事务在现代工程中已被At-Least-Once结合下游业务幂等所取代。

AI-assisted summary of [Chinese] Chapter 9: transaction part 1 — original source. Check the original for full context.

Source published:

Key ideas

  • ACID 四要素并非处于同一维度的数据库特性:原子性(A)本质是可安全回滚重试的“可中止性”,一致性(C)实际上是业务层约束而非数据库底层技术保障,持久性(D)则依赖 WAL 刷盘或写时复制指针切换作为决定提交的核心检查点。 (20:00)
  • 快照隔离(Snapshot Isolation)主流基于 MVCC 实现,达成了“读不阻塞写、写也不阻塞读”的高并发一致性快照,但其理论模型天然无法防御“读取-修改-写入”循环所导致的更新丢失(Lost Update)灾难。 (45:00)
  • 防范更新丢失需跳出快照隔离本身,依赖数据库原生原子更新操作、SELECT FOR UPDATE 显式排他锁、引擎级冲突检测回滚或 CAS 条件更新机制将并发竞争压缩为原子胜负判定点。 (70:00)
  • Kafka 宣称的 Exactly-Once 具备严格的作用域边界,仅在其内部“读-流处理-写回”闭环结合幂等 Producer 与 2PC 事务下成立;一旦流转触及外部异构系统或通知接口,语义必然退化为 At-Least-Once。 (76:15)

Detailed notes

## Background 该视频由 Scott Shi 于 2020 年 12 月 22 日发布在 YouTube 平台上。视频是其系列系统设计与数据库分享的第 9 章第 1 部分,重点围绕事务(Transaction)的核心概念、设计权衡以及底层机制展开深入讲解,并配套提供了专属的演示文稿供参考。

## TL;DR 本期内容系统性拆解了数据库事务的核心抽象与其背后的并发控制机制。主讲人从经典 ACID 的现代本质与微事务演进入手,剖析了存储过程在软件工程中的痛点以及分片和外键的架构权衡,并通过转账与邮箱等经典场景深入阐释了脏读、脏写与读偏差等并发问题。随后,内容重点解析了快照隔离(Snapshot Isolation)如何依托多版本并发控制(MVCC)版本链与垃圾回收实现“读写互不阻塞”,以及如何利用原子操作、显式锁和 CAS 应对快照隔离无法防御的更新丢失(Lost Update)缺陷。在末尾问答中,Scott 进一步划定了 Kafka 闭环 Exactly-Once 语义在跨异构系统边界下的工程现实,并指出了传统 XA 协议在生产实践中的高昂代价。

## Atmosphere & style 分享散发着极其硬核且务实的大厂工程沉淀气息,宛如资深架构师在白板前带团队复盘生产踩坑经验。讲解风格兼具学术体系的严密性与工业界实战的辛辣直白,既有对数据库厂商生造术语的幽默调侃,也有拿事务悬挂 48 小时与点赞计数失真为例的生动现身说法。现场问答节奏紧凑、直切要害,通过“GitHub 版本 Pin 住”、“时间段坍缩为时间点”等精妙比喻,带领听众穿透复杂的并发概念迷雾。

## Bulletin - [20:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1200s) ACID 四要素并非处于同一维度的数据库特性:原子性(A)本质是可安全回滚重试的“可中止性”,一致性(C)实际上是业务层约束而非数据库底层技术保障,持久性(D)则依赖 WAL 刷盘或写时复制指针切换作为决定提交的核心检查点。 - [45:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2700s) 快照隔离(Snapshot Isolation)主流基于 MVCC 实现,达成了“读不阻塞写、写也不阻塞读”的高并发一致性快照,但其理论模型天然无法防御“读取-修改-写入”循环所导致的更新丢失(Lost Update)灾难。 - [01:10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4200s) 防范更新丢失需跳出快照隔离本身,依赖数据库原生原子更新操作、SELECT FOR UPDATE 显式排他锁、引擎级冲突检测回滚或 CAS 条件更新机制将并发竞争压缩为原子胜负判定点。 - [01:16:15](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4575s) Kafka 宣称的 Exactly-Once 具备严格的作用域边界,仅在其内部“读-流处理-写回”闭环结合幂等 Producer 与 2PC 事务下成立;一旦流转触及外部异构系统或通知接口,语义必然退化为 At-Least-Once。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=0s) 现代互联网架构普遍放弃了早期长交互式事务与跨分片分布式事务,转向细粒度的微事务,并通过业务自定义 Partition Key 将事务范围物理收敛在单一分片内以兼顾性能与安全。 - [20:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1200s) ACID 概念已逐渐演进:原子性核心在于“可中止性(Abortability)”,一致性属于应用层业务契约,隔离性负责防止多事务互踩,持久性则以 WAL 刷盘为提交边界。 - [25:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1500s) 存储过程由于缺乏版本控制、无法在线单步断点调试、日志监控困难且极易引发长达数十小时的锁悬挂与全库阻塞,在现代软件工程实践中已被 ORM 和应用层编排淘汰。 - [30:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1800s) 互联网分布式系统普遍坚决禁用数据库物理外键约束,以避免跨分片网络开销剧增以及级联删除可能导致的悬垂指针与性能断崖。 - [40:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2400s) 读已提交(Read Committed)通过行级排他锁杜绝脏写,并借助未提交数据的临时副本来消除脏读,但面对跨账户转账等跨记录场景依然会出现“读偏差(Read Skew)”。 - [45:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2700s) 快照隔离(Snapshot Isolation)依靠多版本并发控制(MVCC)彻底解耦读写竞争,实现“读不阻塞写,写也不阻塞读”,读事务在启动时定格时间戳并仅可见该历史快照。 - [50:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3000s) MVCC 底层以数据行为粒度维护单向版本链表(记录创建与删除事务 ID),事务状态由集中式 Commit Log 统一校验,历史废弃版本在早于其创建的所有事务关闭后由后台 GC 安全清理。 - [01:00:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3600s) 隔离级别是抽象的行为规范与理论契约,并发控制则是底层具体实现手段;可重复读的心智模型如同将代码 Pin 在 GitHub 的某一个特定 Commit Hash 上,不受后续提交影响。 - [01:05:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3900s) 快照隔离无法解决“读取-修改-写入(Read-Modify-Write)”循环引发的更新丢失(Lost Update),在并发自增计数器或覆盖编辑大型 JSON/Wiki 文档时会导致大量数据被静默冲刷。 - [01:10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4200s) 解决更新丢失的标准手段包括数据库下推原子操作(如 UPDATE SET c = c + 1)、SELECT FOR UPDATE 悲观排他锁、引擎自动冲突检测重试,以及基于比较并设置(CAS)的条件更新。 - [01:14:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4470s) 并发控制与强隔离方案的核心认知模型,在于借助锁机制或原子判定将充满交错不确定性的“时间段”坍缩为一个不可分割且具备全局顺序的“时间点”。 - [01:16:15](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4575s) Kafka 的 Producer 幂等性仅能杜绝网络超时导致的重传副作用,是实现精确一次的必要不充分条件;且其端到端 Exactly-Once 仅局限在 Kafka 自身闭环内,跨系统调用仍需下游业务去重保障。 - [01:20:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4830s) 经典的跨异构系统 XA 协议与两阶段提交(2PC)因高昂的性能延迟、脆弱的单点瓶颈和运维黑洞,已在现代工业界被边缘化,主流实践转向本地事务结合消息重试与下游业务幂等键。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=0s) 事务的核心价值与术语迷雾

作为后端工程师,事务(Transaction)是整个系统设计中最核心的一环。现代应用架构大多基于 CRUD(Create, Read, Update, Delete),但在设计数据层时,必须思考操作的粒度(granularity)以及需要提供何种安全性保证(safety guarantee)。最典型的例子就是银行转账:当 A 账户扣减 500 元时,B 账户必须增加 500 元。如果在传统关系型数据库(SQL)与 NoSQL 之间做架构选型,首要考量通常就是系统是否必须具备事务能力。如果无需事务,NoSQL 该如何组织;如果需要事务,能否将事务的粒度切得更细,或者部分链路改用非事务方案替代。这种权衡极为关键,因为一旦事务保护缺失,在中间状态出现数据损坏(data corruption)或数据丢失(data loss),例如 A 和 B 账户总和不再是 1000 元,即使机器重启也无法恢复。

```mermaid flowchart LR accTitle: 交互式长事务与现代微事务对比 accDescr: 展示了早期交互式事务与现代拆分为细粒度小事务的演变模式。 subgraph Early ["早期传统交互式事务"] direction TB E1["开启订票事务"] --> E2["多步交互选座"] E2 --> E3["最终统一 Commit"] end subgraph Modern ["现代互联网微事务"] direction TB M1["加购独立事务"] M2["偏好修改独立事务"] M3["支付扣款独立事务"] end ```

早期的数据库事务大多是长时间的交互式事务(interactive transaction),比如从用户开始选机票就启动事务,直到最后全部完成才提交(commit);而现代互联网公司则将其拆分为大量细粒度的微小事务(例如用户添加某件商品算一个独立事务,设置一项偏好算另一个)。这一领域的专业名词极其混乱: - **概念多义性**:例如快照隔离(Snapshot Isolation)到底属于并发控制级别(Concurrency Control Level)还是隔离级别(Isolation Level)。 - **最混淆的单词 Consistency**:一致性(Consistency)在不同语境下含义天差地别,一致性哈希(Consistent Hashing)指的是节点映射平衡性,CAP 定理中的 C 指的是线性一致性(Linearizability),而 ACID 里的 C 又指的是应用层的业务约束。 - **商业厂商的造词包装**:数据库历来是利润极其丰厚的基础设施,像 Oracle 这样依靠技术老本几十年赚取高利润的公司,为了销售产品发明了海量新术语,导致工程师在交流时名同实异。

在面试场景中,许多面试官乃至求职者对事务的底层机制理解偏弱,单纯背诵名词往往难以服人。最有效的沟通方式是**举反例**:明确指明在何种具体的时序(timing)与竞态条件(race condition)下,系统的安全性(safety)会被打破,导致转账时不仅没能正确扣除与增加,反而出现多扣或少增等严重资损现象。

---

[05:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=300s) 真实世界中的支付系统(Payment System)与复杂故障

针对学员提出的“如何设计一个涉及大额/复杂交易的支付系统(Payment System)”这一问题,支付正是事务与分布式系统结合的核心典型。支付面试中最常见的基础用例是消费扣款:用户在咖啡馆消费 500 元,需要从用户的信用卡扣除 500 元,同时向咖啡馆的商户账户增加 500 元。

在整个支付生命周期(lifecycle)中,任何一个环节都可能发生单点故障(single point of failure): - 扣减用户信用卡可能失败(例如余额不足或风控拦截); - 转入咖啡馆账户可能网络超时或系统异常; - 往往需要引入两阶段检查或预冻结(hold/lock)机制,在每一步校验通过后再往下执行。

```mermaid flowchart TD accTitle: 支付扣款的核心链路与异常分支 accDescr: 展示了用户到商户转账过程中的关键检查点与失败分支。 subgraph Flow ["支付生命周期与校验"] A["发起消费 500 元"] --> B{"预扣/冻结账户余额"} B -- "余额不足/风控失败" --> F1["终止交易并报错"] B -- "冻结成功" --> C{"入账商户账户"} C -- "入账失败/超时" --> R1["解冻/回滚补偿"] C -- "入账成功" --> D["扣款正式结算 (Commit)"] end ```

支付系统之所以复杂且面试高频,是因为它处理的是真实资金。真实的工程实现远比简单的扣款流程复杂得多: - **多币种结算(Currency Exchange)**:涉及汇率折算与并发换汇。 - **坏账追缴(RAR / Accounts Receivable)**:例如在打车平台(如 Uber)中,乘客产生欠款后如何进行后续追缴与结算对账。 - **现金支付(Cash Payment)与混合结算**:现金流转与线上系统同步状态的容错与状态机管理。 - **分布式容错(Fault Tolerance)**:系统必须假设每个流转步骤都会宕机,要求具备完备的幂等性与重试机制。

---

[10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=600s) 抽象的代价、网络异常与相对的时间维度

事务是数据库为开发者提供的最精妙、最重要的抽象(abstraction)。从最底层的原子性与读已提交(Read Committed,防止脏读 Dirty Reads 与脏写 Dirty Writes),到快照隔离(Snapshot Isolation),再到可串行化(Serializable)。在 Serializable 的实现中,涵盖了悲观的两阶段锁(Two-Phase Locking, 2PL)、可串行化快照隔离(Serializable Snapshot Isolation, SSI)以及实际单线程串行执行(Actual Serial Execution)等多种并发控制机制。

"在 Spanner 这篇 paper 里面,作者说因为 2PC(Two-Phase Commit)可能太贵了,所以可能就不支持它,但是其实 transaction 这个部分是整个 database 的 abstraction 带来中最重要的一部分。"对于上层应用而言,如果数据库不提供事务,倒不如弃用数据库,因为由业务自行从零实现事务极其艰难,几十年来这一领域踩坑无数,自研往往会落得一败涂地(fail miserably)。

在真实的分布式与单机环境下,系统需要面临极其不可靠的环境: - **各种故障交织**:数据库软硬件损坏、应用进程崩溃、网络分区(Network Partition)。 - **网络震荡的破坏力**:例如 2020 年 7 月 Cloudflare 曾因骨干网(backbone network)配置失误,所有流量被错误路由到单一数据中心导致全网瘫痪;在分布式共识算法(如 Raft、Paxos)中,节点间若出现网络时断时连(flapping),会导致共识迟迟无法收敛(converge),整个集群失去可用性。 - **时间的相对性与并发争用**:在高度繁忙的系统中,时间是相对的。正如数学上有理数之间存在无穷多个无理数,任意两个时间点之间可能穿插着无限多的并发事务请求。多个客户端竞争同一份资源时,必须依赖完备的并发控制来判定胜负。

---

[15:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=900s) 部分更新、竞态条件排查与 NoSQL 的取舍

在高并发读写中,部分更新数据(partial updated data)是常见隐患。例如在一个文档数据库(Document Database)中写入一个 JSON 数据,整个写入耗时 5 毫秒(即 500 万纳秒);如果将时间轴无限放大,在这个写入窗口的中间,若有并发读请求进入,客户端就有可能读到只写了一半的数据残片。

复杂的竞态条件往往具有隐蔽性。在类似 Airflow 的任务调度或复杂业务引擎中,采用乐观锁(Optimistic Locking)处理状态机时,由于时序交错造成的 Race Condition 可能在系统上线一年半甚至数年后才在极小概率下暴发。这类问题的排查与 debug 成本极高,有时一旦触发概率仅有千万分之一,彻底修复可能需要推倒重构整个架构;许多团队在权衡代价后只能选择暂时容忍,但工程师必须透彻理解其触发机理,以便在未来架构重构时彻底根治。

```mermaid flowchart LR accTitle: 事务的抽象与原子性选择 accDescr: 展示了事务将多个离散读写包裹成单一逻辑单元,要么提交要么回滚。 subgraph TxAbstraction ["事务逻辑单元 (Logical Unit)"] R1["Read"] --> W1["Write"] W1 --> R2["Read"] R2 --> W2["Write"] end TxAbstraction --> Commit["全部生效 (Commit)"] TxAbstraction --> Abort["全不生效 / 安全重试 (Abort / Rollback)"] ```

事务的本质是将一系列离散的读写操作打包为一个单一的逻辑单元(logical unit): - **全有或全无(All or Nothing)**:执行成功则提交(commit);遇到任何问题则中止或回滚(abort / rollback)。 - **应用安全重试(Safely Retry)**:一旦回滚,上层应用可以无副作用地重新发起尝试。

在 2000 年代 NoSQL 运动兴起时,绝大部分 NoSQL 数据库(除 Apple 后来收购并开源的 FoundationDB 等极少数支持 SSI 的例外)为了极高的吞吐性能和高可用性,普遍放弃了强事务支持,仅提供弱隔离或无事务保证。业界曾一度流行一种片面观点,认为大型分布式系统必须抛弃事务换取性能;但事实证明,脱离底层事务保证去实现核心资产系统(如支付、区块链)风险极大。2013-2014 年区块链早期开发中缺乏成熟的智能合约约束,双重支付(Double Spending)漏洞频发;美图秀秀当年涉足发行的“美币”便遭遇了因双花漏洞导致代币可被无限复制的灾难,币值在暴跌到 1% 之后再次狂跌万分之一,最终沦为废纸。

---

[20:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1200s) 重新审视经典 ACID 原则的局限

ACID 作为 1983 年就确立的教科书概念,如今已经略显过时和概念模糊,相对应地演化出了分布式领域的 BASE(Basically Available, Soft state, Eventual consistency)哲学。对 ACID 进行现代视角的拆解,可以发现其定义并不完全处于同一维度:

```mermaid flowchart TD accTitle: ACID 四要素的本质属性划分 accDescr: 展示了 ACID 各自归属于底层机制、应用层约束、并发控制还是持久化机制。 subgraph ACID ["ACID 原则本质拆解"] A["Atomicity (原子性)"] --> A_desc["本质是 Abortability:遇到错误可安全回滚并重试"] C["Consistency (一致性)"] --> C_desc["属于业务规则要求,非数据库自身物理能力"] I["Isolation (隔离性)"] --> I_desc["解决多并发事务互踩,提供可串行化假象"] D["Durability (持久性)"] --> D_desc["依赖 WAL 刷盘或写时复制指针切换作为提交检查点"] end ```

- **Atomicity(原子性)**:实质上是“可中止性”(Abortability)。当事务在提交前发现假设不满足或出现异常,能够干净地中止,使系统无损地回到初始状态并允许安全重试。 - **Consistency(一致性)**:并非数据库的技术属性,而是**业务端需求(Business / Client Requirement)**。数据库只负责执行增减指令,无法感知业务语义。如果业务要求 A 与 B 账户总和恒定,但代码逻辑只写了 A 加 50 而漏掉了 B,数据库无法阻止这种荒谬的操作;因此一致性实际上是由应用层逻辑保障的业务约束。 - **Isolation(隔离性)**:防范多个并发事务相互踩踏(trampling)。它解决的是几万人同时冲进耐克店抢购限量版球鞋时,系统底层如何保证最终执行效果如同顾客一个个按次序排队入场(Serializability)一样严密有序。 - **Durability(持久性)**:核心在于**检查点(Checkpoint)**的划定。在关系型数据库中,这个分界线就是预写日志(Write-Ahead Log, WAL): - 在 WAL 成功刷入磁盘(flush to disk)之前,操作仍可被中止或丢弃; - 一旦 WAL 完成刷盘,数据即被确认为永久提交,除非物理磁盘整体损坏,否则不可丢失; - 在写时复制(Copy-on-Write, 如部分 B-Tree 变体)系统中,更新旧版本指针指向新版本的瞬间,便是那个决定 Commit 与否的关键检查点。明确向客户端确认接收请求与保证完成服务的时序边界,是架构设计中至关重要的一环。

[25:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1500s) 单对象事务与存储过程的痛点

在事务处理中,单对象(Single-Object)事务与多对象(Multi-Object)事务有着本质的区别。从概念上讲,单对象事务的表现与分布式系统中的线性一致性(Linearizability)非常相似。两者的核心区别在于:分布式系统的线性一致性要求读操作必须实时反映最新的写入;而单对象事务在快照隔离(Snapshot Isolation)等级别下,可能会读到旧的陈旧数据(stale data)。除了这一点读时效性的差异外,两者在保证单一记录的原子性(Atomicity)与隔离性(Isolation)上高度重合。例如,向数据库写入一个 20KB 的 JSON 数据,若底层在写到 10KB 时发生崩溃,关系型数据库能确保要么完全写入要么完全不写入,绝不会呈现只写了一半的半成品(half-baked)状态;而在普通文件系统(File System)中,这种写了一半的损坏状态是直接可见的。

谈到复杂的事务逻辑,早期的实现广泛依赖存储过程(Stored Procedure)。无论是在 Salesforce 还是在 Uber 的部分团队(如印度组),老系统里经常能见到写满 `BEGIN TRANSACTION`、`SELECT... FOR UPDATE` 和 `COMMIT` 的存储过程代码。然而,存储过程在现代互联网软件工程体系中已被绝大多数团队所抛弃,主要原因在于其极度不透明且难以维护: - **版本控制缺失**:存储过程直接存在数据库中,往往不通过 Git 进行常规的代码 Check-in 与管理。 - **无法打断点与单步调试**:在纯 SQL 编写的存储过程内部根本无法放置 Debug Pointer 进行实时单步调试。 - **缺乏标准日志记录(Logging)**:难以在执行流中插入标准应用层日志,出现异常时排查链路极度痛苦。

正因为存储过程承载了业务逻辑却缺乏工程配套,稍有不慎就会引发生产灾难。在实际排查分布式系统的经历中,曾出现过一个事务由于逻辑挂起足足运行了 48 小时的离奇事件。在计算机的世界里,48 小时无异于“从公元前一直活到现在的长生不老事务”。更致命的是,该系统采用了两阶段加锁(Two-Phase Locking, 2PL),被该事务所锁定的对象在整整两天内无人可读、无人可写,数据彻底变成了封锁的机密。因此,现代应用开发基本依赖 ORM 框架进行事务包装,极力避免手动在存储过程内编写复杂的事务控制。

[30:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1800s) 多对象事务、分片与外键的权衡

虽然存储过程在通用关系型数据库中式微,但在特定系统(如 Redis 使用 Lua 脚本)中依然存在。Redis 借助 Lua 脚本的单线程原子执行来规避客户端与服务端之间反复交互的网络往返(Round-trip),这与存储过程减少交互的设计初衷一致。然而在分布式存储领域,很多现代数据系统(Data Store)选择直接放弃跨分片(Cross-shard / Cross-partition)的多对象事务,因为跨节点的分布式事务协调会导致系统性能极具断崖式下滑。

针对性能瓶颈,业界的折中方案是让业务自定义分区键(Partition Key)。只要能指定 Partition Key,开发者就能把可能存在关联事务操作的数据集中路由到同一个分片(Shard)内,从而在单个 Shard 内部实现高性能的本地事务支持: - **Cassandra**:支持指定 Partition Key,在单一分区内维持操作的局部性。 - **Uber DockStore**:Uber 早期架构(如 Schemaless)没有该机制,但后期的自研存储系统 DockStore 支持在单 Shard 内执行事务,并开放了 Shard 定义能力。 - **Google Bigtable 等系统**:允许应用层定义 Row Key / Partition Key 来控制数据的物理分布聚合。

与分布式事务同样被互联网架构严格限制的还有数据库的外键约束(Foreign Key)。外键在单机环境下看起来方便,但在分布式系统中极度危险且繁琐: - **级联删除(Cascade Delete)隐患**:如果 A 记录引用了 B 记录,B 被删除时若级联处理稍有遗漏,A 就会变成悬垂指针(Dangling Pointer)。 - **跨分片一致性代价**:在分库分表环境下,A 和 B 往往落在不同的 Shard,要跨网络维持外键约束会导致写入延迟与系统复杂度剧增。因此,绝大多数互联网公司选择由应用层业务逻辑来维护关联一致性,坚决不在数据库层启用 Foreign Key。

[35:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2100s) 读已提交与脏读(Dirty Read)现象

在进入更复杂的隔离级别前,典型的非规范化(Denormalization)数据场景最能揭示底层数据并发访问的问题。以收件箱(Email Inbox)为例,系统通常需要展示两个关键要素:未读邮件计数(Unread Counter)以及邮件列表详情。若采用严格范式化设计,每次打开邮箱都必须执行类似 `SELECT COUNT(*) FROM emails WHERE user_id =? AND is_read = false` 的全表扫描;当单个用户的邮件量达到数万封时,频繁扫描行记录的性能开销是不可接受的。

为了加速读取,系统往往会对数据进行非规范化:单独维护一个未读数计数器,并在每封邮件上维护布尔类型的未读状态标记。然而,若缺乏事务隔离保证,在用户 1 向用户 2 发送邮件的过程中,数据的不一致便会暴露无遗:

```mermaid flowchart TD accTitle: Dirty Read Email Inconsistency Flow accDescr: Sequence showing how an uncommitted email insert and delayed counter update cause inconsistent reads between two timestamps.

subgraph Writer["用户 1(发件事务)"] W1["1. 插入新邮件 (未读状态 = true)"] W2["2. 更新未读计数器 (0 -> 1)"] W1 -->|存在时间窗口| W2 end

subgraph Reader["用户 2(收件人读取)"] R1["在时间窗口内读取邮件列表"] R2["在时间窗口内读取计数器"] end

W1 -.->|邮件已可见| R1 W2 -.->|计数器尚未更新| R2 ```

当发件事务分步执行时,如果邮件已经插入成功,但计数器更新滞后,此时恰好有读请求介入,用户 2 就会看到一封醒目的未读邮件,但顶部的未读计数器赫然写着 `0`。即使调换执行顺序(先更新计数器为 1,再插入邮件),读端同样会遇到提示有 1 封未读邮件、点开却全都是已读邮件的错乱状态。

这一异常的本质即为**脏读(Dirty Read)**:事务读取到了并发写事务中尚未完全提交(Uncommitted)的中间数据。不同数据库对脏读的容忍度取决于其内部机制: - **Copy-on-Write / MVCC 机制**:如复制 B-tree 页或通过指针重定向,未提交的修改对外部不可见,天然防止脏读。 - **原地覆盖更新(In-place Overwrite)**:如 DynamoDB 等系统,在缺乏多键事务保护时直接覆盖底层存储,应用层便极易读到部分生效的中间状态。 - **系统修复策略差异**:Dynamo 具备 Read Repair 与后台主动的反熵检测(Anti-Entropy),会周期性扫描处理不一致;而像 Voldemort DB 这类系统仅依赖 Read Repair,冷门不常读的数据甚至可能面临永久丢失或不一致的风险。

[40:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2400s) 读已提交(Read Committed)与脏写(Dirty Write)

针对上述问题,SQL 标准中最基础的隔离级别是**读已提交(Read Committed)**。该级别主要提供两项核心防御:杜绝脏读(No Dirty Reads)与杜绝脏写(No Dirty Writes)。所谓“杜绝脏写”,即后一个写操作绝对不能覆盖前一个尚未提交事务所写入的数据,只有已经 Committed 的数据才允许被覆写。

若系统允许脏写发生,将会导致不同字段上的更新归属权错乱。经典案例是两个人(Alice 和 Bob)同时在二手交易系统上购买同一辆二手车:

| 执行时序 | 事务 1(Alice) | 事务 2(Bob) | 最终存储状态说明 | |:--- |:--- |:--- |:--- | | **步骤 1** | 更新车辆归属人 = Alice | — | Alice 尝试购车 | | **步骤 2** | — | 更新车辆归属人 = Bob | Bob 的未提交写入覆盖了 Alice | | **步骤 3** | — | 更新账单接收人 = Bob | Bob 关联自己的账单 | | **步骤 4** | 更新账单接收人 = Alice | — | Alice 的写入覆盖了 Bob |

最终交织提交的结果荒诞不经:汽车的所有权字段被 Bob 覆盖,归属于 Bob;然而发票账单的接收人字段却被 Alice 覆盖,归属于 Alice。最终的局面变成了“Bob 把车开走了,付款账单却寄给了 Alice”。

为了杜绝脏读和脏写,绝大多数传统关系型数据库将 Read Committed 作为默认或基础配置。其底层实现普遍依赖**行级锁(Row-level Lock)**: - **防止脏写**:写操作必须获取对应数据行的写锁(排他锁),若另一个事务想要修改该行,必须阻塞等待直到持有锁的事务提交并释放锁。 - **防止脏读(基础实现机制)**:数据库无需为了读而对整行加锁阻塞。在简单的系统设计中(如教学型分布式存储),写操作可以在未提交时将变更写入带事务编号的临时副本;读操作在此期间始终只读取旧的已提交版本,直到写事务通过原子重命名或指针覆写完成提交,读端才切换到新值,从而确保读不阻塞、也不受未提交数据干扰。

[45:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2700s) 快照隔离(Snapshot Isolation)与 MVCC

即使满足了 Read Committed,在面对横跨多条记录的读取时,系统依然无法保证一致的视图。最典型的失效场景是跨账户转账引发的“读偏差”(Read Skew / Non-Repeatable Read): 假设用户有账户 1(余额 500 元)和账户 2(余额 500 元),总资产为 1000 元。现在系统执行一笔转账事务:从账户 1 向账户 2 转账 100 元,流程为账户 1 扣减 100 元变成 400 元,账户 2 增加 100 元变成 600 元。

```mermaid flowchart TD accTitle: Read Skew Under Read Committed accDescr: Demonstrates how a reader observing accounts at different stages of a transfer transaction sees missing money.

subgraph TransferTx["转账事务 (T1)"] T1_Step1["从账户 1 扣除 100 元 (余额: 400)"] T1_Step2["向账户 2 增加 100 元 (余额: 600)"] T1_Step1 --> T1_Step2 end

subgraph ReadTx["查询事务 (T2)"] R_Acc1["读取账户 1 余额 (在扣减后读: 400)"] R_Acc2["读取账户 2 余额 (在增款前读: 500)"] R_Acc1 -.->|跨越写操作时间差| R_Acc2 end

T1_Step1 -.->|已生效| R_Acc1 R_Acc2 -.->|在增款之前完成读取| T1_Step2 ```

由于各步操作之间存在物理时间差,若第三方读事务在此期间介入,其读取账户 1 的时间点落在转账发生后(读到 400 元),而读取账户 2 的时间点却横跨到了账户 2 入账之前(读到 500 元)。读事务将两者相加,计算出的总资产变成了 900 元,凭空蒸发了 100 元。虽然用户再次刷新后数据能恢复到平衡的 1000 元,但在执行数据备份、大规模审计或报表分析(Analytic Database)时,这种不一致可能引发严重后果——原本正常的区域销售报表可能突然在某天凭空缩水 10%,隔天又暴涨 50%。

导致该现象的核心根源在于:读事务跨越了正在执行的一个或多个并发写入事务,导致读取的不同对象来自不同的逻辑时间截面。解决此问题的标准方案是**快照隔离(Snapshot Isolation)**,其最主流的工业界工程实现为**多版本并发控制(Multi-Version Concurrency Control, MVCC)**。

MVCC 的灵魂在于解耦读写冲突:**“读不阻塞写,写也不阻塞读”(Readers never block writers, and writers never block readers)**。每个事务启动时都会获取一个单调递增的事务快照版本号,读操作根据版本号仅可见在该事务开始前已经提交的历史版本。与性能损耗极大的两阶段加锁(2PL)相比,MVCC 在保证只读事务看到全局一致快照的同时,彻底消除了读操作排队加锁的巨大开销。

[50:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3000s) MVCC 与快照隔离的读写工作流:版本链与垃圾回收

在多版本并发控制(MVCC)与快照隔离(Snapshot Isolation)机制下,写操作与读操作之间不会相互阻塞。Scott 借助 Alice 的银行转账案例深入剖析了这一机制的底层数据流:假设读取账户的事务为 Transaction 12,而进行资金划转的写事务为 Transaction 13。此时,数据表中的每一行(row)不再是一条静态记录,而演变为了由事件序列串联而成的版本链条(a sequence of events / single linked list)。当 Transaction 12 读取账户 1 时,上一个已提交的写入事务是 Transaction 1,金额为 500 元,因此读到 500;读取账户 2 时,虽然该记录正在被 Transaction 13 进行逻辑删除标记(`deleted_by = 13`),但因为 Transaction 13 的事务 ID 大于 12 且发生在当前读事务开始之后,根据可见性规则(Visibility Rule),Transaction 12 必须直接忽略 Transaction 13 的变更,而是沿着链表回溯读取在 Transaction 13 之前由 Transaction 5 写入的旧版本,因此读出的同样是 500 元。

```mermaid flowchart TD accTitle: MVCC 版本链与可见性判断 accDescr: 展示事务基于版本链读取历史快照以及垃圾回收触发机制的流程

subgraph TX12["Reader (Transaction 12)"] R1["发起读操作 (T12)"] VCheck{"检查数据版本与 commit 顺序"} ReadVal["读取旧版本数据: 500 元"] end

subgraph RowVer["数据行版本链 (MVCC Row Versions)"] VOld["旧版本 (由 T5 创建)<br/>状态: 已提交 (Committed)"] VNew["新版本 (由 T13 创建 / 覆盖)<br/>状态: T13 晚于 T12 / 未提交"] end

subgraph GC["后台垃圾回收 (Backend GC)"] GC_Cond{"T13 之前开始的所有事务<br/>是否均已 Close?"} Reclaim["安全回收 T5 旧数据 Block"] end

R1 --> VCheck VCheck -- "忽略晚于 T12 的 T13" --> VOld VOld --> ReadVal VOld -.-> VNew VOld --> GC_Cond GC_Cond -- "是" --> Reclaim ```

从写事务的视角来看,当 Transaction 13 对账户 1 与账户 2 执行转账更新时,它并不是原地覆盖,而是新建数据块(Block),将最新数据的元数据标记为 `created_by = 13`、`deleted_by = NULL`。在账户 1 增加 100 元、账户 2 扣除 100 元且两处操作均准备就绪后,Transaction 13 触发 Commit。在提交的这一个“原子点”(Single Point)之前,所有并发或先启动的读事务看到的都只能是旧数据 500/500;而在该提交点之后启动的后续事务,则只会读到最新的 600/400。

- **行级粒度与版本链结构**:以 MySQL/InnoDB 为代表的数据库以行(Row)为粒度维护链表指针,通过记录 `created_by` 与 `deleted_by` 的事务标识构建版本历史。 - **后台垃圾回收(Garbage Collection)**:旧版本的数据块不会永久保存。当早于 Transaction 13 启动的所有读写事务全部关闭结束(Closed)后,意味着整个数据库系统中已经没有任何活动事务能够读到 Transaction 5 的旧版本,后台守护进程(Backend Process)就会安全回收并清理这些历史无用数据块。

---

[55:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3300s) 单点时间戳与 Commit Log:快照隔离的高层心智模型

抛开底层指针细节,Scott 强调理解快照隔离最核心的设计理念在于“时间轴上的单点切片(Single Point)”。若用时间轴(Timeline)直观表达,写事务如 Transaction 1、3、5、2 陆续提交;如果读操作发生在某个具体的时间点,它绝对不会出现“一部分数据读在某事务发生前,另一部分数据读在该事务发生后”的撕裂状态。读操作在特定的时间点只读所有在此单点之前已提交的事务,并无条件忽略该时间点之后的所有事务。

在答疑互动中,有学员提问:“每个数据 Block 中除了 `created_by` 和 `deleted_by`,是否还要维护一个字段表示事务是否已经 Commit?如果写事务未完成提交,中间去读取是否会读到脏数据(Dirty Read)?”Scott 指出,事务的提交状态绝不会直接写在各个数据 Block 上。如果写在 Block 内部,多个数据块之间就可能发生“一个标记已提交、另一个尚未标记”的状态撕裂。数据库通常拥有一个专门集中记录事务状态的提交日志(Commit Log)。读操作必须先通过 Commit Log 校验事务是否真正提交,确认已提交后再读取相应的数据块,从而彻底杜绝脏读。快照隔离在隔离保证上完全强于读已提交(Read Committed)。

针对学员关于“单机与分布式数据库如何判定谁先 Commit”的疑惑,Scott 笑着感叹:“先别考虑分布式数据库,光是单个单机数据库的并发处理就足够让人头疼了。”在没有可串行化快照隔离(SSI, Serializable Snapshot Isolation)保证时,不同数据库在面对大事务并发覆盖、后发事务先提交等边界情况下的写入策略与冲突判定各有不同的工程实现。

---

[01:00:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3600s) 隔离级别 vs 并发控制:可重复读的本质与 GitHub 版本比喻

针对群内关于理论与实践的探讨,Scott 重点厘清了两个极易混淆的核心概念:**隔离级别(Isolation Level)**与**并发控制(Concurrency Control)**。隔离级别是理论规范与抽象契约,定义了系统对外保证的读写一致性特征(例如读操作绝不切断在事务中途,只读确定时间点);而锁机制、指针链表、MVCC 等则是具体的并发控制手段。只要某种工程实现能保证理论所要求的可见性边界,就满足了该隔离级别。

学员继续深究快照隔离、可重复读(Repeatable Read)与读已提交(Read Committed)的本质界限:为什么读已提交不能满足可重复读?Scott 通过对比两次读取的执行过程解释道: - **Read Committed 的局限**:事务在读取过程中确实没有读到任何“未提交的脏数据”,但如果一个长事务在读操作 1 时某外部转账尚未提交,而在读操作 2 时该转账已提交,事务在两次读取之间就会拿到前后不一致的数字,产生不可重复读或倾斜现象。 - **Snapshot Isolation / Repeatable Read 的确定性**:Scott 打了一个非常生动的比喻:“这就有点像我把代码 Pin 在了 GitHub 仓库的某一个具体的 Branch Version(版本 Commit Hash)上”。一旦读事务启动并定格在当前时间戳(Timestamp),即使背后有其他事务持续提交甚至运行数个小时,当前事务在后续所有读取中也只能看到该时间戳之前已经固化的历史,后续产生的新版本与其毫无关系。这一机制从根本上保证了同一个事务内部无论重复读取多少次,数据视图始终完全一致。

---

[01:05:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3900s) 更新丢失(Lost Update)陷阱与并发写防御机制

虽然快照隔离完美解决了脏读与不可重复读,但 Scott 严肃指出,快照隔离**无法**防范并发写入中最致命的问题——**更新丢失(Lost Update)**。其经典诱因是“读取-修改-写入(Read-Modify-Write)”循环: 假设有两个并发事务同时为 YouTube 视频播放量(YouTube View Counter)执行自增操作。原播放量为 42: 1. 用户 1 读取播放量为 42; 2. 用户 2 在用户 1 尚未提交时同样读取到已提交版本 42; 3. 用户 1 计算得到 43 并写入提交; 4. 用户 2 基于自己读到的 42 同样计算出 43 并写入覆盖。 最终明明有两批观众点击观看,计数器却仅增加了 1,发生了严重的更新丢失。

```mermaid flowchart TD accTitle: Read-Modify-Write 更新丢失时序 accDescr: 说明并发环境下只靠快照隔离无法解决的更新丢失现象

subgraph State["初始状态: Counter = 42"] end

subgraph User1["用户 1 事务"] R1["读取 Counter: 42"] M1["内存计算: 42 + 1 = 43"] W1["写入提交: Counter = 43"] end

subgraph User2["用户 2 并发事务"] R2["读取 Counter: 42 (T1 未提交)"] M2["内存计算: 42 + 1 = 43"] W2["覆盖提交: Counter = 43 (更新丢失!)"] end

State --> R1 State --> R2 R1 --> M1 R2 --> M2 M1 --> W1 M2 --> W2 ```

Scott 分享了自己早期工程实践中的惨痛踩坑经历:当时在一个项目中基于 MongoDB 实现计数器更新,原本预期最终数值是准确递增的,上线压测后发现数据完全失真混乱。在极低的并发压力下(仅仅 10 到 20 QPS),更新丢失率居然高达 40% 到 50%。不仅是简单的整数计数器,对于大型 JSON 文档(JSON-Doc)或维基词条(Wiki)的更新更是重灾区——如果文档体积达到数兆(例如 Mongo 允许单文档最大 16 MB),一次复杂修改需要花费数秒甚至更长,两名用户同时编辑 Wiki 页面时,最后提交的“最后写入胜出(Last Write Wins)”策略便会无情抹杀掉前一人的所有劳动成果。

针对 Lost Update,Scott 介绍了两类数据库原生的刚性防线: - **原子更新操作(Atomic Operations)**:直接在数据库层下推原子语句,例如利用数据库内置原子指令规避应用层的两阶段往返: ```sql UPDATE counters SET count = count + 1 WHERE id = 'youtube_view'; ``` - **显式悲观锁(Explicit Locking)**:在查询阶段显式加排他锁,例如在 SQL 中使用 `SELECT... FOR UPDATE`。这种方式强制让并发事务排队等待,在企业级复杂业务(如 Salesforce 系统中的复杂逻辑)中被广泛应用,以绝对的串行化锁定规避数据被无感知覆盖。

[01:10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4200s) 解决更新丢失的进阶策略:系统自动检测、CAS 与 CRDT

在探讨如何避免更新丢失(Lost Update)时,直接编写数据库显式锁语句虽然行之有效,但必须格外谨慎。如果不小心写出糟糕的逻辑,很可能会将整个数据库锁死长达数十个小时。除了显式加锁,第三种解决途径是依赖数据库系统的自动冲突检测。这类系统能够追踪“读取-修改-写入”(Read-Modify-Write)周期,在事务写入前比对是否发生了脏写(Dirty Write);一旦检测到并发导致的更新丢失风险,系统便会自动中止(Abort)该事务并触发重试。

第四种方式则是基于条件更新的比较并设置(Compare-and-Set / CAS)。在执行更新的 SQL 语句中附加条件约束,例如将更新写成原子条件语句:当且仅当变量当前值等于旧值时(如 `UPDATE table SET var = 43 WHERE id = 1 AND var = 42`),才允许更新成功;若在此期间其他并发事务已将该值修改,条件不满足则更新失败。Scott 强调,使用 CAS 必须提防快照隔离(Snapshot Isolation)带来的隐患:因为快照隔离在定义上就意味着事务读取的可能不是最新版本的实时数据,若基于陈旧快照做条件判定,依然存在数据丢失的可能。不过在实际工程中,CAS 依然是一种“相对 low-risk、比较无脑”且直接的选择。通过把一段时间范围内的并发竞争压缩为一个原子判定的时间点,系统便能决出胜负者(Winner and Loser),失败方只需简单重试即可。最后一种思路是采用 CRDT(无冲突复制数据模型),正如 Uber 曾分享过的无序哈希表实现案例:写入阶段极度宽松,无条件接受所有并发写,但代价是把复杂度转移到了读端,客户端在读取时必须执行额外的合并逻辑以厘清因果先后。

```mermaid flowchart TD accTitle: CAS并发冲突处理机制 accDescr: 展示两个并发事务竞争同一行数据时通过CAS判定胜负的流程

A["初始状态: var = 42"] --> B["事务 T1 读取: 42"] A --> C["事务 T2 读取: 42"] B --> D["T1 尝试更新: SET var=43 WHERE var=42"] C --> E["T2 尝试更新: SET var=44 WHERE var=42"] D -->|"先到达 (Winner)"| F["更新成功: var 变为 43"] F --> G["T2 条件不满足 (Loser)"] E -.-> G G --> H["T2 Abort 并触发 Retry"] ```

- **系统自动检测更新丢失**:若检测到并发写冲突,由存储引擎主动执行 Abort,迫使事务重试。 - **Compare-and-Set (CAS)**:用条件更新将并发时间段坍缩为原子判定点,但需警惕在 Snapshot Isolation 下可能因快照旧读引发的数据丢失。 - **CRDT 权衡**:写入端无锁且极度轻量,但读端与客户端需承担复杂的冲突合并与乱序仲裁成本。

---

[01:13:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4380s) Q&A 互动:Java 原子类 CAS 与数据库并发控制机制

讲解告一段落后,Scott 提到事务与后续的分布式共识(Consensus)机制非常烧脑, consensus 显然无法在一周之内讲完,因此后续两周会分阶段逐步拆解。在自由答疑环节,一位学员提问关于原子操作的底层机制:Java 语言并发包中的 `AtomicInteger` 同样依赖 Compare-and-Swap(CAS),这与数据库所谈论的 CAS 和锁机制有何异同?底层是否也是加锁逻辑?

Scott 澄清道,Java 的 `AtomicInteger` 属于多线程并发编程(Multi-threaded Coding)的范畴,其底层依托于 CPU 硬件级别的原子指令(如 x86 的 `CMPXCHG`),避免了传统互斥锁的上下文切换开销;而数据库的并发控制面对的是持久化存储与事务上下文。不过,两者在设计哲学与临界区保护的思想上是高度相通的:操作系统或多线程中在执行临界区代码时加锁、结束后释放锁的流程,与数据库行级锁及原子条件判定的核心目标完全一致,本质上都是为了消除并发竞争造成的数据不一致。

---

[01:14:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4470s) 快照隔离下的回滚机制与“时间段坍缩为时间点”的本质

随后有学员继续追问关于快照隔离(Snapshot Isolation)的读写细节:在一个包含多个操作的事务中(例如经典的银行转账场景,先减 100 元,再加 100 元),如果第二个操作执行失败导致事务触发回滚(Rollback),那么前面已经生成的操作快照是否会被直接删除?其他并发事务能否看到第一步的中间状态?

Scott 解答指出,在快照隔离级别下,其他事务能够读取到该修改的前提当且仅当该事务已经成功提交(Committed)。如果事务在中途失败中止,整笔事务就像“从来没有发生过一样”,所有未提交的修改都会被回滚并丢弃,外部读取绝不可能读到任何半成品的中间状态。Scott 由此总结了理解事务隔离机制的核心认知模型:并发控制的关键在于分清“时间点”与“时间段”。在任意一个持续的时间段内,可能重叠着无限多个并发时间段与时间点,由此便滋生了各种并发冲突与异常读写;而串行化(Serializable)等强隔离方案的精髓,就是通过锁或原子判定,把一段充满不确定性的“时间段”坍缩为一个不可分割的“时间点”(Atomic),一旦所有操作都规整为互不重叠的时间点,系统便能对其进行全局排序,从而彻底消除竞态条件。

---

[01:16:15](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4575s) Kafka 幂等性与 Exactly-Once 语义的真实边界

讨论随后转向分布式消息系统,学员向 Scott 请教幂等性(Idempotence)、精确一次(Exactly-Once)与至少一次(At-Least-Once)之间的底层联系。针对 Kafka 宣称的“Exactly-Once”能力,Scott 提醒大家必须以审慎的工程眼光看待厂商的宣传技术博客(Sales/Marketing 文章)。实现绝对意义上的端到端 Exactly-Once 极其复杂,现实中绝大多数场景依然是以 At-Least-Once 为基底构建。

Scott 解析了 Kafka 实现语义的关键细节。在消息生产端,可以通过配置开启 Producer 幂等性(`enable.idempotence=true`)。幂等性的核心定义在于:客户端即使因为超时重传等原因向 Broker 多次重复写入相同数据,在服务端底层也只会持久化单次有效写入,完全消除重复写入带来的副作用(Side Effect)。然而,Producer 端的幂等性仅仅是实现 Exactly-Once 的“必要不充分条件”。若要达成 Kafka 宣称的 Exactly-Once 处理,不仅要求 Producer 必须具备幂等性,还依赖其跨 Topic 事务机制(Cross-Topic Transaction),该机制内部依赖事务协调器(Transaction Coordinator)并采用两阶段提交(2PC)协议,且消费端需要配置为只读已提交消息(`read_committed`)。

最为关键的架构误区在于 Exactly-Once 的作用域。Scott 着重强调,Kafka 所谓的 Exactly-Once 严格局限在“Kafka 内部闭环”,即“从 Kafka 读取数据 -> 流式计算处理 -> 写回 Kafka”的系统闭环中。一旦涉及跨系统交互——例如下游消费端读取消息后需要触发外部通知服务(Notification System)发送邮件或短信,或者写入外部第三方数据库——Kafka 自身的保证就会失效。在这种跨越边界的场景下,系统退化为 At-Least-Once 或 At-Most-Once,若下游不支持外部幂等,必然无法达成真正的 Exactly-Once。

```mermaid flowchart LR accTitle: Kafka 闭环与跨系统边界下的语义保障差异 accDescr: 展现Kafka内部流转支持精确一次处理,而跨系统输出退化为至少一次

subgraph KafkaCore ["Kafka 内部闭环 (保证 Exactly-Once)"] direction TB P["Idempotent Producer"] -->|"事务写入"| B1["Source Topic"] B1 -->|"read_committed"| C1["Stream Processor"] C1 -->|"2PC 事务提交"| B2["Sink Topic"] end

subgraph ExternalBoundary ["外部异构系统边界 (无法保证 Exactly-Once)"] direction TB B2 -->|"Consumer 读取"| App["业务系统 / Notification"] App -->|"网络调用 / 外部副作用"| Ext["外部 Email / SMS / DB"] end ```

- **幂等性与 Exactly-Once 的关系**:Producer 幂等性是必要不充分条件;没有 Producer 幂等,绝无可能实现 Exactly-Once。 - **Kafka 事务核心机制**:依赖 Transaction Coordinator、两阶段提交(2PC)协议以及 `read_committed` 事务隔离级别。 - **系统边界约束**:Kafka 的 Exactly-Once 仅对自身生态内的“读-处理-写回”生效;跨系统外部调用(如推送通知)必须由外部系统自行承担幂等与去重责任。

---

[01:20:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4830s) 跨系统分布式事务的理想与现实:XA 协议与两阶段提交

对话进一步延伸到跨系统的事务保障上。学员提出,若下游通知系统要求绝对不能重发,是否必须依赖分布式事务协议来实现跨系统 Exactly-Once。Scott 与学员回顾了经典的分布式事务规范——XA 协议。

Scott 分析指出,XA 是一个非常古老的规范,其本质就像定义了一套跨异构系统的 REST API 协议标准,其底层同样采用两阶段提交(Two-Phase Commit / 2PC)来协调分布式事务。在理论模型中,只要参与的各个系统——无论是关系型数据库、消息中间件还是邮件服务——均严格实现 XA 规范,协调器便能统治这组异构系统(Heterogeneous Systems),确保全局事务的一致性。Java 平台也曾推出过对应的标准 API 支持。然而 Scott 直言,XA 属于“理论上很丰满,但实际上现在工业界基本没人愿意在生产中使用”的技术,不仅性能开销极其高昂,运维极度复杂,且容易出现长时间锁悬挂与单点故障问题。现代分布式系统设计通常会放弃这种重量级的全局强一致事务,转而采用本地事务配合消息队列、At-Least-Once 重试以及下游基于业务唯一键做幂等去重的方式来落地。讨论在此告一段落,Scott 感谢大家的提问并结束了本场分享。

## Notable quotes > [10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=600s) 在 Spanner 这篇 paper 里面,作者说因为 2PC(Two-Phase Commit)可能太贵了,所以可能就不支持它,但是其实 transaction 这个部分是整个 database 的 abstraction 带来中最重要的一部分。 — Scott Shi

> [25:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=1500s) 从公元前一直活到现在的长生不老事务 — Scott Shi

> [40:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2400s) Bob 把车开走了,付款账单却寄给了 Alice — Scott Shi

> [45:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=2700s) 读不阻塞写,写也不阻塞读 — Scott Shi

> [55:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3300s) 先别考虑分布式数据库,光是单个单机数据库的并发处理就足够让人头疼了。 — Scott Shi

> [01:00:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=3600s) 这就有点像我把代码 Pin 在了 GitHub 仓库的某一个具体的 Branch Version(版本 Commit Hash)上 — Scott Shi

> [01:10:00](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4200s) 相对 low-risk、比较无脑 — Scott Shi

> [01:14:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4470s) 从来没有发生过一样 — Scott Shi

> [01:20:30](https://www.youtube.com/watch?v=XCqLFER4Xww&t=4830s) 理论上很丰满,但实际上现在工业界基本没人愿意在生产中使用 — Scott Shi

## Mentioned Scott Shi, YouTube, Oracle, Cloudflare, Google Spanner, Raft, Paxos, Apache Airflow, FoundationDB, Apple, 美图秀秀, 美币, Salesforce, Uber, Uber DockStore, Uber Schemaless, Redis, Apache Cassandra, Google Bigtable, DynamoDB, Voldemort DB, MySQL, InnoDB, GitHub, MongoDB, Apache Kafka, Java, XA

## Video description In this video, Scott will walk through the transaction, the slide could be found here: https://docs.google.com/presentation/d/1bo4SVBEVWgWPmagMuA2jsIpEco9hfGZ3sNZk25g4RKg/edit?usp=sharing

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

中文简报

Scott Shi警示快照隔离难防更新丢失 跨系统一致性须靠业务幂等 快照隔离因并发旧读缺陷无法阻止更新丢失,而高成本的XA分布式事务在现代工程中已被At-Least-Once结合下游业务幂等所取代。 本期内容系统性拆解了数据库事务的核心抽象与其背后的并发控制机制。主讲人从经典 ACID 的现代本质与微事务演进入手,剖析了存储过程在软件工程中的痛点以及分片和外键的架构权衡,并通过转账与邮箱等经典场景深入阐释了脏读、脏写与读偏差等并发问题。随后,内容重点解析了快照隔离(Snapshot Isolation)如何依托多版本并发控制(MVCC)版本链与垃圾回收实现“读写互不阻塞”,以及如何利用原子操作、显式锁和 CAS 应对快照隔离无法防御的更新丢失(Lost Update)缺陷。在末尾问答中,Scott 进一步划定了 Kafka 闭环 Exactly-Once 语义在跨异构系统边界下的工程现实,并指出了传统 XA 协议在生产实践中的高昂代价。 ACID 四要素并非处于同一维度的数据库特性:原子性(A)本质是可安全回滚重试的“可中止性”,一致性(C)实际上是业务层约束而非数据库底层技术保障,持久性(D)则依赖 WAL 刷盘或写时复制指针切换作为决定提交的核心检查点。 (20:00) 快照隔离(Snapshot Isolation)主流基于 MVCC 实现,达成了“读不阻塞写、写也不阻塞读”的高并发一致性快照,但其理论模型天然无法防御“读取-修改-写入”循环所导致的更新丢失(Lost Update)灾难。 (45:00) 防范更新丢失需跳出快照隔离本身,依赖数据库原生原子更新操作、SELECT FOR UPDATE 显式排他锁、引擎级冲突检测回滚或 CAS 条件更新机制将并发竞争压缩为原子胜负判定点。 (70:00) Kafka 宣称的 Exactly-Once 具备严格的作用域边界,仅在其内部“读-流处理-写回”闭环结合幂等 Producer 与 2PC 事务下成立;一旦流转触及外部异构系统或通知接口,语义必然退化为 At-Least-Once。 (76:15)

Reading summary

Scott Shi 围绕事务的核心价值展开分析,指出事务是数据库最精妙的底层抽象;工程师需跳脱商业厂商与过时理论的术语迷雾,从并发时序、状态异常与系统边界出发,理解多版本并发控制(MVCC)、更新丢失防范及分布式事务落地的工程权衡。 事务的抽象价值与 ACID 的现代解构 (0:00) Scott 指出,事务将离散读写封装为全有或全无的逻辑单元,免去了业务端自研事务极易崩溃的巨大风险。传统 ACID 理论在现代架构下需要重新审视:原子性(A)本质是可中止性(Abortability),允许异常时安全回滚重试;一致性(C)并非数据库物理机制,而是应用层保障的业务约束;隔离性(I)旨在消除并发互踩;持久性(D)则以 WAL 刷盘或指针切换为提交检查点。 在工程实践中,由于缺乏版本控制、无法单步调试且缺乏标准日志,存储过程在现代架构中已被普遍淘汰;为避免跨分片高昂代价与级联悬垂指针,外键约束也多被应用层逻辑取代,互联网架构更倾向通过业务定义分区键(Partition Key)在单分片内执行高效事务。 隔离级别演进与 MVCC 的快照哲学 (45:00) 基础的“读已提交”(Read Committed)通过排他行锁防止脏写(如二手车买卖中归属人与账单接收人被交叉覆盖),并规避脏读;但面对跨对象读取时仍会出现“读偏差”(例如转账中间态导致查询到的总金额凭空缩水)。快照隔离(Snapshot Isolation)通过多版本并发控制(MVCC)实现了“读不阻塞写、写不阻塞读”。 系统以单调递增版本号串联行级版本链,由集中式的提交日志(Commit Log)统一校验提交状态,并在早期并发事务全部关闭后由后台安全回收旧数据块。Scott 比喻快照隔离就像将数据视图片定在 GitHub 某一特定的 Commit 版本上,让读操作在时间轴的确定单点上观测全局历史,确保了同一个事务内的可重复读。 更新丢失的破局与并发时间坍缩 (65:00) Scott 强调,快照隔离无法防范“读取-修改-写入”循环引发的更新丢失(Lost Update)。例如并发自增 YouTube 播放量时,后提交的事务会直接覆盖前者的结果;Scott 分享其早期在 MongoDB 项目中,仅 10 到 20 QPS 的并发就导致了高达 40% 至 50% 的数据丢失率。 应对策略包括:直接下推数据库内置的原子更新语句、使用 SELECT... FOR UPDATE 显式排他锁、依赖存储引擎自动检测脏写并中止重试,或采用条件更新(CAS)与读端合并的 CRDT。强隔离的认知本质,在于通过控制手段将充满并发竞态的不确定“时间段”坍缩为单一不可分割的“时间点”,从而完成全局确定性排序。 分布式环境下的语义边界与事务落差 (76:15) 针对分布式消息与事务,Scott 澄清 Kafka 宣称的 Exactly-Once 仅严格闭环在“Kafka 内部读-处理-写回”体系内,依赖生产者幂等性(enable.idempotence=true)、事务协调器与跨 Topic 两阶段提交协议;一旦跨越边界向外部系统发送通知、短信或写入异构数据库,语义必然退化为 At-Least-Once,必须由下游系统基于业务唯一键自行实现去重幂等。 与此同时,经典的跨系统 XA 协议由于性能开销巨大、易发长时间锁悬挂与运维极度复杂,在现代工业界生产中已被基本弃用,实际架构普遍转向本地事务配合可靠重试补偿的务实路线。 本讲座系统梳理了事务在单机与分布式场景中的理论边界与工程取舍,强调架构设计必须立足于不可靠环境的故障假设与明确的系统边界,而非盲目迷信厂商包装或强一致性分布式协议。

Source quotes

[10:00] Scott Shi: 在 Spanner 这篇 paper 里面,作者说因为 2PC(Two-Phase Commit)可能太贵了,所以可能就不支持它,但是其实 transaction 这个部分是整个 database 的 abstraction 带来中最重要的一部分。 [25:00] Scott Shi: 从公元前一直活到现在的长生不老事务 [40:00] Scott Shi: Bob 把车开走了,付款账单却寄给了 Alice [45:00] Scott Shi: 读不阻塞写,写也不阻塞读 [55:00] Scott Shi: 先别考虑分布式数据库,光是单个单机数据库的并发处理就足够让人头疼了。 [60:00] Scott Shi: 这就有点像我把代码 Pin 在了 GitHub 仓库的某一个具体的 Branch Version(版本 Commit Hash)上 [70:00] Scott Shi: 相对 low-risk、比较无脑 [74:30] Scott Shi: 从来没有发生过一样 [80:30] Scott Shi: 理论上很丰满,但实际上现在工业界基本没人愿意在生产中使用

Original transcript

[0:00] 好我们开始录音然后Transaction的话这块的话也是也是就是做一个backend程序员的话其实我觉得是最核心的一部分吧然后很多时候设计的时候你看要想到这个怎么样能保证这个Data的crackness就是safety 是吧就是你设计的时候你这个data肯定是经过说白了就是现在大部分设计都是一个crew的嘛就是create read update和delete但是这个create read update 和delete的话你的granularity 是什么然后你设计的时候有哪一些有哪一些这个Sifted Guarantee要保障是吧,怎么样这个Data,你比如说你转账的时候,A账户减少了500块,那B账户应该增加500块,这个Sifted Guarantee要在这里是吧,然后是比较核心的一部分。然后Transaction同时呢也是,我觉得也是传统的Database里面讨论最多的一部分。嗯很多时候,SQL和NoSQL选择的时候,第一选择可能就是需不需要Transaction,是吧?如果不需要Transaction的话,那NoSQL应该怎么样来?SQL应该怎么样来?如果需要Transaction的话,那我能不能把Granularity,Transsection设计的小一点我们看是不是有一些地方可以用非Transaction的东西代替然后这个是比较核心的一部分就是SQL和NoSQL的选择中比较核心的一部分然后这个的话,这部分也是选database中和这个后端实践中最比较花时间的一部分因为这个如果你这个transaction没有达成的话,比如说你A账户转B账户转500块,然后你出现了一个中间态,这两个加起来不等于1000的话,这个中间态是recover不了的,机器重启recover不了,因为这个data是cropted, data loss,你是recover不了的。所以这个transaction也是比较重要的。特别早的时候,database transaction就是一个interactive transaction,比如说从订一个机票开始,就开始一个transaction,然后最后全部搞完了以后是commit,是吧?然后就把你这个transaction定下来,但是现在的互联网公司的transaction,我讲就是小的transaction,就brick down下来,是吧?比如说你这块加了一个什么东西,然后你就是一个transaction,然后你这个,比如说你这个选了一个preference,就是一个transaction,所以和之前的transaction的概念不是完全一样的。一方面是Wording比较confusing你比如说一直在提什么是snapshot isolation然后它到底是个concurrency control level还是一个还是一个这个这个就是这个isolation level 是吧这个word是非常confusing的特别是这个consistency, consistent 是最为confusing的一个word因为这个consistent 是在很多地方不停的被提到的比如说 consistent hashing 的 consistency 然后 CEP 的 consistency然后这个AC的这个Consistency还有什么 consistency,反正说的这个 consistency,其实意义都不一样,就是意义都不一样。就是并且这个 transaction 这块的话,我觉得因为它这个 database 是 database 最核心的一部分。也是DataBase中最核心的部分,所以它这个DataBase本身是利润率很高的一个地方,像Oracle这么挣钱就是吃的老本是吧,十几年前的老本,几十年前老本。所以它这个Oracle为了这个Cells,它其实是发明了很多新的概念的。所以非常confusing,Oracle发明了很多新的概念,非常conforming然后它这个就是,所以说你需要去鉴别出来,它到底这个概念是什么意思?所以你靠 word 来说的话,很多时候你说一个名词,别人说的名词,他理解的意义是不一样的。所以很多时候你还是要理解这个 transaction 这个里面就是你在讨论这个问题的本质是什么,就是这个 transaction 这里面本质是什么。所以这个也是非常核心的一部分。好,然后就是这样,然后大家这块有什么想额外问的问题的话也可以说一下,但是总体来说这张是非常非常重要的一张,然后我给大家直观的建议是大部分面试官对transaction理解其实都是比较弱的,就是他其实面试官的理解就是比较弱的,就是我说实话,我见的面试官很多时候对transaction理解本身是比较弱的,包括面试的人就是比较弱的。所以你这块的话,你给它抛名词是没用的,你可以抛名词,但是抛名词很多时候没用的,所以最好举个例子是吧。就像比如说如果做二的话其实就是完全是一个transaction的面试其他的部分都是之前讲过很多次的,但是这个transaction这部分其实是比较幸福的然后,所以说,就是举例子嘛,就是举反例,你就要找一个timing的例子,在什么,在什么timing的情况下,你这块有risk condition,然后你这个这个database,整个你的safety就被broke了。就是你的Safety Broker了 [5:00] 对不对就是你转账的时候就不是500转500了就变成了这个人增加了500那个人减少了1000这种所以是其实是找反例的一个过程,transaction其实是找反例的一个过程,然后是这个方面,然后其次的话,谢谢大家我现在想到的就是这么些了,我刚才想到一些其他的,然后突然忘掉了,现在想到的就这么些了。所以这个transaction是非常重要的一招。比如说,我有一个问题,我说如果要设计一个payment system,比如说,我们需要设计到一个billage transaction,这样的话我们应该去怎么样来做一个design呢?乐度感我不是很了解,如果可以的话那稍微讲。对,Payment系统,其实你面试的时候,它最常见的问题,Payment系统就是说,你比如说你是,我举个例子吧,我还真没有做过Payment Marking Interview,其实我可以考虑做一下,就是我可以去考虑再深入研究一下但是payment的话最简单的一个问题就是比如说你现在从A公司,比如说你是开发咖啡馆,我去咖啡馆消费了,我要从我的信用卡里花着500块,然后去咖啡馆里面增加500块。然后咖啡馆的balance增加500块,然后我的balance减少500块,是吧?那我做这个payment的时候,我做payment的时候,从我的账户里滑走500块的操作可能会失败,然后另一方面的话,把500块把500块转到咖啡馆的这个可能会失败是吧,可能多种原因,比如说鱼饿不足啊,所以说对所以需要有些事情,我只能先锁一下,然后怎样的,然后就分析不承认做每一步的check一下是不是可以做下一步这样子?对的,我也不是很了解这一部分,但是我想大家可以讨论一下,大概这样的话应该怎么样给实现吧?对,我们可以下周和下下周讨论一下吧,因为payment也是非常高频的一个面试题,我其实进Uber的时候面的也是payment。但是我忘了当时怎么达到了因为当时理解可能也不一样其实payment是非常高的平等面试题我觉得payment最重要的一点的话是你需要理解就是在payment的任何一个flow中任何一个life cycle中任何一步都可能fail掉所以这个处理是比较复杂的但是这个value是所有不管是什么不管是Fayment系统或什么系统都是有可能Fayment系统,但是Payment的比较重要一点就是Rocquefield的话就因为这个是真实的那个钱比较重要的一个交易可能所以需要需要有一些特殊处理我理解是就不知道到底现实生活人是怎么样的处理法?我们可以讨论一下细节,并且我知道我们其实群里面有很多专门做payment,我记得那个嗯我记得review了几个ERD是在那个里面的,就是design review是这个ERD,他们那个payment的其实更复杂一点,还有就是concurrency,不是那个currency的问题,还有货币的问题,还有并且好多其实payment的,做payment的其实是欠Uber欠的,有个RAR的问题,那你怎么样collect RAR的话是并且还有是cash payment 的问题,其实这个payment 的做起来是比较复杂的。对,有各种情况。好,我们确实可以把做一个mock intro 的面试题,payment 其实有很多很有趣的地方,payment 也属于相当于是比较 call transaction 和这个系统,就是这个distribute system,任何一个 single point failure,就是 single point failure 和这个 fault tolerance 这个也是比较就是考的比较多的地方,怎么样处理 transactions,怎么样处理各种各样的 faults好,我们继续好了所以Transaction呢,我们今天呢是讲的是集中Transaction,然后这边的话,所以大家今天的课会真的比较有气啊,今天讨论的东西会真的比较有气啊,你会看到各种情况下,在这个Data Base不同的isolation level里面guarantee,你会看到不同的结果是吧?所以这个里面这个例子呢,希望大家就是仔细想一想,然后把它记下来,名词记下来最好,但是例子记下来的话,其实对里面是很有帮助的,包括对你个人的理解是很有帮助的。这里面比如说一上来的话,最简单的可能是有一个multi object的一个,就是autonomic,这个就是transaction的概念是吧?就是第九章讨论,然后就是下周讨论,然后这个read committed的话,那你是dirty rights和dirty reads有什么结果是吧,有什么可能造成的结果,那就是也是level最低的一种transaction是吧,就是你这个isolation level是guarantee最低的,然后往上的话就是snapshot isolation,snapshot isolation的话那为什么需要有信息呢, [10:00] 能保证什么?然后再往上的话,这是Serializable。Serializable 的话,其实也是有几种Serializable 的。然后它这块的话,有一个Isolation Level,叫Serializable。然后有几种Concurrency Control的一个实践,比如Two-Face Locking, SSI,或者是Actual Serialized Execution,这都是有几种实践的。然后总体来说,它们又分成悲观的这个锁,其实也是非常有讨论的地方,所以今天就是比较开脑洞的一天,就是大家想一下各种出现可能的情况。这个说并且所有Transaction面试呢也是,我个人是比较喜欢面Transaction的这个面试的,就是有的面试官很喜欢面这个面试,因为就是可以看一下这个人对这个系统理解和这个系统组件的理解,然后各种counter case怎么处理。然后像在周二的面试其实就是找反例这个过程其实就很有趣的一部分就是怎么样找出一个反例来,然后说这个系统不work早反例有时候比设计一个系统有时候会更难一点在Spanner这篇paper里面,就是他这个Spanner作者说,因为这个Two-Face Committed可能太贵了,所以可能就不支持他,但是其实这个transaction这个部分是整个database的extraction带来中最重要的一部分最重要的一部分,我可以认为MySQL里面给我带了最好的一个abstraction,假设它是一个abstraction,最重要的abstraction就是transaction。如果说让我重新发明一个transaction,我还不如不用这个database,是吧?所以说提供不提供transaction,不应该是说提供不提供transaction,而是说我提供给你transaction,那你用不用是你的问题,是吧?而不是说这个database,其实没有重新的,我尝试去我尝试去自己实践一个transaction,这个是非常非常困难的。这个基本上来说都feel mis-volved 了,因为这个transaction已经经过近50年的发展,它已经非常完备了,自己实践的transaction很有可能会出现各种各样的问题。好,所以Database中,其实很有可能就是各种各样的问题都会出现,比如说你的Database software hardware failure,是吧,然后application可以crash,然后Network可以partition并且一个非常简单的Network partition就可以导致你的系统整个就down掉我们见过这个就是上个月的时候不是见过一个简单的人为的Network出问题了它就是那个Cloud Flare然后它整个它自己有个private network然后它把backbone的private network设置出问题的,不算是partition,相当于是人类的network出问题了然后把所有的traffic direct到一个data center,然后整个cloud flyer的整个network就down掉了这是上个月刚出现的事,7月份刚出现的事但是,包括我们下周就讨论的这个Consensus Algorithm,不管是Draft,不管是Paxos,还是Rift,Rift,Rift,它其实都有一个非常simple的一个Network的一个case,就是你A点和B点之间,它Network时段时连,然后你的Rift,你的那个RAFT就可能花很久才能converse到一个consensus了,就是A点和B点,就是你一个node和另一个node之间,时段时连,时段时连,时段时连,然后你这个系统就不响应了,就不available了。所以这个Network这块其实有很多counter case,很有趣的case可以研究,然后也是比较复杂的地方。好,然后从系统的角度来讲的话,就是你可能是这个就是这些transaction这些讨论的问题是你可能在每一个时间点上,你都有多个client去访问然后我想说这块系统来说对系统来说时间是相对的就是像那个数学里面例子一样就是你任何两个时间点之间有无限大的时间点你任何两个transaction之间有无限多的transaction,你可以这么想,假设你是真的一个非常crowded的系统的话,你任何两个时间点之间有无限的。就像你任何两个有理数之间有无限多个无理数,是这样的感觉。所以这个时间是相对的概念。所以这个时间是相对的概念的话,那你这么多这个computer,就是contender来去content的这个transaction,那么你怎么样决定谁是winner,谁是loser呢?所以这个的话,你就必须要考虑一下怎么样处理这个concurrency control。你怎么样处理这个concurrency control了,我们之后今天也会稍后谈一下这个concurrency control。控制的问题然后其次的话你可能会读到一个partial updated data 是吧?比如说你在写database的时候,我不知道大家想过这个问题没有比如说你database是一个document database你是个Json然后把这个Json呢写到这个database里面那我读这个Json的时候 [15:00] 如果这个databaseupdate一半怎么办,这个datum写了一半怎么办,因为你毕竟写的时候,你可以把时间无限放大,是吧,假设这个写需要5毫秒,那这个5毫秒的话,你可以认为是有5000个什么纳秒,是吧,你这个时间无限放大的话,你可以想到就是如果在读的时候读到一半怎么办,是吧,那怎么样partial updated data也是有可能的。我之前的话,我之前在Southforce的时候专门做这个专门做这个就是有点做了一个有点像Airflow 的东西,但是它其实中间有大量的这个transaction 和这个我当时写了一个optimistic locking,写了一个optimistic locking,然后大量的risk condition。就是有的risk condition,我们系统推出了一年半以后,然后我写的那个系统推出一年半以后才发现然后发现这个系统以后我们就推敲这个 risk condition 是怎么来的这部分是最难的, debug 是怎么来的然后这个复杂度就很高,所以这个Risk Condition 是比较难的就是你很多时候你系统推出了好几年后才发现然后那个Risk Condition 可能都没法解决那没法解决怎么办就希望它不发生很多时候就Risk Condition 你想解决这个Risk Condition 如果千万分之一一分之一发生的事情啊 你想解决它那你造成的结果可能就是你整个系统要重写,然后你就假装它没看见,希望它没发生,很多时候有时这样,但是你要搞清楚为什么会有 risk condition,以后真的要重构的时候,真的要存 architecture的时候,你是不是能解决这个 risk condition好,Transaction的话,我个人觉得是Database提供的最好的一个非常neat的一个 abstraction就是你实际上是把很多的group-reads和reads变成一个logical unit了所以你认为你这个就是执行了一大堆读和写,然后变成一个operation你要不就是 get all,要不就 get nothing,就是 about or roll back.get out 的话就是你 committed,如果没有get nothing的话就是about或rollback,然后如果是rollback的话,你的application可以safely retry所以这是Transaction,你可以认为这是Transaction的一个 abstraction。所以在2000年的时候,就是这个NoSQL新起的时候,他们整个NoSQL database是普遍的放弃了Transaction,有一些特例,比如说FoundationDB,Apple II的FoundationDB,其实它是提供Transaction的,它是Serialized Snapshot Isolation,有一些特例,但是大部分NoSQL,基本上是放弃Transaction的,因为Transaction是Performance最主要的影响。Transaction就变成了NoSQL运动的牺牲者。它不提供Transaction,不提供Strong的Transaction,提供一个Wake Side的Transaction。然后这样的话就变就是用那个效率换取它这个safety 是吧就是efficiency和safety之间的考虑他们选择了efficiency没有选择safety这个是可以理解的,毕竟也是从2000年左右互联网用户还比较增加多的,互联网的需求还比较增多的,所以他们就是选择了Efficiency,没有选择这个Safety所以说就是现在有一种popular观点是transaction呢,是任何一个大的large-skilled system应该去abort transaction为了更好的performance和high availability,然后如果你是需要的话,比如说你是essential requirement是需要transaction的话,那你应该用这个需要transaction的这个,比如说你是valuable data,比如说payment这种,那你应该用一个有transaction的系统。这个说法还是相对比较片面的,有多种原因,一个是用transaction的系统的时候,不代表说你用transaction系统,但是transaction,就能guarantee你能做payment了这个不是啊它中间有很多很细的东西然后这是一点然后其次的话,这个当时有很多paper,就是2013年2014年的时候很多这个,Blockchain 里面做Transaction,就出现了各种各样很奇葩的事情,然后各种Blockchain,系统设计没设计好丢钱是吧,就是Double Spending,这种特别之前不是花了,就是在Ethereal之前有很多这种奇怪的他们设计的东西,然后就没有在Ethereal这个平台上进行coding,没有这个Smart Contract平台上进行coding,出现了各种黑外的事情,所以我记得有一个非常有名的就是美图秀秀出了一个美币,然后这个美币呢,本来就是被人拉过来投资的炒作的然后他推出了以后从最高点低了跌到最高点的1%以后呢就在1%的基础上跌了1/10000因为它有个double spending的问题所以我可以不停的copy它里面的currency所以我记得每币什么最高的时候100块跌到1块钱觉得肯定会涨了然后结果跌到之前的1/10000导致这个 [20:00] 他想已经不能数了是吧,因为你可以copy b 的话就一分不值嘛它就有个bug就是这样子的好Double Spanding没有解决好。好,在讲到Transaction的时候,最popular的一个概念就是ACID,但是 ACID 是一个 out of date concept, ACID 是一个不是很好的 concept这个也是我在 school 里面学到一个最好的concept,也估计是你面试的时候可能会被念到的concept,因为这个是教科书上的东西,1983年就有了教科书上的东西,然后这个是不是很好的concept。那对应的ACID是个basic,表示basic available,然后soft state,eventual consistence,基本上您可以拿到任何一个database甚至您可以拿比如说卡卡也可以说是这个是个贝斯是吧卡夫卡这种不是database的,你可以说它是个base好,AC的值是什么呢?A的话是autonomicAtomicity就是原子性你要不get all, you'll get nothing如果这个transaction决定about的话,它应该能被safely retry所以更像是aboutability,就是意思是你可以about这个transaction如果你的一个assumption就是你在如果在Commit的时候遇见任何你的Assumption变了,或者什么不满足的地方,你可以About这个Transaction然后Consistency的话就是什么意思呢?Consistence就是说,当你去写你的Data的时候,你会有一个这个 expectation,比如说你是做transaction 是吧?你希望A和B账户加起来是固定的钱数,那你会有个 expectation,如果这个 expectation 被broke 的话,如果这个Expectation被broke的话,你应该就是相当于是Consistency被broke,那Consistency被broke的话,你应该去,就是应该,就是这个系统呢应该是能组织你的。但是这个Consistency呢,其实是一个user或者business oriented thing,所以它其实不是database,从database的角度来讲,你A账户多50块钱和B账户少50块钱,Database没有觉得有任何问题是吧?你设计的时候说这两个人必须一起执行,Database的角度来讲,这是两个操作嘛。那他觉得不bondor在一起没有什么问题。所以这个Consistency更多的是一种Client的需求,就是Client Requirements,其实不是对于Database的一个需求,它只不过是Business的需求,所以我们必须很多时候你这个做技术,最重要的是Business,Business Orientated,所以你必须做Business的时候,但其实不是database的事情Database不能阻止你做一些crazy的事情。然后isolation的话就是我们说过在任何一个时间段上,你可能有无数的东西在写进的database,你有无限多的东西,transaction在写的database。那么我们怎么样保证这个database不出现问题,是吧?你不会两个transaction相互践踏,然后出现各种各样奇怪的结果,我们很快会看到各种各样奇怪的结果。那么就像刚开了一个咖啡店刚开了一个那个耐克鞋,耐克鞋突然限量版鞋开店了,然后几万个人往一个耐克店里面抢,那怎么样保证最后的结果就是虽然有几万个人同事在抢,最后结果感觉像他们一个一个排斥对境来似的感觉,就是要做的事情。就是Serializability 的事情。最后一个 Durability 的话是,如果你这个 data committed,应该有一个 checking point,在这个 checking point 之前,我是不 guarantee 的 data 会 committed。在这个 checking point 之后,你的 data 是一定会被 committed 下来的。如果我们记得之前的讲东西吧,这个 checking point 在这个 SQL 里面,就是 write ahead log,是 Red Hat Log所以在 Red Hat Log 之前你认为你没有写到Ride-A-Hide-Log,这个Ride-A-Hide-Log没有被flush了disk的时候,你可以认为这个还是可以被abot it,可以被扔掉的。但Ride-A-Hide-Log被flush了disk以后呢,你可以认为这个东西已经彻底的存下来了,就是不应该丢失,就为整个硬盘坏掉了,是吧,或者硬盘这一部分坏掉了,是吧。不应该被丢失然后这个有的系统database是copy on write的,所以如果是copy on write的系统的话,可能就是你这个有个pointer,是吧?这个B-Trait里面有个pointer,你把这个旧的version指向新的version,就是一个checking point,就是一个时间点。在这个时间点之前,你都是没有committed,在这个时间点之后,你都是committed。所以这个分清在哪个时间点是committed,是很重要的一个时间。所以这个面试的时候也是很重要的一个概念。或者user是吧你什么时候跟人提出我拿到你的request了我guarantee 会 serve 你的 request 但是我可能不会当下收 request 可能需要过段时间的 serve request 是很重要的一点好 [25:00] 其次的话,Transaction里面有三个object和多个object的概念啊如果是单个object的话,其实这个transaction和lineability就很像了。这个transaction,我觉得这个单个object的transaction,和这个单个就是和Linearability就很像很像很像嗯我想单个objects/transsection基本和单个objects/linearability很像大家不知道有没有能想到一个反例了,我在这里没想到,但是Transaction和Linearability,就是Distribute System的Linearability,我们之后会在Consensus,就是下周讲到Linearability的一个概念主要的不同就是Transaction一般是有多个多个object的,就是transaction有多个object的。哦,还有一点不一样的是,transaction,Linearability是要求你读的已经是实时的,但是你单个object的这个transaction有可能会读到这个就是旧的still的data,因为你可能是snap chart isolation所以还是有点不一样的但是除了这点以外应该都是一致的谢谢好,我看这块的话,对,Transaction的话,如果是DogObject的话,写的时候就写,不知道大家有没有写过Post,那个procedure, 是吧,procedure call,那叫什么来着,Stop procedure,不知道有没有写过Stop procedure,之前在Southforce,我之前在Southforce的时候,还有很多人在写Stop procedure,我看到Uber印度组有很多人喜欢写Stop procedure,写的时候就经常是现在有个begin transaction,然后什么select for update,然后committed,然后这种比较不透明,这种code其实写起来就是跟现在的互联网设计是很不一致的,为什么呢,因为它这个会有各种各样的问题,首先它这个code,不是version的,就是你其实,depends on you check in, or not check in to GitHub.如果不是Github的话,你都没办法version然后其次的话 你这个写的是SQL code的 没法debug 你没法写log 是吧 没法log 没法debug然后其次的话你也没法 login,你不能说下一个debug pointer,你没办法 login,这个login也是有问题的,这个SQL里面怎么login,这个是很有问题的,很难login的。所以这个version control,login,和debug,都在stop procedure里面都有问题,然后stop procedure,如果你写过stop procedure,你就知道stop procedure,既然上business logic用最重要的一部分。所以说话现在基本就被大家遗弃了就是你现在基本上很少自己写 begin transaction commit 了或者是写的话也是就是这个 ORM 经常会给你 wrapper 一层让你写的比较简单点,single object的这个transaction。如果你要写这个的话,你要非常小心的写。Self-host 写这个stop procedure是有很多很多问题的。我记得最常见的一个问题就是,我们当时debug这个distribute system的时候,就说有一个transaction,它run了,48 小时,他们当时做Performance Evaluation,有一个Transaction,乱了48 小时,48 小时就是Forever了,48 小时在计算机中就是在那Forever了感觉从公元前零年,活到现在了,活到四千多年以后了从埃及开始建金字塔的时候,5000多年一直存在的一个transaction并且因为它是个two-faced locking,就是说他lock的object没有人能读,就是两天之内他lock的object是没有人能读的就这么悲惨,就是你lock了一个东西,然后从公元前3000年到现在没有人能赌,就变成了一个机密的东西,这个就是非常悲惨的一个过程,所以这个stop procedure和transaction写不好也会很惨的结果好然后这块的话,Atomic和Isolation对SingleObject也是相关的,是吧?比如说你在写一个20KB的一个 data,20KB的一个JSON data当你写了database的时候,假设你写了10KB的时候,那么你database里是存了10KB的刚写的data和之前的data呢,还是怎么处理,所以这个是很重要的。大部分Database,基本上所有Database,不是说不包含所有Database,基本上所有Database都保证单个object的,atomic,和这个isolation,就是你只能看到之前的结果和之后的结果,你没有half-baked的结果,但是file system里面是可以看到写了一半的结果的。然后这块有个问题说是, maybe I feel around the charge procedure different from transaction.你可能在做到任何通用的通用程中好像也可以的,这么说好像是可以的,但一般来说,Stop Procedure是减少了你的interactive 的过程,就是你不是读到A,然后处理一下,分约A的结果是满足条件,然后再写个B,不是这样子的,所以Stop Procedure是,你像现在, [30:00] 其实还会写Stop Procedure你在写Redis的时候Redis的那个Lua languageScript language其实是相当于是写一个Stop Procedure这也跟Redis的设计有关我们之后会看到这个Redis的设计为什么要求他写这个Stop Procedure我这块只是举个例子就是举一个时间上的例子这是我过去工作中真实见到一个例子就是你有一个Transaction有一个Transaction在Run了48小时以上还在Run的然后不知道他就是他在等一个什么东西不知道他等什么东西但是他lock了一大堆东西然后lock了一大堆东西没有人能读也没有人能写这是真实中存在的一个例子最后的话很多这个操作系统很多datastore是about这个abundant这个multi-object transaction因为如果你这个multi-object transaction是cross charts或者cross partition的话你的performance会很糟糕像这个Cassandra还有其他的一些database它是可以允许你定义这个partition key的因为你能定义partition key所以你能决定你的data就是属于一个transaction data能把它放到一个shard里面那你这样的话你这个可以做这个在Visicnshard可以做一些transaction那这样的话,这个performance会比较好,像Uber的这个Skinivision是没有这个partition但最新的这个storage is DockStore我不知道会不会发一个engineering block,但是这个 DockStore是支持这个这个就是在一个shard的引内是支持transaction,并且能让你自己来定义shard然后Google的这个,我记得Google的bigtable还是还是另一个 service 也是支持你自己定义shard的我忘了具体是哪一个 serviceGoogle有一个 service我对Google的这个 text tag不是很熟但Google有一个 service是允许你定义partition key的就是你定义partition key的话你就可以决定哪个 data在哪一个shard里面然后你可以决定把你想做 transaction data全部放到一个shard里面其实有很多好处在relational database里面就是我看这块有没有什么想跳过的地方然后然后对这个ForeignKate这块其实大部分你看到大部分互联网公司你会尝试去减少这个ForeignKate的使用就是你尝试在Application Logic然后Maintain这个ForeignKate因为这个ForeignKate其实非常非常Dangerous然后非常这个Tidious然后你这个CascadeDelete包括你有这有这个 cascade delete比如说你这个比如说你之前这个你A指向B是吧那B delete的话A应该也对应的被delete如果但是你这块如果没有handle的话就downgling pointer就是A指向B结果B被delete了你读A的时候就变成downgling pointer了这个是非常糟糕的地方所以在设计的时候就是ForeignKey看起来是一个很不错的feature但是在在这个互联网公司和distribute system设计的时候你要非常小心这个ForeignKey并且ForeignK这块有更麻烦的地方就是你这个ForeignK可能在不同的share里面所以当你这个ForeignK在不同的share里面的话这个crossshare的这个consistency就很糟糕了就变得非常非常糟糕所以这个ForeignK一般来说是尽量避免使用的然后就是这样子了然后看基本上这个multi-object这块就cover了好我们开始看一些有趣的例子我们开始看一些有趣的例子基本上后面就全部讲例子了前面讲了一些这个实践后面就全部讲例子了第一个例子是这样子的就是我们在写这个email的时候打开Google email的时候你实际上是看到两个东西一个是你有没有onread的email如果有onread的email它可能highlight一下然后其次的话它是显示哪一个是onread的email这个email也没有onread的这是两个东西但是假设你想说假设你是normalized data实践的话你就必须用这样一个SQL来写就是select count from email是属于你的email,但是没有被read过这个是要从你的database从这个table里面scan出所有的table这个road belong to your the road这个performance是很糟糕很糟糕的是吧你比如说我随便看我email这个folder可能有上万个email我每当打开email的时候它要读上万个road这个performance是很糟糕的所以为了使这个performance快一点的话所以经常会把这个data denomalize一下就是读成两个road你专门有一个column是说你有没有onread的email是吧就是有column是有没有onread的email然后其次的话每个email有个on-read和read的这个booling就是在这个field上有个on-read的booling然后我们这块有一个很有趣的例子是这样子的因为你现在是两个field一个field是这个email和他read的情况另一个field是有没有on-read的email我们这块是有一个transactionuser是新写了一个emailuser1刚给user2写了一个email [35:00] 他insert的时候先把email insert进去了所以在这种情况下的话,在这儿的时候,在这个第一个时间点和第二个时间点之间,这个email是被insert进去。但是这个on-read update 稍后发生了那Unraid是从0变成了1,就是在这个很后面的时间点从0变成1如果我在他发邮件的这一瞬间这个时间虽然是一瞬间但是可以无限大就是时间是相对的这个时间可以无限大我在他发邮件的一瞬间我读这个我自己的email的话那么我会看到我现在没有onread的email,但我读到第一个email是onread的这个data是inconsistent的 对吧就是一在跟我发email的时候然后他发email的email和update的email的email这两个步骤是有一个空档的所以导致我结果是我读到inconsistent data 我没有看到email写的是0但是我读到第一封email就是Enray的email这个是一个简单的 brokenBroken好,这个例子带来没有什么问题。有没有什么绝对想像? 这里的话就是应该先先把那个unread先set,然后再做吗?不想理解是不是要fix个问题的话?一样的,一样的,我们说时间节点是无限长的,你比如说这个unread先写过来,unread先写过来,当你读的时候说,我没有任何email,就是我有一个unread的email,在读第二的时候,我说的email都read过了就这两个换一下这两个换一下,这两个换一下就好了。那应该怎么样解决这样的问题?我们稍后会考虑一下怎么样解决这个问题,但是您可以想象这两个换一下以后是一样的结果,就是只不过换下的结果就是第一个说你有一个on-read的email但是你看说的email都是read过了因为你那个on-read的email还没到你这个box里面谢谢大家是好,这个问题是什么呢?就是大家想一下这个问题的本质是什么?我可能给个10秒钟时间,15~15秒,我看大家有没有能想到这个问题的本质是什么?好我看到有人说是DirtyRate了,Karatoids能解释一下什么DirtyRate吗?谢谢大家好你说我吗?对啊。好的,Dirty Rate 的话就是你可以看到还没有 commit 的 transaction。好的,所以这块的本质是不是就是你读到了一些,就是同时一堆人在写一个东西的时候,你作为一个user,你读到别人还没有 committed transaction,就是你读到这个没有 committed transaction,所以它是这个问题的本质就是 dirty read是吧 如果你读的时候别人没有committed transaction 就是这一段这个email insert,如果读的时候,你这个email insert,因为它没有committed,读不到,那这个读的是consistent的就相当于对于user1来说这个transaction就变成了一个时间点在这个时间点之前读的假设这个时间点在这儿啊,我们把这个时间点画这儿你任何读在这个时间点之前读的,你都能读,就是读到这个没有任何on ready的一码,在这时间点之后读的,你可以认为是就是读到了NRAD email所以这个问题的本质是Dirty Rate你在这个Rate里面你读到了别人没有Committed data不是很明显但是如果你还没有Committed它怎么会到已经到Dirted Base里面来了你是怎么可能读到这个星期了?Depend on database是怎么实践的,Depend on database是怎么实践的。有database是,比如说我举个copy on write这种事,你实际上是就是你是把 b-share copy了一份是吧,把 b-share copy了一份,然后用 pointer 来指向的但你如果是一个如果有database是直接在上面覆盖的话你比如说DynamoDB是直接放在覆盖的话,那你是有可能读到的Denamore是有可能做到的,你如果在这个Denamore上写的话,你是有可能读到这个结果的。我只保证这个email对于这个email来说我只能保证要么就是一个email没有要么就是一个email全部加进去但我不能保证是email和另一个相互关联的写是同时加进去的在Dynamo里面,是吧?那我在用Dynamo的话,我有可能读到这个结果,就是DirtyRate的。所以这个问题只存在于比如说unlow software database 如果是socware database 一般都会有一个transaction保护创始,对吧?不会出现 dirty read 的情况吗?对,现在其实 database 很多,database 真的很多,有很多很奇怪的 database,我记得和翔静讨论一个就是 voltdb 的时候Voltmore DB,Voltmore DB 的时候,它就是,它对于这个, [40:00] Ibis的isolation是比Denomal还要糟糕的,就是什么呢?Animal 还有 read/repair 和这个就是嗯Red Repair和这个Enchophe,就说白了是,它会不停的scan它的区间,然后找一些Inconsistent data,然后去Puretically show full time in consistent data但这个VaultModeDB呢是你只有read repair,就是你这个data只有长读的data相对来说可靠性高一点你这个data如果从来没有读过,读得很频繁的话,你这个data很有可能会丢失所以不同的 database 它的 implementation 跟它的 use case是差别非常非常大的,所以你要非常小心,你到底用哪一种database然后提供什么样的guarantee好,我们去下一张好了,就是。对好 所以这块的话就是最basic的isolation,isolation level的计划讨论的就是 committed data,就是 no dirty reads,这种情况下,还有一个是 no dirty writes,就是 notated writes 是什么意思呢?就是你只有 overwrite data,被 committed data,你不能 overwrite,没有 committed data我们看就是,比如说,有一个例子,在这个书里面有个例子,就是两个人同时买一辆车然后可能的结果如果你有dirty rights的话,可能结果就是车归了一个人,然后另一个人付账单就是比如说我和我和比如说我和AJ一起买车 是吧?然后做了一个payment然后结果侧归了我了,这个侧的ownership 是我的,但是invoice是够agy的,这是有可能的。如果你有做tree writes,还是有这种可能性的。这个就不细地谈了,就是如果感兴趣的话可以看一下那个例子,也是很有趣的一个例子这是第一个Transaction Level,就是Rate Committed Level然后像我们之前谈过denomalDB是不提供这个transaction guarantee的,就是这个isolation leveldenomalDB是不提供的所以就是,但是一般来说这个SQL的话,这个是最低的level,就是你在写SQL的时候,你可以configure这个SQL的isolation level,就是最低的一个level。基本上是最低的一个level了,就是没有比这个更低的了。好,然后我们再看这个我们再看第二个例子第二个例子我看一下这个例子是这个就是Invoice Dirty Rights的例子,比如说Alice和Bob一起买车,然后呢 你现在把这个Bair 车的ownership给了Alice了然后把invoice给Alice 是吧这个时间是相对的所以你这个中间可以有无限大的时间点在你这个时间点的时候你又把这个车的bear给从Bob了然后invoice给了Bob 就是账单给了Bob这种情况下当这个事情结束的时候,你看一下最终的结果是什么呢?这个owner,Bob是Overwrite Alice,对于从owner来说,Bob是owner但是从这个invoice副葬单的情况来讲Alice是overwrite Bob所以这个车归了Bob但是这个葬单归了Alice是吧所以这就是这个 dirty write 就是你说白了是你的write overwrite的一个 uncommitted write 是dirty write的一个结果好我们再看一下简单实践30Rx基本上是被所有的Mansickel database实践的,基本上是被它实践的有比 dirty rights, dirty,就是 read committed,有比 read committed更低的一个level,但是我们基本上就不讨论了,就是rate committed的更低level,比如说这个denomal db 属于rate committed的,这个isolation level更低的level,然后rate committed呢说白了是no dirty rates和no dirty rate,然后我们之前有一个例子是dirty rate的例子,一个例子是dirty rate的例子所以说从high level的角度来讲的话,基本上能guarantee什么呢?就是对于任何一个object来说,如果一个 transaction 两个 transaction 想写到同一个 object, 那它必须等到一个 transaction committed 了以后, 然后才能去拿到锁, 然后再写这个另一个 update所以这个呢实际上是基本上是用lock来实现的,是用row level lock来实现的所以当你写data的时候,如果你没有committed的时候,很多时候比如说,我记得我上研究生的时候,本科的时候上了一个distribute system,就是实践一个committed过程。当你写的时候,比如说你要做一个update的时候,你实际上是把这个写到那个肉的同名文件,然后加一些random number,加你的transaction,我们当时要求加一个transaction number,就比如说我要update row1,那我把 row1这个呢写到一个file里面,然后加一个transaction number。那当我这样committed的时候,我其实把这个row1加我的transaction numberreplace 掉肉依,这样的话相当于是个 committed 的操作那这样的话 [45:00] 那其实是能guarantee就是你在任何情况下你没有corrupted data的,当时是建了个最简单的distribute system的本科的时候。所以是这样子的。所以好对,然后如果你是读的话,假设你在Committed的时候,如果你在读的话那你像我们举刚才的例子就是有一个肉1,然后有个肉1+transaction,然后肉1+transaction因为没有committed,所以你在读的时候就只读肉1的data然后如果这个肉依加transaction committed的话就overwrite这个肉依是吧,你就读了新的data是吧所以你读的时候呢,你要么读old committed data,要么读新的transaction data,所以你不希望在读的时候有blocking,可能会做一些优化然后这是一个 dirty read 和 dirty read 的一个 isolation level, 很多时候觉得看到这个以后,好像这个已经足够了是吧,那会有什么potentially 会有什么地方可能会 go run 呢?那我们再看第二个例子,叫 snapshot isolation这也是相当于在这个讨论期里面讨论最多的一个例子了,就是snapshot isolation这个例子也是比较简单的一个例子,也是比较有趣的一个例子,就是你Account1往Account2里面转100块钱,是吧,Account1和Account2里面转100块钱,那么我在做这个Transfer的时候,我是先从Account1里面加了100块钱,然后从Account2里面减了100块钱,然后again这个时间是无限大的,是吧,所以在另一个用户看的时候,他可能看到Account1的时候,他这个Account1的读是在我这个Transsection之前,Account2的读的时候,是在我这个 transaction 之后然后导致他看到两个账户加起来之后900块钱但我这边呢是看起来是1000块钱就是我 transaction 之前是500块钱我 transaction 之后是就是600 400 是吧所以我这个从我的角度来看我的资算户是Consistent的但是从他的角度来看凭空的少了100块钱这100块钱不知道去哪了这个叫这个reschool 是吧这个也叫做这个就是non-repeatable write如果这个Alice在这个时间点之后再进行一次读的话它会看到一个变成600 一个变成400了那这个造成的最主要的原因是什么呢?是它的这个毒,有一部分毒是在transaction之前的,有一部分毒是在transaction之后的就是你读的不同的路是横跨了一个transaction,一个或多个transaction,因为时间是相对的,你是横跨了一个或多个transaction的。所以就造成了结果就是你读之前,你读第一个object的时候是第一个transaction,你读第二个object的时候是第二个transaction,读第三个object的时候中间又插了几个transaction所以这个是显然是不能接受的,虽然你再读一下的话,Data will consistence,但是这个显然是不能接受的。特别是做analytic的话,比如说你要看每个区的销售量,是吧,如果你是没有snapshot isolation的话,没有snapshot isolation的话,你看的这个销售量可能今天看的时候已经down了10%了,如果大宗商品的话,汽车的销售的话,今天看的时候down了10%了,结果明天一看变成增加了50%。这个很糟糕的是吧,所以这个,所以这个,这个,这个,这个,一般analytic database会要求有一些这个,有时候会要求Snap Shot Accelerator 但是很多时候确实不处理它是有所谓的好,这是Snapshot Isolation,我先停10秒钟,然后看大家有什么问题想问一下嗯谢谢大家好,所以我再看一下,这个snapshot isolation 主要的原因是什么呢?你读的时候,你就是读的时候你有读了不同的 object 多个 object其中一部分object在transaction发生之前,然后另一部分object的transaction发生之后,导致你读的data是inconsistent的。是 snapshot isolation那么我们怎么样去做这个实践来保证 Snapshot Isolation?我们这块一直在讨论,群里面讨论的很火的就是Multi-Version Concurrency Control.然后这个Multi-Version Concurrency Control很重要的一个概念是什么呢?就是你Reader和Writer之间是不相互block,你Reader和Writer之间是相互block,这个是和这个Performance是和这个Two-Face Locking有本质的区别的,我们之后会讨论Two-Face Locking,然后看它这个是怎么样Reader和Writer [50:00] writer 之间的相互 block比如说在这种情况下在刚才这个例子情况下我进行锐的这个 transaction 我可能是 transaction 12 是吧?就是当我Alice 进行锐的这个 transaction 的时候它是 transaction 12然后她进行转账的这个 transaction 是 transaction 13然后对于每一个transaction来说对于每个data肉来说它不是一个简单的肉了它是一个 sequence of data, a sequence of event所以当我读这个的时候,我会看到我现在读Account1Account1上一个Transaction是Transaction1,并且已经Committed,所以我读的Account1是500块当我读Account 2的时候我读 Account 2 的时候我会看到 Account 2 呢上一个 transaction 我能读到的 transaction它是什么呢?它是Created by Transaction created的它是Transaction created的,它会被deleted,它deleted的话是有点像Single Point List,Single Link List的这种,它是被Transaction 13给deleted掉了,但是Transaction 13在我发生了之后,那我应该ignore它。因为我是Transaction12,Transaction13比我大,我应该ignote它,所以我应该读Transaction13之前的那个transaction的data,那就是我读应该transaction5的data,所以我读出来也是500好好 我们再想一下 我们从这个writer的角度来想一下当我进行对这个 account 1和 account 2 进行 update 的时候当我对Account1和Account2进行update的时候,我现在要从Account1把500变成600 所以我建一个新的block 是吧 就变成这个Create 13,Delete by,相当于这个是最新的状态,Delete by 空的话是最新的状态那我从account 1里面增加100块钱我从Account2里面减少100块钱当我这两个准备好的时候,我同时表示这个 committed,是吧?就是这个 13,这个 transaction 13 被 committed 应该真的是变成 600 和 400 了。那之后的 transaction 应该只成 600 和 400 了。然后,但是之前的 transaction在之前的Transaction,应该还是500,还是500,它应该ignign.就是一个闹新的这个13的committed所以之前的transaction相当于是把它变成一个一个一个原子的点是吧在这个原子点之后我都是600 400在这个原子点之前都是500和500那这个原子点这个single point在哪呢是在这个transaction13就是当这个single point是什么呢是在这个transaction13 committed的这个时间然后你其他transaction没有开始下传现在也没有开始 对吧所以这个的话其实你要考虑是个visibility rule,就是说当你这个Reader的Transaction Started的时候,那么你有没有Committed?所以对于这个来说,因为这个13这个commit的点是在这儿,所以你这个transaction在这个13这个commit点之前,你是读不到13的,在这个commit点之后是读到13。这是一点是吧,然后这相当于是定义了一个 single point,然后在这之前之后是读到不同的 data其次的话,你其实每个Data的每个Role这个Mexico是以肉为Granularity的,每个肉来说谢谢大家对于MySQL的每个Role来说,你都是有个link list的,当你最后面的这个,就是最后面的Data,比如说这个Transaction5,已经close了Transaction 5已经closed了,并且所有能看到Transaction,这个block都已经closed的话,那你这个block是可以被可以被这个回收的就是说,怎么说呢?就是你在13之前的transaction全部close的话,就是在13committed之前开始的transaction,全部close的话,那这个block和这个block就可以被回收了因为没有人能看到就对他了所以它可能会有一个backend process来不停的回收这个data所以假设你这个是三 Committed 之前的Transaction之前开始的交易全部结束的话,这两个block就可以被回收了,因为已经没有data,没有transaction能读到这两个data了。所以这是Snapshot Isolation。我们就是我从这个写的角度和读的角度分开讨论了一下这块但是我们high level的想一下就是不需要想那么细的想一下就是high level想一下因为这个detail的level其实过段时间都会忘的high level的想一下它这个设计理念是什么呢?就是说,它可以认为你这个transaction都是连成一块的,就是你最后的时候,比如说,比如说我们举个例子我去给你了把三笙拿出来谢谢大家请不吝点赞 订阅 转发 打赏支持明镜与点点栏目 [55:00] sumline调出来好然后我们这块比如说举个例子你在这里面写的时候你可能有开始的时候有transaction1是吧然后有transaction3然后有transaction5然后有transaction2这是committed时间段这是committed时间段就是transaction2这是committed时间段那么你在这里面读的时候写的时候是这么committed如果你读的时候的话你不会一部分data读在transaction发生之前一部分data读在transaction之后你一定会有个single point就是一个single point就在这个point然后你进行了读的操作就是read1是吧那你会读到transaction1的data或者是另一个single point比如在这个point的时候你又进行操作那你可能会读到transaction1和3的data就读不到5和2的data所以这是它的highlight我想法它实践的话是通过这么实践的其实想法也比较简单就是我只读我之前在我之前的transaction我之后的transaction全部忽略就是如果是这样的话你是没有转账100块你会读到一个500一个400的情况好对于这个snapshot isolation,大家有什么疑问吗?我有个问题它这里是不是它每个block里面除了有它的有它的id create by, delete by,是不是还有一个Fill是表示这个transaction有没有commit呢还有表示一个transaction没有commit我觉得这个可能还是和实践有关因为如果是serialized的如果看后面的话有一个serialized snapshot isolation的话它会详细的讲哪一个transaction应该被abot然后哪一个transaction应该能commit这块的话如果只是这个也看不同的时间了如果让这个做一个最简单的实践的话那么就是谁先写过来的话谁就committed是吧谁先写过来谁就committed所以他这个的话可能会有lost update的情况这个我觉得是跟时间有关对如果他没有表示这个transaction有没有commit那我在这个id为13的transaction就是下面的这个他这两个operation中间去读那我就读到DirtyWay了这个Commit是有一个单独的地方是记载它Commit的这个Log的所以就像我们刚才看到这个一样就是你有一个单独的地方是记载CommitLog的这是一个单独的地方是记载CommitLog的就是说这块是单独的地方这也和时间相关的是单独的地方记载Commit Log你先读这个先读Commit Log然后再读这个就没有问题了I see它不会写到这个上面的因为你写到这个上面的话你就有问题了你到底写到这个Block上还是写到这个Block上那这两个Block可能值不一样你这话说Committed这话没说Committed怎么办好的那所以 somehowSignature Isolation一定会比ReCommitted要强势了对,一定比read committed要强它实际上在read committed上面时间一定会比read committed强的好的,谢谢好的我有一个问题就是我怎么能知道谁先committed,谁后committed那如果是比如说我database它是一个这个commit就是它不是一个分布式的数据库的话那它很容易知道谁先commit谁后commit但如果它是一个分布式的数据库我怎么能知道谁先commit谁后commit不考虑分布式数据库你单个数据库就够你头疼了其实今天讲的说的课单个数据库就够你头疼了但是这个谁先commit谁后commit还是一个非常复杂的问题在这的话你可能会有种情况是比如说Transaction 13,因为它特别大,所以它先写到这里面,后面有个Transaction 14,又覆盖了Transaction 13那它Commit的时间特别晚,就是Transaction 14先Commit了,然后Transaction 13也没有Commit有可能这种情况,这种情况下的话,这个Database应该还是覆盖的,就是你这块Commit的时候,应该是一个覆盖的操作就是你相当于是这个值呢我举个例子这也看实践了不同database的实践可能会不一样但如果是没有serialized这个snapshot transaction的guarantee的话如果没有serializedsnapshot transaction guarantee的话这种情况下的话可能会出现什么情况呢就是这个14呢因为14先committed所以14在下面1313在上面就是13是最后 [60:00] 写在最后的,14是在它下面的,有可能的,这也跟实践相关就是说,我们说的时候,我们现在谈论的其实我觉得群里面有个讨论特别好我不知道这个Tax的Tax同学是,他是专门研究这个PhD,但是他这个理论非常深厚就是说,其实是有两个东西,一个是Isolation Level,一个是Concurrency ControlIsolation Level是什么意思呢?是一个理论上,我觉得我个人理解更像是一个理论上的概念就是说我现在这个isolation level就讲的是什么意思呢就是我读的时候我不会一半读在transaction一半读在transaction我只会读到一个具体的时间点我只会读到具体的一个时间点所以这个是有snapshot isolationsnapshot isolation是一个isolation level是个理论的概念但我实践的时候像这个是一种实践方式是一种concurrency control的实践方式它只要满足这个isolation level就guarantee Snapshot Isolation那它具体如果它满足这条件以后那它具体怎么实践那到底是覆盖呢还是这个就是 Single Point Fillure不是 Single Point就是那个Poiter 指的时候还是什么 Lock 住 指呢还是怎么样指呢那它实践的问题但是它只要能guarantee这个 Isolation level能guarantee这个high level的东西那它就满足snapshot isolation所以到时候这个committed的时候到13先committed了14后committed那它们之间怎么覆盖呢那是实践的一个问题了不同database可能实践的不一样OK那也就是说我这个东西它能保证我重复读都能读到同一个数是吧你重复读对,如果你没有覆盖的话,你从不读是读同一个书但是如果他读到不同的书呢,他就不是这个不是这个第三个级别了他就是rate committed这个是repeatable rate这个就是您现在讲的repeatable rate那如果是repeatable rate的话,那他一定能保证就是我比如说我这个东西我这个transaction我一个transaction我一直读的话我一直会读到同样一个数字对吧你不干净你读到同一个数字如果你在读第一遍的时候13没有committed读第二遍的时候13committed的话你读的是不同的数字哦那它跟这个recommitted有什么区别呢这个地方我不太理解它和read committed有什么区别read committed这个里面假使就是我们那次回述一下这个里面是read committed这个是read committed为什么是read committed你没有读任何un committed data嗯你只不过是读的时候这个transaction没有发生读读读一的时候transaction没有发生读二的时候transaction发生了但你没有读到任何committeduncommitted data所以这个是read committed嗯但是它不guarantee snapshot isolationOK这个这个东西它就比如我两次就是recommitted它可以比如读两次我发现,就是您刚才说的那个,就是我读两次,比如说,我就两个,我读到两个不同的结果,因为中间有一些committed,是吧?对,对,对,是这个意思。所以你在这个里面其实是read committed,但是你读的data是非常糟糕的,就是你是read committed,但是你读到的data是一个在transaction之前,一个在transaction之后,导致你data inconsistent,但是它是read committed。OK,那如果假如说我是snapshot,就是保证了它repeatable read,那也就是说我两次读之前它不可能有commit,是吧?比如说我一个transaction,两次读之间是不可能有commit的。对,你其实是不是表示你这个读之间,你这个读可能很久的,像我们不是说就是SellSource有的读能持续好几个小时吗这个读可能持续很久但是我读的时候它后面不管有多少个transaction我相当于就只在这了就有点像我拼到这个GitHub这个branch这个version了我之后的时候我都只向这个version之后的version跟我没关了就这个意思OKOK那也就是说我比如说我就像您刚才说这个timestep这个东西就是我就是在这个timestep下我只能读它timestep之前的数据我不能读到之后的数据你只能读到之前的transaction不能读到之后的transaction我只能读到之前的transaction不能读到之后的transaction这样就保证我一个transaction里边多次读它的读到数据是一样的对的对的OK,谢谢好的好,那step [65:00] file transaction Snapshot isolation, there's no other problem. Thank you. Okay, I'll talk a little bit about this, then I'll just stop. I don't want to talk too much. I think once you've done this, if you've never been able to do this, you'll understand it. I'll just talk about this two-step. I'll talk about this two-step. I'll talk about this two-step. I'll talk about this five-step. I'll talk about this five-step. Then we'll talk about this five-step. Then we'll talk about this five-step. Then we'll talk about this five-step. How? This is a bit of snapshot isolation, which is more than a more important place. This is a bit more simple. This is Read, Modify, Write. It's a very common exercise. For example, you have a counter. This counter is YouTube View. YouTube View is the number of people who are watching. For example, YouTube has 42 people watching. Then you click on the search, you can read 42 and write 43. Now you can see the YouTube page. Then the user is reading 42 and then reading 43. In the situation, you have more people who watch. But you read only once. Incremental is increased. Snapshot isolation is not possible to solve the snapshot. 因为这里面 Every transaction between each time It's the same thing. It's a real-time data. You read the user 2, when user 1 is not committed, so he read the user 1 is not committed. User 1 read the user 2 is not committed, so user 1 is not committed. So it will be a lost update. This was the first time I had seen before. I was a project that was similar to the case, but it was a counter. When I was updated, I thought I had the number of updates as the final one. I found that number was very bad. I used the dynamoDB, but the mongodb. MongoDB. And the loss rate is very high. The data is very slow. I remember the QPS rate was very low. 40-50% of the size, not 40% of the size. I'm just going to click on the test. I'm just going to click on the test. I'm just going to click on the test. I'm just going to click on the test. I'm just going to click on the test. 10-20 QPS, lost 40% of the loss update. In this case, it's not a unique thing. For example, if you just mentioned MongoDB, you can use JSON-Doc. If you just update a number, it's very fast. But if you use JSON-Doc, it's about 100k. Like Mongo, I remember it was 16 megabytes. If you update 16 megabytes, you need a minute. 1 second time to update 16 megabytes That's the time for forever. I think the time for forever is a year. It's almost like a year. It's almost like a year. It's almost like a year. If you have a year update, you will have lost update. It's not just a few things that you have to be able to do. It's like that person has 1000, and then you have to do 50. And then 1000, the process is not done. And on the other hand, you may see the VK update is the same. When the two of us updated VK, the people who wrote the last one Last write, and then just to cover the previous one. This is a lot of solution for the launch update. The most famous solution is Autonomic. Autonomic right solution, just from database of guarantee. From database's perspective guarantee, this is the case. Atomic operation, like this, set update, counter, v-don, v-e, and for update, like this, it's not the middle of the middle of the time. It's not the time period, it's the time period. That's not a problem, it's not a period of time Atomic is just a time frame, that's no problem. You won't have lost updates. You can also write a "sort" box. Explicitly lock in. This is what I think many Southforce engineers have written. You should be able to write this thing. Because this thing is a problem. You should be able to write this thing. This thing is not a problem. You should be able to write this thing. This thing is... You can write this thing and ensure it has lost updates. It can work. [70:00] 这个Database transaction 写成这个语句呢嗯你肯定可以walk,但你要非常非常小心,否则的话你可能有整个lock database,lock 48 小时,就可能是你写出来的,就比较糟糕了。好,然后呢,有的系统是支持自动的检测这个Lost updated的。有的系统是支持的他这个的话,他就比如说,他这个info是一个read,modify,write,然后再compare一下你写之前是不是一个dirtywrite,是吧?有的系统是支持的,然后谢谢大家这是第三种解决方式,就是有的系统是自动支持的,如果它是Loss Update的话,它会abot然后第四种解决方式呢就是这个 comparison 就是在你写之前 你就是你在写之前你就是写这个update的操作呢我们可以想象写个简单的sigle query 是吧就是update这个row当且紧张,就是update var equal to 42的时候update,如果它不等于42,那这个var这个条件就不满足了是吧?所以这个compare就不满足了,那你这种情况下的话是,你这个compare的set也是可以work的。但compare的set要非常小心,是snapshot isolation还是会丢失data,因为你snapshot,就刚才我们看到过,by定义就是你读的data可能不是最新的data,如果你用snapshot isolation,那这concurrency control的话,你读的data可能不是最新的data,如果你读的不是最新的data的话,当你做compared set的时候,你读的这个data已经不是最新的data,所以你这个set还有可能data loss。所以这ComparedSide是不可能在snapshot的isolation但是总体来说这ComparedSide是比较简单的,比较无脑的一种选择就是当你真的要自己实践的话相对来说比较low risk,比较无脑的一个选择,就是其他的不满足的话就是updateID=43, whereID=42,是吧?当时的警告ID=42的时候,你把它变成43那当你同时有几个transaction进行commit的时候,你相当于是把这个时间段变成了一个时间点那就是就有winner和loser,loser的话可以retry最后的话就是这个CRDT Data Model了,这个CRDT呢我之前讲过,我这块就不讲了,不讲太多了,如果感兴趣的话可以看一下之前这个我记得是在应该是上周了,应该是上周,Replication的时候,讲过这个Uber这块怎么样事件的这个怎么样实践了一个CRDT的一个,怎么样实践了一个on order的hashmap这个database啊,它也是一个CRDT的一个model,就说白了是,我接受你所有的写,我接受你所有的write,你在写的时候非常容易,但你在读的时候,你要做一些额外的,client需要做一些额外的复杂的操作,然后分清,就是谁先谁后,就是写的时候会很容易,但你读的时候会有额外复杂的操作。好,我就讲到这吧,后面的这个也是挺烧脑的,其实。还有七个slides,可能这次的话就是分开点,因为Consensus肯定是一周讲不完的,所以有可能下周和下下周讲完记彰和Consensus,带有什么问题的话,我可以帮忙答一下,或讨论一下。您好,我想问您一个问题,就是刚才您说的这个atomic operation这个问题,就是我看它比如java它里面,它里面有个叫atomic integer,它用那个compare and swap这种方法。你说加班有个什么?Atomic Integer这种class那个不一样,Atomic Integer是Multi-Sped CodingOK它那个实现跟这个实现有什么区别吗?谢谢大家它的底层是用那种锁的都是假设吗?就是,如果...可以可以可以...都是加索嘛,就是都是加索嘛,说白了就是加索逻辑也比较简单,就是在你这个,我不知道大家有没有学过那个operating system里面不是加索那个逻辑,在你要执行critical fields的时候在前面加一个锁,然后在之后unlock 掉是吧?都是枷锁吗? OK,谢谢。我忘了那个具体是怎么讲的,但那个是Multi-Swag的这个,这个是DataBase的但是实践都是加索来实践的,这个其实不完全一样,但是也有些相似的。OK,谢谢.这个时候问一下那个SypeShop的isolation的问题如果减100那个操作如果失败的话是不是就可能要就发那个rollback 然后这个snapshot就被删除了对 我就是想一下 [75:00] 这个snapshot isolation 当你读到这个减100和+100操作当时紧当13committed 如果13committed这个操作从来没有发生过的话 你读到任何操作都是没有13的但是如果第一个加100成功了,第二个减100失败的话,是不是第一个snapshot也会被删除?对的,就会被信出了就才会留办理工的出发就相当于这个没有committed,没有committed的话你就读不到它了就 about it了,对。所以我觉得这个transaction 这块,包括这个isolation control 和这个transaction 这块,你就是一定要想清楚什么是一个时间点。什么是一个时间段 任何一个时间段之间可能有无限多个时间点 有无限多个时间段跟它重合但是如果它是个时间点的变成atomic的话那它就不会有冲突了所以这个Serialize很多时候就把所有的操作变成一个atomic,一个atomic是吧,变成一个时间点,一个时间点,一个时间点。那这样的话就可以排序了然后可以就像,就可以排序了谢谢大家嗨,Scott,我在这里可以问我之前看到的Item Potence的大概我可以啊对就是我想想找你解释一下Idempotence,以及那个Exactly one, at least one 的关系。哦,就是那个Cafka那个是吧?我记得Cafka那个是吧?嗯,那个呢,我也对,嗯,- A message,对,对。 - 对,这个的话,我把那篇文章调出来。其实这个,我的理解不一定比你,就是如果你读过这篇文章的话,我觉得你也可能跟我在同一个level,这个我也是读了一下这个文章,然后理解了一下。我觉得你读过这篇文章的话可能跟我理解是同一个level的这里面有一些K-words要非常小心,因为它是个Sales文章,这个Engineer我见过他本人,我参加conference见过他本人。它是个SELS文章所以有一些SELS文章你要非常小心的读起来其实它就exactly once呢总体来说是非常复杂的就是你很难去guarantee,所以这个卡夫卡很多时候是,就是你这个at least wants 和at most wants 是吧?然后这里面读起来的话,我读了一下,我记得有几个地方很有趣啊,就是你在producer的时候你就可以set这个itempotent的adempotent的在producer的时候setadempotent你producersetadempotent什么意思呢?就是说首先adempotent就是你重复的写你是实际上最后从server端从接收端只有一次写的操作就是多次duplicate的写是没有set effect多次duplicate是没有satisfied所以这种情况下的话它其实是producer写到broker然后consumer从broker上读你在producer那块set item potent的话实际上就是说在这个broker里面你在producer不管写多少次broker是接收的一次谢谢大家OK 那这个是Exactly Once 还是Addis Once? 啊对这个是... 这两个是两个不同的概念。 对这两个不同的概念。然后这个这个Adempotent呢?就是你相当于是这样子就是Exactly wants一定要要求这个producer是adempotentproducer 的血一定是 idem potent,是必须要求充分不必要条件,还是必要不充分条件,必要不充分条件,必要不充分条件,就是你producer 一定是 idem potent,然后除了这个以外呢它的consumer也需要做这个consumer也要做一些额外的这个操作我忘了他 consumer 要做一些额外的什么操作了就像producer和Adam Potent 是以后的true嘛然后这块我觉得有一些地方读的很有趣的是它其实也提供Craft的CraftCraft Topic Transaction但是可以很明显的因为我Transaction Coordinator所以很明显的是在用这个two-phase login来做这个two-phase commit在做这个这个cross topic transaction就是 cross topic 这个 transaction 概念然后它这块有个read committed和read uncommitted,就是刚才我们讲的是吧,read committed,read uncommitted,就是isolation level,就是刚刚讲的第一个level,是吧?我觉得这里面最重要的一点是什么呢?就是consumer这个实践,我记得好像是Consumer这个实践我记得他好像没有写很细,他Consumer这块是用什么样的setting来guarantee exact ones,但是好像,嗯,我这块有个 another takeaway point 是什么呢嗯就是这个它这个Exactly Wants是在卡夫卡系统内的Exactly Wants,它不包含卡夫卡,比如说你要写卡夫卡,变成个Notification System,你这个你其实是这个 notification system就是你consumer读到这个卡卡message以后 [80:00] notification. This is not guaranteed exact amounts, because it is already in Kafka. It's the exact one, it's just in Kafka system. It will produce a message And then consumer sends this message, and produces this message. This is guaranteed exact ones, cross system is not guaranteed exact ones Oh, sounds good. That you mentioned before the notification system. at most once. It's probably ignored failure. At least once, is it used to be an idempotent? At least once, actually, we can think that it can use idempotent, but it can also not use idempotent. You can flash, you can do it 100 times and 1 times. So she at least once more. Oh, right, right, right. It's not related. But you can try to get it. You can try to get it. You can try to get it. Yeah, you can try to get it. But the exact ones are the producers are important. Processing, this part is called Exactly Once. Exactly once, that's what you do. just use a cross system the transition Cross-system transaction with this is not related, because exactly wants is one topic and one topic. It is a system, a node or topic is quite complicated. It is not to think so complicated. Cross-topic exactly wants is two-face locking. Sorry, two-phase commit. I was just saying two-phase commit. - Yes, I know it's in this one. But I said it's the notification system in the notification, so you hope, can be exactly one. That's what they need to distribute the system. I remember there was a protocol, very old. I forgot what it was. It was XAR or what? XA protocol. It's a protocol. It's like REST API. It's a It's as if you've done XA, is the number of the transactions. 理论上的一个东西实际上的话我觉得好像现在基本没有人在用它 I remember Java has a API endpoint. It actually is with two-phase commit It's a similar thing. Two-phase commit, many times it's a "tong-chic" uh,统治了一个,就是Henergineous的一个,一个,一个System, 它这个的话是,你这个你可以,一个是SQL,一个是什么,什么email server也可以,什么只要你实践它的protocol就能guarantee这个,这件事 Exactly one, is it right? Guarantee transaction, but this Now we don't have any use, this is a lot of questions. I see, I see, I see, thank you. Okay, that's it. Thank you. Thank you. Thank you. Thank you.