5.1 同時性問題

讓我們從一個遙遠的理論開始。

程序員創建的任何信息系統(或簡單地說,一個應用程序)都由幾個典型的塊組成,每個塊都提供一部分必要的功能。例如,緩存用於記住資源密集型操作的結果以確保客戶端更快地讀取數據,流處理工具允許您將消息發送給其他組件進行異步處理,批處理工具用於“以一定的周期性“耙”累積的數據量。 .

在幾乎每個應用程序中,數據庫 (DB) 都以一種或另一種方式涉及,它們通常執行兩個功能:從您那裡收到數據時存儲數據,然後根據請求提供給您。很少有人會想到創建自己的數據庫,因為已經有很多現成的解決方案。但是如何為您的應用選擇合適的呢?

所以,讓我們假設您編寫了一個帶有移動界面的應用程序,允許您加載以前保存的房屋周圍任務列表 - 即從數據庫中讀取,並用新任務對其進行補充,以及對每個特定任務進行優先級排序任務 - 從 1(最高)到 3(最低)。假設您的移動應用程序一次僅供一個人使用。但是現在你敢把你的創作告訴你媽媽,現在她已經成為第二個普通用戶了。如果您在同一時間,就在同一毫秒內,決定將某項任務(“擦窗”)設置為不同的優先級,會發生什麼情況?

用專業術語來說,你和媽媽的數據庫查詢可以被認為是對數據庫進行查詢的2個進程。進程是計算機程序中可以在一個或多個線程上運行的實體。通常,進程具有機器代碼映像、內存、上下文和其他資源。換句話說,該過程可以表徵為在處理器上執行程序指令。當您的應用程序向數據庫發出請求時,我們談論的是您的數據庫處理通過網絡從一個進程收到的請求這一事實。如果有兩個用戶同時坐在應用程序中,那麼在任何特定時刻都可能有兩個進程。

當某個進程向數據庫發出請求時,它發現它處於某種狀態。有狀態系統是一種會記住以前的事件並存儲一些信息的系統,這些信息稱為“狀態”。聲明為 as 的變量integer可以具有 0、1、2 或 42 狀態。互斥鎖(互斥)有兩種狀態:鎖定解鎖,就像二進制信號量(“必需”與“已釋放”)一樣,通常是二進制(二進制)數據類型和變量只能有兩種狀態 - 1 或 0。

基於狀態的概念,建立了幾種數學和工程結構,例如有限自動機 - 一種具有一個輸入和一個輸出並且在每個時刻都處於有限狀態集中之一的模型 - 以及“狀態”設計模式,其中對像根據內部狀態改變行為(例如,根據分配給一個或另一個變量的值)。

因此,機器世界中的大多數對像都有一些可以隨時間變化的狀態:我們的管道,它處理一個大數據包,拋出錯誤並變得失敗,或者錢包對象屬性,它存儲用戶的剩餘金額帳戶,工資收據後的變化。

從一種狀態到另一種狀態的轉換(“轉換”)——例如,從進行中失敗——稱為操作。可能每個人都知道CRUD操作- createreadupdatedelete類似的HTTP方法- POSTGETPUTDELETE。但是程序員經常在他們的代碼中給操作起其他名字,因為操作可能比僅僅從數據庫中讀取某個值更複雜——它還可以檢查數據,然後是我們的操作,它採取了函數的形式,將被稱為,例如,validate()誰執行這些操作功能?已經描述的過程。

再多一點,你就會明白為什麼我把這些術語描述得這麼詳細了!

任何操作——無論是函數,還是在分佈式系統中,向另一台服務器發送請求——都有 2 個屬性:調用時間完成時間(completion time),這將嚴格大於調用時間(來自 Jepsen 的研究人員從理論假設出發,即這兩個時間戳都將被賦予虛構的、完全同步的、全局可用的時鐘)。

讓我們想像一下我們的待辦事項列表應用程序。你在 中通過手機界面向數據庫發出請求14:00:00.014,你媽媽在 中13:59:59.678(即 336 毫秒前)通過相同的界面更新了待辦事項列表,將洗碗添加到其中。考慮到你的數據庫的網絡延遲和可能的任務隊列,如果除了你和你媽媽之外,你媽媽的所有朋友也都在使用你的應用程序,數據庫處理完你的請求後可以執行媽媽的請求。也就是說,有可能你的兩個請求,還有你媽媽女朋友的請求,會同時(並發)發送同一個數據。

於是我們就來到了數據庫和分佈式應用領域最重要的術語——並發。兩個操作的同時性到底意味著什麼?如果給出了一些操作 T1 和一些操作 T2,那麼:

  • T1可以在執行T2的開始時間之前開始,在T2的開始和結束時間之間完成
  • T2可以在T1的開始時間之前開始,在T1的開始和結束之間完成
  • T1可以在T1執行的開始和結束時間之間開始和結束
  • 以及 T1 和 T2 具有共同執行時間的任何其他場景

很明顯,在本講的框架內,我們主要討論進入數據庫的查詢以及數據庫管理系統如何感知這些查詢,但是術語並發很重要,例如,在操作系統的上下文中。我不會偏離本文的主題太遠,但我認為重要的是要提一下,我們在這裡談論的並發與並發和並發的困境及其區別無關,這在上下文中討論操作系統和高性能計算。並行是在具有多個內核、處理器或計算機的環境中實現並發的一種方法。我們在不同進程同時訪問公共數據的意義上談論並發。

事實上,純理論上會出錯的是什麼?

在處理共享數據時,可能會出現許多與並發相關的問題,也稱為“競爭條件”。第一個問題發生在進程收到它不應該收到的數據時:不完整的、臨時的、已取消的或其他“錯誤”的數據。第二個問題是當進程接收到陳舊數據時,即與數據庫上次保存的狀態不對應的數據。假設某個應用程序從餘額為零的用戶賬戶中取款,因為數據庫將賬戶狀態返回給應用程序,沒有考慮最後一次取款,這發生在幾毫秒前。情況一般,不是嗎?

5.2 交易來拯救我們

為了解決這類問題,出現了事務的概念——對數據庫進行的某組順序操作(狀態變化),在邏輯上是一個單一的操作。我將再次舉一個銀行的例子——這不是偶然的,因為交易的概念顯然恰好出現在與貨幣打交道的背景下。交易的經典例子是從一個銀行賬戶向另一個銀行賬戶轉賬:您需要先從源賬戶中提取金額,然後將其存入目標賬戶。

要執行此交易,應用程序需要在數據庫中執行多項操作:檢查發送方的餘額、凍結髮送方賬戶中的金額、將金額添加到接收方的賬戶以及從發送方扣除金額。這樣的交易會有幾個要求。例如,應用程序無法接收有關餘額的過時或不正確的信息——例如,如果同時並行交易在中途以錯誤結束,資金未從賬戶中扣除——而我們的應用程序已經接收到信息資金被註銷。

為了解決這個問題,調用了事務的“隔離”屬性:我們的事務執行時就好像沒有其他事務在同一時刻執行一樣。我們的數據庫執行並發操作,就好像它是一個接一個地按順序執行它們——事實上,最高的隔離級別稱為Strict Serializable。是的,最高的,也就是說有好幾個等級。

“停下來,”你說。抓住你的馬,先生。

讓我們記住我是如何描述每個操作都有調用時間和執行時間的。為了方便起見,您可以考慮將調用和執行作為 2 個操作。那麼所有調用和執行動作的排序列表就可以稱為數據庫的歷史。那麼事務隔離級別就是一組歷史。我們使用隔離級別來確定哪些故事是“好”的。當我們說一個故事“破壞可序列化”或“不可序列化”時,我們的意思是該故事不在可序列化故事集中。

為了弄清楚我們在談論什麼樣的故事,我將舉例說明。比如有這麼一種history- intermediate read。當允許事務 A 從已被另一個正在運行的事務 B 修改且尚未提交(“未提交”)的行中讀取數據時發生 - 也就是說,實際上,更改尚未最終提交事務 B,它可以隨時取消它們。而且,例如,中止讀取只是我們取消取款交易的示例

有幾種可能的異常。也就是說,異常是在對數據庫的競爭訪問期間可能發生的某種不良數據狀態。並且為了避免某些不需要的狀態,數據庫使用不同級別的隔離 - 即針對不需要的狀態的不同級別的數據保護。這些級別(4 個)列在 ANSI SQL-92 標準中。

對一些研究人員來說,這些級別的描述似乎含糊不清,他們提供了自己的、更詳細的分類。我建議您關注已經提到的 Jepsen,以及 Hermitage 項目,該項目旨在準確闡明特定 DBMS(例如 MySQL 或 PostgreSQL)提供的隔離級別。如果你打開這個存儲庫中的文件,你可以看到他們使用什麼樣的 SQL 命令序列來測試數據庫的某些異常,你可以對你感興趣的數據庫做類似的事情)。這是存儲庫中的一個示例,可讓您感興趣:

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

重要的是要了解,對於同一個數據庫,通常您可以選擇幾種隔離類型中的一種。為什麼不選擇絕緣最強的呢?因為,就像計算機科學中的一切一樣,選擇的隔離級別應該對應於我們準備做出的權衡——在這種情況下,執行速度的權衡:隔離級別越強,請求將越慢處理。要了解您需要什麼級別的隔離,您需要了解您的應用程序的要求,並了解您選擇的數據庫是否提供此級別,您將必須查看文檔 - 對於大多數應用程序來說這就足夠了,但是如果你有一些特別嚴格的要求,最好像 Hermitage 項目的人那樣安排一個測試。

5.3 ACID中的“I”等字母

隔離基本上就是人們談論 ACID 時的一般意思。正是出於這個原因,我開始孤立地分析這個首字母縮略詞,並沒有像那些試圖解釋這個概念的人通常那樣按順序進行。現在讓我們看看剩下的三個字母。

再次回顧我們的銀行轉賬示例。將資金從一個賬戶轉移到另一個賬戶的交易包括從第一個賬戶提取操作和對第二個賬戶進行補充操作。如果第二個賬戶的充值操作失敗,您可能不希望第一個賬戶的取款操作發生。換句話說,要么交易完全成功,要么根本沒有發生,但不能只對某部分進行交易。此屬性稱為“原子性”,它是 ACID 中的“A”。

當我們的事務被執行時,就像任何操作一樣,它將數據庫從一個有效狀態轉移到另一個有效狀態。一些數據庫提供所謂的約束——即適用於存儲數據的規則,例如,關於主鍵或輔助鍵、索引、默認值、列類型等。因此,在進行交易時,我們必須確保所有這些約束都得到滿足。

這種保證在 ACID 中稱為“一致性”和一個字母C(不要與分佈式應用程序世界中的一致性混淆,我們將在後面討論)。我將給出一個關於 ACID 意義上的一致性的明確示例:在線商店的應用程序想要orders向表中添加一行,並且表中的IDproduct_id將在列中指示- typical 。productsforeign key

例如,如果產品已從分類中移除,並相應地從數據庫中移除,則行插入操作不應發生,我們將收到錯誤消息。在我看來,與其他保證相比,這種保證有點牽強 - 如果僅僅是因為主動使用數據庫的約束意味著轉移數據的責任(以及業務邏輯的部分轉移,如果我們正在談論從應用程序到數據庫的諸如 CHECK 之類的約束,正如他們現在所說的那樣。

最後,它仍然是D- “阻力”(耐用性)。系統故障或任何其他故障不應導致交易結果或數據庫內容丟失。也就是說,如果數據庫回復交易成功,那麼這意味著數據被記錄在非易失性存儲器中——例如,在硬盤上。順便說一句,這並不意味著您將立即看到下一個讀取請求的數據。

就在前幾天,我正在使用來自 AWS(亞馬遜網絡服務)的 DynamoDB,並發送了一些數據進行保存,在收到答案HTTP 200(OK)或類似的東西後,我決定檢查它 - 但沒有看到這個數據庫中接下來 10 秒的數據。也就是說,DynamoDB 提交了我的數據,但並非所有節點都立即同步以獲取數據的最新副本(儘管它可能已經在緩存中)。在這裡,我們再次進入了分佈式系統上下文中一致性的領域,但是談論它的時候還沒有到來。

所以現在我們知道什麼是 ACID 保證了。我們甚至知道它們為什麼有用。但是我們真的在每個應用程序中都需要它們嗎?如果不是,具體是什麼時候?是否所有數據庫都提供這些保證,如果沒有,它們提供什麼?