先写后读原则
The write last, read first rule

原始链接: https://tigerbeetle.com/blog/2025-11-06-the-write-last-read-first-rule/

## 虎甲虫:在没有事务的情况下构建正确的金融系统 虎甲虫是一个为正确性设计的金融交易数据库,但从单独正确的组件实现全系统正确性具有挑战性。本文探讨了在*没有*传统事务的情况下维持一致性,重点关注安全性和活性属性,例如“一致性”(账户在Postgres和虎甲虫中都存在)和“可追溯性”(正余额对应于有效账户)。 关键在于分离关注点:Postgres管理主数据(如账户持有人信息),而虎甲虫处理基于整数的交易本身。这允许扩展和独立的安全性/合规性。由于这些系统不共享事务,应用程序必须通过协调操作和重试来强制一致性。 一个关键的架构决策是指定一个**记录系统**(在本例中为虎甲虫——在虎甲虫中存在定义了全系统的存在)和一个**参考系统**(Postgres)。应用了**先写参考系统,后写记录系统**的原则。 为了处理潜在的故障,系统利用具有检查点(通过Resonate的分布式异步等待)的持久化执行。这要求所有操作都是**幂等的**——重复它们不会产生额外的效果。应用程序层编排这些幂等操作,解释子系统响应,并标记不一致之处以供操作员干预。 通过仔细选择记录系统,强制执行顺序,并利用持久化执行,开发人员即使在不依赖传统事务的情况下,也可以构建正确且可靠的金融系统。

## 黑客新闻讨论:“先写后读”规则 (tigerbeetle.com) 最近黑客新闻上围绕一篇博客文章展开讨论,该文章介绍了TigerBeetle提出的“先写后读”规则,旨在维护不同数据库管理系统之间的一致性。TigerBeetle的Joran对此规则的核心理念进行了阐述:**在从数据库读取数据*之前*,先提交写入操作到“记录系统”,** 从而确保结果的权威性。 然而,该规则引发了争论。一些人认为它含糊不清,质疑其实用性。另一些人则认为这是对诸如两阶段提交等概念的重新发明,而TigerBeetle团队澄清说,它与两阶段提交的不同之处在于,并非所有系统都需要参与暂存/提交过程。 讨论还涉及指定“记录系统”与“参考系统”的重要性。一个关键点是,该规则专门适用于对同一数据进行多系统读写的情况,强调操作顺序。团队认为该规则的名称具有记忆性,其优点超过了潜在的混淆,并强调了他们工作的精雕细琢,包括随附的艺术作品。
相关文章

原文

TigerBeetle is a financial transactions database built for correctness. Yet, building a correct system from correct components remains a challenge:

Composing systems, each correct in isolation, does not necessarily yield a correct system. In this post, we’ll explore how to maintain consistency in the absence of transactions, how to reason about correctness when intermediate states are externalized, and how to recover from partial failures.

TigerBeetle is a financial transactions database that offers two primitives for double-entry bookkeeping: accounts and transfers. A separate data store, such as Postgres, stores master data, such as name and address of the account holder or terms and conditions of the account.

This separation enables transfers to scale independently of general purpose master data (for example dealing with Black Friday events) and solves different security, compliance, or retention requirements of the independent data sets (for example enforce immutability of transfers).

Architecture

Just as a bank may have need for both a filing cabinet and a bank vault, Postgres specializes in strings and describing entities (master data), while TigerBeetle specializes in integers and moving integers between these entities.

A transaction is a sequence of operations ending in a commit, where all operations take effect, or an abort, where no operation takes effect. The completion is instant, intermediate state is not externalized and is not observable. Disruptions (such as process failure or network failure) are mitigated transparently.

Transaction Boundaries

However, the sequential composition of two transactions is not itself a transaction. The completion of the entire sequence is (at best) eventual, intermediate state is externalized and observable. Disruptions are not mitigated transparently.

Transaction Boundaries

Since Postgres and TigerBeetle do not share a transaction boundary, the application must ensure consistency through repeated attempts at completion and coordination, not transactions.

To reason about such coordination, we need to understand the guarantees we expect our system to uphold.

A system is characterized by a set of safety and liveness properties. A safety property states that nothing bad ever happens, while a liveness property states that something good eventually happens.

In this post, we will focus on two safety properties:

  • Consistent
    We consider the system consistent if every account in Postgres has an account in TigerBeetle, and vice versa.
Consistent =
  ∧ ∀ a₁ ∈ PG: ∃ a₂ ∈ TB: id(a₁) = id(a₂)
  ∧ ∀ a₁ ∈ TB: ∃ a₂ ∈ PG: id(a₁) = id(a₂)
  • Traceable
    We consider the system traceable if every account in TigerBeetle with a positive balance corresponds to an account in Postgres.
Traceable = ∀ a₁ ∈ TB: balance(a₁) > 0 => ∃ a₂ ∈ PG: id(a₁) = id(a₂)

In the absence of transactions, the system may be temporarily inconsistent. However, the system must always remain traceable to avoid the possibility of losing—or, more precisely, orphaning—money.

In the absence of transactions, we need to make an explicit architectural decision that transactions used to make implicitly: Which system determines the existence of an account? In other words: Which system is the source of truth?

We must designate a:

  • System of Record. The champion. If the account exists here, the account exists on a system level.

  • System of Reference. The supporter. If the account exists here but not in the system of record, the account does not exist on a system level.

So which system is the system of record and which is the system of reference? That is an architectural decision that depends on your requirements and the properties of the subsystems. In this case, TigerBeetle is the system of record:

  • If the account is present in Postgres, the account is not able to process transfers, so the account in Postgres merely represents a staged record.

  • If the account is present in TigerBeetle, the account is able to process transfers, so the account in TigerBeetle represents a committed record.

In other words, as soon as the account is created in TigerBeetle, the account exists system wide.

Once the system of record is chosen, correctness depends on performing operations in the right order.

Since the system of reference doesn’t determine existence, we can safely write to it first without committing anything. Only when we write to the system of record does the account spring into existence.

Conversely, when reading to check existence, we must consult the system of record, because reading from the system of reference tells us nothing about whether the account actually exists.

This principle—Write Last, Read First—ensures that we maintain application level consistency.

Remarkably, if the system of record provides strict serializability, like TigerBeetle, and if ordering is correctly applied, then the system as a whole preserves strict serializability, leading to a delightful developer experience.

Choosing the correct system of record and the correct order of operations is not just a philosophical exercise. If we designate the wrong system as the source of truth and perform operations in the wrong order, we may quickly violate safety properties.

For example, if we create the account in TigerBeetle but not in Postgres, the system may start processing transfers without containing any information about who this account belongs to. If the system crashes and forensics do not surface the necessary information to establish ownership, we violated the golden rule: traceability.

However, if we create the account in Postgres but subsequently not in TigerBeetle, no harm, no foul. Any transfer attempt is simply rejected by TigerBeetle, money cannot flow to an account that doesn’t exist in the ledger.

Clients interact with the system exclusively via the Application Programming Interface exposed by the application layer, which in turn interacts via the interfaces exposed by the subsystems, Postgres and TigerBeetle.

The Application Programming Interface has two responsibilities, Orchestration and aggregation: The API determines the order of operations and aggregates operation results into application level semantics.

We will implement the API with Resonate’s durable execution framework, Distributed Async Await. Distributed Async Await guarantees eventual completion simplifying reaching consistency even in the absence of transactions.

Resonate guarantees eventual completion via language integrated checkpointing and reliable resumption in case of disruptions: Executions resume where they left off by restarting from the beginning and skipping steps that have already been recorded (see Figure 4.)

Idempotence

However, we must consider a subtle issue inherent to checkpointing: In the event of a disruption, after performing an operation but before recording its completion, the operation will be performed again.

Therefore, every operation must be idempotent, i.e. the repeated application of an operation does not have any effects beyond the initial application.

For each subsystem, Postgres and TigerBeetle, we implement an idempotent function to create an account. In our case, both the Postgres and TigerBeetle account creation functions return whether the account was created, already existed with the same values, or already existed with different values:

https://github.com/resonatehq-examples/example-tigerbeetle-account-creation-ts.


Thanks to Dominik Tornow, Founder and CEO of Resonate, for penning this guest post!

联系我们 contact @ memedata.com