01

資料整合:讓資料流到對的地方

一份訂單,同時要進資料庫、搜尋索引、報表倉儲——誰說了算,順序才不會兜不起來

沒有一種資料庫,能把每件事都做到最好

這本書一路走來反覆出現同一個主題:同一個問題,往往有好幾種解法,每種都有不同的優缺點與取捨。儲存引擎有 log-structured、B-tree、column-oriented 三種路線;複寫有 single-leader、multi-leader、leaderless 三種路線。沒有哪一種是「唯一正確答案」——連號稱「general-purpose」的資料庫,骨子裡也是為某種特定的 使用模式(usage pattern) 設計的。

🧵
書裡的一句重話

「想把所有事情塞進同一個軟體,幾乎保證做出來的東西樣樣稀鬆。」——這就是為什麼複雜應用裡,同一份資料常常要被好幾套系統分頭伺候:OLTP 資料庫管交易、 全文檢索索引(full-text search index) 管關鍵字搜尋、資料倉儲(data warehouse) 管分析,還有快取、機器學習、推薦、通知……一個都不能少。

不可能有單一軟體同時擅長這一切,所以你勢必要把好幾個系統拼起來——這就是本章要談的 資料整合(data integration) 問題。

⚠️
小心「99% 的人不需要 X」這種話

工程師常說「以我的經驗,99% 的人根本不需要某功能」。這句話透露的其實是說話者自己經驗的侷限,而不是這個技術真正有沒有用。資料的用途廣得驚人,某人眼中的冷門功能,可能是另一個人的核心需求——資料整合的必要性,往往要拉遠到「整個組織的資料流」才看得清楚。

直接讀原文:整合的核心問題是什麼

書裡用很精簡的幾句話,把「資料整合」到底在整合什麼講清楚了。原文在左,白話在右,一句對一句。

原文 · DDIA Ch.12 In complex applications, data is often used in several different ways. There is unlikely to be one piece of software that is suitable for all the different circumstances in which the data is used. When copies of the same data need to be maintained in several storage systems in order to satisfy different access patterns, you need to be very clear about the inputs and outputs: where is data written first, and which representations are derived from which sources? If change data capture (CDC) is the only way of updating the index, you can be confident that the index is entirely derived from the system of record, and therefore consistent with it. Allowing the application to directly write to both the search index and the database introduces the problem in which two clients concurrently send conflicting writes, and the two storage systems process them in a different order. In this case, neither the database nor the search index is “in charge” of determining the order of writes, and so they may make contradictory decisions and become permanently inconsistent with each other.
白話翻譯

複雜應用裡,同一份資料常被用在很多不同地方。不太可能有一套軟體,同時適合所有這些不同的使用情境。

當同一份資料的副本要維護在好幾個儲存系統裡,才能滿足不同的存取模式時,你必須非常清楚:資料先寫到哪裡?哪些表示法是從哪些來源衍生出來的?

如果 CDC(變更資料擷取)是更新索引的唯一途徑,你就能確信索引完全是「真相來源」的衍生品,因此一定跟它一致。

如果讓應用程式同時直接寫資料庫和搜尋索引,就會出現這個問題:兩個客戶端幾乎同時送出互相衝突的寫入,而兩個儲存系統卻用不同順序去處理它們。

這種情況下,資料庫和搜尋索引都沒有「說了算」,順序沒人決定,於是兩邊可能做出互相矛盾的決定,永遠對不起來。

🍽️
把它想成一間餐廳

櫃台的點餐單就是真相來源(system of record)——所有事情以它為準。廚房的出餐板、收銀機的帳單、外送平台的通知,都是照著點餐單「衍生」出來的 衍生資料(derived data)。 如果讓客人同時對廚房、收銀機、外送三邊各喊一次(也就是雙重寫入 dual writes),三邊聽到的順序可能不一樣,菜、帳、通知單就全部兜不起來了。

兩種整合方式:蜘蛛網 vs 星型日誌

解法就在原文最後一句話:如果沒人「說了算」,順序就會亂。所以與其讓每個系統互相直接同步,不如讓大家都只認同一份「有順序的日誌」。點點看下面兩種架構,感受一下差在哪裡:

A. 點對點整合(Dual Writes)
🕸️ 應用程式(要記得通知所有人)
💥 資料庫 / 搜尋索引 / 快取(各自為政)
B. 透過事件日誌整合
🗄️ 真相來源(system of record)
📜 CDC → 有總順序的事件日誌
搜尋索引 / 分析倉儲 / 快取(全部訂閱同一份日誌)
點一個節點看說明。從「蜘蛛網」到「星型」,差別不在工具,而在「誰決定順序」。
💡
為什麼這樣才可靠

更新衍生系統的動作,通常可以做成確定性(deterministic)冪等(idempotent)——同一事件重放幾次結果都一樣,所以故障後很容易復原:把日誌從某個點重新播放一次就好。

「一份總順序」聽起來很美,但撐不住規模

把所有輸入漏斗進一個單一系統,由它決定所有寫入的順序,這件事在術語上叫 全序廣播(total order broadcast)—— 本質上就是狀態機複寫(state machine replication)的應用。系統夠小時完全可行:單一 leader 就能建出這樣一條總順序的日誌。但規模一大,限制就一個個冒出來:

1
分區(Partition)

單一 leader 吞吐量撐不住時要分區,不同分區之間的事件順序就變得不明確。

2
跨資料中心

每個資料中心通常各有自己的 leader(因為跨中心同步太慢),跨中心事件沒有定義好的順序。

3
微服務

各服務各自管理自己的狀態,跨服務事件之間沒有既定順序。

4
離線客戶端

客戶端先在本地更新、之後才同步,客戶端與伺服器很可能看到不同的事件順序。

🎫
抽號機比喻

銀行只有一台抽號機(單一 leader)時,每個人抽張號碼牌,櫃員嚴格照號叫號——這是完美的總順序,誰先誰後一清二楚。但生意一大,開了好幾台抽號機(分區),A 機的 5 號和 B 機的 5 號到底誰先,沒人說得準;不同分行各自一台(跨資料中心),台北跟高雄的號碼根本無法比較。決定總順序,正式講法等於 共識(consensus) ——而大多數共識演算法都假設單機吞吐量就夠用,如何讓共識「超越單機、跨地域」仍是研究中的難題。

不是所有事件都需要排序——但因果依賴例外

好消息是:事件之間如果沒有因果關聯,誰先誰後其實無所謂,任意排都不影響結果。真正麻煩的是有 因果依賴(causal dependency) 的事件。書裡舉了一個經典反例:

💔
分手後的訊息

使用者 A 把前任 B 移除好友(unfriend 事件),接著發訊息向其他朋友抱怨 B(message 事件)。A 的本意是「B 不該看到這則抱怨」。但如果好友狀態存在一處、訊息存在另一處,通知服務可能先處理 message、後處理 unfriend,於是錯誤地通知了 B——因果依賴一旦沒被系統保留,順序顛倒就會出包。

書中列出幾個能派上用場的起手式,但也坦白說:目前沒有一勞永逸的簡單答案。

🔢
邏輯時間戳

不需協調就能給出總順序,但收方仍要處理亂序到達的事件,還得傳遞額外的 metadata。

🪪
記錄決策前的狀態

給使用者看到的狀態一個唯一 ID,後續事件引用它,藉此記錄因果依賴。

🤝
衝突解決演算法

能處理亂序到達的事件、維持狀態正確,但對已經發生的外部副作用(例如已寄出的通知)無能為力。

🧭
同一物件的更新,其實好辦

單一物件被多次更新時很簡單:把同一個物件 ID 的更新,都路由到同一個日誌分區,就能在該分區內全序排好——真正棘手的,是像上面這種橫跨不同物件、不同系統的因果依賴。

批次 vs 串流:Lambda 架構讓兩條路一起跑

資料整合的目標,是讓資料以正確形式出現在所有正確的地方——這需要消費輸入、轉換、join、過濾、聚合,最後寫到適當的輸出。批次(batch)與串流(stream)處理器,就是做這件事的工具。兩者的根本差異只有一個:串流處理 無界(unbounded) 資料,批次處理已知有限大小的資料。Lambda 架構就是讓這兩條路「同時跑」,一快一慢互補:

📼
不可變事件流
串流層(快,近似)
🐘
批次層(慢,精確)
🧩
合併視圖
🙋
使用者查詢
按「下一步」看 Lambda 架構怎麼跑
😤
Lambda 的實務痛點

好處看起來很美,代價也不小:同樣的計算邏輯,要在批次與串流兩套框架裡各寫一份,除錯、調校、維運全部加倍;兩條管線各自輸出,簡單聚合還好合併,但 join、sessionization 這類複雜運算就很難對齊;對大資料集頻繁 重新處理(reprocessing) 代價又高,常被迫改成增量批次,反而把批次層搞複雜。

🛤️
鐵路換軌的智慧:漸進演進

19 世紀英國鐵路統一軌距時,做法不是停運硬換,而是先在舊軌旁加第三條軌(混合軌距),新舊火車並行一段時間,最後才拆掉多餘的軌道。資料系統的 reprocessing 也一樣:讓新舊 schema 視圖並存,先把一小撮使用者導到新視圖,逐步提高比例,最後淘汰舊視圖——每一步都可逆,所以你才敢更大膽、更快地改進系統。統一批次與串流的新做法,需要能重放歷史事件、串流 exactly-once 語意,以及用 事件時間(event time) 而非處理時間來開窗——例如 Apache Beam 這類 API,可以跑在 Flink 或 Cloud Dataflow 上。

小試身手

資料整合的核心,就是「順序」跟「誰說了算」。來兩題檢查一下:

為什麼把搜尋索引設計成「只能透過 CDC 從資料庫更新」,會比「讓應用程式同時直接寫資料庫和索引」更可靠?
情境

某團隊抱怨他們的 Lambda 架構維運成本太高、常常人力吃緊。下列哪一點最符合書中指出的 Lambda 痛點?

🔓
下一站:拆解資料庫

如果任何衍生資料都可以由日誌重新算出來,那「資料庫」這個一體成型的黑盒子,是不是可以被拆開重組?

02

拆解資料庫:把資料庫拆開重組

如果整個公司的資料流就是一個超大資料庫,那 batch/stream 處理器就是它的觸發器——現在,我們要把這台「全自動料理機」拆成一間開放式廚房

你以為的「不同系統」,其實是同一件事的不同分身

資料庫、Hadoop、作業系統,最抽象地看都在做同一件事:把資料存起來,讓你之後能處理與查詢它。差別只在包裝。

想想你每次下 CREATE INDEX 時發生了什麼:資料庫掃過一份一致的快照,挑出要索引的欄位、排序、寫出索引——這流程跟「建立新的複寫節點」「串流系統做初始快照的 CDC」根本是同一套動作,只是換了名字。

🏢
一個大膽的視角

把整個組織的資料流攤開來看,它其實就像一個巨大的資料庫。當 batch、stream、ETL 流程把資料從一個地方搬到另一個地方、從一種形式變成另一種形式,它做的事跟資料庫內部「維護索引與物化視圖」的子系統一模一樣。

換句話說,batch 與 stream 處理器就像精緻加強版的 triggerstored procedure、 物化視圖維護程式;它們所維護的衍生系統,就像不同種類的索引。唯一不同的是:這些能力不再是單一資料庫產品的內建功能,而是由不同軟體、不同機器、不同團隊各自提供。

直接讀原文:兩條組合之路

既然沒有一種資料模型能滿足所有存取模式,作者在書裡提出兩條「把工具組合成一致系統」的路——這段是全模組的思想核心,值得逐句對照原文看。

原文 · DDIA Ch.12 I speculate that there are two avenues by which different storage and processing tools can nevertheless be composed into a cohesive system. Federated databases: unifying reads. It is possible to provide a unified query interface to a wide variety of underlying storage engines and processing methods — an approach known as a federated database or polystore. A federated query interface follows the relational tradition of a single integrated system with a high-level query language and elegant semantics, but a complicated implementation. Unbundled databases: unifying writes. While federation addresses read-only querying across several different systems, it does not have a good answer to synchronizing writes across those systems. The unbundled approach follows the Unix tradition of small tools that do one thing well, that communicate through a uniform low-level API, and that can be composed using a higher-level language.
白話翻譯

我推測有兩條路,能把不同的儲存與處理工具組合成一套一致的系統。

聯邦式資料庫:統一「讀」。可以提供一個統一的查詢介面,去查各式各樣底層的儲存引擎與處理方式——這種做法叫做聯邦式資料庫,或叫 polystore。

聯邦式查詢介面延續了關聯式資料庫的傳統:一套整合系統、一種高階查詢語言、優雅的語意——但實作起來很複雜。

拆解式資料庫:統一「寫」。聯邦式解決了跨系統的唯讀查詢,卻沒解決一個問題:怎麼把「寫入」同步到那些系統裡。

拆解式做法延續了 Unix 的傳統:小工具各做好一件事,透過統一的低階介面溝通,再用高階語言(shell)把它們組裝起來。

🍳
兩種哲學,兩種廚房

關聯式資料庫像一台全自動料理機:按個鈕就出菜,磁碟結構、並行、當機復原全部幫你藏好。Unix 則像一間開放式廚房:給你刀、鍋、爐——一堆檔案和管線,都只是位元組序列,你自己把小工具串起來做菜。「拆解資料庫」,就是把那台全自動料理機拆開,變回開放式廚房。

把「全自動料理機」的外殼掀開

資料庫平常幫你把下面這些功能都包好、藏在同一個產品裡。拆解,就是把它們一個一個攤開,讓你看見:它們其實都只是「用資料流方式維護的衍生資料」,各自可以獨立抽出來,交給專門的工具做。

🔎
次要索引 Secondary index

挑出被索引欄位的值、排序、寫出索引——資料庫內建 CREATE INDEX。轉換方式太標準化,所以被包進產品裡。

📚
全文搜尋索引 Full-text index

套語言偵測、斷詞、詞幹還原、同義詞,再建反向索引——像 Elasticsearch 這種專門系統做得更好。

🗃️
物化視圖 Materialized view

預先算好、存起來的查詢結果,本質上是一種會隨資料更新的快取。

🔁
複寫日誌 Replication log

把資料的副本,持續同步到其他節點——讓別的機器也能保有一份最新資料。

⚖️
彈性 vs 開箱即用的取捨

資料庫把這些功能包好,你只要下一句宣告式查詢,優化器、索引、join、複寫全部自動幫你動起來——省心,但你管不到內部。拆開重組後,你可以自由挑最適合的工具(像用 mysql | elasticsearch 這樣的管線),但也要自己扛起「同步」與「一致性」的責任。天下沒有白吃的彈性。

拆解之後,「寫入」要怎麼同步到每個地方?

聯邦式資料庫解決了「統一讀」,但沒解決「跨系統同步寫」。作者認為傳統做法——跨系統的分散式交易——並不實際:不同團隊寫的系統之間根本沒有共同的交易協定,而且分散式交易本身容錯與效能都差。更務實的做法,是有序事件日誌加上 冪等消費者。按下一步,看一筆寫入怎麼安全地擴散到三個異質系統。

✍️
寫入事件
📜
有序事件日誌
🗄️
KV store
📊
分析系統
按「下一步」開始
🚚
鬆耦合的兩大好處

系統層面:非同步事件流讓故障被侷限,不會像分散式交易的同步互動那樣,把局部故障升級成大範圍故障。人的層面:不同團隊能各自獨立開發維護自己的元件——事件日誌的介面夠強(有持久性與順序)也夠通用,幾乎任何資料都適用。

程式碼和狀態,其實該分家

每一份衍生資料背後,都藏著一個 衍生函式 ——次要索引的轉換很標準化,所以被內建成 CREATE INDEX;但全文搜尋的語言處理、機器學習的特徵工程,往往高度應用相關,需要自訂程式碼,這正是傳統資料庫的弱項:trigger、stored procedure、UDF 雖然存在,卻一直不好用。

理論上資料庫可以像作業系統一樣執行任意應用程式碼,但實務上它們不擅長相依管理、版本控制、滾動升級、監控。反倒是 Kubernetes、Docker 這類工具,因為專注做一件事,把「跑應用程式碼」這件事做得好得多。

🧮
試算表早就會的事

1979 年的 VisiCalc 試算表裡,你在某格放一條公式,只要任何輸入變了,結果就自動重算。這正是資料系統理想的樣子:紀錄一變,它的索引就自動更新,依賴它的快取與聚合也自動刷新——但現實中的資料庫是被動的,像一個可變的共享變數,你只能不斷 poll(輪詢)去問它變了沒。

這種「訂閱變化、而非每次去問」的精神,放到真實場景裡是什麼樣子?來看一個經典案例:跨幣別購買。顧客用一種貨幣付款,買的是另一種貨幣計價的商品,需要即時匯率換算。這是 stream-table join 的經典場景,有兩種做法:

📞
做法一:同步 RPC

處理購買的程式,每次都跑去問匯率服務要「現在」的匯率。簡單直覺,但每次都是一次網路請求,也綁死在對方服務是否可用上。

📡
做法二:訂閱+本地表

事先訂閱匯率更新串流,每當匯率變就記進本地資料庫;處理購買時只查本地。最快、最可靠的網路請求,就是根本不發網路請求

別忘了時間依賴

這個 join 是時間相依的:若日後重新處理同一筆購買事件,匯率早就不同了。要重建當初的原始輸出,必須拿到「購買當下」的歷史匯率——不管你查服務還是訂閱串流,都得處理這個問題。大方向仍是:訂閱變化、而非每次查當前狀態,讓系統更接近試算表式的運算模型。

寫入路徑、讀取路徑:一條可以移動的界線

直接讀原文,這是本節最核心的一句總結:

原文 · DDIA Ch.12 Viewed like this, the role of caches, indexes, and materialized views is simple: they shift the boundary between the read path and the write path. They allow us to do more work on the write path, by precomputing results, in order to save effort on the read path.
白話翻譯

這樣看的話,快取、索引、物化視圖的角色很單純:它們移動了讀取路徑與寫入路徑之間的界線。

它們讓我們能在寫入時多做一點——事先算好結果——來省下讀取時的力氣。

資料的一生分兩段: 寫入路徑 是資料一進來就急切先做好的處理; 讀取路徑 是只有人來查才發生的處理。以搜尋為例:完全沒索引,寫入輕鬆、但每次查都要全掃;預先算好所有查詢結果,寫入工作量趨近無限、讀取幾乎零成本——物化視圖與快取,就落在這兩個極端之間。

🍱
便當店的比喻

完全現點現做:出餐慢,但不浪費。熱門便當先做好擺著:客人一點就拿,超快,冷門口味才現做。把菜單所有組合都先做好:不可能,口味組合無窮多。店長其實是在移動「事先做」與「現場做」的界線——名人帳號就像超人氣招牌便當,值得預先大量備好;冷門帳號現點現做就好。這呼應全書開頭 Twitter 的例子:界線可以為不同使用者畫在不同地方。

把寫入路徑一路推到你的手機螢幕

傳統假設是「客戶端無狀態、伺服器握有資料權威」。但今天的裝置能存大量狀態、甚至離線運作——換個角度看,裝置上的狀態,其實就是伺服器狀態的快取:螢幕上的像素是 model 的物化視圖,model 又是遠端資料中心狀態的本地副本。

傳統 web 只在載入那一刻讀了資料,之後資料變了要靠使用者重新整理才知道。server-sent eventsWebSocket 打破了這個假設,讓伺服器能主動把變化推給瀏覽器——用寫入/讀取路徑的語言說:這等於把寫入路徑一路延伸到終端使用者。裝置離線期間錯過的訊息,靠 consumer offset 這個老概念補回來——每個裝置都是一個小小的事件流訂閱者,重連後從書籤位置繼續讀。

作者把「整個組織的資料流」看成一個大資料庫,在這個比喻裡,batch 與 stream 處理器扮演什麼角色?
依本模組的說法,快取、索引、物化視圖共同扮演的角色是什麼?
下一站:追求正確性

把資料庫拆開重組後,正確性不再是資料庫一手包辦,我們得自己扛起責任。

03

追求正確性:在故障中守住資料

網路會斷、訊息會重送、磁碟會悄悄壞掉——這一站,我們去看系統怎麼在一團混亂裡,還能讓帳目對得起來

重送一次,就可能多扣一次款

訊息處理出錯時,你其實只有兩個選擇:放棄(丟掉訊息,資料遺失),或是重試(但搞不好第一次其實成功了,只是你沒收到回報,於是同一件事被做了兩次)。處理兩次,本身就是一種資料損毀——重複扣款、計數器多加一次,都是活生生的例子。

書裡把這個目標叫做 精確一次(exactly-once,也叫 effectively-once) ——不是「真的只執行一次」,而是「不管重試幾次,最後效果都像只做了一次」。最有效的做法,是讓操作 冪等(idempotent) ——但把一個天生不冪等的操作變冪等,需要用心維護額外資訊(例如記下處理過的操作 ID),還要處理節點切換時的邊界情況。

😵
教科書的轉帳例子,其實是錯的

轉帳 11 元、扣一邊加一邊,常被拿來示範交易的原子性。但如果客戶端送出 COMMIT 後、還沒收到回應就斷線,它根本不知道交易到底成功了沒。重連後重試,這筆非冪等的轉帳可能就變成轉了 22 元。

🔌
TCP 的去重,管不到這麼遠

TCP 靠序號去除重複封包——但這只在「同一條 TCP 連線」內有效。使用者手動重按送出,是全新的連線、全新的請求,早就跳出了 TCP 能罩到的範圍。

⚠️
故障永遠發生在你意想不到的那一層

資料庫交易保護得了資料庫、TCP 保護得了那條連線——但真正會讓使用者重複下單的地方,是「使用者本人」跟「應用伺服器」之間,沒有任何底層機制天生罩得住這一段。

直接讀原文:端到端論證是怎麼講的

這一段是本章、甚至整本書最重要的原則之一。左邊是原文,右邊是白話——一句對一句,慢慢讀。

原文 · DDIA Ch.12 The function in question can completely and correctly be implemented only with the knowledge and help of the application standing at the endpoints of the communication system. Therefore, providing that questioned function as a feature of the communication system itself is not possible. (Sometimes an incomplete version of the function provided by the communication system may be useful as a performance enhancement.) — Saltzer, Reed, and Clark (1984) By themselves, TCP, database transactions, and stream processors cannot entirely rule out these duplicates. Solving the problem requires an end-to-end solution: a transaction identifier that is passed all the way from the end-user client to the database.
白話翻譯

某個功能,只有在通訊「兩端的應用程式」都具備知識與協助的情況下,才能被完整、正確地實作出來。

因此,把這個功能做成通訊系統本身的一部分,是不可能的。

(底層機制提供的「不完整版本」,有時仍然有用——當作效能優化。)

光靠 TCP、資料庫交易、串流處理器,都沒辦法完全擋住這種重複。

要解決,得靠一個「端到端」的方案:一個從使用者端一路帶到資料庫的交易識別碼。

💡
翻成人話

去重、完整性檢查、加密——這些事只有「頭」跟「尾」兩端的應用程式最清楚該怎麼判斷,中間經過的網路、資料庫、訊息佇列頂多是幫忙降低出錯機率,但沒辦法單獨保證正確。

跑一遍:一次網路逾時的重送,怎麼被接住

手機訊號差,你按下「送出訂單」。轉圈圈太久,你又按了一次。跟著下面的角色,看端到端的操作 ID 怎麼在最後一刻攔住重複扣款。

📱
你(弱訊號手機)
⚙️
應用伺服器
🗄️
資料庫(唯一性約束)
按「下一步」開始這趟旅程
🔑
關鍵不在「防止重送」,而在「認出重複」

網路一定會逾時、使用者一定會重按——這些擋不住。真正有效的做法,是讓伺服器和資料庫「認得出」這兩個請求其實是同一件事,靠的就是那個從頭跟到尾的 操作識別碼

動手配配看:哪一層該負責防止重複?

下面幾種故障場景,各自該由哪一層機制負責擋住重複?拖拖看,配完按「對答案」。

TCP 序號去重
串流的 exactly-once 處理
端到端操作識別碼(應用層)
同一條連線內,網路重複送了一模一樣的封包
拖到這裡
串流處理器在故障後重新從 checkpoint 讀取,重複消費了同一則訊息
拖到這裡
使用者因為看不到回應,手動重按了一次「送出訂單」,形成全新的請求與連線
拖到這裡
🧩
底層機制不是沒用,是不夠

TCP 去重、串流的 exactly-once,都能降低問題出現的機率——但只要有一段路徑跳出了它們的管轄範圍(像使用者手動重按),就得靠應用層的端到端方案補上最後一哩。

搶同一個 username,誰說了算?

很多功能都需要 唯一性(uniqueness) :帳號名稱不能重複、同一個座位不能賣兩次、帳戶餘額不能變負的。在分散式環境裡,強制唯一性本質上需要 共識 ——因為一旦有多個並行請求帶著相同的值,總得有人拍板決定誰留下、誰被拒絕。

書裡給的做法很像「單行道收費亭」:把要求唯一的值(例如 username)雜湊後路由到同一個分區,由一個 單一物件寫入 天生原子的特性撐腰的串流處理器,單執行緒、依序處理這個分區——誰先到、誰就拿到這個名字,後到的一律被拒絕。要衝吞吐量,就多開幾條互不相干的「單行道」(增加分區)。

🚫
有一種複寫方式天生做不到唯一性

非同步多主(multi-master)複寫會被排除——因為不同的 master 可能並行接受了衝突的寫入,值就不再唯一了。想「立即拒絕」違反限制的寫入,同步協調無可避免。

💸
跨分區轉帳也能靠這招

轉帳 A→B 橫跨請求 ID、付款方、收款方三個分區。與其做昂貴的 原子提交,不如先把「請求」當一筆不可變事件寫進日誌(單一物件寫入天生原子),再從它衍生出扣款、入帳兩則訊息,下游各自依 request ID 去重套用——照樣正確,卻不必讓吞吐量為了跨分區協調而受害。

「一致性」裡藏著兩件很不一樣的事

作者認為「consistency」這個詞混淆了兩種該分開看的需求:

⏱️
即時性 Timeliness

你看到的是不是「最新」的?讀到舊資料只是暫時性的落後,等一下重讀通常就對了——就像餐廳上菜慢了五分鐘,惱人,但菜終究會來。

🧾
完整性 Integrity

帳目對不對得起來?遺失、矛盾、錢憑空消失,都是完整性被破壞——就像帳單金額跟你點的菜對不上,這不是等等就好,得有人查帳、修正。

📌
一句話記住差別

「違反即時性,是 eventual consistency(最終一致);違反完整性,是 perpetual inconsistency(永久不一致)。」前者等等就會自己好,後者不會——需要明確檢查與修復。作者更斷言:多數應用裡,完整性遠比即時性重要。

正因為完整性比即時性重要,很多業務其實能接受寬鬆一點的唯一性——先做、有問題再事後修正,這叫 補償交易。航空公司故意超賣機位,賭的就是總有人不會來,真的衝突了就退款、升等打發——這是一個商業決策,不是技術上的失敗。這樣的 避免協調的資料系統 ,用較弱的即時性換來高可用與高效能,卻依然守住完整性。

別只是相信,要親自查

硬體不完美:磁碟資料會靜靜損毀、記憶體會因輻射出現隨機翻轉位元、就連 MySQL、PostgreSQL 這樣身經百戰的資料庫,作者都親眼見過唯一性約束失守、隔離機制出錯。這叫我們對 系統模型 多一分警覺——「很少發生」不等於「絕不會發生」。

HDFS、S3 這類大型儲存系統的做法很像一個聰明的倉庫管理員:相信貨在架上,但仍然 定期親自盤點 ——背景程序持續讀回檔案、和其他副本比對,才能在損毀變嚴重前發現它。檢查資料完整性、找出並追查損毀源頭,這件事就叫 稽核

🔍
端到端的稽核,涵蓋越多越安心

把使用者輸入表示成一筆不可變事件、狀態由確定性的程式衍生而來(event sourcing),你就能重跑整條管線來驗證:拿同一串事件重算一次,看是不是得到一樣的結果。檢查涵蓋越多系統,損毀就越無處遁形——沿途的磁碟、網路、服務都隱含被驗證到了。

小試身手

這一站談的都是「怎麼在故障中守住正確性」,來檢查一下有沒有抓到重點。

教科書常用「轉帳 11 元」示範交易的原子性,但作者指出它其實不正確。為什麼?
情境

信用卡帳單上,24 小時內的交易還沒顯示出來;另一種情況是帳單餘額和交易總和對不上。依本節的區分,哪一種才是真正嚴重的問題?

🧭
下一站:做對的事

技術上,我們守住了正確性——重複的操作被認出來、唯一性被撐住、損毀能被稽核發現。但這是全書最後一站,也該談談技術之外的事:資料背後,是一個個真實的人。

04

做對的事:資料背後的人與責任

當演算法能決定誰能借到錢、誰能租到房、誰會被貼上「高風險」——工程師手上握的不只是程式碼,還有別人的人生

技術沒有善惡,用法才有

整本書走到這裡,我們談了可靠、可擴展、可維護的地基,談了資料模型、儲存引擎、複製、分區、交易、一致性,也談了批次與串流。但最後一章,作者把鏡頭轉向一個問題:這些系統最終服務的,是。每個系統都為某個目的而建,每個行動都有預期與非預期的後果——身為工程師,認真思考這些後果,是我們的責任。

先看 預測分析 的兩種臉孔。用資料預測天氣、預測疾病傳播是一回事;用來預測「某人是否會再犯罪」「是否會違約」「是否會提出昂貴的保險理賠」,是完全不同層次的事——後者直接影響一個人的人生。

🚦
組織的「有疑慮就說不」

錯失一筆生意,成本很低;放出一筆壞帳、雇到一個問題員工,成本高得多。所以組織天生傾向謹慎。但當這種「說不」被演算法大量複製,一個被(正確或錯誤地)貼上「高風險」標籤的人,可能同時被工作、旅行、保險、租屋、金融服務系統性地拒於門外——這被稱為 演算法監獄

原文最重的一句話:機器學習像替偏見洗錢

這一段是本章語氣最犀利的地方。左邊是原文,右邊是白話——你會看到作者怎麼一步步把「資料很客觀」這個常見迷思拆穿。

原文 · DDIA Ch.12 If there is a systematic bias in the input to an algorithm, the system will most likely learn and amplify that bias in its output. In racially segregated neighborhoods, a person's postal code or even their IP address is a strong predictor of race. It seems ridiculous to believe that an algorithm could somehow take biased data as input and produce fair and impartial output from it. Yet this belief often seems to be implied by proponents of data-driven decision making, an attitude that has been satirized as "machine learning is like money laundering for bias." Predictive analytics systems merely extrapolate from the past; if the past is discriminatory, they codify that discrimination. If we want the future to be better than the past, moral imagination is required, and that's something only humans can provide.
白話翻譯

如果演算法的輸入資料有系統性偏見,系統很可能會學習並放大那個偏見。

在種族隔離的社區,一個人的郵遞區號、甚至 IP 位址,都能強烈預測他的種族。

相信演算法能吃進帶偏見的資料、卻吐出公正無偏的結果,其實很荒謬。

但這種想法卻常隱含在「資料驅動決策」的擁護者身上,這種心態被諷刺為「機器學習就像替偏見洗錢」。

預測分析系統做的事,只是把過去外推到未來;如果過去是歧視性的,它就把那份歧視原封不動地編碼進去。

如果我們想要未來比過去更好,就需要「道德想像力」——而這是只有人類才能提供的東西。

💰
洗錢,洗不出乾淨的資料

髒錢進去、髒錢出來,就算多繞幾手也不會變乾淨——偏見資料也一樣。單純換一個模型、換一種演算法,不會讓輸出自動變公正。受保護特徵就算沒被直接使用,也可能被郵遞區號這類「代理變數」悄悄找回來。

只看後照鏡開車,會一路歪下去

書裡另一個生活化的畫面:預測分析像一個只盯著後照鏡開車的司機——他用「過去的路」決定「未來怎麼走」。如果過去那條路是歪的、坑坑疤疤的,他只會一路歪下去,因為他完全照著後方的軌跡前進,還披著「數學很客觀」的外衣。

更麻煩的是 回饋迴圈 ——書中舉了信用分數與就業的例子:一時的財務困難壓低信用分數,雇主用信用分數篩選求職者,分數低的人更難找到工作,失業又把人推向更深的貧窮,分數因此更慘。跟著下面的動畫走一遍這個下坡螺旋。

😟
財務困難
💳
信用紀錄變差
📉
信用分數下降
🚪
求職被拒
按「下一步」看這個惡性循環怎麼滾
🔁
回饋迴圈很難事先算準

但書中強調:把人也納入考量的 系統思考 能幫我們預見許多後果。該問的問題是:這個系統是在強化並放大人與人之間既有的差距(富者愈富、貧者愈貧),還是在對抗不公?

傷害有跡可循,因應原則也是

把前面談的攤開來看,預測分析可能造成的傷害,其實對應著幾條更負責任的設計原則。左邊是傷害,右邊是書中提到、能對症下藥的做法。

🧫
傷害:偏見放大

輸入資料若帶有系統性偏見,演算法很可能學習並放大它——就算不用受保護特徵,郵遞區號這類代理變數也能悄悄重現歧視。

🔁
傷害:回饋迴圈自我強化

分數下降、求職被拒、更加貧窮,再壓低分數——輸出回頭餵養下一輪輸入,弱勢者更難翻身,且藏在「數學很嚴謹」的外衣下。

🔢
傷害:把人化約成數字

預測分析看的常是「和你相似的人過去怎麼做」,等於用居住地、消費紀錄把人歸類、貼標籤——統計上正確不代表個案正確。

🙋
原則:讓被預測的人能申訴

人犯錯可被追責、受影響者可以申訴;演算法做錯決定時,也該留一條說明與救濟的路,而不是無聲無息地說「不」。

🔍
原則:審計演算法

若你的機器學習決策接受司法審查,你能不能向法官解釋演算法是怎麼做出這個決定的?可被檢視、可被解釋,是問責的前提。

🌱
原則:資料最小化

資料與模型該是工具,不是主人。用系統思考檢視系統是在放大差距還是對抗不公,並在收集資料時就克制、只留必要的。

把「資料」換成「監控」再唸一次

作者做了一個很挑釁的思想實驗:把常見的資料科學術語裡的「data」全部換成「surveillance(監控)」,再讀一次同一句話:

原文 · DDIA Ch.12 "In our surveillance-driven organization we collect real-time surveillance streams and store them in our surveillance warehouse. Our surveillance scientists use advanced analytics and surveillance processing in order to derive new insights." Even the most totalitarian and repressive regimes could only dream of putting a microphone in every room and forcing every person to constantly carry a device capable of tracking their location and movements. Yet we apparently voluntarily, even enthusiastically, throw ourselves into this world of total surveillance. The difference is just that the data is being collected by corporations rather than government agencies.
白話翻譯

「在我們『監控』驅動的組織,我們收集即時『監控』串流、存進『監控』倉儲。我們的『監控』科學家用進階分析與『監控』處理,得出新的洞見。」

連最極權、最壓迫的政權,都只能夢想在每個房間裝一支麥克風,逼每個人隨身攜帶能追蹤位置與行動的裝置。

但我們卻自願、甚至熱情地投入這個全面監控的世界。唯一的差別,只是資料收集者換成了企業,而不是政府機關。

聽起來毛骨悚然,對吧?並非所有資料收集都等於監控,但用「監控」這個詞重新檢視一次,能幫我們看清自己跟資料收集者之間,究竟是什麼關係。

🎡
免費遊樂園,你才是商品

想像一座「免費」遊樂園,門口寫著不用錢,但每個攤位都偷偷記下你走哪條路、在哪停留、買了什麼——再把這份「你的人生地圖」賣給想推銷的廠商。當服務靠廣告獲利,真正的顧客是廣告主,你以為自己是顧客,其實是商品。

隱私不是保密一切,資料是資產也是負債

有人說「隱私已死」,因為很多人什麼都往社群上貼。書裡認為這是誤解。

原文 · DDIA Ch.12 Having privacy does not mean keeping everything secret; it means having the freedom to choose which things to reveal to whom, what to make public, and what to keep secret. The right to privacy is a decision right: it enables each person to decide where they want to be on the spectrum between secrecy and transparency in each situation. When data is extracted from people through surveillance infrastructure, privacy rights are not necessarily eroded, but rather transferred to the data collector.
白話翻譯

擁有隱私,不代表把一切都藏起來;而是擁有自由,可以自己決定哪些事要揭露給誰、什麼要公開、什麼要保密。

隱私權是一種決定權:它讓每個人能自己決定,在每個情境裡,自己想站在「保密」與「透明」這條光譜的哪個位置。

當資料透過監控基礎設施被從人身上抽取出來時,隱私權不一定會消失,而是被轉移給了資料收集者。

換句話說, 隱私作為決定權 一旦被公司說「相信我們會妥善處理你的資料」拿走,「該揭露還是該保密」的決定權,就從你手上交到了公司手上。

而公司手上的這批行為資料,常被叫做「data exhaust(資料廢氣)」,好像是沒用的廢料。但如果它撐起了整套廣告獲利模式,它其實是核心資產——只是這個資產很難保全,會被駭客竊取、被內部人員外洩、甚至在公司易主或政權更迭時被迫交出。所以書裡說,資料不只是資產,更是 有毒資產

☣️
收集之前,先想想「落入壞人之手」

「安裝一套日後可能助長警察國家的技術,是很差的公民衛生習慣」——收集資料時,不只要衡量眼前的好處,還要考慮所有未來可能上台的政府。工程師能做的具體起手式:不必要的資料別留、不再需要就清除;用加密協定而不是只靠公司政策,去落實存取控制。

小試身手

這是全書最後一個模組的隨堂測驗,來兩題壓壓底。

「機器學習就像替偏見洗錢」這句諷刺,最想表達的核心是什麼?
本節主張「隱私不是保密一切」。那麼隱私最準確的定義是什麼?
🏁
全書收尾

這趟旅程,從一開始「可靠、可擴展、可維護」這三根支柱打地基,走到資料模型與儲存引擎怎麼把資料放好、複製與分區怎麼把系統撐大,再一頭栽進分散式系統裡種種不可靠與共識的難題,接著看批次與串流處理如何把資料流動起來——最後,我們把鏡頭轉回這一切背後真正的主角:。演算法會犯錯、資料會被濫用、隱私會被悄悄轉移,而工程師手上握的不只是程式碼,還有別人的人生。這不是「下一站」,而是這趟旅程真正的終點:技術終究要服務於人,而不是相反。