ACID 是軟體領域使用最廣泛的技術之一,它是關聯式資料庫的基石,是企業級中介軟體不可或缺的部分,但通常通過黑盒的方式提供。但是在許多情況下,這種古老的事務方式已經不能夠適應現代大規模系統和NoSQL資料庫的需要了,現代系統要求更高的效能要求,更大的資料量,更高的可用性。在這種情況下,傳統的事務模型被定製的事務或者半事務模型所取代,而在這些模型中事務性並不像以往那樣被看重。
在本文中我們會討論一下key-value資料庫的無鎖事務操作,這種技術可以廣泛應用於任何一種資料庫系統。在GridDynamics中,我們就用這種技術在Oracle Coherence上實現了一個輕量級的非標準的事務機制。在第一部分我們會通過幾個重要的用例來了解兩種簡單的方法,在第二部分我們會研究更多更通用的方法,比如說PostgreSQL的MVCC實現。
原子性快取切換,讀提交隔離
讓我們從一個簡單易於實現的方法開始,這個方法適用於讀遠多於寫的系統。比如說電子商務系統中每天要進行的資料更新,一些管理性操作例如無效貨品的修復以及快取更新。
最簡單的例子是把所有資料都載入進快取裡,然後通過一個代理介面來執行諸如 get() 和 put() 這樣的操作。這個介面會與兩個快取打交道,A和B,按照以下邏輯執行(圖 1):
- 任何時候只能有一個快取處於可用狀態,代理介面會把所有的請求路由給它(圖1.1)。
- 更新資料的時候把新資料載入到目前不可用的快取中(圖1.2)。
- 更新程式切換標誌哪個快取可用的標記(圖1.3),代理介面開始把新的讀請求分發到新標記為可用的快取。
- 快取切換階段的事務可以依據不用的永續性和隔離性要求來分別處理。如果允許“不可重複讀” ,那麼切換很簡單,老資料會被立刻清理掉。否則,代理介面會維護一個仍未結束的事務列表,並把屬於這個列表中的每一個請求都路由到原來的快取中。只有當列表中的所有事物都提交或者放棄之後老資料才會被清空。
相同的技術也可用於部分更新。依據儲存方式的不同也有多種實現方法,我們來看一個有三個快取簡單例子。這個例子中的框架遇上一個類似,但是代理介面按照以下邏輯執行(圖 2):
- 使用者請求被路由到主快取(”PRIMARY”快取)(圖 2.1)
- 新增資料和更新資料載入進2號快取(“NEW”快取),刪除項的key放入3號快取(”DELETE”快取)(圖2.2)
- 提交程式(特指寫事務)切換全域性標示,這個標示會告訴代理介面先去”NEW”和”DELETE”快取去查詢所請求的資料,如果在這兩個區域中沒有發現再去”PRIMARY”快取查詢(圖2.3)。換句話說,在這一步所有的請求都被改派到了更新過的資料中查詢。
- 提交程式將 NEW 和 DELETE 區域的變化傳遞給PRIMARY。也即在PRIMARY快取區以非原子的方式更新、增加、刪除資料項(圖2.4)。
- 最後,所有的提交程式把全域性標識切換回來,所有的請求仍然路由到 PRIMARY 快取區域(圖2.5)。
- 在第4步,可以把老資料拷貝到另一個快取區,這樣就可以支援回滾操作。即使是全量更新也可以用這種方法。
從上面的兩個例子我們可以看出,專用於讀的資料快照避免了資料更新的干擾,大大降低了複雜性。在一個寫密集型的環境中就不容易做到這一點了。在下一節我們會討論一種非常好的方法可以完美的解決這個問題。
MVCC 事務,可重複讀隔離
事物間的隔離可以通過給資料項加上版本號來實現。有許多方法能做到這一點,下面我們會介紹一種與PostgreSQL 的事務處理方法非常相似的辦法。
正如前面所說,每個事務可以對應於一個部分資料快照。在同一時間,每一個資料項都有他自己的生命週期 – 從加入快取到移出快取或者被更新(被新版本所取代)。所以可以通過給每條資料打兩個時間戳來實現隔離,每個事物通過開始時間(兩個時間戳之一,譯者注)來找出在事務開始時處於可見狀態的資料。但在實踐中常用一個單調遞增的計數來代替時間戳:
- 當新事務開始的時候:
- 它會獲得一個全域性唯一且單調遞增的事務ID ,也叫 XID。
- 程式裡儲存著所有事務的XID.
- 快取裡的每個資料項有兩個額外標記,xmin 和 xmax。按照以下規則賦值:
- 當資料項被某個事務建立的時候, xmin 設定為該事務的XID ,xmax 無值。
- 當資料被某個事務移除的時候,xmin 不變,xmax 設定為該事務的XID。資料並沒有真的從快取中清除,只是被標記為已刪除。
- 當資料被某個事務更新的時候,老資料仍然儲存在快取裡,xmax 被賦值為事務的XID,同時增加一條新的資料,新資料的 xmin 也賦值為XID 並且xmax 為空。換句話說更新操作等於一次刪除加一次增加。
- 如果以下兩個條件成立,那麼資料對於某次事務是可見的:
- xmin 有值並且小於或等於當前事務ID。
- xmax 為空,或者等於未提交事務(放棄的或者還未完成的)的XID ,或者大於當前事務ID。
- xmin 和 xmax 可以儲存兩個位標記,表明事務是否放棄或者提交,這樣才能進行上面的檢查(xmax 是否等於未提交事務的ID)。
邏輯如下圖所示:
這種方法的缺點是廢棄資料的移除有些繁瑣。因為不同事務看到的資料版本不同,決定何時將資料標為不可見或者移除是比較複雜的。不過也有兩種以上的方法能夠做到,第一種是PostgreSQL中使用的,第二種是Oracle使用的:
- 所有的版本都儲存在同一個key-value空間中,對版本數量沒有限制(也即可以儲存任意多的版本,譯者注)。由一個後臺程式來回收老版本資料,這個回收可以按計劃排程執行也可以再讀或者寫的時候觸發。
- 主key-value 空間只儲存最新的版本,之前的版本儲存在另外的地方,且儲存老版本的空間大小是固定的。 最新的版本會指向之前的版本,但是卻不能夠由此上溯到之前的任意版本, 因為儲存老版本資料的區域大小是固定的, 太早的版本會被移除。如果某個事務不能夠找到指定版本的資料就會失敗。