mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
update zh-tw content
This commit is contained in:
parent
284e2d657f
commit
641cc67ac8
@ -432,7 +432,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用
|
||||
|
||||
通常情況下,事實被視為單獨的事件,因為這樣可以在以後分析中獲得最大的靈活性。但是,這意味著事實表可以變得非常大。像蘋果、沃爾瑪或 eBay 這樣的大企業在其資料倉庫中可能有幾十 PB 的交易歷史,其中大部分儲存在事實表中【56】。
|
||||
|
||||
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤餘額)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
|
||||
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤率)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
|
||||
|
||||
例如,在 [圖 3-9](../img/fig3-9.png) 中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品,包括庫存單位(SKU)、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外來鍵表明在特定交易中銷售了什麼產品。 (簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
|
||||
|
||||
|
50
zh-tw/ch7.md
50
zh-tw/ch7.md
@ -233,7 +233,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
最基本的事務隔離級別是 **讀已提交(Read Committed)**[^v],它提供了兩個保證:
|
||||
|
||||
1. 從資料庫讀時,只能看到已提交的資料(沒有 **髒讀**,即 dirty reads)。
|
||||
2. 寫入資料庫時,只會覆蓋已經提交的資料(沒有 **髒寫**,即 dirty writes)。
|
||||
2. 寫入資料庫時,只會覆蓋已提交的資料(沒有 **髒寫**,即 dirty writes)。
|
||||
|
||||
我們來更詳細地討論這兩個保證。
|
||||
|
||||
@ -285,7 +285,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
### 快照隔離和可重複讀
|
||||
|
||||
如果只從表面上看讀已提交隔離級別你就認為它完成了事務所需的一切,這是情有可原的。它允許 **中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混亂。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
|
||||
如果只從表面上看讀已提交隔離級別,你可能就認為它完成了事務所需的一切,這是情有可原的。它允許 **中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混亂。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
|
||||
|
||||
但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如 [圖 7-6](../img/fig7-6.png) 說明了讀已提交時可能發生的問題。
|
||||
|
||||
@ -293,9 +293,9 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
**圖 7-6 讀取偏差:Alice 觀察資料庫處於不一致的狀態**
|
||||
|
||||
愛麗絲在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元。現在有一筆事務從她的一個賬戶轉移了 100 美元到另一個賬戶。如果她非常不幸地在事務處理的過程中檢視其賬戶餘額列表,她可能會在收到付款之前先看到一個賬戶的餘額(收款賬戶,餘額仍為 500 美元),在發出轉賬之後再看到另一個賬戶的餘額(付款賬戶,新餘額為 400 美元)。對愛麗絲來說,現在她的賬戶似乎總共只有 900 美元 —— 看起來有 100 美元已經憑空消失了。
|
||||
Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元。現在有一筆事務從她的一個賬戶轉移了 100 美元到另一個賬戶。如果她非常不幸地在事務處理的過程中檢視其賬戶餘額列表,她可能會在收到付款之前先看到一個賬戶的餘額(收款賬戶,餘額仍為 500 美元),在發出轉賬之後再看到另一個賬戶的餘額(付款賬戶,新餘額為 400 美元)。對 Alice 來說,現在她的賬戶似乎總共只有 900 美元 —— 看起來有 100 美元已經憑空消失了。
|
||||
|
||||
這種異常被稱為 **不可重複讀(nonrepeatable read)** 或 **讀取偏差(read skew)**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額,她將看到與她之前的查詢中看到的不同的值(600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的:Alice 看到的帳戶餘額時確實在閱讀時已經提交了。
|
||||
這種異常被稱為 **不可重複讀(nonrepeatable read)** 或 **讀取偏差(read skew)**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額,她將看到與她之前的查詢中看到的不同的值(600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的:Alice 看到的帳戶餘額確實在閱讀時已經提交了。
|
||||
|
||||
> 不幸的是,術語 **偏差(skew)** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
|
||||
|
||||
@ -317,13 +317,13 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
#### 實現快照隔離
|
||||
|
||||
與讀取提交的隔離類似,快照隔離的實現通常使用寫鎖來防止髒寫(請參閱 “[讀已提交](#讀已提交)”),這意味著進行寫入的事務會阻止另一個事務修改同一個物件。但是讀取不需要任何鎖定。從效能的角度來看,快照隔離的一個關鍵原則是:**讀不阻塞寫,寫不阻塞讀**。這允許資料庫在處理一致性快照上的長時間查詢時,可以正常地同時處理寫入操作。且兩者間沒有任何鎖定爭用。
|
||||
與讀取提交的隔離類似,快照隔離的實現通常使用寫鎖來防止髒寫(請參閱 “[讀已提交](#讀已提交)”),這意味著進行寫入的事務會阻止另一個事務修改同一個物件。但是讀取則不需要加鎖。從效能的角度來看,快照隔離的一個關鍵原則是:**讀不阻塞寫,寫不阻塞讀**。這允許資料庫在處理一致性快照上的長時間查詢時,可以正常地同時處理寫入操作,且兩者間沒有任何鎖爭用。
|
||||
|
||||
為了實現快照隔離,資料庫使用了我們看到的用於防止 [圖 7-4](../img/fig7-4.png) 中的髒讀的機制的一般化。資料庫必須可能保留一個物件的幾個不同的提交版本,因為各種正在進行的事務可能需要看到資料庫在不同的時間點的狀態。因為它同時維護著單個物件的多個版本,所以這種技術被稱為 **多版本併發控制(MVCC, multi-version concurrency control)**。
|
||||
|
||||
如果一個數據庫只需要提供 **讀已提交** 的隔離級別,而不提供 **快照隔離**,那麼保留一個物件的兩個版本就足夠了:提交的版本和被覆蓋但尚未提交的版本。支援快照隔離的儲存引擎通常也使用 MVCC 來實現 **讀已提交** 隔離級別。一種典型的方法是 **讀已提交** 為每個查詢使用單獨的快照,而 **快照隔離** 對整個事務使用相同的快照。
|
||||
如果一個數據庫只需要提供 **讀已提交** 的隔離級別,而不提供 **快照隔離**,那麼保留一個物件的兩個版本就足夠了:已提交的版本和被覆蓋但尚未提交的版本。不過支援快照隔離的儲存引擎通常也使用 MVCC 來實現 **讀已提交** 隔離級別。一種典型的方法是 **讀已提交** 為每個查詢使用單獨的快照,而 **快照隔離** 對整個事務使用相同的快照。
|
||||
|
||||
[圖 7-7](../img/fig7-7.png) 說明了如何在 PostgreSQL 中實現基於 MVCC 的快照隔離【31】(其他實現類似)。當一個事務開始時,它被賦予一個唯一的,永遠增長 [^vii] 的事務 ID(`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
|
||||
[圖 7-7](../img/fig7-7.png) 說明了 PostgreSQL 如何實現基於 MVCC 的快照隔離【31】(其他實現類似)。當一個事務開始時,它被賦予一個唯一的,永遠增長 [^vii] 的事務 ID(`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
|
||||
|
||||
[^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。 PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID,確保事務 ID 溢位(回捲)不會影響到資料。
|
||||
|
||||
@ -363,7 +363,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
在 CouchDB、Datomic 和 LMDB 中使用另一種方法。雖然它們也使用 [B 樹](ch3.md#B樹),但它們使用的是一種 **僅追加 / 寫時複製(append-only/copy-on-write)** 的變體,它們在更新時不覆蓋樹的頁面,而為每個修改頁面建立一份副本。從父頁面直到樹根都會級聯更新,以指向它們子頁面的新版本。任何不受寫入影響的頁面都不需要被複制,並且保持不變【33,34,35】。
|
||||
|
||||
使用僅追加的 B 樹,每個寫入事務(或一批事務)都會建立一顆新的 B 樹,當建立時,從該特定樹根生長的樹就是資料庫的一個一致性快照。沒必要根據事務 ID 過濾掉物件,因為後續寫入不能修改現有的 B 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
|
||||
使用僅追加的 B 樹,每個寫入事務(或一批事務)都會建立一棵新的 B 樹,當建立時,從該特定樹根生長的樹就是資料庫的一個一致性快照。沒必要根據事務 ID 過濾掉物件,因為後續寫入不能修改現有的 B 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
|
||||
|
||||
#### 可重複讀與命名混淆
|
||||
|
||||
@ -384,7 +384,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
如果應用從資料庫中讀取一些值,修改它並寫回修改的值(讀取 - 修改 - 寫入序列),則可能會發生丟失更新的問題。如果兩個事務同時執行,則其中一個的修改可能會丟失,因為第二個寫入的內容並沒有包括第一個事務的修改(有時會說後面寫入 **狠揍(clobber)** 了前面的寫入)這種模式發生在各種不同的情況下:
|
||||
|
||||
- 增加計數器或更新賬戶餘額(需要讀取當前值,計算新值並寫回更新後的值)
|
||||
- 在複雜值中進行本地修改:例如,將元素新增到 JSON 文件中的一個列表(需要解析文件,進行更改並寫回修改的文件)
|
||||
- 將本地修改寫入一個複雜值中:例如,將元素新增到 JSON 文件中的一個列表(需要解析文件,進行更改並寫回修改的文件)
|
||||
- 兩個使用者同時編輯 wiki 頁面,每個使用者透過將整個頁面內容傳送到伺服器來儲存其更改,覆寫資料庫中當前的任何內容。
|
||||
|
||||
這是一個普遍的問題,所以已經開發了各種解決方案。
|
||||
@ -397,7 +397,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
UPDATE counters SET value = value + 1 WHERE key = 'foo';
|
||||
```
|
||||
|
||||
類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作,Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如維基頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
|
||||
類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作,Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如 wiki 頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
|
||||
|
||||
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”。
|
||||
|
||||
@ -440,7 +440,7 @@ COMMIT;
|
||||
|
||||
在不提供事務的資料庫中,有時會發現一種原子操作:**比較並設定**(CAS, 即 Compare And Set,先前在 “[單物件寫入](#單物件寫入)” 中提到)。此操作的目的是為了避免丟失更新:只有當前值從上次讀取時一直未改變,才允許更新發生。如果當前值與先前讀取的值不匹配,則更新不起作用,且必須重試讀取 - 修改 - 寫入序列。
|
||||
|
||||
例如,為了防止兩個使用者同時更新同一個 wiki 頁面,可以嘗試類似這樣的方式,只有當用戶開始編輯頁面內容時,才會發生更新:
|
||||
例如,為了防止兩個使用者同時更新同一個 wiki 頁面,可以嘗試類似這樣的方式,只有當用戶開始編輯後頁面內容未發生改變時,才會更新成功:
|
||||
|
||||
```sql
|
||||
-- 根據資料庫的實現情況,這可能安全也可能不安全
|
||||
@ -480,7 +480,7 @@ UPDATE wiki_pages SET content = '新內容'
|
||||
|
||||
#### 寫偏差的特徵
|
||||
|
||||
這種異常稱為 **寫偏差**【28】。它既不是 **髒寫**,也不是 **丟失更新**,因為這兩個事務正在更新兩個不同的物件(Alice 和 Bob 各自的待命記錄)。在這裡發生的衝突並不是那麼明顯,但是這顯然是一個競爭條件:如果兩個事務一個接一個地執行,那麼第二個醫生就不能歇班了。異常行為只有在事務併發進行時才有可能。
|
||||
這種異常稱為 **寫偏差**【28】。它既不是 **髒寫**,也不是 **丟失更新**,因為這兩個事務正在更新兩個不同的物件(Alice 和 Bob 各自的待命記錄)。在這裡發生的衝突並不是那麼明顯,但是這顯然是一個競爭條件:如果兩個事務一個接一個地執行,那麼第二個醫生就不能歇班了。異常行為只有在事務併發進行時才有可能發生。
|
||||
|
||||
可以將寫入偏差視為丟失更新問題的一般化。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),則可能發生寫入偏差。在多個事務更新同一個物件的特殊情況下,就會發生髒寫或丟失更新(取決於時序)。
|
||||
|
||||
@ -509,7 +509,7 @@ COMMIT;
|
||||
|
||||
#### 寫偏差的更多例子
|
||||
|
||||
寫偏差乍看像是一個深奧的問題,但一旦意識到這一點,很容易會注意到更多可能的情況。以下是一些例子:
|
||||
寫偏差乍看像是一個深奧的問題,但一旦意識到這一點,很容易會注意到它可能發生在更多場景下。以下是一些例子:
|
||||
|
||||
* 會議室預訂系統
|
||||
|
||||
@ -538,7 +538,7 @@ COMMIT;
|
||||
|
||||
* 多人遊戲
|
||||
|
||||
在 [例 7-1]() 中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。按照你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
|
||||
在 [例 7-1]() 中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。取決於你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
|
||||
|
||||
* 搶注使用者名稱
|
||||
|
||||
@ -546,7 +546,7 @@ COMMIT;
|
||||
|
||||
* 防止雙重開支
|
||||
|
||||
允許使用者花錢或積分的服務,需要檢查使用者的支付數額不超過其餘額。可以透過在使用者的帳戶中插入一個試探性的消費專案來實現這一點,列出帳戶中的所有專案,並檢查總和是否為正值【44】。有了寫入偏差,可能會發生兩個支出專案同時插入,一起導致餘額變為負值,但這兩個事務都不會注意到另一個。
|
||||
允許使用者花錢或使用積分的服務,需要檢查使用者的支付數額不超過其餘額。可以透過在使用者的帳戶中插入一個試探性的消費專案來實現這一點,列出帳戶中的所有專案,並檢查總和是否為正值【44】。在寫入偏差場景下,可能會發生兩個支出專案同時插入,一起導致餘額變為負值,但這兩個事務都不會注意到另一個。
|
||||
|
||||
#### 導致寫入偏差的幻讀
|
||||
|
||||
@ -574,7 +574,7 @@ COMMIT;
|
||||
|
||||
現在,要建立預訂的事務可以鎖定(`SELECT FOR UPDATE`)表中與所需房間和時間段對應的行。在獲得鎖定之後,它可以檢查重疊的預訂並像以前一樣插入新的預訂。請注意,這個表並不是用來儲存預訂相關的資訊 —— 它完全就是一組鎖,用於防止同時修改同一房間和時間範圍內的預訂。
|
||||
|
||||
這種方法被稱為 **物化衝突(materializing conflicts)**,因為它將幻讀變為資料庫中一組具體行上的鎖衝突【11】。不幸的是,弄清楚如何物化衝突可能很難,也很容易出錯,而讓併發控制機制洩漏到應用資料模型是很醜陋的做法。出於這些原因,如果沒有其他辦法可以實現,物化衝突應被視為最後的手段。在大多數情況下。**可序列化(Serializable)** 的隔離級別是更可取的。
|
||||
這種方法被稱為 **物化衝突(materializing conflicts)**,因為它將幻讀變為資料庫中一組具體行上的鎖衝突【11】。不幸的是,弄清楚如何物化衝突可能很難,也很容易出錯,並且讓併發控制機制洩漏到應用資料模型是很醜陋的做法。出於這些原因,如果沒有其他辦法可以實現,物化衝突應被視為最後的手段。在大多數情況下。**可序列化(Serializable)** 的隔離級別是更可取的。
|
||||
|
||||
|
||||
## 可序列化
|
||||
@ -608,7 +608,7 @@ COMMIT;
|
||||
- RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中。(請參閱 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
|
||||
- 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
|
||||
|
||||
序列執行事務的方法在 VoltDB/H-Store,Redis 和 Datomic 中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統更好,因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個 CPU 核的吞吐量。為了充分利用單一執行緒,需要與傳統形式的事務不同的結構。
|
||||
序列執行事務的方法在 VoltDB/H-Store,Redis 和 Datomic 中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統性能更好,因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個 CPU 核的吞吐量。為了充分利用單一執行緒,需要與傳統形式的事務不同的結構。
|
||||
|
||||
#### 在儲存過程中封裝事務
|
||||
|
||||
@ -648,7 +648,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
|
||||
|
||||
但是,對於需要訪問多個分割槽的任何事務,資料庫必須在觸及的所有分割槽之間協調事務。儲存過程需要跨越所有分割槽鎖定執行,以確保整個系統的可序列性。
|
||||
|
||||
由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加【49】。
|
||||
由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加吞吐量【49】。
|
||||
|
||||
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
|
||||
|
||||
@ -740,7 +740,7 @@ WHERE room_id = 123 AND
|
||||
- 假設你的索引位於 `room_id` 上,並且資料庫使用此索引查詢 123 號房間的現有預訂。現在資料庫可以簡單地將共享鎖附加到這個索引項上,指示事務已搜尋 123 號房間用於預訂。
|
||||
- 或者,如果資料庫使用基於時間的索引來查詢現有預訂,那麼它可以將共享鎖附加到該索引中的一系列值,指示事務已經將 12:00~13:00 時間段標記為用於預定。
|
||||
|
||||
無論哪種方式,搜尋條件的近似值都附加到其中一個索引上。現在,如果另一個事務想要插入,更新或刪除同一個房間和 / 或重疊時間段的預訂,則它將不得不更新索引的相同部分。在這樣做的過程中,它會遇到共享鎖,它將被迫等到鎖被釋放。
|
||||
無論哪種方式,搜尋條件的近似值都附加到其中一個索引上。現在,如果另一個事務想要插入、更新或刪除同一個房間和 / 或重疊時間段的預訂,則它將不得不更新索引的相同部分。在這樣做的過程中,它會遇到共享鎖,它將被迫等到鎖被釋放。
|
||||
|
||||
這種方法能夠有效防止幻讀和寫入偏差。索引範圍鎖並不像謂詞鎖那樣精確(它們可能會鎖定更大範圍的物件,而不是維持可序列化所必需的範圍),但是由於它們的開銷較低,所以是一個很好的折衷。
|
||||
|
||||
@ -753,7 +753,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
也許不是:一個稱為 **可序列化快照隔離(SSI, serializable snapshot isolation)** 的演算法是非常有前途的。它提供了完整的可序列化隔離級別,但與快照隔離相比只有很小的效能損失。 SSI 是相當新的:它在 2008 年首次被描述【40】,並且是 Michael Cahill 的博士論文【51】的主題。
|
||||
|
||||
今天,SSI 既用於單節點資料庫(PostgreSQL9.1 以後的可序列化隔離級別)和分散式資料庫(FoundationDB 使用類似的演算法)。由於 SSI 與其他併發控制機制相比還很年輕,還處於在實踐中證明自己表現的階段。但它有可能因為足夠快而在未來成為新的預設選項。
|
||||
今天,SSI 既用於單節點資料庫(PostgreSQL9.1 以後的可序列化隔離級別),也用於分散式資料庫(FoundationDB 使用類似的演算法)。由於 SSI 與其他併發控制機制相比還很年輕,還處於在實踐中證明自己表現的階段。但它有可能因為足夠快而在未來成為新的預設選項。
|
||||
|
||||
#### 悲觀與樂觀的併發控制
|
||||
|
||||
@ -765,7 +765,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
樂觀併發控制是一個古老的想法【52】,其優點和缺點已經爭論了很長時間【53】。如果存在很多 **爭用**(contention,即很多事務試圖訪問相同的物件),則表現不佳,因為這會導致很大一部分事務需要中止。如果系統已經接近最大吞吐量,來自重試事務的額外負載可能會使效能變差。
|
||||
|
||||
但是,如果有足夠的備用容量,並且事務之間的爭用不是太高,樂觀的併發控制技術往往比悲觀的要好。可交換的原子操作可以減少爭用:例如,如果多個事務同時要增加一個計數器,那麼應用增量的順序(只要計數器不在同一個事務中讀取)就無關緊要了,所以併發增量可以全部應用且無需衝突。
|
||||
但是,如果有足夠的空閒容量,並且事務之間的爭用不是太高,樂觀的併發控制技術往往比悲觀的效能要好。可交換的原子操作可以減少爭用:例如,如果多個事務同時要增加一個計數器,那麼應用增量的順序(只要計數器不在同一個事務中讀取)就無關緊要了,所以併發增量可以全部應用且不會有衝突。
|
||||
|
||||
顧名思義,SSI 基於快照隔離 —— 也就是說,事務中的所有讀取都是來自資料庫的一致性快照(請參閱 “[快照隔離和可重複讀取](#快照隔離和可重複讀)”)。與早期的樂觀併發控制技術相比這是主要的區別。在快照隔離的基礎上,SSI 添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。
|
||||
|
||||
@ -804,7 +804,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱 “[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `WHERE shift_id = 1234`。可以在這裡使用類似的技術,除了 SSI 鎖不會阻塞其他事務。
|
||||
|
||||
在 [圖 7-11](../img/fig7-11.png) 中,事務 42 和 43 都在班次 1234 查詢值班醫生。如果在 `shift_id` 上有索引,則資料庫可以使用索引項 1234 來記錄事務 42 和 43 讀取這個資料的事實。 (如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
|
||||
在 [圖 7-11](../img/fig7-11.png) 中,事務 42 和 43 都在班次 1234 查詢值班醫生。如果在 `shift_id` 上有索引,則資料庫可以使用索引項 1234 來記錄事務 42 和 43 讀取這個資料的事實。(如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
|
||||
|
||||
當事務寫入資料庫時,它必須在索引中查詢最近曾讀取受影響資料的其他事務。這個過程類似於在受影響的鍵範圍上獲取寫鎖,但鎖並不會阻塞事務直到其他讀事務完成,而是像警戒線一樣只是簡單通知其他事務:你們讀過的資料可能不是最新的啦。
|
||||
|
||||
@ -816,9 +816,9 @@ WHERE room_id = 123 AND
|
||||
|
||||
在某些情況下,事務可以讀取被另一個事務覆蓋的資訊:這取決於發生了什麼,有時可以證明執行結果無論如何都是可序列化的。 PostgreSQL 使用這個理論來減少不必要的中止次數【11,41】。
|
||||
|
||||
與兩階段鎖定相比,可序列化快照隔離的最大優點是一個事務不需要阻塞等待另一個事務所持有的鎖。就像在快照隔離下一樣,寫不會阻塞讀,反之亦然。這種設計原則使得查詢延遲更可預測,變數更少。特別是,只讀查詢可以執行在一致快照上,而不需要任何鎖定,這對於讀取繁重的工作負載非常有吸引力。
|
||||
與兩階段鎖定相比,可序列化快照隔離的最大優點是一個事務不需要阻塞等待另一個事務所持有的鎖。就像在快照隔離下一樣,寫不會阻塞讀,反之亦然。這種設計原則使得查詢延遲更可預測,波動更少。特別是,只讀查詢可以執行在一致快照上,而不需要任何鎖定,這對於讀取繁重的工作負載非常有吸引力。
|
||||
|
||||
與序列執行相比,可序列化快照隔離並不侷限於單個 CPU 核的吞吐量:FoundationDB 將檢測到的序列化衝突分佈在多臺機器上,允許擴充套件到很高的吞吐量。即使資料可能跨多臺機器進行分割槽,事務也可以在保證可序列化隔離等級的同時讀寫多個分割槽中的資料【54】。
|
||||
與序列執行相比,可序列化快照隔離並不侷限於單個 CPU 核的吞吐量:FoundationDB 將序列化衝突的檢測分佈在多臺機器上,允許擴充套件到很高的吞吐量。即使資料可能跨多臺機器進行分割槽,事務也可以在保證可序列化隔離等級的同時讀寫多個分割槽中的資料【54】。
|
||||
|
||||
中止率顯著影響 SSI 的整體表現。例如,長時間讀取和寫入資料的事務很可能會發生衝突並中止,因此 SSI 要求同時讀寫的事務儘量短(只讀的長事務可能沒問題)。對於慢事務,SSI 可能比兩階段鎖定或序列執行更不敏感。
|
||||
|
||||
@ -871,7 +871,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
一個相當新的演算法,避免了先前方法的大部分缺點。它使用樂觀的方法,允許事務執行而無需阻塞。當一個事務想要提交時,它會進行檢查,如果執行不可序列化,事務就會被中止。
|
||||
|
||||
本章中的示例主要是在關係資料模型的上下文中。但是,正如在討論中,無論使用哪種資料模型,如 “**[多物件事務的需求](#多物件事務的需求)**” 中所討論的,事務都是有價值的資料庫功能。
|
||||
本章中的示例主要是在關係資料模型的上下文中。但是,正如在 “**[多物件事務的需求](#多物件事務的需求)**” 中所討論的,無論使用哪種資料模型,事務都是有價值的資料庫功能。
|
||||
|
||||
本章主要是在單機資料庫的上下文中,探討了各種想法和演算法。分散式資料庫中的事務,則引入了一系列新的困難挑戰,我們將在接下來的兩章中討論。
|
||||
|
||||
|
@ -43,7 +43,7 @@
|
||||
>
|
||||
> —— 柯達黑爾
|
||||
|
||||
在分散式系統中,儘管系統的其他部分工作正常,但系統的某些部分可能會以某種不可預知的方式被破壞。這被稱為 **部分失效(partial failure)**。難點在於部分失效是 **不確定性的(nonderterministic)**:如果你試圖做任何涉及多個節點和網路的事情,它有時可能會工作,有時會出現不可預知的失敗。正如我們將要看到的,你甚至不知道是否成功了,因為訊息透過網路傳播的時間也是不確定的!
|
||||
在分散式系統中,儘管系統的其他部分工作正常,但系統的某些部分可能會以某種不可預知的方式被破壞。這被稱為 **部分失效(partial failure)**。難點在於部分失效是 **不確定性的(nondeterministic)**:如果你試圖做任何涉及多個節點和網路的事情,它有時可能會工作,有時會出現不可預知的失敗。正如我們將要看到的,你甚至不知道是否成功了,因為訊息透過網路傳播的時間也是不確定的!
|
||||
|
||||
這種不確定性和部分失效的可能性,使得分散式系統難以工作【5】。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user