2
0
mirror of https://github.com/Vonng/ddia.git synced 2025-04-05 15:50:09 +08:00

translate

This commit is contained in:
afunTW 2020-10-06 09:25:46 +08:00
parent 9746baf3c0
commit ef5a627d73
13 changed files with 266 additions and 266 deletions

View File

@ -1,6 +1,6 @@
# 第一章:可靠性,可擴充套件性,可維護性
![](img/ch1.png)
![](../img/ch1.png)
> 網際網路做得太棒了,以至於大多數人將它看作像太平洋這樣的自然資源,而不是什麼人工產物。上一次出現這種大規模且無差錯的技術, 你還記得是什麼時候嗎?
>
@ -40,9 +40,9 @@
其次,越來越多的應用程式有著各種嚴格而廣泛的要求,單個工具不足以滿足所有的資料處理和儲存需求。取而代之的是,總體工作被拆分成一系列能被單個工具高效完成的任務,並透過應用程式碼將它們縫合起來。
例如如果將快取應用管理的快取層Memcached或同類產品和全文搜尋全文搜尋伺服器例如Elasticsearch或Solr功能從主資料庫剝離出來那麼使快取/索引與主資料庫保持同步通常是應用程式碼的責任。[圖1-1](img/fig1-1.png) 給出了這種架構可能的樣子(細節將在後面的章節中詳細介紹)。
例如如果將快取應用管理的快取層Memcached或同類產品和全文搜尋全文搜尋伺服器例如Elasticsearch或Solr功能從主資料庫剝離出來那麼使快取/索引與主資料庫保持同步通常是應用程式碼的責任。[圖1-1](../img/fig1-1.png) 給出了這種架構可能的樣子(細節將在後面的章節中詳細介紹)。
![](img/fig1-1.png)
![](../img/fig1-1.png)
**圖1-1 一個可能的組合使用多個元件的資料系統架構**
@ -174,7 +174,7 @@
大體上講,這一對操作有兩種實現方式。
1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如[圖1-2](img/fig1-2.png)所示的關係型資料庫中,可以編寫這樣的查詢:
1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如[圖1-2](../img/fig1-2.png)所示的關係型資料庫中,可以編寫這樣的查詢:
```sql
SELECT tweets.*, users.*
@ -183,13 +183,13 @@
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user
```
![](img/fig1-2.png)
![](../img/fig1-2.png)
**圖1-2 推特主頁時間線的關係型模式簡單實現**
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖1-3](img/fig1-3.png))。 當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。 因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖1-3](../img/fig1-3.png))。 當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。 因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
![](img/fig1-3.png)
![](../img/fig1-3.png)
**圖1-3 用於分發推特至關注者的資料流水線2012年11月的負載引數【16】**
@ -220,9 +220,9 @@
即使不斷重複傳送同樣的請求,每次得到的響應時間也都會略有不同。現實世界的系統會處理各式各樣的請求,響應時間可能會有很大差異。因此我們需要將響應時間視為一個可以測量的數值**分佈distribution**,而不是單個數值。
在[圖1-4](img/fig1-4.png)中每個灰條表代表一次對服務的請求其高度表示請求花費了多長時間。大多數請求是相當快的但偶爾會出現需要更長的時間的異常值。這也許是因為緩慢的請求實質上開銷更大例如它們可能會處理更多的資料。但即使你認為所有請求都花費相同時間的情況下隨機的附加延遲也會導致結果變化例如上下文切換到後臺程序網路資料包丟失與TCP重傳垃圾收集暫停強制從磁碟讀取的頁面錯誤伺服器機架中的震動【18】還有很多其他原因。
在[圖1-4](../img/fig1-4.png)中每個灰條表代表一次對服務的請求其高度表示請求花費了多長時間。大多數請求是相當快的但偶爾會出現需要更長的時間的異常值。這也許是因為緩慢的請求實質上開銷更大例如它們可能會處理更多的資料。但即使你認為所有請求都花費相同時間的情況下隨機的附加延遲也會導致結果變化例如上下文切換到後臺程序網路資料包丟失與TCP重傳垃圾收集暫停強制從磁碟讀取的頁面錯誤伺服器機架中的震動【18】還有很多其他原因。
![](img/fig1-4.png)
![](../img/fig1-4.png)
**圖1-4 展示了一個服務100次請求響應時間的均值與百分位數**
@ -232,7 +232,7 @@
如果想知道典型場景下使用者需要等待多長時間那麼中位數是一個好的度量標準一半使用者請求的響應時間少於響應時間的中位數另一半服務時間比中位數長。中位數也被稱為第50百分位點有時縮寫為p50。注意中位數是關於單個請求的如果使用者同時發出幾個請求在一個會話過程中或者由於一個頁面中包含了多個資源則至少一個請求比中位數慢的概率遠大於50
為了弄清異常值有多糟糕可以看看更高的百分位點例如第95、99和99.9百分位點縮寫為p95p99和p999。它們意味著9599或99.9的請求響應時間要比該閾值快例如如果第95百分位點響應時間是1.5秒則意味著100個請求中的95個響應時間快於1.5秒而100個請求中的5個響應時間超過1.5秒。如[圖1-4](img/fig1-4.png)所示。
為了弄清異常值有多糟糕可以看看更高的百分位點例如第95、99和99.9百分位點縮寫為p95p99和p999。它們意味著9599或99.9的請求響應時間要比該閾值快例如如果第95百分位點響應時間是1.5秒則意味著100個請求中的95個響應時間快於1.5秒而100個請求中的5個響應時間超過1.5秒。如[圖1-4](../img/fig1-4.png)所示。
響應時間的高百分位點(也稱為**尾部延遲tail latencies**非常重要因為它們直接影響使用者的服務體驗。例如亞馬遜在描述內部服務的響應時間要求時以99.9百分位點為準,即使它隻影響一千個請求中的一個。這是因為請求響應最慢的客戶往往也是資料最多的客戶,也可以說是最有價值的客戶 —— 因為他們掏錢了【19】。保證網站響應迅速對於保持客戶的滿意度非常重要亞馬遜觀察到響應時間增加100毫秒銷售量就減少1【20】而另一些報告說慢 1 秒鐘會讓客戶滿意度指標減少16%【2122】。
@ -246,13 +246,13 @@
> #### 實踐中的百分位點
>
> 在多重呼叫的後端服務裡,高百分位數變得特別重要。即使並行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。如[圖1-5](img/fig1-5.png)所示只需要一個緩慢的呼叫就可以使整個終端使用者請求變慢。即使只有一小部分後端呼叫速度較慢如果終端使用者請求需要多個後端呼叫則獲得較慢呼叫的機會也會增加因此較高比例的終端使用者請求速度會變慢效果稱為尾部延遲放大【24】
> 在多重呼叫的後端服務裡,高百分位數變得特別重要。即使並行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。如[圖1-5](../img/fig1-5.png)所示只需要一個緩慢的呼叫就可以使整個終端使用者請求變慢。即使只有一小部分後端呼叫速度較慢如果終端使用者請求需要多個後端呼叫則獲得較慢呼叫的機會也會增加因此較高比例的終端使用者請求速度會變慢效果稱為尾部延遲放大【24】
>
> 如果您想將響應時間百分點新增到您的服務的監視儀表板則需要持續有效地計算它們。例如您可能希望在最近10分鐘內保持請求響應時間的滾動視窗。每一分鐘您都會計算出該視窗中的中值和各種百分數並將這些度量值繪製在圖上。
>
> 簡單的實現是在時間視窗內儲存所有請求的響應時間列表並且每分鐘對列表進行排序。如果對你來說效率太低那麼有一些演算法能夠以最小的CPU和記憶體成本如前向衰減【25】t-digest【26】或HdrHistogram 【27】來計算百分位數的近似值。請注意平均百分比例如減少時間解析度或合併來自多臺機器的資料在數學上沒有意義 - 聚合響應時間資料的正確方法是新增直方圖【28】。
![](img/fig1-5.png)
![](../img/fig1-5.png)
**圖1-5 當一個請求需要多個後端請求時,單個後端慢請求就會拖慢整個終端使用者的請求**
@ -376,7 +376,7 @@
不幸的是,使應用可靠、可擴充套件或可維護並不容易。但是某些模式和技術會不斷重新出現在不同的應用中。在接下來的幾章中,我們將看到一些資料系統的例子,並分析它們如何實現這些目標。
在本書後面的[第三部分](part-iii.md)中,我們將看到一種模式:幾個元件協同工作以構成一個完整的系統(如[圖1-1](img/fig1-1.png)中的例子)
在本書後面的[第三部分](part-iii.md)中,我們將看到一種模式:幾個元件協同工作以構成一個完整的系統(如[圖1-1](../img/fig1-1.png)中的例子)

View File

@ -1,6 +1,6 @@
# 10. 批處理
![](img/ch10.png)
![](../img/ch10.png)
> 帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩定時,不同的人們以自己的方式進行測試,真正的考驗才開始。
>
@ -247,11 +247,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
在分散式計算中可以使用標準的Unix工具作為Mapper和Reducer【25】但更常見的是它們被實現為傳統程式語言的函式。在Hadoop MapReduce中Mapper和Reducer都是實現特定介面的Java類。在MongoDB和CouchDB中Mapper和Reducer都是JavaScript函式參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”)。
[圖10-1]()顯示了Hadoop MapReduce作業中的資料流。其並行化基於分割槽參見[第6章](ch6.md)作業的輸入通常是HDFS中的一個目錄輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽可以單獨處理map任務[圖10-1](img/fig10-1.png)中的m1m2和m3標記
[圖10-1]()顯示了Hadoop MapReduce作業中的資料流。其並行化基於分割槽參見[第6章](ch6.md)作業的輸入通常是HDFS中的一個目錄輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽可以單獨處理map任務[圖10-1](../img/fig10-1.png)中的m1m2和m3標記
每個輸入檔案的大小通常是數百兆位元組。 MapReduce排程器圖中未顯示試圖在其中一臺儲存輸入檔案副本的機器上執行每個Mapper只要該機器有足夠的備用RAM和CPU資源來執行Mapper任務【26】。這個原則被稱為**將計算放在資料附近**【27】它節省了透過網路複製輸入檔案的開銷減少網路負載並增加區域性性。
![](img/fig10-1.png)
![](../img/fig10-1.png)
**圖10-1 具有三個Mapper和三個Reducer的MapReduce任務**
@ -297,9 +297,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
#### 示例:分析使用者活動事件
[圖10-2](img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件activity events**或**點選流資料clickstream data**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件activity events**或**點選流資料clickstream data**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
![](img/fig10-2.png)
![](../img/fig10-2.png)
**圖10-2 使用者行為日誌與使用者檔案的連線**
@ -313,9 +313,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
#### 排序合併連線
回想一下Mapper的目的是從每個輸入記錄中提取一對鍵值。在[圖10-2](img/fig10-2.png)的情況下這個鍵就是使用者ID一組Mapper會掃過活動事件提取使用者ID作為鍵活動事件作為值而另一組Mapper將會掃過使用者資料庫提取使用者ID作為鍵使用者的出生日期作為值。這個過程如[圖10-3](img/fig10-3.png)所示。
回想一下Mapper的目的是從每個輸入記錄中提取一對鍵值。在[圖10-2](../img/fig10-2.png)的情況下這個鍵就是使用者ID一組Mapper會掃過活動事件提取使用者ID作為鍵活動事件作為值而另一組Mapper將會掃過使用者資料庫提取使用者ID作為鍵使用者的出生日期作為值。這個過程如[圖10-3](../img/fig10-3.png)所示。
![](img/fig10-3.png)
![](../img/fig10-3.png)
**圖10-3 在使用者ID上進行的Reduce端連線。如果輸入資料集分割槽為多個檔案則每個分割槽都會被多個Mapper並行處理**
@ -375,11 +375,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
適用於執行Map端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小以便可以將其全部載入到每個Mapper的記憶體中。
例如,假設在[圖10-2](img/fig10-2.png)的情況下使用者資料庫小到足以放進記憶體中。在這種情況下當Mapper啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的雜湊中。完成此操作後Map程式可以掃描使用者活動事件並簡單地在散列表中查詢每個事件的使用者ID[^vi]。
例如,假設在[圖10-2](../img/fig10-2.png)的情況下使用者資料庫小到足以放進記憶體中。在這種情況下當Mapper啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的雜湊中。完成此操作後Map程式可以掃描使用者活動事件並簡單地在散列表中查詢每個事件的使用者ID[^vi]。
[^vi]: 這個例子假定散列表中的每個鍵只有一個條目這對使用者資料庫使用者ID唯一標識一個使用者可能是正確的。通常雜湊表可能需要包含具有相同鍵的多個條目而連線運算子將對每個鍵輸出所有的匹配。
參與連線的較大輸入的每個檔案塊各有一個Mapper在[圖10-2](img/fig10-2.png)的例子中活動事件是較大的輸入。每個Mapper都會將較小輸入整個載入到記憶體中。
參與連線的較大輸入的每個檔案塊各有一個Mapper在[圖10-2](../img/fig10-2.png)的例子中活動事件是較大的輸入。每個Mapper都會將較小輸入整個載入到記憶體中。
這種簡單有效的演算法被稱為**廣播雜湊連線broadcast hash join****廣播**一詞反映了這樣一個事實每個連線較大輸入端分割槽的Mapper都會將較小輸入端資料集整個讀入記憶體中所以較小輸入實際上“廣播”到較大資料的所有分割槽上**雜湊**一詞反映了它使用一個散列表。 Pig名為“**複製連結replicated join**”Hive“**MapJoin**”Cascading和Crunch支援這種連線。它也被諸如Impala的資料倉庫查詢引擎使用【41】。
@ -387,7 +387,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
#### 分割槽雜湊連線
如果Map端連線的輸入以相同的方式進行分割槽則雜湊連線方法可以獨立應用於每個分割槽。在[圖10-2](img/fig10-2.png)的情況中你可以根據使用者ID的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽因此連線兩側各有10個分割槽。例如Mapper3首先將所有具有以3結尾的ID的使用者載入到散列表中然後掃描ID為3的每個使用者的所有活動事件。
如果Map端連線的輸入以相同的方式進行分割槽則雜湊連線方法可以獨立應用於每個分割槽。在[圖10-2](../img/fig10-2.png)的情況中你可以根據使用者ID的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽因此連線兩側各有10個分割槽。例如Mapper3首先將所有具有以3結尾的ID的使用者載入到散列表中然後掃描ID為3的每個使用者的所有活動事件。
如果分割槽正確無誤可以確定的是所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個Mapper只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個Mapper都可以在記憶體散列表中少放點資料。
@ -612,7 +612,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
> 像SparkFlink和Tez這樣的資料流引擎參見“[中間狀態的物化](#中間狀態的物化)”)通常將運算元作為**有向無環圖DAG**的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**閉包傳遞transitive closure**)。
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**閉包傳遞transitive closure**)。
可以在分散式檔案系統中儲存圖包含頂點和邊的列表的檔案但是這種“重複至完成”的想法不能用普通的MapReduce來表示因為它只掃過一趟資料。這種演算法因此經常以**迭代**的風格實現:

View File

@ -1,6 +1,6 @@
# 11. 流處理
![](img/ch11.png)
![](../img/ch11.png)
> 有效的複雜系統總是從簡單的系統演化而來。 反之亦然:從零設計的複雜系統沒一個能有效工作的。
>
@ -94,7 +94,7 @@
#### 多個消費者
當多個消費者從同一主題中讀取訊息時,有使用兩種主要的訊息傳遞模式,如[圖11-1](img/fig11-1.png)所示:
當多個消費者從同一主題中讀取訊息時,有使用兩種主要的訊息傳遞模式,如[圖11-1](../img/fig11-1.png)所示:
***負載均衡load balance***
@ -104,7 +104,7 @@
每條訊息都被傳遞給**所有**消費者。扇出允許幾個獨立的消費者各自“收聽”相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 JMS中的主題訂閱與AMQP中的交叉繫結提供了這一功能
![](img/fig11-1.png)
![](../img/fig11-1.png)
**圖11-1 a負載平衡在消費者間共享消費主題b扇出將每條訊息傳遞給多個消費者。**
@ -116,9 +116,9 @@
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。 (請注意可能發生這樣的情況,訊息**實際上是**處理完畢的,但**確認**在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”中所討論的那樣)
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖11-2](img/fig11-2.png)中消費者通常按照生產者傳送的順序處理訊息。然而消費者2在處理訊息m3時崩潰與此同時消費者1正在處理訊息m4。未確認的訊息m3隨後被重新發送給消費者1結果消費者1按照m4m3m5的順序處理訊息。因此m3和m4的交付順序與以生產者1的傳送順序不同。
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖11-2](../img/fig11-2.png)中消費者通常按照生產者傳送的順序處理訊息。然而消費者2在處理訊息m3時崩潰與此同時消費者1正在處理訊息m4。未確認的訊息m3隨後被重新發送給消費者1結果消費者1按照m4m3m5的順序處理訊息。因此m3和m4的交付順序與以生產者1的傳送順序不同。
![](img/fig11-2.png)
![](../img/fig11-2.png)
**圖11-2 在處理m3時消費者2崩潰因此稍後重傳至消費者1**
@ -142,11 +142,11 @@
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix工具`tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
為了擴充套件到比單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(在[第6章](ch6.md)的意義上)。不同的分割槽可以託管在不同的機器上,且每個分割槽都拆分出一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](img/fig11-3.png)所示。
為了擴充套件到比單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(在[第6章](ch6.md)的意義上)。不同的分割槽可以託管在不同的機器上,且每個分割槽都拆分出一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或**偏移量offset**(在[圖11-3](img/fig11-3.png)中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或**偏移量offset**(在[圖11-3](../img/fig11-3.png)中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
![](img/fig11-3.png)
![](../img/fig11-3.png)
**圖11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
@ -223,9 +223,9 @@
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是**雙寫dual write**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。
但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如[圖11-4](img/fig11-4.png)所示。在這個例子中兩個客戶端同時想要更新一個專案X客戶端1想要將值設定為A客戶端2想要將其設定為B。兩個客戶端首先將新值寫入資料庫然後將其寫入到搜尋索引。因為運氣不好這些請求的時序是交錯的資料庫首先看到來自客戶端1的寫入將值設定為A然後來自客戶端2的寫入將值設定為B因此資料庫中的最終值為B。搜尋索引首先看到來自客戶端2的寫入然後是客戶端1的寫入所以搜尋索引中的最終值是A。即使沒發生錯誤這兩個系統現在也永久地不一致了。
但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如[圖11-4](../img/fig11-4.png)所示。在這個例子中兩個客戶端同時想要更新一個專案X客戶端1想要將值設定為A客戶端2想要將其設定為B。兩個客戶端首先將新值寫入資料庫然後將其寫入到搜尋索引。因為運氣不好這些請求的時序是交錯的資料庫首先看到來自客戶端1的寫入將值設定為A然後來自客戶端2的寫入將值設定為B因此資料庫中的最終值為B。搜尋索引首先看到來自客戶端2的寫入然後是客戶端1的寫入所以搜尋索引中的最終值是A。即使沒發生錯誤這兩個系統現在也永久地不一致了。
![](img/fig11-4.png)
![](../img/fig11-4.png)
**圖11-4 在資料庫中X首先被設定為A然後被設定為B而在搜尋索引處寫入以相反的順序到達**
@ -233,7 +233,7 @@
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(參閱“[原子提交和兩階段提交2PC](ch7.md#原子提交和兩階段提交2PC)”)。
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(參見“[多領導者複製](ch5.md#多領導者複製)“)。
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(參見“[多領導者複製](ch5.md#多領導者複製)“)。
如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎?
@ -245,9 +245,9 @@
最近,人們對**變更資料捕獲change data capture, CDC** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。 CDC是非常有意思的尤其是當變更能在被寫入後立刻用於流時。
例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如[圖11-5](img/fig11-5.png)所示。
例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如[圖11-5](../img/fig11-5.png)所示。
![](img/fig11-5.png)
![](../img/fig11-5.png)
**圖11-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統**
@ -255,7 +255,7 @@
我們可以將日誌消費者叫做**衍生資料系統**,正如在第三部分的[介紹](part-iii.md)中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是**記錄系統**資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了[圖11-2](img/fig11-2.png)的重新排序問題)。
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了[圖11-2](../img/fig11-2.png)的重新排序問題)。
資料庫觸發器可用來實現變更資料捕獲(參閱“[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如應對模式變更。
@ -275,7 +275,7 @@
如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但**日誌壓縮log compaction** 提供了一個很好的備選方案。
我們之前在日誌結構儲存引擎的上下文中討論了“[Hash索引](ch3.md#Hash索引)”中的日誌壓縮(參見[圖3-2](img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
我們之前在日誌結構儲存引擎的上下文中討論了“[Hash索引](ch3.md#Hash索引)”中的日誌壓縮(參見[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
在日誌結構儲存引擎中具有特殊值NULL**墓碑tombstone**)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
@ -299,7 +299,7 @@
與變更資料捕獲類似,事件溯源涉及到**將所有對應用狀態的變更** 儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了幾個不同的抽象層次上:
* 在變更資料捕獲中,應用以**可變方式mutable way** 使用資料庫,任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
* 在變更資料捕獲中,應用以**可變方式mutable way** 使用資料庫,任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
* 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。
事件源是一種強大的資料建模技術從應用的角度來看將使用者的行為記錄為不可變的事件更有意義而不是在可變資料庫中記錄這些行為的影響。事件代理使得應用隨時間演化更為容易透過事實更容易理解事情發生的原因使得除錯更為容易並有利於防止應用Bug請參閱“[不可變事件的優點](#不可變事件的優點)”)。
@ -345,12 +345,12 @@
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌—— **變化日誌change log**,表示了隨時間演變的狀態。
如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如[圖11-6](img/fig11-6.png)所示【49,50,51】。這個比喻有一些侷限性例如狀態的二階導似乎沒有意義但這是考慮資料的一個實用出發點。
如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如[圖11-6](../img/fig11-6.png)所示【49,50,51】。這個比喻有一些侷限性例如狀態的二階導似乎沒有意義但這是考慮資料的一個實用出發點。
$$
state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\
stream(t) = \frac{d\ state(t)}{dt}
$$
![](img/fig11-6.png)
![](../img/fig11-6.png)
**圖11-6 應用當前狀態與事件流之間的關係**
@ -372,7 +372,7 @@ $$
#### 從同一事件日誌中派生多個檢視
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖11-5](img/fig11-5.png)例如分析型資料庫Druid使用這種方式直接從Kafka攝取資料【55】Pistachio是一個分散式的鍵值儲存使用Kafka作為提交日誌【56】Kafka Connect能將來自Kafka的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統如搜尋伺服器來說是很有意義的當系統要從分散式日誌中獲取輸入時亦然參閱“[保持系統同步](#保持系統同步)”)。
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖11-5](../img/fig11-5.png)例如分析型資料庫Druid使用這種方式直接從Kafka攝取資料【55】Pistachio是一個分散式的鍵值儲存使用Kafka作為提交日誌【56】Kafka Connect能將來自Kafka的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統如搜尋伺服器來說是很有意義的當系統要從分散式日誌中獲取輸入時亦然參閱“[保持系統同步](#保持系統同步)”)。
新增從事件日誌到資料庫的顯式轉換能夠使應用更容易地隨時間演進如果你想要引入一個新功能以新的方式表示現有資料則可以使用事件日誌來構建一個單獨的針對新功能的讀取最佳化檢視無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行復雜的模式遷移更容易。一旦不再需要舊的系統你可以簡單地關閉它並回收其資源【47,57】。
@ -412,7 +412,7 @@ $$
剩下的就是討論一下你可以用流做什麼 —— 也就是說,你可以處理它。一般來說,有三種選項:
1. 你可以將事件中的資料寫入資料庫,快取,搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如[圖11-5](img/fig11-5.png)所示,這是資料庫與系統其他部分發生變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中所討論的,它是寫入儲存系統的流等價物。
1. 你可以將事件中的資料寫入資料庫,快取,搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如[圖11-5](../img/fig11-5.png)所示,這是資料庫與系統其他部分發生變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中所討論的,它是寫入儲存系統的流等價物。
2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可實時顯示的儀表板上。在這種情況下,人是流的最終消費者。
3. 你可以處理一個或多個輸入流併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線最後再輸出選項1或2
@ -507,9 +507,9 @@ $$
[^ii]: 感謝Flink社群的Kostas Kloudas提出這個比喻。
將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖11-7](img/fig11-7.png))。
將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖11-7](../img/fig11-7.png))。
![](img/fig11-7.png)
![](../img/fig11-7.png)
**圖11-7 按處理時間分窗,會因為處理速率的變動引入人為因素**
@ -580,7 +580,7 @@ $$
#### 流表連線(流擴充套件)
在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖10-2](img/fig10-2.png)我們看到了連線兩個資料集的批處理作業示例一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流並在流處理器中連續執行相同的連線是很自然的想法輸入是包含使用者ID的活動事件流而輸出還是活動事件流但其中使用者ID已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為 使用資料庫的資訊來**擴充enriching** 活動事件。
在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖10-2](../img/fig10-2.png)我們看到了連線兩個資料集的批處理作業示例一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流並在流處理器中連續執行相同的連線是很自然的想法輸入是包含使用者ID的活動事件流而輸出還是活動事件流但其中使用者ID已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為 使用資料庫的資訊來**擴充enriching** 活動事件。
要執行此聯接流處理器需要一次處理一個活動事件在資料庫中查詢事件的使用者ID並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在“[示例:分析使用者活動事件](ch10.md#示例:分析使用者活動事件)”一節中討論的此類遠端查詢可能會很慢並且有可能導致資料庫過載【75】。
@ -615,7 +615,7 @@ GROUP BY follows.follower_id
流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當基礎表發生變化時都會更新[^iii]。
[^iii]: 如果你將流視作表的衍生物,如[圖11-6](img/fig11-6.png)所示而把一個連線看作是兩個表的乘法u·v那麼會發生一些有趣的事情物化連線的變化流遵循乘積法則(u·v)'= u'v + uv'(u·v)'= u'v + uv'。 換句話說任何推文的變化量都與當前的關注聯絡在一起任何關注的變化量都與當前的推文相連線【49,50】。
[^iii]: 如果你將流視作表的衍生物,如[圖11-6](../img/fig11-6.png)所示而把一個連線看作是兩個表的乘法u·v那麼會發生一些有趣的事情物化連線的變化流遵循乘積法則(u·v)'= u'v + uv'(u·v)'= u'v + uv'。 換句話說任何推文的變化量都與當前的關注聯絡在一起任何關注的變化量都與當前的推文相連線【49,50】。
#### 連線的時間依賴性

View File

@ -1,6 +1,6 @@
# 12. 資料系統的未來
![](img/ch12.png)
![](../img/ch12.png)
> 如果船長的終極目標是保護船隻,他應該永遠待在港口。
>
@ -42,7 +42,7 @@
例如,你可能會首先將資料寫入**記錄資料庫**系統,捕獲對該資料庫所做的變更(參閱“[捕獲資料變更](ch11.md#捕獲資料變更)”然後將變更應用於資料庫中的搜尋索引相同的順序。如果變更資料捕獲CDC是更新索引的唯一方式則可以確定該索引完全派生自記錄系統因此與其保持一致除軟體錯誤外。寫入資料庫是向該系統提供新輸入的唯一方式。
允許應用程式直接寫入搜尋索引和資料庫引入瞭如[圖11-4](img/fig11-4.png)所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
允許應用程式直接寫入搜尋索引和資料庫引入瞭如[圖11-4](../img/fig11-4.png)所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
如果您可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在“[全序廣播](ch9.md#全序廣播)”中看到。無論您使用變更資料捕獲還是事件源日誌,都不如僅對全域性順序達成共識更重要。
@ -328,9 +328,9 @@
### 觀察衍生資料狀態
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引,物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為**寫路徑write path**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖12-1](img/fig12-1.png)顯示了一個更新搜尋索引的例子。
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引,物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為**寫路徑write path**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖12-1](../img/fig12-1.png)顯示了一個更新搜尋索引的例子。
![](img/fig12-1.png)
![](../img/fig12-1.png)
**圖12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)**
@ -338,7 +338,7 @@
總而言之,寫路徑和讀路徑涵蓋了資料的整個旅程,從收集資料開始,到使用資料結束(可能是由另一個人)。寫路徑是預計算過程的一部分 —— 即,一旦資料進入,即刻完成,無論是否有人需要看它。讀路徑是這個過程中只有當有人請求時才會發生的部分。如果你熟悉函數語言程式設計語言,則可能會注意到寫路徑類似於立即求值,讀路徑類似於惰性求值。
如[圖12-1](img/fig12-1.png)所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。
如[圖12-1](../img/fig12-1.png)所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。
#### 物化檢視和快取
@ -454,7 +454,7 @@
除了流處理之外其他許多地方也需要抑制重複的模式。例如TCP使用資料包上的序列號在接收方將它們正確排序。並確定網路上是否有資料包丟失或重複。任何丟失的資料包都會被重新傳輸而在將資料交付應用前TCP協議棧會移除任何重複資料包。
但是這種重複抑制僅適用於單條TCP連線的場景中。假設TCP連線是一個客戶端與資料庫的連線並且它正在執行[例12-1]()中的事務。在許多資料庫中事務是繫結在客戶端連線上的如果客戶端傳送了多個查詢資料庫就知道它們屬於同一個事務因為它們是在同一個TCP連線上傳送的。如果客戶端在傳送`COMMIT`之後但在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖8-1](img/fig8-1.png))。
但是這種重複抑制僅適用於單條TCP連線的場景中。假設TCP連線是一個客戶端與資料庫的連線並且它正在執行[例12-1]()中的事務。在許多資料庫中事務是繫結在客戶端連線上的如果客戶端傳送了多個查詢資料庫就知道它們屬於同一個事務因為它們是在同一個TCP連線上傳送的。如果客戶端在傳送`COMMIT`之後但在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖8-1](../img/fig8-1.png))。
**例12-1 資金從一個賬戶到另一個賬戶的非冪等轉移**

View File

@ -1,6 +1,6 @@
# 2. 資料模型與查詢語言
![](img/ch2.png)
![](../img/ch2.png)
> 語言的邊界就是思想的邊界。
>
@ -65,13 +65,13 @@
像ActiveRecord和Hibernate這樣的 **物件關係對映ORM object-relational mapping** 框架可以減少這個轉換層所需的樣板程式碼的數量,但是它們不能完全隱藏這兩個模型之間的差異。
![](img/fig2-1.png)
![](../img/fig2-1.png)
**圖2-1 使用關係型模式來表示領英簡介**
例如,[圖2-1](img/fig2-1.png)展示瞭如何在關係模式中表示簡歷一個LinkedIn簡介。整個簡介可以透過一個唯一的識別符號`user_id`來標識。像`first_name`和`last_name`這樣的欄位每個使用者只出現一次所以可以在User表上將其建模為列。但是大多數人在職業生涯中擁有多於一份的工作人們可能有不同樣的教育階段和任意數量的聯絡資訊。從使用者到這些專案之間存在一對多的關係可以用多種方式來表示
例如,[圖2-1](../img/fig2-1.png)展示瞭如何在關係模式中表示簡歷一個LinkedIn簡介。整個簡介可以透過一個唯一的識別符號`user_id`來標識。像`first_name`和`last_name`這樣的欄位每個使用者只出現一次所以可以在User表上將其建模為列。但是大多數人在職業生涯中擁有多於一份的工作人們可能有不同樣的教育階段和任意數量的聯絡資訊。從使用者到這些專案之間存在一對多的關係可以用多種方式來表示
* 傳統SQL模型SQL1999之前最常見的規範化表示形式是將職位教育和聯絡資訊放在單獨的表中對User表提供外來鍵引用如[圖2-1](img/fig2-1.png)所示。
* 傳統SQL模型SQL1999之前最常見的規範化表示形式是將職位教育和聯絡資訊放在單獨的表中對User表提供外來鍵引用如[圖2-1](../img/fig2-1.png)所示。
* 後續的SQL標準增加了對結構化資料型別和XML資料的支援;這允許將多值資料儲存在單行內並支援在這些文件內查詢和索引。這些功能在OracleIBM DB2MS SQL Server和PostgreSQL中都有不同程度的支援【6,7】。JSON資料型別也得到多個數據庫的支援包括IBM DB2MySQL和PostgreSQL 【8】。
* 第三種選擇是將職業教育和聯絡資訊編碼為JSON或XML文件將其儲存在資料庫的文字列中並讓應用程式解析其結構和內容。這種配置下通常不能使用資料庫來查詢該編碼列中的值。
@ -119,11 +119,11 @@
有一些開發人員認為JSON模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。不過正如我們將在[第4章](ch4.md)中看到的那樣JSON作為資料編碼格式也存在問題。缺乏一個模式往往被認為是一個優勢;我們將在“[文件模型中的模式靈活性](#文件模型中的模式靈活性)”中討論這個問題。
JSON表示比[圖2-1](img/fig2-1.png)中的多表模式具有更好的**區域性性locality**。如果在前面的關係型示例中獲取簡介,那需要執行多個查詢(透過`user_id`查詢每個表或者在User表與其下屬表之間混亂地執行多路連線。而在JSON表示中所有相關資訊都在同一個地方一個查詢就足夠了。
JSON表示比[圖2-1](../img/fig2-1.png)中的多表模式具有更好的**區域性性locality**。如果在前面的關係型示例中獲取簡介,那需要執行多個查詢(透過`user_id`查詢每個表或者在User表與其下屬表之間混亂地執行多路連線。而在JSON表示中所有相關資訊都在同一個地方一個查詢就足夠了。
從使用者簡介檔案到使用者職位教育歷史和聯絡資訊這種一對多關係隱含了資料中的一個樹狀結構而JSON表示使得這個樹狀結構變得明確見[圖2-2](img/fig2-2.png))。
從使用者簡介檔案到使用者職位教育歷史和聯絡資訊這種一對多關係隱含了資料中的一個樹狀結構而JSON表示使得這個樹狀結構變得明確見[圖2-2](../img/fig2-2.png))。
![](img/fig2-2.png)
![](../img/fig2-2.png)
**圖2-2 一對多關係構建了一個樹結構**
@ -157,18 +157,18 @@ JSON表示比[圖2-1](img/fig2-1.png)中的多表模式具有更好的**區域
***組織和學校作為實體***
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(參見[圖2-3](img/fig2-3.png)來自LinkedIn的一個例子
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(參見[圖2-3](../img/fig2-3.png)來自LinkedIn的一個例子
***推薦***
假設你想新增一個新的功能:一個使用者可以為另一個使用者寫一個推薦。在使用者的簡歷上顯示推薦,並附上推薦使用者的姓名和照片。如果推薦人更新他們的照片,那他們寫的任何建議都需要顯示新的照片。因此,推薦應該擁有作者個人簡介的引用。
![](img/fig2-3.png)
![](../img/fig2-3.png)
**圖2-3 公司名不僅是字串還是一個指向公司實體的連結LinkedIn截圖**
[圖2-4](img/fig2-4.png)闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。
[圖2-4](../img/fig2-4.png)闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。
![](img/fig2-4.png)
![](../img/fig2-4.png)
**圖2-4 使用多對多關係擴充套件簡歷**
@ -178,7 +178,7 @@ JSON表示比[圖2-1](img/fig2-1.png)中的多表模式具有更好的**區域
20世紀70年代最受歡迎的業務資料處理資料庫是IBM的資訊管理系統IMS最初是為了阿波羅太空計劃的庫存管理而開發的並於1968年有了首次商業釋出【13】。目前它仍在使用和維護執行在IBM大型機的OS/390上【14】。
IMS的設計中使用了一個相當簡單的資料模型稱為**層次模型hierarchical model**它與文件資料庫使用的JSON模型有一些驚人的相似之處【2】。它將所有資料表示為巢狀在記錄中的記錄樹這很像[圖2-2](img/fig2-2.png)的JSON結構。
IMS的設計中使用了一個相當簡單的資料模型稱為**層次模型hierarchical model**它與文件資料庫使用的JSON模型有一些驚人的相似之處【2】。它將所有資料表示為巢狀在記錄中的記錄樹這很像[圖2-2](../img/fig2-2.png)的JSON結構。
同文檔資料庫一樣IMS能良好處理一對多的關係但是很難應對多對多的關係並且不支援連線。開發人員必須決定是否複製非規範化資料或手動解決從一個記錄到另一個記錄的引用。這些二十世紀六七十年代的問題與現在開發人員遇到的文件資料庫問題非常相似【15】。
@ -226,7 +226,7 @@ CODASYL中的查詢是透過利用遍歷記錄列和跟隨訪問路徑表在資
#### 哪個資料模型更方便寫程式碼?
如果應用程式中的資料具有類似文件的結構(即,一對多關係樹,通常一次性載入整個樹),那麼使用文件模型可能是一個好主意。將類似文件的結構分解成多個表(如[圖2-1](img/fig2-1.png)中的`positions``education`和`contact_info`)的關係技術可能導致繁瑣的模式和不必要的複雜的應用程式程式碼。
如果應用程式中的資料具有類似文件的結構(即,一對多關係樹,通常一次性載入整個樹),那麼使用文件模型可能是一個好主意。將類似文件的結構分解成多個表(如[圖2-1](../img/fig2-1.png)中的`positions``education`和`contact_info`)的關係技術可能導致繁瑣的模式和不必要的複雜的應用程式程式碼。
文件模型有一定的侷限性例如不能直接引用文件中的巢狀的專案而是需要說“使用者251的位置列表中的第二項”很像分層模型中的訪問路徑。但是只要檔案巢狀不太深這通常不是問題。
@ -274,7 +274,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
#### 查詢的資料區域性性
文件通常以單個連續字串形式進行儲存編碼為JSONXML或其二進位制變體如MongoDB的BSON。如果應用程式經常需要訪問整個文件例如將其渲染至網頁那麼儲存區域性性會帶來效能優勢。如果將資料分割到多個表中如[圖2-1](img/fig2-1.png)所示),則需要進行多次索引查詢才能將其全部檢索出來,這可能需要更多的磁碟查詢並花費更多的時間。
文件通常以單個連續字串形式進行儲存編碼為JSONXML或其二進位制變體如MongoDB的BSON。如果應用程式經常需要訪問整個文件例如將其渲染至網頁那麼儲存區域性性會帶來效能優勢。如果將資料分割到多個表中如[圖2-1](../img/fig2-1.png)所示),則需要進行多次索引查詢才能將其全部檢索出來,這可能需要更多的磁碟查詢並花費更多的時間。
區域性性僅僅適用於同時需要文件絕大部分內容的情況。資料庫通常需要載入整個文件即使只訪問其中的一小部分這對於大型文件來說是很浪費的。更新文件時通常需要整個重寫。只有不改變文件大小的修改才可以容易地原地執行。因此通常建議保持相對小的文件並避免增加文件大小的寫入【9】。這些效能限制大大減少了文件資料庫的實用場景。
@ -533,9 +533,9 @@ db.observations.aggregate([
在剛剛給出的例子中圖中的所有頂點代表了相同型別的事物網頁或交叉路口。不過圖並不侷限於這樣的同類資料同樣強大地是圖提供了一種一致的方式用來在單個數據儲存中儲存完全不同型別的物件。例如Facebook維護一個包含許多不同型別的頂點和邊的單個圖頂點表示人地點事件簽到和使用者的評論;邊緣表示哪些人是彼此的朋友哪個簽到發生在何處誰評論了哪條訊息誰參與了哪個事件等等【35】。
在本節中,我們將使用[圖2-5](img/fig2-5.png)所示的示例。它可以從社交網路或系譜資料庫中獲得它顯示了兩個人來自愛達荷州的Lucy和來自法國Beaune的Alain。他們已婚住在倫敦。
在本節中,我們將使用[圖2-5](../img/fig2-5.png)所示的示例。它可以從社交網路或系譜資料庫中獲得它顯示了兩個人來自愛達荷州的Lucy和來自法國Beaune的Alain。他們已婚住在倫敦。
![](img/fig2-5.png)
![](../img/fig2-5.png)
**圖2-5 圖資料結構示例(框代表頂點,箭頭代表邊)**
@ -586,7 +586,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
2. 給定任何頂點,可以高效地找到它的入邊和出邊,從而遍歷圖,即沿著一系列頂點的路徑前後移動。(這就是為什麼[例2-2]()在`tail_vertex`和`head_vertex`列上都有索引的原因。)
3. 透過對不同型別的關係使用不同的標籤,可以在一個圖中儲存幾種不同的資訊,同時仍然保持一個清晰的資料模型。
這些特性為資料建模提供了很大的靈活性,如[圖2-5](img/fig2-5.png)所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和州美國有不同的州和州國中國的怪事先忽略主權國家和國家錯綜複雜的爛攤子不同的資料粒度Lucy現在的住所被指定為一個城市而她的出生地點只是在一個州的級別
這些特性為資料建模提供了很大的靈活性,如[圖2-5](../img/fig2-5.png)所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和州美國有不同的州和州國中國的怪事先忽略主權國家和國家錯綜複雜的爛攤子不同的資料粒度Lucy現在的住所被指定為一個城市而她的出生地點只是在一個州的級別
你可以想象延伸圖還能包括許多關於Lucy和Alain或其他人的其他更多的事實。例如你可以用它來表示食物過敏為每個過敏源增加一個頂點並增加人與過敏源之間的一條邊來指示一種過敏情況並連結到過敏源每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後你可以寫一個查詢找出每個人吃什麼是安全的。圖表在可演化性是富有優勢的當嚮應用程式新增功能時可以輕鬆擴充套件圖以適應應用程式資料結構的變化。
@ -594,7 +594,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
Cypher是屬性圖的宣告式查詢語言為Neo4j圖形資料庫而發明【37】。它是以電影“駭客帝國”中的一個角色來命名的而與密碼術中的密碼無關【38】。
[例2-3]()顯示了將[圖2-5](img/fig2-5.png)的左邊部分插入圖形資料庫的Cypher查詢。可以類似地新增圖的其餘部分為了便於閱讀而省略。每個頂點都有一個像`USA`或`Idaho`這樣的符號名稱,查詢的其他部分可以使用這些名稱在頂點之間建立邊,使用箭頭符號:`Idaho - [WITHIN] ->USA`建立一條標記為`WITHIN`的邊,`Idaho`為尾節點,`USA`為頭節點。
[例2-3]()顯示了將[圖2-5](../img/fig2-5.png)的左邊部分插入圖形資料庫的Cypher查詢。可以類似地新增圖的其餘部分為了便於閱讀而省略。每個頂點都有一個像`USA`或`Idaho`這樣的符號名稱,查詢的其他部分可以使用這些名稱在頂點之間建立邊,使用箭頭符號:`Idaho - [WITHIN] ->USA`建立一條標記為`WITHIN`的邊,`Idaho`為尾節點,`USA`為頭節點。
**例2-3 將圖2-5中的資料子集表示為Cypher查詢**
@ -608,7 +608,7 @@ CREATE
(Lucy) -[:BORN_IN]-> (Idaho)
```
當[圖2-5](img/fig2-5.png)的所有頂點和邊被新增到資料庫後,讓我們提些有趣的問題:例如,找到所有從美國移民到歐洲的人的名字。更確切地說,這裡我們想要找到符合下麵條件的所有頂點,並且返回這些頂點的`name`屬性:該頂點擁有一條連到美國任一位置的`BORN_IN`邊,和一條連到歐洲的任一位置的`LIVING_IN`邊。
當[圖2-5](../img/fig2-5.png)的所有頂點和邊被新增到資料庫後,讓我們提些有趣的問題:例如,找到所有從美國移民到歐洲的人的名字。更確切地說,這裡我們想要找到符合下麵條件的所有頂點,並且返回這些頂點的`name`屬性:該頂點擁有一條連到美國任一位置的`BORN_IN`邊,和一條連到歐洲的任一位置的`LIVING_IN`邊。
[例2-4]()展示瞭如何在Cypher中表達這個查詢。在MATCH子句中使用相同的箭頭符號來查詢圖中的模式`(person) -[:BORN_IN]-> ()` 可以匹配`BORN_IN`邊的任意兩個頂點。該邊的尾節點被綁定了變數`person`,頭節點則未被繫結。
@ -896,9 +896,9 @@ Cypher和SPARQL使用SELECT立即跳轉但是Datalog一次只進行一小步
2. 資料庫存在`within(usa, namerica)`,在上一步驟中生成`within_recursive(namerica, 'North America')`故運用規則2。它會產生`within_recursive(usa, 'North America')`。
3. 資料庫存在`within(idaho, usa)`,在上一步生成`within_recursive(usa, 'North America')`故運用規則2。它產生`within_recursive(idaho, 'North America')`。
透過重複應用規則1和2`within_recursive`謂語可以告訴我們在資料庫中包含北美(或任何其他位置名稱)的所有位置。這個過程如[圖2-6](img/fig2-6.png)所示。
透過重複應用規則1和2`within_recursive`謂語可以告訴我們在資料庫中包含北美(或任何其他位置名稱)的所有位置。這個過程如[圖2-6](../img/fig2-6.png)所示。
![](img/fig2-6.png)
![](../img/fig2-6.png)
**圖2-6 使用示例2-11中的Datalog規則來確定愛達荷州在北美。**

View File

@ -1,6 +1,6 @@
# 3. 儲存與檢索
![](img/ch3.png)
![](../img/ch3.png)
> 建立秩序,省卻搜尋
>
@ -83,9 +83,9 @@ $ cat database
鍵值儲存與在大多數程式語言中可以找到的**字典dictionary**型別非常相似,通常字典都是用**雜湊對映hash map**(或**雜湊表hash table**實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經有**記憶體中**資料結構 —— 雜湊對映,為什麼不使用它來索引在**磁碟上**的資料呢?
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣。那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到一個數據檔案中的位元組偏移量,指明瞭可以找到對應值的位置,如[圖3-1](img/fig3-1.png)所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找seek** 該位置並讀取該值。
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣。那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到一個數據檔案中的位元組偏移量,指明瞭可以找到對應值的位置,如[圖3-1](../img/fig3-1.png)所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找seek** 該位置並讀取該值。
![](img/fig3-1.png)
![](../img/fig3-1.png)
**圖3-1 以類CSV格式儲存鍵值對的日誌並使用記憶體雜湊對映進行索引。**
@ -93,15 +93,15 @@ $ cat database
像Bitcask這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是影片的URL值可能是它播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵——每個鍵有很多的寫操作但是將所有鍵儲存在記憶體中是可行的。
直到現在,我們只是追加寫入一個檔案 —— 所以如何避免最終用完磁碟空間?一種好的解決方案是,將日誌分為特定大小的段,當日志增長到特定尺寸時關閉當前段檔案,並開始寫入一個新的段檔案。然後,我們就可以對這些段進行**壓縮compaction**,如[圖3-2](img/fig3-2.png)所示。壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
直到現在,我們只是追加寫入一個檔案 —— 所以如何避免最終用完磁碟空間?一種好的解決方案是,將日誌分為特定大小的段,當日志增長到特定尺寸時關閉當前段檔案,並開始寫入一個新的段檔案。然後,我們就可以對這些段進行**壓縮compaction**,如[圖3-2](../img/fig3-2.png)所示。壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
![](img/fig3-2.png)
![](../img/fig3-2.png)
**圖3-2 壓縮鍵值更新日誌(統計貓影片的播放次數),只保留每個鍵的最近值**
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如[圖3-3](img/fig3-3.png)所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,在進行時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新的合併段而不是舊段 —— 然後可以簡單地刪除舊的段檔案。
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如[圖3-3](../img/fig3-3.png)所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,在進行時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新的合併段而不是舊段 —— 然後可以簡單地刪除舊的段檔案。
![](img/fig3-3.png)
![](../img/fig3-3.png)
**圖3-3 同時執行壓縮和分段合併**
@ -148,30 +148,30 @@ $ cat database
### SSTables和LSM樹
在[圖3-3](img/fig3-3.png)中,每個日誌結構儲存段都是一系列鍵值對。這些對按照它們寫入的順序出現,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
在[圖3-3](../img/fig3-3.png)中,每個日誌結構儲存段都是一系列鍵值對。這些對按照它們寫入的順序出現,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
現在我們可以對段檔案的格式做一個簡單的改變:我們要求鍵值對的序列按鍵排序。乍一看,這個要求似乎打破了我們使用順序寫入的能力,但是我們馬上就會明白這一點。
我們把這個格式稱為**排序字串表Sorted String Table**簡稱SSTable。我們還要求每個鍵只在每個合併的段檔案中出現一次壓縮過程已經保證。與使用雜湊索引的日誌段相比SSTable有幾個很大的優勢
1. 合併段是簡單而高效的,即使檔案大於可用記憶體。這種方法就像歸併排序演算法中使用的方法一樣,如[圖3-4](img/fig3-4.png)所示:您開始並排讀取輸入檔案,檢視每個檔案中的第一個鍵,複製最低鍵(根據排序順序)到輸出檔案,並重復。這產生一個新的合併段檔案,也按鍵排序。
1. 合併段是簡單而高效的,即使檔案大於可用記憶體。這種方法就像歸併排序演算法中使用的方法一樣,如[圖3-4](../img/fig3-4.png)所示:您開始並排讀取輸入檔案,檢視每個檔案中的第一個鍵,複製最低鍵(根據排序順序)到輸出檔案,並重復。這產生一個新的合併段檔案,也按鍵排序。
![](img/fig3-4.png)
![](../img/fig3-4.png)
##### 圖3-4 合併幾個SSTable段只保留每個鍵的最新值
如果在幾個輸入段中出現相同的鍵,該怎麼辦?請記住,每個段都包含在一段時間內寫入資料庫的所有值。這意味著一個輸入段中的所有值必須比另一個段中的所有值更新(假設我們總是合併相鄰的段)。當多個段包含相同的鍵時,我們可以保留最近段的值,並丟棄舊段中的值。
2. 為了在檔案中找到一個特定的鍵,你不再需要儲存記憶體中所有鍵的索引。以[圖3-5](img/fig3-5.png)為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道段檔案中該關鍵字的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著您可以跳到 `handbag` 的偏移位置並從那裡掃描,直到您找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
2. 為了在檔案中找到一個特定的鍵,你不再需要儲存記憶體中所有鍵的索引。以[圖3-5](../img/fig3-5.png)為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道段檔案中該關鍵字的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著您可以跳到 `handbag` 的偏移位置並從那裡掃描,直到您找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
![](img/fig3-5.png)
![](../img/fig3-5.png)
**圖3-5 具有記憶體索引的SSTable**
您仍然需要一個記憶體中索引來告訴您一些鍵的偏移量,但它可能很稀疏:每幾千位元組的段檔案就有一個鍵就足夠了,因為幾千位元組可以很快被掃描[^i]。
3. 由於讀取請求無論如何都需要掃描所請求範圍內的多個鍵值對,因此可以將這些記錄分組到塊中,並在將其寫入磁碟之前對其進行壓縮(如[圖3-5](img/fig3-5.png)中的陰影區域所示) 。稀疏記憶體中索引的每個條目都指向壓縮塊的開始處。除了節省磁碟空間之外壓縮還可以減少IO頻寬的使用。
3. 由於讀取請求無論如何都需要掃描所請求範圍內的多個鍵值對,因此可以將這些記錄分組到塊中,並在將其寫入磁碟之前對其進行壓縮(如[圖3-5](../img/fig3-5.png)中的陰影區域所示) 。稀疏記憶體中索引的每個條目都指向壓縮塊的開始處。除了節省磁碟空間之外壓縮還可以減少IO頻寬的使用。
[^i]: 如果所有的鍵與值都是定長的,你可以使用段檔案上的二分查詢並完全避免使用記憶體索引。然而實踐中鍵值通常都是變長的,因此如果沒有索引,就很難知道記錄的分界點(前一條記錄結束,後一條記錄開始的地方)
@ -217,25 +217,25 @@ Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎它使
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序編寫段。相比之下B樹將資料庫分解成固定大小的塊或頁面傳統上大小為4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為磁碟也被安排在固定大小的塊中。
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在磁碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如[圖3-6](img/fig3-6.png)所示。
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在磁碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如[圖3-6](../img/fig3-6.png)所示。
![](img/fig3-6.png)
![](../img/fig3-6.png)
**圖3-6 使用B樹索引查詢一個鍵**
一個頁面會被指定為B樹的根在索引中查詢一個鍵時就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵引用之間的鍵指明瞭引用子頁面的鍵範圍。
在[圖3-6](img/fig3-6.png)的例子中,我們正在尋找關鍵字 251 ,所以我們知道我們需要遵循邊界 200 和 300 之間的頁面引用。這將我們帶到一個類似的頁面進一步打破了200 - 300到子範圍。
在[圖3-6](../img/fig3-6.png)的例子中,我們正在尋找關鍵字 251 ,所以我們知道我們需要遵循邊界 200 和 300 之間的頁面引用。這將我們帶到一個類似的頁面進一步打破了200 - 300到子範圍。
最後,我們可以看到包含單個鍵(葉頁)的頁面,該頁面包含每個鍵的內聯值,或者包含對可以找到值的頁面的引用。
在B樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如在[圖3-6](img/fig3-6.png)中,分支因子是 6 。在實踐中,分支因子取決於儲存頁面參考和範圍邊界所需的空間量,但通常是幾百個。
在B樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如在[圖3-6](../img/fig3-6.png)中,分支因子是 6 。在實踐中,分支因子取決於儲存頁面參考和範圍邊界所需的空間量,但通常是幾百個。
如果要更新B樹中現有鍵的值則搜尋包含該鍵的葉頁更改該頁中的值並將該頁寫回到磁碟對該頁的任何引用保持有效 。如果你想新增一個新的鍵,你需要找到其範圍包含新鍵的頁面,並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵,則將其分成兩個半滿頁面,並更新父頁面以解釋鍵範圍的新分割槽,如[圖3-7](img/fig3-7.png)所示[^ii]。
如果要更新B樹中現有鍵的值則搜尋包含該鍵的葉頁更改該頁中的值並將該頁寫回到磁碟對該頁的任何引用保持有效 。如果你想新增一個新的鍵,你需要找到其範圍包含新鍵的頁面,並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵,則將其分成兩個半滿頁面,並更新父頁面以解釋鍵範圍的新分割槽,如[圖3-7](../img/fig3-7.png)所示[^ii]。
[^ii]: 向B樹中插入一個新的鍵是相當符合直覺的但刪除一個鍵同時保持樹平衡就會牽扯很多其他東西了。
![](img/fig3-7.png)
![](../img/fig3-7.png)
**圖3-7 透過分割頁面來生長B樹**
@ -299,7 +299,7 @@ B樹在資料庫體系結構中是非常根深蒂固的為許多工作負載
到目前為止,我們只討論了關鍵值索引,它們就像關係模型中的**主鍵primary key** 索引。主鍵唯一標識關係表中的一行或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵或ID引用該行/文件/頂點,並且索引用於解析這樣的引用。
有二級索引也很常見。在關係資料庫中,您可以使用 `CREATE INDEX` 命令在同一個表上建立多個二級索引,而且這些索引通常對於有效地執行聯接而言至關重要。例如,在[第2章](ch2.md)中的[圖2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一個二級索引,以便您可以在每個表中找到屬於同一使用者的所有行。
有二級索引也很常見。在關係資料庫中,您可以使用 `CREATE INDEX` 命令在同一個表上建立多個二級索引,而且這些索引通常對於有效地執行聯接而言至關重要。例如,在[第2章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個二級索引,以便您可以在每個表中找到屬於同一使用者的所有行。
一個二級索引可以很容易地從一個鍵值索引構建。主要的不同是鍵不是唯一的。即可能有許多行文件頂點具有相同的鍵。這可以透過兩種方式來解決或者透過使索引中的每個值成為匹配行識別符號的列表如全文索引中的釋出列表或者透過向每個索引新增行識別符號來使每個關鍵字唯一。無論哪種方式B樹和日誌結構索引都可以用作輔助索引。
@ -398,9 +398,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
這些OLTP系統往往對業務運作至關重要因而通常會要求 **高可用****低延遲**。所以DBA會密切關注他們的OLTP資料庫他們通常不願意讓業務分析人員在OLTP資料庫上執行臨時分析查詢因為這些查詢通常開銷巨大會掃描大部分資料集這會損害同時執行的事務的效能。
相比之下資料倉庫是一個獨立的資料庫分析人員可以查詢他們想要的內容而不影響OLTP操作【48】。資料倉庫包含公司各種OLTP系統中所有的只讀資料副本。從OLTP資料庫中提取資料使用定期的資料轉儲或連續的更新流轉換成適合分析的模式清理並載入到資料倉庫中。將資料存入倉庫的過程稱為“**抽取-轉換-載入ETL**”,如[圖3-8](img/fig3-8)所示。
相比之下資料倉庫是一個獨立的資料庫分析人員可以查詢他們想要的內容而不影響OLTP操作【48】。資料倉庫包含公司各種OLTP系統中所有的只讀資料副本。從OLTP資料庫中提取資料使用定期的資料轉儲或連續的更新流轉換成適合分析的模式清理並載入到資料倉庫中。將資料存入倉庫的過程稱為“**抽取-轉換-載入ETL**”,如[圖3-8](../img/fig3-8)所示。
![](img/fig3-8.png)
![](../img/fig3-8.png)
**圖3-8 ETL至資料倉庫的簡化提綱**
@ -424,7 +424,7 @@ TeradataVerticaSAP HANA和ParAccel等資料倉庫供應商通常使用昂
圖3-9中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表在這個例子中它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽量或點選量。
![](img/fig3-9.png)
![](../img/fig3-9.png)
**圖3-9 用於資料倉庫的星型模式的示例**
@ -432,7 +432,7 @@ TeradataVerticaSAP HANA和ParAccel等資料倉庫供應商通常使用昂
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(允許計算利潤餘額)。事實表中的其他列是對其他表(稱為維表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件的發生地點,時間,方式和原因。
例如,在[圖3-9](img/fig3-9.md)中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括**庫存單位SKU**,說明,品牌名稱,類別,脂肪含量,包裝尺寸等。`fact_sales` 表中的每一行都使用外部表明在特定交易中銷售了哪些產品。 (為了簡單起見,如果客戶一次購買幾種不同的產品,則它們在事實表中被表示為單獨的行)。
例如,在[圖3-9](../img/fig3-9.md)中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括**庫存單位SKU**,說明,品牌名稱,類別,脂肪含量,包裝尺寸等。`fact_sales` 表中的每一行都使用外部表明在特定交易中銷售了哪些產品。 (為了簡單起見,如果客戶一次購買幾種不同的產品,則它們在事實表中被表示為單獨的行)。
即使日期和時間通常使用維度表來表示,因為這允許對日期(諸如公共假期)的附加資訊進行編碼,從而允許查詢區分假期和非假期的銷售。
@ -469,13 +469,13 @@ GROUP BY
我們如何有效地執行這個查詢?
在大多數OLTP資料庫中儲存都是以面向行的方式進行佈局的表格的一行中的所有值都相鄰儲存。文件資料庫是相似的整個文件通常儲存為一個連續的位元組序列。你可以在[圖3-1](img/fig3-1.png)的CSV例子中看到這個。
在大多數OLTP資料庫中儲存都是以面向行的方式進行佈局的表格的一行中的所有值都相鄰儲存。文件資料庫是相似的整個文件通常儲存為一個連續的位元組序列。你可以在[圖3-1](../img/fig3-1.png)的CSV例子中看到這個。
為了處理像[例3-1]()這樣的查詢,您可能在 `fact_sales.date_key` `fact_sales.product_sk`上有索引它們告訴儲存引擎在哪裡查詢特定日期或特定產品的所有銷售情況。但是面向行的儲存引擎仍然需要將所有這些行每個包含超過100個屬性從磁碟載入到記憶體中解析它們並過濾掉那些不符合要求的條件。這可能需要很長時間。
面向列的儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如[圖3-10](img/fig3-10.png)所示。
面向列的儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如[圖3-10](../img/fig3-10.png)所示。
![](img/fig3-10.png)
![](../img/fig3-10.png)
**圖3-10 使用列儲存關係型資料,而不是行**
@ -489,9 +489,9 @@ GROUP BY
除了僅從磁碟載入查詢所需的列以外,我們還可以透過壓縮資料來進一步降低對磁碟吞吐量的需求。幸運的是,面向列的儲存通常很適合壓縮。
看看[圖3-10](img/fig3-10.png)中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如[圖3-11](img/fig3-11.png)所示。
看看[圖3-10](../img/fig3-10.png)中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如[圖3-11](../img/fig3-11.png)所示。
![](img/fig3-11.png)
![](../img/fig3-11.png)
**圖3-11 壓縮點陣圖索引儲存佈局**
@ -536,9 +536,9 @@ WHERE product_sk = 31 AND store_sk = 3
相反,即使按列儲存資料,也需要一次對整行進行排序。資料庫的管理員可以使用他們對常見查詢的知識來選擇表格應該被排序的列。例如,如果查詢通常以日期範圍為目標,例如上個月,則可以將 `date_key` 作為第一個排序鍵。然後,查詢最佳化器只能掃描上個月的行,這比掃描所有行要快得多。
第二列可以確定第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是[圖3-10](img/fig3-10.png)中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,因此同一天的同一產品的所有銷售都將在儲存中組合在一起。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
第二列可以確定第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是[圖3-10](../img/fig3-10.png)中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,因此同一天的同一產品的所有銷售都將在儲存中組合在一起。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
排序順序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有多個不同的值,那麼在排序之後,它將具有很長的序列,其中相同的值連續重複多次。一個簡單的執行長度編碼(就像我們用於[圖3-11](img/fig3-11.png)中的點陣圖一樣)可以將該列壓縮到幾千位元組 —— 即使表中有數十億行。
排序順序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有多個不同的值,那麼在排序之後,它將具有很長的序列,其中相同的值連續重複多次。一個簡單的執行長度編碼(就像我們用於[圖3-11](../img/fig3-11.png)中的點陣圖一樣)可以將該列壓縮到幾千位元組 —— 即使表中有數十億行。
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長時間的重複值。排序優先順序下面的列以基本上隨機的順序出現,所以它們可能不會被壓縮。但前幾列排序仍然是一個整體。
@ -568,13 +568,13 @@ WHERE product_sk = 31 AND store_sk = 3
當底層資料發生變化時物化檢視需要更新因為它是資料的非規範化副本。資料庫可以自動完成但是這樣的更新使得寫入成本更高這就是在OLTP資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中它們可能更有意義不管它們是否實際上改善了讀取效能取決於個別情況
物化檢視的常見特例稱為資料立方體或OLAP立方【64】。它是按不同維度分組的聚合網格。[圖3-12](img/fig3-12.png)顯示了一個例子。
物化檢視的常見特例稱為資料立方體或OLAP立方【64】。它是按不同維度分組的聚合網格。[圖3-12](../img/fig3-12.png)顯示了一個例子。
![](img/fig3-12.png)
![](../img/fig3-12.png)
**圖3-12 資料立方的兩個維度,透過求和聚合**
想象一下,現在每個事實都只有兩個維度表的外來鍵——在[圖3-12](img/fig-3-12.png)中,這些是日期和產品。您現在可以繪製一個二維表格,一個軸線上的日期和另一個軸上的產品。每個單元包含具有該日期 - 產品組合的所有事實的屬性(例如,`net_price`)的聚集(例如,`SUM`)。然後,您可以沿著每行或每列應用相同的彙總,並獲得一個維度減少的彙總(按產品的銷售額,無論日期,還是按日期銷售,無論產品如何)。
想象一下,現在每個事實都只有兩個維度表的外來鍵——在[圖3-12](../img/fig-3-12.png)中,這些是日期和產品。您現在可以繪製一個二維表格,一個軸線上的日期和另一個軸上的產品。每個單元包含具有該日期 - 產品組合的所有事實的屬性(例如,`net_price`)的聚集(例如,`SUM`)。然後,您可以沿著每行或每列應用相同的彙總,並獲得一個維度減少的彙總(按產品的銷售額,無論日期,還是按日期銷售,無論產品如何)。
一般來說事實往往有兩個以上的維度。在圖3-9中有五個維度日期產品商店促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的但是原理是一樣的每個單元格都包含特定日期產品-商店-促銷-客戶)組合的銷售。這些值可以在每個維度上重複概括。

View File

@ -1,6 +1,6 @@
# 4. 編碼與演化
![](img/ch4.png)
![](../img/ch4.png)
> 唯變所適
>
@ -113,7 +113,7 @@ JSON比XML簡潔但與二進位制格式一比還是太佔地方。這一
在下面的章節中能達到比這好得多的結果只用32個位元組對相同的記錄進行編碼。
![](img/fig4-1.png)
![](../img/fig4-1.png)
**圖4-1 使用MessagePack編碼的記錄例4-1**
@ -141,9 +141,9 @@ message Person {
```
Thrift和Protocol Buffers每一個都帶有一個程式碼生成工具它採用了類似於這裡所示的模式定義並且生成了以各種程式語言實現模式的類【18】。您的應用程式程式碼可以呼叫此生成的程式碼來對模式的記錄進行編碼或解碼。
用這個模式編碼的資料是什麼樣的令人困惑的是Thrift有兩種不同的二進位制編碼格式[^iii]分別稱為BinaryProtocol和CompactProtocol。先來看看BinaryProtocol。使用這種格式的編碼來編碼[例4-1]()中的訊息只需要59個位元組如[圖4-2](img/fig4-2.png)所示【19】。
用這個模式編碼的資料是什麼樣的令人困惑的是Thrift有兩種不同的二進位制編碼格式[^iii]分別稱為BinaryProtocol和CompactProtocol。先來看看BinaryProtocol。使用這種格式的編碼來編碼[例4-1]()中的訊息只需要59個位元組如[圖4-2](../img/fig4-2.png)所示【19】。
![](img/fig4-2.png)
![](../img/fig4-2.png)
**圖4-2 使用Thrift二進位制協議編碼的記錄**
@ -151,17 +151,17 @@ Thrift和Protocol Buffers每一個都帶有一個程式碼生成工具它採
與[圖4-1](Img/fig4-1.png)類似,每個欄位都有一個型別註釋(用於指示它是一個字串,整數,列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串`(“Martin”, “daydreaming”, “hacking”)`也被編碼為ASCII或者說UTF-8與之前類似。
與[圖4-1](img/fig4-1.png)相比,最大的區別是沒有欄位名`(userName, favoriteNumber, interest)`。相反,編碼資料包含欄位標籤,它們是數字`(1, 2和3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
與[圖4-1](../img/fig4-1.png)相比,最大的區別是沒有欄位名`(userName, favoriteNumber, interest)`。相反,編碼資料包含欄位標籤,它們是數字`(1, 2和3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
Thrift CompactProtocol編碼在語義上等同於BinaryProtocol但是如[圖4-3](img/fig4-3.png)所示它只將相同的資訊打包成只有34個位元組。它透過將欄位型別和標籤號打包到單個位元組中並使用可變長度整數來實現。數字1337不是使用全部八個位元組而是用兩個位元組編碼每個位元組的最高位用來指示是否還有更多的位元組來。這意味著-64到63之間的數字被編碼為一個位元組-8192和8191之間的數字以兩個位元組編碼等等。較大的數字使用更多的位元組。
Thrift CompactProtocol編碼在語義上等同於BinaryProtocol但是如[圖4-3](../img/fig4-3.png)所示它只將相同的資訊打包成只有34個位元組。它透過將欄位型別和標籤號打包到單個位元組中並使用可變長度整數來實現。數字1337不是使用全部八個位元組而是用兩個位元組編碼每個位元組的最高位用來指示是否還有更多的位元組來。這意味著-64到63之間的數字被編碼為一個位元組-8192和8191之間的數字以兩個位元組編碼等等。較大的數字使用更多的位元組。
![](img/fig4-3.png)
![](../img/fig4-3.png)
**圖4-3 使用Thrift壓縮協議編碼的記錄**
最後Protocol Buffers只有一種二進位制編碼格式對相同的資料進行編碼如[圖4-4](img/fig4-4.png)所示。 它的打包方式稍有不同但與Thrift的CompactProtocol非常相似。 Protobuf將同樣的記錄塞進了33個位元組中。
最後Protocol Buffers只有一種二進位制編碼格式對相同的資料進行編碼如[圖4-4](../img/fig4-4.png)所示。 它的打包方式稍有不同但與Thrift的CompactProtocol非常相似。 Protobuf將同樣的記錄塞進了33個位元組中。
![](img/fig4-4.png)
![](../img/fig4-4.png)
**圖4-4 使用Protobuf編碼的記錄**
@ -183,7 +183,7 @@ Thrift CompactProtocol編碼在語義上等同於BinaryProtocol但是如[圖4
如何改變欄位的資料型別這可能是可能的——檢查檔案的細節——但是有一個風險值將失去精度或被扼殺。例如假設你將一個32位的整數變成一個64位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料因為解析器可以用零填充任何缺失的位。但是如果舊程式碼讀取由新程式碼寫入的資料則舊程式碼仍使用32位變數來儲存該值。如果解碼的64位值不適合32位則它將被截斷。
Protobuf的一個奇怪的細節是它沒有列表或陣列資料型別而是有一個欄位的重複標記這是第三個選項旁邊必要和可選。如[圖4-4](img/fig4-4.png)所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
Protobuf的一個奇怪的細節是它沒有列表或陣列資料型別而是有一個欄位的重複標記這是第三個選項旁邊必要和可選。如[圖4-4](../img/fig4-4.png)所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
Thrift有一個專用的列表資料型別它使用列表元素的資料型別進行引數化。這不允許Protocol Buffers所做的從單值到多值的相同演變但是它具有支援巢狀列表的優點。
@ -217,11 +217,11 @@ record Person {
}
```
首先,請注意架構中沒有標籤號碼。 如果我們使用這個模式編碼我們的例子記錄([例4-1]()Avro二進位制編碼只有32個位元組長這是我們所見過的所有編碼中最緊湊的。 編碼位元組序列的分解如[圖4-5](img/fig4-5.png)所示。
首先,請注意架構中沒有標籤號碼。 如果我們使用這個模式編碼我們的例子記錄([例4-1]()Avro二進位制編碼只有32個位元組長這是我們所見過的所有編碼中最緊湊的。 編碼位元組序列的分解如[圖4-5](../img/fig4-5.png)所示。
如果您檢查位元組序列,您可以看到沒有什麼可以識別字段或其資料型別。 編碼只是由連在一起的值組成。 一個字串只是一個長度字首後跟UTF-8位元組但是在被包含的資料中沒有任何內容告訴你它是一個字串。 它可以是一個整數,也可以是其他的整數。 整數使用可變長度編碼與Thrift的CompactProtocol相同進行編碼。
![](img/fig4-5.png)
![](../img/fig4-5.png)
**圖4-5 使用Avro編碼的記錄**
@ -235,11 +235,11 @@ record Person {
當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料,從網路接收資料等)時,它希望資料在某個模式中,這就是讀者的模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能是從該模式生成的。
Avro的關鍵思想是作者的模式和讀者的模式不必是相同的 - 他們只需要相容。當資料解碼讀取Avro庫透過並排檢視作者的模式和讀者的模式並將資料從作者的模式轉換到讀者的模式來解決差異。 Avro規範【20】確切地定義了這種解析的工作原理如[圖4-6](img/fig4-6.png)所示。
Avro的關鍵思想是作者的模式和讀者的模式不必是相同的 - 他們只需要相容。當資料解碼讀取Avro庫透過並排檢視作者的模式和讀者的模式並將資料從作者的模式轉換到讀者的模式來解決差異。 Avro規範【20】確切地定義了這種解析的工作原理如[圖4-6](../img/fig4-6.png)所示。
例如,如果作者的模式和讀者的模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在作者模式中但不在讀者模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是作者的模式不包含該名稱的欄位,則使用在讀者模式中宣告的預設值填充。
![](img/fig4-6.png)
![](../img/fig4-6.png)
**圖4-6 一個Avro Reader解決讀寫模式的差異**
@ -344,7 +344,7 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能但是
解決這個問題不是一個難題,你只需要意識到它。
![](img/fig4-7.png)
![](../img/fig4-7.png)
**圖4-7 當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時,如果不小心,資料可能會丟失。**
@ -474,7 +474,7 @@ RPC方案的前後向相容性屬性從它使用的編碼方式中繼承
訊息代理通常不會執行任何特定的資料模型 - 訊息只是包含一些元資料的位元組序列,因此您可以使用任何編碼格式。如果編碼是向後相容的,則您可以靈活地更改發行商和消費者的獨立編碼,並以任意順序進行部署。
如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖4-7](img/fig4-7.png))。
如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖4-7](../img/fig4-7.png))。
#### 分散式的Actor框架

View File

@ -1,6 +1,6 @@
# 5. 複製
![](img/ch5.png)
![](../img/ch5.png)
> 與可能出錯的東西比,'不可能'出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。
>
@ -36,7 +36,7 @@
[^i]: 不同的人對**熱hot****溫warm****冷cold** 備份伺服器有不同的定義。 例如在PostgreSQL中**熱備hot standby**指的是能接受客戶端讀請求的副本。而**溫備warm standby**只是追隨領導者,但不處理客戶端的任何查詢。 就本書而言,這些差異並不重要。
![](img/fig5-1.png)
![](../img/fig5-1.png)
**圖5-1 基於領導者(主-從)的複製**
這種複製模式是許多關係資料庫的內建功能如PostgreSQL從9.0版本開始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性組【3】。 它也被用於一些非關係資料庫包括MongoDBRethinkDB和Espresso 【4】。 最後基於領導者的複製並不僅限於資料庫像Kafka 【5】和RabbitMQ高可用佇列【6】這樣的分散式訊息代理也使用它。 某些網路檔案系統例如DRBD這樣的塊複製裝置也與之類似。
@ -47,9 +47,9 @@
想象[圖5-1](fig5-1.png)中發生的情況,網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時刻,主庫又會將資料變更轉發給自己的從庫。最後,主庫通知客戶更新成功。
[圖5-2](img/fig5-2.png)顯示了系統各個元件之間的通訊:使用者客戶端,主庫和兩個從庫。時間從左到右流動。請求或響應訊息用粗箭頭表示。
[圖5-2](../img/fig5-2.png)顯示了系統各個元件之間的通訊:使用者客戶端,主庫和兩個從庫。時間從左到右流動。請求或響應訊息用粗箭頭表示。
![](img/fig5-2.png)
![](../img/fig5-2.png)
**圖5-2 基於領導者的複製:一個同步從庫和一個非同步從庫**
在[圖5-2]()的示例中從庫1的複製是同步的在向用戶報告寫入成功並使結果對其他使用者可見之前主庫需要等待從庫1的確認確保從庫1已經收到寫入操作。以及在使寫入對其他客戶端可見之前接收到寫入。跟隨者2的複製是非同步的主庫傳送訊息但不等待從庫的響應。
@ -205,7 +205,7 @@
但對於非同步複製,問題就來了。如[圖5-3](fig5-3.png)所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,使用者會不高興,可以理解。
![](img/fig5-3.png)
![](../img/fig5-3.png)
**圖5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀(read-after-write)的一致性來防止這種異常**
@ -236,9 +236,9 @@
從非同步從庫讀取第二個異常例子是,使用者可能會遇到 **時光倒流moving backward in time**
如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖5-4](img/fig5-4.png)顯示了使用者2345兩次進行相同的查詢首先查詢了一個延遲很小的從庫然後是一個延遲較大的從庫。 如果使用者重新整理網頁而每個請求被路由到一個隨機的伺服器這種情況是很有可能的。第一個查詢返回最近由使用者1234新增的評論但是第二個查詢不返回任何東西因為滯後的從庫還沒有拉取寫入內容。在效果上相比第一個查詢第二個查詢是在更早的時間點來觀察系統。如果第一個查詢沒有返回任何內容那問題並不大因為使用者2345可能不知道使用者1234最近添加了評論。但如果使用者2345先看見使用者1234的評論然後又看到它消失那麼對於使用者2345就很讓人頭大了。
如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖5-4](../img/fig5-4.png)顯示了使用者2345兩次進行相同的查詢首先查詢了一個延遲很小的從庫然後是一個延遲較大的從庫。 如果使用者重新整理網頁而每個請求被路由到一個隨機的伺服器這種情況是很有可能的。第一個查詢返回最近由使用者1234新增的評論但是第二個查詢不返回任何東西因為滯後的從庫還沒有拉取寫入內容。在效果上相比第一個查詢第二個查詢是在更早的時間點來觀察系統。如果第一個查詢沒有返回任何內容那問題並不大因為使用者2345可能不知道使用者1234最近添加了評論。但如果使用者2345先看見使用者1234的評論然後又看到它消失那麼對於使用者2345就很讓人頭大了。
![](img/fig5-4.png)
![](../img/fig5-4.png)
**圖5-4 使用者首先從新副本讀取,然後從舊副本讀取。時光倒流。為了防止這種異常,我們需要單調的讀取。**
@ -260,7 +260,7 @@
這兩句話之間有因果關係Cake夫人聽到了Poons先生的問題並回答了這個問題。
現在,想象第三個人正在透過從庫來聽這個對話。 Cake夫人說的內容是從一個延遲很低的從庫讀取的但Poons先生所說的內容從庫的延遲要大的多見[圖5-5](img/fig5-5.png))。 於是,這個觀察者會聽到以下內容:
現在,想象第三個人正在透過從庫來聽這個對話。 Cake夫人說的內容是從一個延遲很低的從庫讀取的但Poons先生所說的內容從庫的延遲要大的多見[圖5-5](../img/fig5-5.png))。 於是,這個觀察者會聽到以下內容:
> *Mrs. Cake*
> 通常約十秒鐘Mr. Poons.
@ -271,7 +271,7 @@
對於觀察者來說看起來好像Cake夫人在Poons先生髮問前就回答了這個問題。
這種超能力讓人印象深刻但也會把人搞糊塗。【25】。
![](img/fig5-5.png)
![](../img/fig5-5.png)
**圖5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者在看到問題之前可能會看到答案。**
@ -311,9 +311,9 @@
假如你有一個數據庫,副本分散在好幾個不同的資料中心(也許這樣可以容忍單個數據中心的故障,或地理上更接近使用者)。 使用常規的基於領導者的複製設定,主庫必須位於其中一個數據中心,且所有寫入都必須經過該資料中心。
多領導者配置中可以在每個資料中心都有主庫。 [圖5-6](img/fig5-6.png)展示了這個架構的樣子。 在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
多領導者配置中可以在每個資料中心都有主庫。 [圖5-6](../img/fig5-6.png)展示了這個架構的樣子。 在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
![](img/fig5-6.png)
![](../img/fig5-6.png)
**圖5-6 跨多個數據中心的多主複製**
@ -333,7 +333,7 @@
有些資料庫預設情況下支援多主配置但使用外部工具實現也很常見例如用於MySQL的Tungsten Replicator 【26】用於PostgreSQL的BDR【27】以及用於Oracle的GoldenGate 【19】。
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如[圖5-6](img/fig5-6.png)中“[衝突解決](#衝突解決)”)。本書將在“[處理寫入衝突](#處理寫入衝突)”中詳細討論這個問題。
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如[圖5-6](../img/fig5-6.png)中“[衝突解決](#衝突解決)”)。本書將在“[處理寫入衝突](#處理寫入衝突)”中詳細討論這個問題。
由於多主複製在許多資料庫中都屬於改裝的功能所以常常存在微妙的配置缺陷且經常與其他資料庫功能之間出現意外的反應。例如自增主鍵、觸發器、完整性約束等都可能會有麻煩。因此多主複製往往被認為是危險的領域應儘可能避免【28】。
@ -361,9 +361,9 @@
多領導者複製的最大問題是可能發生寫衝突,這意味著需要解決衝突。
例如,考慮一個由兩個使用者同時編輯的維基頁面,如[圖5-7](img/fig5-7.png)所示。使用者1將頁面的標題從A更改為B並且使用者2同時將標題從A更改為C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時會發現衝突【33】。單主資料庫中不會出現此問題。
例如,考慮一個由兩個使用者同時編輯的維基頁面,如[圖5-7](../img/fig5-7.png)所示。使用者1將頁面的標題從A更改為B並且使用者2同時將標題從A更改為C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時會發現衝突【33】。單主資料庫中不會出現此問題。
![](img/fig5-7.png)
![](../img/fig5-7.png)
**圖5-7 兩個主庫同時更新同一記錄引起的寫入衝突**
@ -385,7 +385,7 @@
單主資料庫按順序進行寫操作:如果同一個欄位有多個更新,則最後一個寫操作將決定該欄位的最終值。
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在[圖5-7](img/fig5-7.png)中在主庫1中標題首先更新為B而後更新為C在主庫2中首先更新為C然後更新為B。兩個順序都不是“更正確”的。
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在[圖5-7](../img/fig5-7.png)中在主庫1中標題首先更新為B而後更新為C在主庫2中首先更新為C然後更新為B。兩個順序都不是“更正確”的。
如果每個副本只是按照它看到寫入的順序寫入那麼資料庫最終將處於不一致的狀態最終值將是在主庫1的C和主庫2的B。這是不可接受的每個複製方案都必須確保資料在所有副本中最終都是相同的。因此資料庫必須以一種**收斂convergent**的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
@ -393,7 +393,7 @@
* 給每個寫入一個唯一的ID例如一個時間戳一個長的隨機數一個UUID或者一個鍵和值的雜湊挑選最高ID的寫入作為勝利者並丟棄其他寫入。如果使用時間戳這種技術被稱為**最後寫入勝利LWW, last write wins**。雖然這種方法很流行但是很容易造成資料丟失【35】。我們將在[本章末尾](#檢測併發寫入)更詳細地討論LWW。
* 為每個副本分配一個唯一的IDID編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在[圖5-7](img/fig5-7.png)中合併的標題可能類似於“B/C”
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在[圖5-7](../img/fig5-7.png)中合併的標題可能類似於“B/C”
* 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。
@ -431,7 +431,7 @@
#### 什麼是衝突?
有些衝突是顯而易見的。在[圖5-7](img/fig5-7.png)的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
有些衝突是顯而易見的。在[圖5-7](../img/fig5-7.png)的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
其他型別的衝突可能更為微妙,難以發現。例如,考慮一個會議室預訂系統:它記錄誰訂了哪個時間段的哪個房間。應用需要確保每個房間只有一組人同時預定(即不得有相同房間的重疊預訂)。在這種情況下,如果同時為同一個房間建立兩個不同的預訂,則可能會發生衝突。即使應用程式在允許使用者進行預訂之前檢查可用性,如果兩次預訂是由兩個不同的領導者進行的,則可能會有衝突。
@ -443,7 +443,7 @@
**複製拓撲**replication topology描述寫入從一個節點傳播到另一個節點的通訊路徑。如果你有兩個領導者如[圖5-7]()所示只有一個合理的拓撲結構領導者1必須把他所有的寫到領導者2反之亦然。當有兩個以上的領導各種不同的拓撲是可能的。[圖5-8]()舉例說明了一些例子。
![](img/fig5-8.png)
![](../img/fig5-8.png)
**圖5-8 三個可以設定多領導者複製的示例拓撲。**
@ -455,13 +455,13 @@
迴圈和星型拓撲的問題是,如果只有一個節點發生故障,則可能會中斷其他節點之間的複製訊息流,導致它們無法通訊,直到節點修復。拓撲結構可以重新配置為在發生故障的節點上工作,但在大多數部署中,這種重新配置必須手動完成。更密集連線的拓撲結構(例如全部到全部)的容錯性更好,因為它允許訊息沿著不同的路徑傳播,避免單點故障。
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如,由於網路擁塞),結果是一些複製訊息可能“超過”其他複製訊息,如[圖5-9](img/fig5-9.png)所示。
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如,由於網路擁塞),結果是一些複製訊息可能“超過”其他複製訊息,如[圖5-9](../img/fig5-9.png)所示。
![](img/fig5-9.png)
![](../img/fig5-9.png)
**圖5-9 使用多主程式複製時,可能會在某些副本中寫入錯誤的順序。**
在[圖5-9](img/fig5-9.png)中客戶端A向主庫1的表中插入一行客戶端B在主庫3上更新該行。然而主庫2可以以不同的順序接收寫入它可以首先接收更新其中從它的角度來看是對資料庫中不存在的行的更新並且僅在稍後接收到相應的插入其應該在更新之前
在[圖5-9](../img/fig5-9.png)中客戶端A向主庫1的表中插入一行客戶端B在主庫3上更新該行。然而主庫2可以以不同的順序接收寫入它可以首先接收更新其中從它的角度來看是對資料庫中不存在的行的更新並且僅在稍後接收到相應的插入其應該在更新之前
這是一個因果關係的問題,類似於我們在“[一致字首讀](ch8.md#一致字首讀)”中看到的更新取決於先前的插入所以我們需要確保所有節點先處理插入然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的因為時鐘不可能被充分地同步以便在主庫2處正確地排序這些事件見[第8章](ch8.md))。
@ -485,9 +485,9 @@
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(參閱「[處理節點宕機](#處理節點宕機)」)。
另一方面,在無領導配置中,故障切換不存在。[圖5-10](img/fig5-10.png)顯示了發生了什麼事情客戶端使用者1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者1234已經收到兩個確定的響應之後我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
另一方面,在無領導配置中,故障切換不存在。[圖5-10](../img/fig5-10.png)顯示了發生了什麼事情客戶端使用者1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者1234已經收到兩個確定的響應之後我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
![](img/fig5-10.png)
![](../img/fig5-10.png)
**圖5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
@ -503,7 +503,7 @@
***讀修復Read repair***
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在[圖5-10](img/fig5-10.png)中使用者2345獲得了來自副本3的版本6值和來自副本1和2的版本7值。客戶端發現副本3具有陳舊值並將新值寫回到該副本。這種方法適用於讀頻繁的值。
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在[圖5-10](../img/fig5-10.png)中使用者2345獲得了來自副本3的版本6值和來自副本1和2的版本7值。客戶端發現副本3具有陳舊值並將新值寫回到該副本。這種方法適用於讀頻繁的值。
***反熵過程Anti-entropy process***
@ -513,7 +513,7 @@
#### 讀寫的法定人數
在[圖5-10](img/fig5-10.png)的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?我們能推多遠呢?
在[圖5-10](../img/fig5-10.png)的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?我們能推多遠呢?
如果我們知道,每個成功的寫操作意味著在三個副本中至少有兩個出現,這意味著至多有一個副本可能是陳舊的。因此,如果我們從至少兩個副本讀取,我們可以確定至少有一個是最新的。如果第三個副本停機或響應速度緩慢,則讀取仍可以繼續返回最新值。
@ -531,10 +531,10 @@
* 如果$w <n$如果節點不可用我們仍然可以處理寫入
* 如果$r <n$如果節點不可用我們仍然可以處理讀取
* 對於$n = 3w = 2r = 2$,我們可以容忍一個不可用的節點。
* 對於$n = 5w = 3r = 3$,我們可以容忍兩個不可用的節點。 這個案例如[圖5-11](img/fig5-11.png)所示。
* 對於$n = 5w = 3r = 3$,我們可以容忍兩個不可用的節點。 這個案例如[圖5-11](../img/fig5-11.png)所示。
* 通常讀取和寫入操作始終並行傳送到所有n個副本。 引數w和r決定我們等待多少個節點即在我們認為讀或寫成功之前有多少個節點需要報告成功。
![](img/fig5-11.png)
![](../img/fig5-11.png)
**圖5-11 如果$w + r > n$讀取r個副本至少有一個r副本必然包含了最近的成功寫入**
@ -544,7 +544,7 @@
### 法定人數一致性的侷限性
如果你有n個副本並且你選擇w和r使得$w + r> n$,你通常可以期望每個讀取返回為一個鍵寫的最近的值。情況就是這樣,因為你寫的節點集合和你讀過的節點集合必須重疊。也就是說,您讀取的節點中必須至少有一個具有最新值的節點(如[圖5-11](img/fig5-11.png)所示)。
如果你有n個副本並且你選擇w和r使得$w + r> n$,你通常可以期望每個讀取返回為一個鍵寫的最近的值。情況就是這樣,因為你寫的節點集合和你讀過的節點集合必須重疊。也就是說,您讀取的節點中必須至少有一個具有最新值的節點(如[圖5-11](../img/fig5-11.png)所示)。
通常r和w被選為多數超過 $n/2$ )節點,因為這確保了$w + r> n$,同時仍然容忍多達$n/2$個節點故障。但是法定人數不一定必須是大多數只是讀寫使用的節點交集至少需要包括一個節點。其他法定人數的配置是可能的這使得分散式演算法的設計有一定的靈活性【45】。
@ -608,17 +608,17 @@
Dynamo風格的資料庫允許多個客戶端同時寫入相同的Key這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多領導者複製相似參閱“[處理寫入衝突](#處理寫入衝突)”但在Dynamo樣式的資料庫中在**讀修復**或**提示移交**期間也可能會產生衝突。
問題在於,由於可變的網路延遲和部分故障,事件可能在不同的節點以不同的順序到達。例如,[圖5-12](img/fig5-12.png)顯示了兩個客戶機A和B同時寫入三節點資料儲存區中的鍵X
問題在於,由於可變的網路延遲和部分故障,事件可能在不同的節點以不同的順序到達。例如,[圖5-12](../img/fig5-12.png)顯示了兩個客戶機A和B同時寫入三節點資料儲存區中的鍵X
* 節點 1 接收來自 A 的寫入,但由於暫時中斷,從不接收來自 B 的寫入。
* 節點 2 首先接收來自 A 的寫入,然後接收來自 B 的寫入。
* 節點 3 首先接收來自 B 的寫入,然後從 A 寫入。
![](img/fig5-12.png)
![](../img/fig5-12.png)
**圖5-12 併發寫入Dynamo風格的資料儲存沒有明確定義的順序。**
如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆蓋了某個鍵的值,那麼節點就會永久地不一致,如[圖5-12](img/fig5-12.png)中的最終獲取請求所示節點2認為 X 的最終值是 B而其他節點認為值是 A 。
如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆蓋了某個鍵的值,那麼節點就會永久地不一致,如[圖5-12](../img/fig5-12.png)中的最終獲取請求所示節點2認為 X 的最終值是 B而其他節點認為值是 A 。
為了最終達成一致,副本應該趨於相同的值。如何做到這一點?有人可能希望複製的資料庫能夠自動處理,但不幸的是,大多數的實現都很糟糕:如果你想避免丟失資料,你(應用程式開發人員)需要知道很多有關資料庫衝突處理的內部資訊。
@ -628,7 +628,7 @@
實現最終融合的一種方法是宣告每個副本只需要儲存最**“最近”**的值,並允許**“更舊”**的值被覆蓋和拋棄。然後,只要我們有一種明確的方式來確定哪個寫是“最近的”,並且每個寫入最終都被複制到每個副本,那麼複製最終會收斂到相同的值。
正如**“最近”**的引號所表明的,這個想法其實頗具誤導性。在[圖5-12](img/fig5-12.png)的例子中,當客戶端向資料庫節點發送寫入請求時,客戶端都不知道另一個客戶端,因此不清楚哪一個先發生了。事實上,說“發生”是沒有意義的:我們說寫入是**併發concurrent**的,所以它們的順序是不確定的。
正如**“最近”**的引號所表明的,這個想法其實頗具誤導性。在[圖5-12](../img/fig5-12.png)的例子中,當客戶端向資料庫節點發送寫入請求時,客戶端都不知道另一個客戶端,因此不清楚哪一個先發生了。事實上,說“發生”是沒有意義的:我們說寫入是**併發concurrent**的,所以它們的順序是不確定的。
即使寫入沒有自然的排序,我們也可以強制任意排序。例如,可以為每個寫入附加一個時間戳,挑選最**“最近”**的最大時間戳,並丟棄具有較早時間戳的任何寫入。這種衝突解決演算法被稱為**最後寫入勝利LWW, last write wins**是Cassandra 【53】唯一支援的衝突解決方法也是Riak 【35】中的一個可選特徵。
@ -673,13 +673,13 @@
4. 同時,客戶端 2 想要加入火腿,不知道客端戶 1 剛剛加了麵粉。客戶端 2 在最後一個響應中從伺服器收到了兩個值[牛奶]和[蛋],所以客戶端 2 現在合併這些值,並新增火腿形成一個新的值,[雞蛋,牛奶,火腿]。它將這個值傳送到伺服器,帶著之前的版本號 2 。伺服器檢測到新值會覆蓋版本 2 [雞蛋],但新值也會與版本 3 [牛奶,麵粉]**併發**所以剩下的兩個是v3 [牛奶,麵粉]和v4[雞蛋,牛奶,火腿]
5. 最後,客戶端 1 想要加培根。它以前在v3中從伺服器接收[牛奶,麵粉]和[雞蛋],所以它合併這些,新增培根,並將最終值[牛奶,麵粉,雞蛋,培根]連同版本號v3發往伺服器。這會覆蓋v3[牛奶,麵粉](請注意[雞蛋]已經在最後一步被覆蓋但與v4[雞蛋,牛奶,火腿]併發,所以伺服器保留這兩個併發值。
![](img/fig5-13.png)
![](../img/fig5-13.png)
**圖5-13 捕獲兩個客戶端之間的因果關係,同時編輯購物車。**
[圖5-13](img/fig5-13.png)中的操作之間的資料流如[圖5-14](img/fig5-14.png)所示。 箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。 在這個例子中,客戶端永遠不會完全掌握伺服器上的資料,因為總是有另一個操作同時進行。 但是,舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
[圖5-13](../img/fig5-13.png)中的操作之間的資料流如[圖5-14](../img/fig5-14.png)所示。 箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。 在這個例子中,客戶端永遠不會完全掌握伺服器上的資料,因為總是有另一個操作同時進行。 但是,舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
![](img/fig5-14.png)
![](../img/fig5-14.png)
**圖5-14 圖5-13中的因果依賴關係圖。**
@ -698,7 +698,7 @@
合併兄弟值,本質上是與多領導者複製中的衝突解決相同的問題,我們先前討論過(參閱“[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中做更聰明的事情。
以購物車為例,一種合理的合併兄弟方法就是集合求並。在[圖5-14](img/fig5-14.png)中,最後的兩個兄弟是[牛奶,麵粉,雞蛋,燻肉]和[雞蛋,牛奶,火腿]。注意牛奶和雞蛋出現在兩個,即使他們每個只寫一次。合併的價值可能是像[牛奶,麵粉,雞蛋,培根,火腿],沒有重複。
以購物車為例,一種合理的合併兄弟方法就是集合求並。在[圖5-14](../img/fig5-14.png)中,最後的兩個兄弟是[牛奶,麵粉,雞蛋,燻肉]和[雞蛋,牛奶,火腿]。注意牛奶和雞蛋出現在兩個,即使他們每個只寫一次。合併的價值可能是像[牛奶,麵粉,雞蛋,培根,火腿],沒有重複。
然而,如果你想讓人們也可以從他們的手推車中**刪除**東西而不是僅僅新增東西那麼把兄弟求並可能不會產生正確的結果如果你合併了兩個兄弟手推車並且只在其中一個兄弟值裡刪掉了它那麼被刪除的專案會重新出現在兄弟的並集中【37】。為了防止這個問題一個專案在刪除時不能簡單地從資料庫中刪除;相反,系統必須留下一個具有合適版本號的標記,以指示合併兄弟時該專案已被刪除。這種刪除標記被稱為**墓碑tombstone**。 (我們之前在“[雜湊索引”](ch3.md#雜湊索引)中的日誌壓縮的上下文中看到了墓碑。)
@ -706,13 +706,13 @@
#### 版本向量
[圖5-13](img/fig5-13.png)中的示例只使用一個副本。當有多個副本但沒有領導者時,演算法如何修改?
[圖5-13](../img/fig5-13.png)中的示例只使用一個副本。當有多個副本但沒有領導者時,演算法如何修改?
[圖5-13](img/fig5-13.png)使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,除了對每個鍵使用版本號之外,還需要在**每個副本**中使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些值,以及保留哪些值作為兄弟。
[圖5-13](../img/fig5-13.png)使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,除了對每個鍵使用版本號之外,還需要在**每個副本**中使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些值,以及保留哪些值作為兄弟。
所有副本的版本號集合稱為**版本向量version vector**【56】。這個想法的一些變體正在使用但最有趣的可能是在Riak 2.0 【58,59】中使用的**分散版本向量dotted version vector**【57】。我們不會深入細節但是它的工作方式與我們在購物車示例中看到的非常相似。
與[圖5-13](img/fig5-13.png)中的版本號一樣,當讀取值時,版本向量會從資料庫副本傳送到客戶端,並且隨後寫入值時需要將其傳送回資料庫。 Riak將版本向量編碼為一個字串它稱為**因果上下文causal context**)。版本向量允許資料庫區分覆蓋寫入和併發寫入。
與[圖5-13](../img/fig5-13.png)中的版本號一樣,當讀取值時,版本向量會從資料庫副本傳送到客戶端,並且隨後寫入值時需要將其傳送回資料庫。 Riak將版本向量編碼為一個字串它稱為**因果上下文causal context**)。版本向量允許資料庫區分覆蓋寫入和併發寫入。
另外,就像在單個副本的例子中,應用程式可能需要合併兄弟。版本向量結構確保從一個副本讀取並隨後寫回到另一個副本是安全的。這樣做可能會建立兄弟,但只要兄弟姐妹合併正確,就不會丟失資料。

View File

@ -1,6 +1,6 @@
# 6. 分割槽
![](img/ch6.png)
![](../img/ch6.png)
> 我們必須跳出電腦指令序列的窠臼。 敘述定義、描述元資料、梳理關係,而不是編寫過程。
>
@ -37,7 +37,7 @@
一個節點可能儲存多個分割槽。 如果使用主從複製模型,則分割槽和複製的組合如[圖6-1]()所示。 每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。
我們在[第5章](ch5.md)討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。 大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
![](img/fig6-1.png)
![](../img/fig6-1.png)
**圖6-1 組合使用複製和分割槽:每個節點充當某些分割槽的領導者,其他分割槽充當追隨者。**
@ -57,7 +57,7 @@
一種分割槽的方法是為每個分割槽指定一塊連續的鍵範圍(從最小值到最大值),如紙百科全書的卷([圖6-2]())。如果知道範圍之間的邊界,則可以輕鬆確定哪個分割槽包含某個值。如果您還知道分割槽所在的節點,那麼可以直接向相應的節點發出請求(對於百科全書而言,就像從書架上選取正確的書籍)。
![](img/fig6-2.png)
![](../img/fig6-2.png)
**圖6-2 印刷版百科全書按照關鍵字範圍進行分割槽**
@ -79,9 +79,9 @@
出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如Cassandra和MongoDB使用MD5Voldemort使用Fowler-Noll-Vo函式。許多程式語言都有內建的簡單雜湊函式它們用於雜湊表但是它們可能不適合分割槽例如在Java的`Object.hashCode()`和Ruby的`Object#hash`同一個鍵可能在不同的程序中有不同的雜湊值【6】。
一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如[圖6-3](img/fig6-3.png)所示。
一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如[圖6-3](../img/fig6-3.png)所示。
![](img/fig6-3.png)
![](../img/fig6-3.png)
**圖6-3 按雜湊鍵分割槽**
@ -125,19 +125,19 @@
### 基於文件的二級索引進行分割槽
假設你正在經營一個銷售二手車的網站(如[圖6-4](img/fig6-4.png)所示)。 每個列表都有一個唯一的ID——稱之為文件ID——並且用文件ID對資料庫進行分割槽例如分割槽0中的ID 0到499分割槽1中的ID 500到999等
假設你正在經營一個銷售二手車的網站(如[圖6-4](../img/fig6-4.png)所示)。 每個列表都有一個唯一的ID——稱之為文件ID——並且用文件ID對資料庫進行分割槽例如分割槽0中的ID 0到499分割槽1中的ID 500到999等
你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是**欄位field**,關係資料庫中這些是**列column** )。 如果您聲明瞭索引,則資料庫可以自動執行索引[^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目`colorred`的文件ID列表中。
[^ii]: 如果資料庫僅支援鍵值模型則你可能會嘗試在應用程式程式碼中建立從值到文件ID的對映來實現輔助索引。 如果沿著這條路線走下去,請萬分小心,確保您的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 參見“[多物件事務的需求]()”。
![](img/fig6-4.png)
![](../img/fig6-4.png)
**圖6-4 基於文件的二級索引進行分割槽**
在這種索引方法中每個分割槽是完全獨立的每個分割槽維護自己的二級索引僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時您需要寫入資料庫新增刪除或更新文件只需處理包含您正在編寫的文件ID的分割槽即可。出於這個原因**文件分割槽索引**也被稱為**本地索引local index**(而不是將在下一節中描述的**全域性索引global index**)。
但是從文件分割槽索引中讀取需要注意除非您對文件ID做了特別的處理否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在[圖6-4](img/fig6-4.png)中紅色汽車出現在分割槽0和分割槽1中。因此如果要搜尋紅色汽車則需要將查詢傳送到所有分割槽併合並所有返回的結果。
但是從文件分割槽索引中讀取需要注意除非您對文件ID做了特別的處理否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在[圖6-4](../img/fig6-4.png)中紅色汽車出現在分割槽0和分割槽1中。因此如果要搜尋紅色汽車則需要將查詢傳送到所有分割槽併合並所有返回的結果。
這種查詢分割槽資料庫的方法有時被稱為**分散/聚集scatter/gather**,並且可能會使二級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散/聚集也容易導致尾部延遲放大(參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)”。然而它被廣泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文件分割槽二級索引。大多數資料庫供應商建議您構建一個能從單個分割槽提供二級索引查詢的分割槽方案但這並不總是可行尤其是當在單個查詢中使用多個二級索引時例如同時需要按顏色和製造商查詢
@ -147,9 +147,9 @@
我們可以構建一個覆蓋所有分割槽資料的**全域性索引**,而不是給每個分割槽建立自己的次級索引(本地索引)。但是,我們不能只把這個索引儲存在一個節點上,因為它可能會成為瓶頸,違背了分割槽的目的。全域性索引也必須進行分割槽,但可以採用與主鍵不同的分割槽方式。
[圖6-5](img/fig6-5.png)述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從`a`到`r`的顏色在分割槽0中`s`到`z`的在分割槽1。汽車製造商的索引也與之類似分割槽邊界在`f`和`h`之間)。
[圖6-5](../img/fig6-5.png)述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從`a`到`r`的顏色在分割槽0中`s`到`z`的在分割槽1。汽車製造商的索引也與之類似分割槽邊界在`f`和`h`之間)。
![](img/fig6-5.png)
![](../img/fig6-5.png)
**圖6-5 基於關鍵詞對二級索引進行分割槽**
@ -188,7 +188,7 @@
#### 反面教材hash mod N
我們在前面說過([圖6-3](img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果$0≤hash(key)<b_0$則將鍵分配給分割槽0如果$b_0 hash(key) <b_1$則分配給分割槽1
我們在前面說過([圖6-3](../img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果$0≤hash(key)<b_0$則將鍵分配給分割槽0如果$b_0 hash(key) <b_1$則分配給分割槽1
也許你想知道為什麼我們不使用***mod***(許多程式語言中的%運算子)。例如,`hash(key) mod 10`會返回一個介於0和9之間的數字如果我們將雜湊寫為十進位制數雜湊模10將是最後一個數字。如果我們有10個節點編號為0到9這似乎是將每個鍵分配給一個節點的簡單方法。
@ -200,11 +200,11 @@
幸運的是有一個相當簡單的解決方案建立比節點更多的分割槽併為每個節點分配多個分割槽。例如執行在10個節點的叢集上的資料庫可能會從一開始就被拆分為1,000個分割槽因此大約有100個分割槽被分配給每個節點。
現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中**竊取**一些分割槽,直到分割槽再次公平分配。這個過程如[圖6-6](img/fig6-6.png)所示。如果從叢集中刪除一個節點,則會發生相反的情況。
現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中**竊取**一些分割槽,直到分割槽再次公平分配。這個過程如[圖6-6](../img/fig6-6.png)所示。如果從叢集中刪除一個節點,則會發生相反的情況。
只有分割槽在節點之間的移動。分割槽的數量不會改變,鍵所指定的分割槽也不會改變。唯一改變的是分割槽所在的節點。這種變更並不是即時的 — 在網路上傳輸大量的資料需要一些時間 — 所以在傳輸過程中,原有分割槽仍然會接受讀寫操作。
![](img/fig6-6.png)
![](../img/fig6-6.png)
**圖6-6 將新節點新增到每個節點具有多個分割槽的資料庫群集。**
@ -264,19 +264,19 @@
以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽-節點之間的分配關係變化?
![](img/fig6-7.png)
![](../img/fig6-7.png)
**圖6-7 將請求路由到正確節點的三種不同方式。**
這是一個具有挑戰性的問題,因為重要的是所有參與者都同意 - 否則請求將被髮送到錯誤的節點,而不是正確處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見[第9章](ch9.md))。
許多分散式資料系統都依賴於一個獨立的協調服務比如ZooKeeper來跟蹤叢集元資料如[圖6-8](img/fig6-8.png)所示。 每個節點在ZooKeeper中註冊自己ZooKeeper維護分割槽到節點的可靠對映。 其他參與者如路由層或分割槽感知客戶端可以在ZooKeeper中訂閱此資訊。 只要分割槽分配發生的改變或者叢集中新增或刪除了一個節點ZooKeeper就會通知路由層使路由資訊保持最新狀態。
許多分散式資料系統都依賴於一個獨立的協調服務比如ZooKeeper來跟蹤叢集元資料如[圖6-8](../img/fig6-8.png)所示。 每個節點在ZooKeeper中註冊自己ZooKeeper維護分割槽到節點的可靠對映。 其他參與者如路由層或分割槽感知客戶端可以在ZooKeeper中訂閱此資訊。 只要分割槽分配發生的改變或者叢集中新增或刪除了一個節點ZooKeeper就會通知路由層使路由資訊保持最新狀態。
![](img/fig6-8.png)
![](../img/fig6-8.png)
**圖6-8 使用ZooKeeper跟蹤分割槽分配給節點。**
例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](img/fig6-8.png)所示的路由層。 HBaseSolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。 MongoDB具有類似的體系結構但它依賴於自己的**配置伺服器config server** 實現和mongos守護程序作為路由層。
例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBaseSolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。 MongoDB具有類似的體系結構但它依賴於自己的**配置伺服器config server** 實現和mongos守護程序作為路由層。
Cassandra和Riak採取不同的方法他們在節點之間使用**流言協議gossip protocol** 來傳播群集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖6-7]()中的方法1。這個模型在資料庫節點中增加了更多的複雜性但是避免了對像ZooKeeper這樣的外部協調服務的依賴。

View File

@ -1,6 +1,6 @@
# 7. 事務
![](img/ch7.png)
![](../img/ch7.png)
> 一些作者聲稱,支援通用的兩階段提交代價太大,會帶來效能與可用性的問題。讓程式設計師來處理過度使用事務導致的效能問題,總比缺少事務程式設計好得多。
>
@ -90,11 +90,11 @@ ACID一致性的概念是**對資料的一組特定約束必須始終成立**
大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到**併發**問題(**競爭條件race conditions**)。
[圖7-1](img/fig7-1.png)是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫中沒有自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖7-1](img/fig7-1.png) 中因為發生了兩次增長計數器應該從42增至44但由於競態條件實際上只增至 43 。
[圖7-1](../img/fig7-1.png)是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫中沒有自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖7-1](../img/fig7-1.png) 中因為發生了兩次增長計數器應該從42增至44但由於競態條件實際上只增至 43 。
ACID意義上的隔離性意味著**同時執行的事務是相互隔離的**:它們不能相互冒犯。傳統的資料庫教科書將隔離性形式化為**可序列化Serializability**這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當事務已經提交時結果與它們按順序執行一個接一個是一樣的儘管實際上它們可能是併發執行的【10】。
![](img/fig7-1.png)
![](../img/fig7-1.png)
**圖7-1 兩個客戶之間的競爭狀態同時遞增計數器**
@ -137,7 +137,7 @@ ACID意義上的隔離性意味著**同時執行的事務是相互隔離的**
同時執行的事務不應該互相干擾。例如,如果一個事務進行多次寫入,則另一個事務要麼看到全部寫入結果,要麼什麼都看不到,但不應該是一些子集。
這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要**多物件事務multi-object transaction** 來保持多塊資料同步。[圖7-2](img/fig7-2.png)展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要**多物件事務multi-object transaction** 來保持多塊資料同步。[圖7-2](../img/fig7-2.png)展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
```sql
SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
@ -145,17 +145,17 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但如果郵件太多,你可能會覺得這個查詢太慢,並決定用單獨的欄位儲存未讀郵件的數量(一種反規範化)。現在每當一個新訊息寫入時,必須也增長未讀計數器,每當一個訊息被標記為已讀時,也必須減少未讀計數器。
在[圖7-2](img/fig7-2.png)中使用者2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生[^ii]。隔離性可以避免這個問題透過確保使用者2 要麼同時看到新郵件和增長後的計數器,要麼都看不到。反正不會看到執行到一半的中間結果。
在[圖7-2](../img/fig7-2.png)中使用者2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生[^ii]。隔離性可以避免這個問題透過確保使用者2 要麼同時看到新郵件和增長後的計數器,要麼都看不到。反正不會看到執行到一半的中間結果。
[^ii]: 可以說郵件應用中的錯誤計數器並不是什麼特別重要的問題。但換種方式來看,你可以把未讀計數器換成客戶賬戶餘額,把郵件收發看成支付交易。
![](img/fig7-2.png)
![](../img/fig7-2.png)
**圖7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。**
[圖7-3](img/fig7-3.png)說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
[圖7-3](../img/fig7-3.png)說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
![](img/fig7-3.png)
![](../img/fig7-3.png)
**圖7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致**
@ -175,7 +175,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(參閱“[使B樹可靠]()”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) )。
一些資料庫也提供更復雜的原子操作,例如自增操作,這樣就不再需要像 [圖7-1](img/fig7-1.png) 那樣的讀取-修改-寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,當值沒有被其他併發修改過時,才允許執行寫操作。
一些資料庫也提供更復雜的原子操作,例如自增操作,這樣就不再需要像 [圖7-1](../img/fig7-1.png) 那樣的讀取-修改-寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,當值沒有被其他併發修改過時,才允許執行寫操作。
這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(參閱“[防止丟失更新](#防止丟失更新)”。但它們不是通常意義上的事務。CAS以及其他單一物件操作被稱為“輕量級事務”甚至出於營銷目的被稱為“ACID”【20,21,22】但是這個術語是誤導性的。事務通常被理解為**將多個物件上的多個操作合併為一個執行單元的機制**。[^iv]
@ -190,7 +190,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
有一些場景中,單物件插入,更新和刪除是足夠的。但是許多其他場景需要協調寫入幾個不同的物件:
* 在關係資料模型中,一個表中的行通常具有對另一個表中的行的外來鍵引用。(類似的是,在一個圖資料模型中,一個頂點有著到其他頂點的邊)。多物件事務使你確信這些引用始終有效:當插入幾個相互引用的記錄時,外來鍵必須是正確的,最新的,不然資料就沒有意義。
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件——更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(參閱“[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖7-2](img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件——更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(參閱“[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
* 在具有二級索引的資料庫中(除了純粹的鍵值儲存以外幾乎都有),每次更改值時都需要更新索引。從事務角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離性,記錄可能出現在一個索引中,但沒有出現在另一個索引中,因為第二個索引的更新還沒有發生。
這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在“[弱隔離級別](#弱隔離級別)”中討論這些問題,並在[第12章]()中探討其他方法。
@ -250,14 +250,14 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
在**讀已提交**隔離級別執行的事務必須防止髒讀。這意味著事務的任何寫入操作只有在該事務提交時才能被其他人看到(然後所有的寫入操作都會立即變得可見)。如[圖7-4]()所示使用者1 設定了`x = 3`但使用者2 的 `get x `仍舊返回舊值2 而使用者1 尚未提交。
![](img/fig7-4.png)
![](../img/fig7-4.png)
**圖7-4 沒有髒讀使用者2只有在使用者1的事務已經提交後才能看到x的新值。**
為什麼要防止髒讀,有幾個原因:
- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在[圖7-2](img/fig7-2.png)中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,則所有寫入操作都需要回滾(如[圖7-3](img/fig7-3.png)所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在[圖7-2](../img/fig7-2.png)中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,則所有寫入操作都需要回滾(如[圖7-3](../img/fig7-3.png)所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
#### 沒有髒寫
@ -267,10 +267,10 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
透過防止髒寫,這個隔離級別避免了一些併發問題:
- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖7-5](img/fig7-5.png)[圖7-5](img/fig7-5.png) 以一個二手車銷售網站為例Alice和Bob兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入網站上的商品列表需要更新以反映買家的購買銷售發票需要傳送給買家。在[圖7-5](img/fig7-5.png)的情況下銷售是屬於Bob的因為他成功更新了商品列表但發票卻寄送給了愛麗絲因為她成功更新了發票表。讀已提交會阻止這樣這樣的事故。
- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖7-5](../img/fig7-5.png)[圖7-5](../img/fig7-5.png) 以一個二手車銷售網站為例Alice和Bob兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入網站上的商品列表需要更新以反映買家的購買銷售發票需要傳送給買家。在[圖7-5](../img/fig7-5.png)的情況下銷售是屬於Bob的因為他成功更新了商品列表但發票卻寄送給了愛麗絲因為她成功更新了發票表。讀已提交會阻止這樣這樣的事故。
- 但是,提交讀取並不能防止[圖7-1]()中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在“[防止更新丟失](#防止丟失更新)”中將討論如何使這種計數器增量安全。
![](img/fig7-5.png)
![](../img/fig7-5.png)
**圖7-5 如果存在髒寫,來自不同事務的衝突寫入可能會混淆在一起**
@ -292,9 +292,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果只從表面上看讀已提交隔離級別你就認為它完成了事務所需的一切,那是可以原諒的。它允許**中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混合。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如[圖7-6](img/fig7-6.png)說明了讀已提交時可能發生的問題。
但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如[圖7-6](../img/fig7-6.png)說明了讀已提交時可能發生的問題。
![](img/fig7-6.png)
![](../img/fig7-6.png)
**圖7-6 讀取偏差Alice觀察資料庫處於不一致的狀態**
@ -332,7 +332,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
[^vii]: 事實上事務ID是32位整數所以大約會在40億次事務之後溢位。 PostgreSQL的Vacuum過程會清理老舊的事務ID確保事務ID溢位回捲不會影響到資料。
![](img/fig7-7.png)
![](../img/fig7-7.png)
**圖7-7 使用多版本物件實現快照隔離**
@ -475,9 +475,9 @@ UPDATE wiki_pages SET content = '新內容'
首先想象一下這個例子你正在為醫院寫一個醫生輪班管理程式。醫院通常會同時要求幾位醫生待命但底線是至少有一位醫生在待命。醫生可以放棄他們的班次例如如果他們自己生病了只要至少有一個同事在這一班中繼續工作【40,41】。
現在想象一下Alice和Bob是兩位值班醫生。兩人都感到不適所以他們都決定請假。不幸的是他們恰好在同一時間點選按鈕下班。[圖7-8](img/fig7-8.png)說明了接下來的事情。
現在想象一下Alice和Bob是兩位值班醫生。兩人都感到不適所以他們都決定請假。不幸的是他們恰好在同一時間點選按鈕下班。[圖7-8](../img/fig7-8.png)說明了接下來的事情。
![](img/fig7-8.png)
![](../img/fig7-8.png)
**圖7-8 寫入偏差導致應用程式錯誤的示例**
@ -626,9 +626,9 @@ COMMIT;
在這種互動式的事務方式中,應用程式和資料庫之間的網路通訊耗費了大量的時間。如果不允許在資料庫中進行併發處理,且一次只處理一個事務,則吞吐量將會非常糟糕,因為資料庫大部分的時間都花費在等待應用程式發出當前事務的下一個查詢。在這種資料庫中,為了獲得合理的效能,需要同時處理多個事務。
出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如[圖7-9](img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中則儲存過程可以非常快地執行而不用等待任何網路或磁碟I/O。
出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如[圖7-9](../img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中則儲存過程可以非常快地執行而不用等待任何網路或磁碟I/O。
![](img/fig7-9.png)
![](../img/fig7-9.png)
**圖7-9 互動式事務和儲存過程之間的區別使用圖7-8的示例事務**
@ -793,7 +793,7 @@ WHERE room_id = 123 AND
回想一下快照隔離通常是透過多版本併發控制MVCC見[圖7-10]()來實現的。當一個事務從MVCC資料庫中的一致快照讀時它將忽略取快照時尚未提交的任何其他事務所做的寫入。在[圖7-10]()中事務43 認為Alice的 `on_call = true` 因為事務42修改Alice的待命狀態未被提交。然而在事務43想要提交時事務42 已經提交。這意味著在讀一致性快照時被忽略的寫入已經生效事務43 的前提不再為真。
![](img/fig7-10.png)
![](../img/fig7-10.png)
**圖7-10 檢測事務何時從MVCC快照讀取過時的值**
@ -803,9 +803,9 @@ WHERE room_id = 123 AND
#### 檢測影響之前讀取的寫入
第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如[圖7-11](img/fig7-11.png)所示。
第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如[圖7-11](../img/fig7-11.png)所示。
![](img/fig7-11.png)
![](../img/fig7-11.png)
**圖7-11 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。**

View File

@ -1,6 +1,6 @@
# 第八章:分散式系統的麻煩
![](img/ch8.png)
![](../img/ch8.png)
> 邂逅相遇
>
@ -97,7 +97,7 @@
**無共享**並不是構建系統的唯一方式,但它已經成為構建網際網路服務的主要方式,其原因如下:相對便宜,因為它不需要特殊的硬體,可以利用商品化的雲端計算服務,透過跨多個地理分佈的資料中心進行冗餘可以實現高可靠性。
網際網路和資料中心(通常是乙太網)中的大多數內部網路都是**非同步分組網路asynchronous packet networks**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果您傳送請求並期待響應,則很多事情可能會出錯(其中一些如[圖8-1](img/fig8-1.png)所示):
網際網路和資料中心(通常是乙太網)中的大多數內部網路都是**非同步分組網路asynchronous packet networks**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果您傳送請求並期待響應,則很多事情可能會出錯(其中一些如[圖8-1](../img/fig8-1.png)所示):
1. 請求可能已經丟失(可能有人拔掉了網線)。
2. 請求可能正在排隊,稍後將交付(也許網路或收件人超載)。
@ -106,7 +106,7 @@
5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。
6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。
![](img/fig8-1.png)
![](../img/fig8-1.png)
**圖8-1 如果傳送請求並沒有得到響應則無法區分a請求是否丟失b遠端節點是否關閉c響應是否丟失。**
@ -168,12 +168,12 @@
在駕駛汽車時由於交通擁堵道路交通網路的通行時間往往不盡相同。同樣計算機網路上資料包延遲的可變性通常是由於排隊【25】
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如[圖8-2](img/fig8-2.png)所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路連線)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如[圖8-2](../img/fig8-2.png)所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路連線)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
* 當資料包到達目標機器時如果所有CPU核心當前都處於繁忙狀態則來自網路的傳入請求將被作業系統排隊直到應用程式準備好處理它為止。根據機器上的負載這可能需要一段任意的時間。
* 在虛擬化環境中正在執行的作業系統經常暫停幾十毫秒而另一個虛擬機器使用CPU核心。在這段時間內虛擬機器不能從網路中消耗任何資料所以傳入的資料被虛擬機器監視器 【26】排隊緩衝進一步增加了網路延遲的可變性。
* TCP執行**流量控制flow control**(也稱為**擁塞避免congestion avoidance**或**背壓backpressure**其中節點限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著在資料甚至進入網路之前在傳送者處需要進行額外的排隊。
![](img/fig8-2.png)
![](../img/fig8-2.png)
**圖8-2 如果有多臺機器將網路流量傳送到同一目的地則其交換機佇列可能會被填滿。在這裡埠1,2和4都試圖傳送資料包到埠3**
@ -319,20 +319,20 @@
讓我們考慮一個特別的情況,一件很有誘惑但也很危險的事情:依賴時鐘,在多個節點上對事件進行排序。 例如,如果兩個客戶端寫入分散式資料庫,誰先到達? 哪一個更近?
[圖8-3](img/fig8-3.png)顯示了在具有多領導者複製的資料庫中對時鐘的危險使用(該例子類似於[圖5-9](img/fig5-9.png))。 客戶端A在節點1上寫入`x = 1`寫入被複制到節點3客戶端B在節點3上增加x我們現在有`x = 2`最後這兩個寫入都被複制到節點2。
[圖8-3](../img/fig8-3.png)顯示了在具有多領導者複製的資料庫中對時鐘的危險使用(該例子類似於[圖5-9](../img/fig5-9.png))。 客戶端A在節點1上寫入`x = 1`寫入被複制到節點3客戶端B在節點3上增加x我們現在有`x = 2`最後這兩個寫入都被複制到節點2。
![](img/fig8-3.png)
![](../img/fig8-3.png)
**圖8-3 客戶端B的寫入比客戶端A的寫入要晚但是B的寫入具有較早的時間戳。**
在[圖8-3]()中當一個寫入被複制到其他節點時它會根據發生寫入的節點上的時鐘時鐘標記一個時間戳。在這個例子中時鐘同步是非常好的節點1和節點3之間的偏差小於3ms這可能比你在實踐中預期的更好。
儘管如此,[圖8-3](img/fig8-3.png)中的時間戳卻無法正確排列事件:寫入`x = 1`的時間戳為42.004秒,但寫入`x = 2`的時間戳為42.003秒,即使`x = 2`在稍後出現。當節點2接收到這兩個事件時會錯誤地推斷出`x = 1`是最近的值,而丟棄寫入`x = 2`。效果上表現為客戶端B的增量操作會丟失。
儘管如此,[圖8-3](../img/fig8-3.png)中的時間戳卻無法正確排列事件:寫入`x = 1`的時間戳為42.004秒,但寫入`x = 2`的時間戳為42.003秒,即使`x = 2`在稍後出現。當節點2接收到這兩個事件時會錯誤地推斷出`x = 1`是最近的值,而丟棄寫入`x = 2`。效果上表現為客戶端B的增量操作會丟失。
這種衝突解決策略被稱為**最後寫入勝利LWW**它在多領導者複製和無領導者資料庫如Cassandra 【53】和Riak 【54】中被廣泛使用參見“[最後寫入勝利(丟棄併發寫入)](#最後寫入勝利(丟棄併發寫入))”一節。有些實現會在客戶端而不是伺服器上生成時間戳但這並不能改變LWW的基本問題
* 資料庫寫入可能會神祕地消失具有滯後時鐘的節點無法覆蓋之前具有快速時鐘的節點寫入的值直到節點之間的時鐘偏差消逝【54,55】。此方案可能導致一定數量的資料被悄悄丟棄而未嚮應用報告任何錯誤。
* LWW無法區分**高頻順序寫入**(在[圖8-3](img/fig8-3.png)中客戶端B的增量操作**一定**發生在客戶端A的寫入之後和**真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止因果關係的衝突(請參閱“[檢測併發寫入](ch5.md#檢測併發寫入)”)。
* LWW無法區分**高頻順序寫入**(在[圖8-3](../img/fig8-3.png)中客戶端B的增量操作**一定**發生在客戶端A的寫入之後和**真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止因果關係的衝突(請參閱“[檢測併發寫入](ch5.md#檢測併發寫入)”)。
* 兩個節點很可能獨立地生成具有相同時間戳的寫入,特別是在時鐘僅具有毫秒解析度的情況下。為了解決這樣的衝突,還需要一個額外的**決勝值tiebreaker**可以簡單地是一個大隨機數但這種方法也可能會導致違背因果關係【53】。
因此,儘管透過保留最“最近”的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近”的定義取決於本地的**時鐘**這很可能是不正確的。即使用頻繁同步的NTP時鐘一個數據包也可能在時間戳100毫秒根據傳送者的時鐘時傳送並在時間戳99毫秒根據接收者的時鐘處到達——看起來好像資料包在傳送之前已經到達這是不可能的。
@ -485,9 +485,9 @@ while(true){
如果一個節點繼續表現為**天選者**,即使大多數節點已經宣告它已經死了,則在考慮不周的系統中可能會導致問題。這樣的節點能以自己賦予的權能向其他節點發送訊息,如果其他節點相信,整個系統可能會做一些不正確的事情。
例如,[圖8-4](img/fig8-4.png)顯示了由於不正確的鎖實現導致的資料損壞錯誤。 這個錯誤不僅僅是理論上的HBase曾經有這個問題【74,75】假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問因為如果多個客戶試圖寫對此該檔案將被損壞。您嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
例如,[圖8-4](../img/fig8-4.png)顯示了由於不正確的鎖實現導致的資料損壞錯誤。 這個錯誤不僅僅是理論上的HBase曾經有這個問題【74,75】假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問因為如果多個客戶試圖寫對此該檔案將被損壞。您嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
![](img/fig8-4.png)
![](../img/fig8-4.png)
**圖8-4 分散式鎖的實現不正確客戶端1認為它仍然具有有效的租約即使它已經過期從而破壞了儲存中的檔案**
@ -495,15 +495,15 @@ while(true){
#### 防護令牌
當使用鎖或租約來保護對某些資源(如[圖8-4](img/fig8-4.png)中的檔案儲存)的訪問時,需要確保一個被誤認為自己是“天選者”的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是**防護fencing**,如[圖8-5]()所示
當使用鎖或租約來保護對某些資源(如[圖8-4](../img/fig8-4.png)中的檔案儲存)的訪問時,需要確保一個被誤認為自己是“天選者”的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是**防護fencing**,如[圖8-5]()所示
![](img/fig8-5.png)
![](../img/fig8-5.png)
**圖8-5 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全**
我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個**防護令牌fencing token**,這個數字在每次授予鎖定時都會增加(例如,由鎖定服務增加)。然後,我們可以要求客戶端每次向儲存服務傳送寫入請求時,都必須包含當前的防護令牌。
在[圖8-5](img/fig8-5.png)中客戶端1以33的令牌獲得租約但隨後進入一個長時間的停頓並且租約到期。客戶端2以34的令牌該數字總是增加獲取租約然後將其寫入請求傳送到儲存服務包括34的令牌。稍後客戶端1恢復生機並將其寫入儲存服務包括其令牌值33.但是儲存伺服器會記住它已經處理了一個具有更高令牌編號34的寫入因此它會拒絕帶有令牌33的請求。
在[圖8-5](../img/fig8-5.png)中客戶端1以33的令牌獲得租約但隨後進入一個長時間的停頓並且租約到期。客戶端2以34的令牌該數字總是增加獲取租約然後將其寫入請求傳送到儲存服務包括34的令牌。稍後客戶端1恢復生機並將其寫入儲存服務包括其令牌值33.但是儲存伺服器會記住它已經處理了一個具有更高令牌編號34的寫入因此它會拒絕帶有令牌33的請求。
如果將ZooKeeper用作鎖定服務則可將事務標識`zxid`或節點版本`cversion`用作防護令牌。由於它們保證單調遞增因此它們具有所需的屬性【74】。

View File

@ -1,6 +1,6 @@
# 9. 一致性與共識
![](img/ch9.png)
![](../img/ch9.png)
> 好死不如賴活著
> —— Jay Kreps, 關於Kafka與 Jepsen的若干筆記 (2013)
@ -60,11 +60,11 @@
在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。維護資料的單個副本的錯覺是指,系統能保障讀到的值是最近的,最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個**新鮮度保證recency guarantee**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。
![](img/fig9-1.png)
![](../img/fig9-1.png)
**圖9-1 這個系統是非線性一致的,導致了球迷的困惑**
[圖9-1 ](img/fig9-1.png)展示了一個關於體育網站的非線性一致例子【9】。Alice和Bob正坐在同一個房間裡都盯著各自的手機關注著2014年FIFA世界盃決賽的結果。在最後得分公佈後Alice重新整理頁面看到宣佈了獲勝者並興奮地告訴Bob。Bob難以置信地重新整理了自己的手機但他的請求路由到了一個落後的資料庫副本上手機顯示比賽仍在進行。
[圖9-1 ](../img/fig9-1.png)展示了一個關於體育網站的非線性一致例子【9】。Alice和Bob正坐在同一個房間裡都盯著各自的手機關注著2014年FIFA世界盃決賽的結果。在最後得分公佈後Alice重新整理頁面看到宣佈了獲勝者並興奮地告訴Bob。Bob難以置信地重新整理了自己的手機但他的請求路由到了一個落後的資料庫副本上手機顯示比賽仍在進行。
如果Alice和Bob在同一時間重新整理並獲得了兩個不同的查詢結果也許就沒有那麼令人驚訝了。因為他們不知道伺服器處理他們請求的精確時刻。然而Bob是在聽到Alice驚呼最後得分**之後**,點選了重新整理按鈕(啟動了他的查詢),因此他希望查詢結果至少與愛麗絲一樣新鮮。但他的查詢返回了陳舊結果,這一事實違背了線性一致性的要求。
@ -72,13 +72,13 @@
線性一致性背後的基本思想很簡單:使系統看起來好像只有一個數據副本。然而確切來講,實際上有更多要操心的地方。為了更好地理解線性一致性,讓我們再看幾個例子。
[圖9-2](img/fig9-2.png) 顯示了三個客戶端線上性一致資料庫中同時讀寫相同的鍵`x`。在分散式系統文獻中,`x`被稱為**暫存器register**,例如,它可以是鍵值儲存中的一個**鍵**,關係資料庫中的一**行**,或文件資料庫中的一個**文件**。
[圖9-2](../img/fig9-2.png) 顯示了三個客戶端線上性一致資料庫中同時讀寫相同的鍵`x`。在分散式系統文獻中,`x`被稱為**暫存器register**,例如,它可以是鍵值儲存中的一個**鍵**,關係資料庫中的一**行**,或文件資料庫中的一個**文件**。
![](img/fig9-2.png)
![](../img/fig9-2.png)
**圖9-2 如果讀取請求與寫入請求併發,則可能會返回舊值或新值**
為了簡單起見,[圖9-2](img/fig9-2.png)採用了使用者請求的視角,而不是資料庫內部的視角。每個柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間——只知道它發生在傳送請求和接收響應的之間的某個時刻。[^i]
為了簡單起見,[圖9-2](../img/fig9-2.png)採用了使用者請求的視角,而不是資料庫內部的視角。每個柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間——只知道它發生在傳送請求和接收響應的之間的某個時刻。[^i]
[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。即使真實的系統通常沒有準確的時鐘(參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”但這種假設是允許的為了分析分散式演算法我們可以假設一個精確的全域性時鐘存在不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和NTP產生的實時逼近。
@ -89,7 +89,7 @@
* $write(x,v)⇒r$ 表示客戶端請求將暫存器 `x` 設定為值 `v` ,資料庫返回響應 `r` (可能正確,可能錯誤)。
在[圖9-2](img/fig9-2.png) 中,`x` 的值最初為 `0`客戶端C 執行寫請求將其設定為 `1`。發生這種情況時客戶端A和B反覆輪詢資料庫以讀取最新值。 A和B的請求可能會收到怎樣的響應
在[圖9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`客戶端C 執行寫請求將其設定為 `1`。發生這種情況時客戶端A和B反覆輪詢資料庫以讀取最新值。 A和B的請求可能會收到怎樣的響應
* 客戶端A的第一個讀操作完成於寫操作開始之前因此必須返回舊值 `0`
* 客戶端A的最後一個讀操作開始於寫操作完成之後。如果資料庫是線性一致性的它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則必須在寫入之後處理讀取,因此它必須看到寫入的新值。
@ -99,16 +99,16 @@
[^ii]: 如果讀取(與寫入同時發生時)可能返回舊值或新值,則稱該暫存器為**常規暫存器regular register**【7,25】
為了使系統線性一致,我們需要新增另一個約束,如[圖9-3](img/fig9-3.png)所示
為了使系統線性一致,我們需要新增另一個約束,如[圖9-3](../img/fig9-3.png)所示
![](img/fig9-3.png)
![](../img/fig9-3.png)
**圖9-3 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。**
在一個線性一致的系統中,我們可以想象,在 `x` 的值從`0` 自動翻轉到 `1` 的時候(在寫操作的開始和結束之間)必定有一個時間點。因此,如果一個客戶端的讀取返回新的值 `1`,即使寫操作尚未完成,所有後續讀取也必須返回新值。
[圖9-3](img/fig9-3.png)中的箭頭說明了這個時序依賴關係。客戶端A 是第一個讀取新的值 `1` 的位置。在A 的讀取返回之後B開始新的讀取。由於B的讀取嚴格在發生於A的讀取之後因此即使C的寫入仍在進行中也必須返回 `1`。 (與[圖9-1](img/fig9-1.png)中的Alice和Bob的情況相同在Alice讀取新值之後Bob也希望讀取新的值。
[圖9-3](../img/fig9-3.png)中的箭頭說明了這個時序依賴關係。客戶端A 是第一個讀取新的值 `1` 的位置。在A 的讀取返回之後B開始新的讀取。由於B的讀取嚴格在發生於A的讀取之後因此即使C的寫入仍在進行中也必須返回 `1`。 (與[圖9-1](../img/fig9-1.png)中的Alice和Bob的情況相同在Alice讀取新值之後Bob也希望讀取新的值。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖9-4](img/fig9-4.png)顯示了一個更復雜的例子【10】。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖9-4](../img/fig9-4.png)顯示了一個更復雜的例子【10】。
在[圖9-4]()中,除了讀寫之外,還增加了第三種類型的操作:
@ -118,7 +118,7 @@
線性一致性的要求是,操作標記的連線總是按時間(從左到右)向前移動,而不是向後移動。這個要求確保了我們之前討論的新鮮性保證:一旦新的值被寫入或讀取,所有後續的讀都會看到寫入的值,直到它被再次覆蓋。
![](img/fig9-4.png)
![](../img/fig9-4.png)
**圖9-4 視覺化讀取和寫入看起來已經生效的時間點。 B的最後讀取不是線性一致性的**
@ -130,7 +130,7 @@
* 此模型不假設有任何事務隔離另一個客戶端可能隨時更改值。例如C首先讀取 `1` ,然後讀取 `2` 因為兩次讀取之間的值由B更改。可以使用原子**比較並設定cas**操作來檢查該值是否未被另一客戶端同時更改B和C的**cas**請求成功但是D的**cas**請求失敗(在資料庫處理它時,`x` 的值不再是 `0` )。
* 客戶B的最後一次讀取陰影條柱中不是線性一致性的。 該操作與C的**cas**寫操作併發(它將 `x``2` 更新為 `4` 。在沒有其他請求的情況下B的讀取返回 `2` 是可以的。然而在B的讀取開始之前客戶端A已經讀取了新的值 `4` 因此不允許B讀取比A更舊的值。再次與[圖9-1](img/fig9-1.png)中的Alice和Bob的情況相同。
* 客戶B的最後一次讀取陰影條柱中不是線性一致性的。 該操作與C的**cas**寫操作併發(它將 `x``2` 更新為 `4` 。在沒有其他請求的情況下B的讀取返回 `2` 是可以的。然而在B的讀取開始之前客戶端A已經讀取了新的值 `4` 因此不允許B讀取比A更舊的值。再次與[圖9-1](../img/fig9-1.png)中的Alice和Bob的情況相同。
這就是線性一致性背後的直覺。 正式的定義【6】更準確地描述了它。 透過記錄所有請求和響應的時序並檢查它們是否可以排列成有效的順序測試一個系統的行為是否線性一致性是可能的儘管在計算上是昂貴的【11】。
@ -182,17 +182,17 @@
#### 跨通道的時序依賴
注意[圖9-1](img/fig9-1.png) 中的一個細節如果Alice沒有驚呼得分Bob就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面並最終看到最後的分數。由於系統中存在額外的通道Alice的聲音傳到了Bob的耳朵中線性一致性的違背才被注意到。
注意[圖9-1](../img/fig9-1.png) 中的一個細節如果Alice沒有驚呼得分Bob就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面並最終看到最後的分數。由於系統中存在額外的通道Alice的聲音傳到了Bob的耳朵中線性一致性的違背才被注意到。
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如[圖9-5](img/fig9-5.png)所示。
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如[圖9-5](../img/fig9-5.png)所示。
影象縮放器需要明確的指令來執行尺寸縮放作業指令是Web伺服器透過訊息佇列傳送的參閱[第11章](ch11.md))。 Web伺服器不會將整個照片放在佇列中因為大多數訊息代理都是針對較短的訊息而設計的而一張照片的空間佔用可能達到幾兆位元組。取而代之的是首先將照片寫入檔案儲存服務寫入完成後再將縮放器的指令放入訊息佇列。
![](img/fig9-5.png)
![](../img/fig9-5.png)
**圖9-5 Web伺服器和影象調整器透過檔案儲存和訊息佇列進行通訊開啟競爭條件的可能性。**
如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖9-5](img/fig9-5.png)中的步驟3和4可能比儲存服務內部的複製更快。在這種情況下當縮放器讀取影象步驟5可能會看到影象的舊版本或者什麼都沒有。如果它處理的是舊版本的影象則檔案儲存中的全尺寸圖和略縮圖就產生了永久性的不一致。
如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖9-5](../img/fig9-5.png)中的步驟3和4可能比儲存服務內部的複製更快。在這種情況下當縮放器讀取影象步驟5可能會看到影象的舊版本或者什麼都沒有。如果它處理的是舊版本的影象則檔案儲存中的全尺寸圖和略縮圖就產生了永久性的不一致。
出現這個問題是因為Web伺服器和縮放器之間存在兩個不同的通道檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證這兩個通道之間的競爭條件是可能的。這種情況類似於[圖9-1](img/fig9-1.png)資料庫複製與Alice的嘴到Bob耳朵之間的真人音訊通道之間也存在競爭條件。
出現這個問題是因為Web伺服器和縮放器之間存在兩個不同的通道檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證這兩個通道之間的競爭條件是可能的。這種情況類似於[圖9-1](../img/fig9-1.png)資料庫複製與Alice的嘴到Bob耳朵之間的真人音訊通道之間也存在競爭條件。
線性一致性並不是避免這種競爭條件的唯一方法但它是最容易理解的。如果你可以控制額外通道例如訊息佇列的例子而不是在Alice和Bob的例子則可以使用在“[讀己之寫](ch5.md#讀己之寫)”討論過的備選方法,不過會有額外的複雜度代價。
@ -230,13 +230,13 @@
#### 線性一致性和法定人數
直覺上在Dynamo風格的模型中嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時就可能存在競爭條件如[圖9-6](img/fig9-6.png)所示。
直覺上在Dynamo風格的模型中嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時就可能存在競爭條件如[圖9-6](../img/fig9-6.png)所示。
![](img/fig9-6.png)
![](../img/fig9-6.png)
**圖9-6 非線性一致的執行,儘管使用了嚴格的法定人數**
在[圖9-6](img/fig9-6.png)中,$x$ 的初始值為0寫入客戶端透過向所有三個副本 $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端A併發地從兩個節點組成的法定人群 $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端B也併發地從兩個不同的節點組成的法定人數中讀取並從兩個節點中取回了舊值 `0`
在[圖9-6](../img/fig9-6.png)中,$x$ 的初始值為0寫入客戶端透過向所有三個副本 $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端A併發地從兩個節點組成的法定人群 $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端B也併發地從兩個不同的節點組成的法定人數中讀取並從兩個節點中取回了舊值 `0`
法定人數條件滿足( $w + r> n$ 但是這個執行是非線性一致的B的請求在A的請求完成後開始但是B返回舊值而A返回新值。 又一次如同Alice和Bob的例子 [圖9-1]()
@ -252,9 +252,9 @@
一些複製方法可以提供線性一致性,另一些複製方法則不能,因此深入地探討線性一致性的優缺點是很有趣的。
我們已經在[第五章](ch5.md)中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(參閱“[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖9-7](img/fig9-7.png)說明了這種部署的一個例子。
我們已經在[第五章](ch5.md)中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(參閱“[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖9-7](../img/fig9-7.png)說明了這種部署的一個例子。
![](img/fig9-7.png)
![](../img/fig9-7.png)
**圖9-7 網路中斷迫使線上性一致性和可用性之間做出選擇。**
@ -311,7 +311,7 @@
## 順序保證
之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們透過操作(似乎)執行完畢的順序來連線操作,以此說明[圖9-4](img/fig9-4.png)中的順序。
之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們透過操作(似乎)執行完畢的順序來連線操作,以此說明[圖9-4](../img/fig9-4.png)中的順序。
**順序ordering**這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它**順序**曾經出現過的上下文:
@ -325,12 +325,12 @@
**順序**反覆出現有幾個原因,其中一個原因是,它有助於保持**因果關係causality**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的:
* 在“[一致字首讀](ch5.md#一致字首讀)”([圖5-5](img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對**因cause**與**果effect**的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在**因果依賴causal dependency**。
* [圖5-9](img/fig5-9.png)中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會“壓倒”其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。
* 在“[一致字首讀](ch5.md#一致字首讀)”([圖5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對**因cause**與**果effect**的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在**因果依賴causal dependency**。
* [圖5-9](../img/fig5-9.png)中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會“壓倒”其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。
* 在“[檢測併發寫入](ch5.md#檢測併發寫入)”中我們觀察到如果有兩個操作A和B則存在三種可能性A發生在B之前或B發生在A之前或者A和B**併發**。這種**此前發生happened before**關係是因果關係的另一種表述如果A在B前發生那麼意味著B可能已經知道了A或者建立在A的基礎上或者依賴於A。如果A和B是**併發**的那麼它們之間並沒有因果聯絡換句話說我們確信A和B不知道彼此。
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中“一致”到底又是什麼意思?這意味著**與因果關係保持一致consistent with causality**如果快照包含答案它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫與因果關係保持一致意味著因果上在該時間點之前發生的所有操作其影響都是可見的但因果上在該時間點之後發生的操作其影響對觀察者不可見。**讀偏差read skew**意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如[圖7-6](img/fig7-6)所示)。
* 事務之間**寫偏差write skew**的例子(參見“[寫偏差和幻象](ch7.md#寫偏差和幻象)”)也說明了因果依賴:在[圖7-8](img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化的快照隔離](ch7.md#可序列化的快照隔離SSI)透過跟蹤事務之間的因果依賴來檢測寫偏差。
* 在愛麗絲和鮑勃看球的例子中([圖9-1](img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在“[跨通道的時序依賴](#跨通道的時序依賴)”一節中,以“影象大小調整服務”的偽裝再次出現。
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中“一致”到底又是什麼意思?這意味著**與因果關係保持一致consistent with causality**如果快照包含答案它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫與因果關係保持一致意味著因果上在該時間點之前發生的所有操作其影響都是可見的但因果上在該時間點之後發生的操作其影響對觀察者不可見。**讀偏差read skew**意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如[圖7-6](../img/fig7-6)所示)。
* 事務之間**寫偏差write skew**的例子(參見“[寫偏差和幻象](ch7.md#寫偏差和幻象)”)也說明了因果依賴:在[圖7-8](../img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化的快照隔離](ch7.md#可序列化的快照隔離SSI)透過跟蹤事務之間的因果依賴來檢測寫偏差。
* 在愛麗絲和鮑勃看球的例子中([圖9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在“[跨通道的時序依賴](#跨通道的時序依賴)”一節中,以“影象大小調整服務”的偽裝再次出現。
因果關係對事件施加了一種**順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。
@ -350,7 +350,7 @@
***線性一致性***
線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序[圖9-4](img/fig9-4.png)中以時間線表示。
線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序[圖9-4](../img/fig9-4.png)中以時間線表示。
***因果性***
@ -358,13 +358,13 @@
因此,根據這個定義,線上性一致的資料儲存中是不存在併發操作的:必須有且僅有一條時間線,所有的操作都在這條時間線上,構成一個全序關係。可能有幾個請求在等待處理,但是資料儲存確保了每個請求都是在唯一時間線上的某個時間點自動處理的,不存在任何併發。
併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在[第五章](ch5.md)中我們看到了這種現象:例如,[圖5-14](img/fig5-14.md) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。
併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在[第五章](ch5.md)中我們看到了這種現象:例如,[圖5-14](../img/fig5-14.md) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。
如果你熟悉像Git這樣的分散式版本控制系統那麼其版本歷史與因果關係圖極其相似。通常一個**提交Commit**發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併Merge**會在這些併發建立的提交相融合時建立。
#### 線性一致性強於因果一致性
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性**隱含著implies**因果關係任何線性一致的系統都能正確保持因果性【7】。特別是如果系統中有多個通訊通道如[圖9-5](img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性**隱含著implies**因果關係任何線性一致的系統都能正確保持因果性【7】。特別是如果系統中有多個通訊通道如[圖9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
線性一致性確保因果性的事實使線性一致系統變得簡單易懂,更有吸引力。然而,正如“[線性一致性的代價](#線性一致性的代價)”中所討論的,使系統線性一致可能會損害其效能和可用性,尤其是在系統具有嚴重的網路延遲的情況下(例如,如果系統在地理上散佈)。出於這個原因,一些分散式資料系統已經放棄了線性一致性,從而獲得更好的效能,但它們用起來也更為困難。
@ -384,7 +384,7 @@
用於確定*哪些操作發生在其他操作之前* 的技術,與我們在“[檢測併發寫入](ch5.md#檢測併發寫入)”中所討論的內容類似。那一節討論了無領導者資料儲存中的因果性為了防止丟失更新我們需要檢測到對同一個鍵的併發寫入。因果一致性則更進一步它需要跟蹤整個資料庫中的因果依賴而不僅僅是一個鍵。可以推廣版本向量以解決此類問題【54】。
為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖5-13 ](img/fig5-13.png)中來自先前操作的版本號在寫入時被傳回到資料庫的原因。在SSI 的衝突檢測中會出現類似的想法,如“[可序列化的快照隔離SSI]()”中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。
為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖5-13 ](../img/fig5-13.png)中來自先前操作的版本號在寫入時被傳回到資料庫的原因。在SSI 的衝突檢測中會出現類似的想法,如“[可序列化的快照隔離SSI]()”中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。
@ -417,7 +417,7 @@
* 每個節點每秒可以處理不同數量的操作。因此,如果一個節點產生偶數序列號而另一個產生奇數序列號,則偶數計數器可能落後於奇數計數器,反之亦然。如果你有一個奇數編號的操作和一個偶數編號的操作,你無法準確地說出哪一個操作在因果上先發生。
* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如[圖8-3](img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^vii]
* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如[圖8-3](../img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^vii]
[^viii]: 可以使物理時鐘時間戳與因果關係保持一致:在“[用於全域性快照的同步時鐘](#用於全域性快照的同步時鐘)”中我們討論了Google的Spanner它可以估計預期的時鐘偏差並在提交寫入之前等待不確定性間隔。 這中方法確保了實際上靠後的事務會有更大的時間戳。 但是大多數時鐘不能提供這種所需的不確定性度量。
@ -429,9 +429,9 @@
儘管剛才描述的三個序列號生成器與因果不一致但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳萊斯利·蘭伯特Leslie Lamport於1978年提出【56】現在是分散式系統領域中被引用最多的論文之一。
[圖9-8](img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。 蘭伯特時間戳就是兩者的簡單組合計數器節點ID$(counter, node ID)$。兩個節點有時可能具有相同的計數器值但透過在時間戳中包含節點ID每個時間戳都是唯一的。
[圖9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。 蘭伯特時間戳就是兩者的簡單組合計數器節點ID$(counter, node ID)$。兩個節點有時可能具有相同的計數器值但透過在時間戳中包含節點ID每個時間戳都是唯一的。
![](img/fig9-8.png)
![](../img/fig9-8.png)
**圖9-8 Lamport時間戳提供了與因果關係一致的總排序。**
@ -440,7 +440,7 @@
迄今,這個描述與上節所述的奇偶計數器基本類似。使蘭伯特時間戳因果一致的關鍵思想如下所示:每個節點和每個客戶端跟蹤迄今為止所見到的最大**計數器**值,並在每個請求中包含這個最大計數器值。當一個節點收到最大計數器值大於自身計數器值的請求或響應時,它立即將自己的計數器設定為這個最大值。
這如 [圖9-8](img/fig9-8.png) 所示,其中客戶端 A 從節點2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點1 。此時節點1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6`
這如 [圖9-8](../img/fig9-8.png) 所示,其中客戶端 A 從節點2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點1 。此時節點1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6`
只要每一個操作都攜帶著最大計數器值,這個方案確保蘭伯特時間戳的排序與因果一致,因為每個因果依賴都會導致時間戳增長。
@ -504,7 +504,7 @@
#### 使用全序廣播實現線性一致的儲存
如 [圖9-4](img/fig9-4.png) 所示,線上性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有者密切的聯絡[^x]。
如 [圖9-4](../img/fig9-4.png) 所示,線上性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有者密切的聯絡[^x]。
[^x]: 從形式上講,線性一致讀寫暫存器是一個“更容易”的問題。 全序廣播等價於共識【67】而共識問題在非同步的崩潰-停止模型【68】中沒有確定性的解決方案而線性一致的讀寫暫存器**可以**在這種模型中實現【23,24,25】。 然而,支援諸如**比較並設定CAS, compare-and-set**,或**自增並返回increment-and-get**的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。
@ -601,7 +601,7 @@
* 某些提交請求可能在網路中丟失,最終由於超時而中止,而其他提交請求則透過。
* 在提交記錄完全寫入之前,某些節點可能會崩潰,並在恢復時回滾,而其他節點則成功提交。
如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖7-3](img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。
如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖7-3](../img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。
事務提交必須是不可撤銷的 —— 事務提交之後,你不能改變主意,並追溯性地中止事務。這個規則的原因是,一旦資料被提交,其結果就對其他事務可見,因此其他客戶端可能會開始依賴這些資料。這個原則構成了**讀已提交**隔離等級的基礎,在“[讀已提交](ch7.md#讀已提交)”一節中討論了這個問題。如果一個事務在提交後被允許中止,所有那些讀取了**已提交卻又被追溯宣告不存在資料**的事務也必須回滾。
@ -611,9 +611,9 @@
**兩階段提交two-phase commit**是一種用於實現跨多個節點的原子事務提交的演算法,即確保所有節點提交或所有節點中止。 它是分散式資料庫中的經典演算法【13,35,75】。 2PC在某些資料庫內部使用也以**XA事務**的形式對應用可用【76,77】例如Java Transaction API支援或以SOAP Web服務的`WS-AtomicTransaction` 形式提供給應用【78,79】。
[ 圖9-9](img/fig9-9)說明了2PC的基本流程。2PC中的提交/中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。
[ 圖9-9](../img/fig9-9)說明了2PC的基本流程。2PC中的提交/中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。
![](img/fig9-9.png)
![](../img/fig9-9.png)
**圖9-9 兩階段提交2PC的成功執行**
@ -653,9 +653,9 @@
如果協調者在傳送**準備**請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了“是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為**存疑in doubt**的或**不確定uncertain**的。
情況如[圖9-10](img/fig9-10) 所示。在這個特定的例子中協調者實際上決定提交資料庫2 收到提交請求。但是協調者在將提交請求傳送到資料庫1 之前發生崩潰因此資料庫1 不知道是否提交或中止。即使**超時**在這裡也沒有幫助如果資料庫1 在超時後單方面中止它將最終與執行提交的資料庫2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。
情況如[圖9-10](../img/fig9-10) 所示。在這個特定的例子中協調者實際上決定提交資料庫2 收到提交請求。但是協調者在將提交請求傳送到資料庫1 之前發生崩潰因此資料庫1 不知道是否提交或中止。即使**超時**在這裡也沒有幫助如果資料庫1 在超時後單方面中止它將最終與執行提交的資料庫2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。
![](img/fig9-10.png)
![](../img/fig9-10.png)
 **圖9-10 參與者投贊成票後協調者崩潰。資料庫1不知道是否提交或中止**
沒有協調者的訊息參與者無法知道是提交還是放棄。原則上參與者可以相互溝通找出每個參與者是如何投票的並達成一致但這不是2PC協議的一部分。
@ -718,7 +718,7 @@
問題在於**鎖locking**。正如在“[讀已提交](ch7.md#讀已提交)”中所討論的那樣,資料庫事務通常獲取待修改的行上的**行級排他鎖**,以防止髒寫。此外,如果要使用可序列化的隔離等級,則使用兩階段鎖定的資料庫也必須為事務所讀取的行加上共享鎖(參見“[兩階段鎖定2PL](ch7.md#兩階段鎖定2PL)”)。
在事務提交或中止之前,資料庫不能釋放這些鎖(如[圖9-9](img/fig9-9.png)中的陰影區域所示。因此在使用兩階段提交時事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰需要20分鐘才能重啟那麼這些鎖將會被持有20分鐘。如果協調者的日誌由於某種原因徹底丟失這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。
在事務提交或中止之前,資料庫不能釋放這些鎖(如[圖9-9](../img/fig9-9.png)中的陰影區域所示。因此在使用兩階段提交時事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰需要20分鐘才能重啟那麼這些鎖將會被持有20分鐘。如果協調者的日誌由於某種原因徹底丟失這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。
當這些鎖被持有時,其他事務不能修改這些行。根據資料庫的不同,其他事務甚至可能因為讀取這些行而被阻塞。因此,其他事務沒法兒簡單地繼續它們的業務了 —— 如果它們要訪問同樣的資料,就會被阻塞。這可能會導致應用大面積進入不可用狀態,直到存疑事務被解決。

View File

@ -59,9 +59,9 @@
將一個大型資料庫拆分成較小的子集(稱為**分割槽partitions**),從而不同的分割槽可以指派給不同的**節點node**(亦稱**分片shard**)。 [第六章](ch6.md)將討論分割槽。
複製和分割槽是不同的機制,但它們經常同時使用。如[圖II-1](img/figii-1.png)所示。
複製和分割槽是不同的機制,但它們經常同時使用。如[圖II-1](../img/figii-1.png)所示。
![](img/figii-1.png)
![](../img/figii-1.png)
**圖II-1 一個數據庫切分為兩個分割槽,每個分割槽都有兩個副本**