《設計資料密集型應用》- Designing Data-Intensive Application - 第7章 事務 讀書筆記
事務
事務的棘手性
ACID
單物件和多物件操作
-
單物件寫入
-
對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性
- 原子性通過日誌做崩潰恢復
- 隔離性通過加鎖實現
-
-
多物件事務
- 關係型資料庫更新帶有外來鍵的表
- 文件型資料同時更新多份文件
- 二級索引與資料的同時更新
-
處理錯誤和中止
- 對於已經提交的事務,如果在給客戶端返回結果的網路中斷,如果此時重試事務,則需要去重機制
- 如果是由網路擁塞導致的重試,則需要限制重試次數
- 非必要問題不用重試,如違反約束不需要重試
- 兩階段提交
弱隔離級別
相比可序列化的強隔離性之外都算做弱隔離級別
讀已提交
-
髒寫
- 同時寫入後面的寫入覆蓋前一個
- 解決方法:加行鎖
-
髒讀
-
讀到未提交的資料
-
解決方法
-
與解決髒寫的方法一樣,加行鎖,但如果寫事務資料時間過長,讀請求會有較大的時間延遲
-
儲存資料的舊值,在事務未提交時讀到舊值,事務提交後讀到新值
- 如果讀取事務的時間範圍內出現了寫事務的提交,則讀事務中相同的兩次或多次讀取事件所讀到的資料可能會不同
-
-
快照隔離和可重複讀
-
支援資料庫:PostgreSQL oracle MySQL(innodb) SQL Server
-
實現快照隔離
-
對於PostgreSQL,按如下方法實現MVCC
- 基於txid事務ID實現,每個事務開始時均有一個唯一的TXID,約40億次後溢位
- 表中每一行資料都有created_by與deleted_by欄位,分別儲存寫入時的txid
- 刪除時標記deleted_by,不實際刪除,在稍候的時間,當確定沒有事務再訪問已標記刪除的行時,垃圾回收器將該行移除,釋放空間
- 對於UPDATE操作實際執行了兩步操作:DELETE INSERT
-
拓展(非本書內容):MYSQL5.6 MVCC實現
-
僅存在於讀已提交與可重複讀兩種隔離級別,特別地,對於可重複讀,使用了MVCC+行鎖實現,讀時不加鎖,寫時加鎖
-
在MYSQL中如果事務更改了某行,則會針對該行生成UNDOLOG(記錄了該行之前的記錄內容),在發生回滾時,將UNDOLOG該行的記錄恢復
-
行鎖 間隙鎖
- MYSQL:基於事務ID,比如undolog中存了v7這條回滾資料,而現在最小的活躍事務ID是v8,那v7這條資料就不可能再被讀到,就可以刪除了。
-
-
-
-
觀察一致性快照的可見性規則
- 列出所有尚且未提交或尚未終止的事務列表,即使這些事務後來提交了,忽略這些寫入
- 具有較晚事務ID的都將被忽略
-
索引和快照隔離
- PostgreSQL:同一物件的不同版本可以放入同一個頁面中,PostgreSQL的優化可以避免更新索引
- CouchDB,Datomic和LMDB:為修改頁面建立一個索引副本,從父級到根進行級聯指向更新指向新版本
-
可重複讀與命名混淆
防止丟失更新
-
定義
- 併發寫入衝突(丟失)問題(併發的資料庫讀值–更改資料–寫回資料操作)
-
解決方法
-
原子寫
-
如UPDATE XX SET a=a+1
-
實現方案
- 在物件上加排它鎖
- 強制原子操作在單一的執行緒上執行
-
-
-
顯式鎖定
-
在查詢範圍上加排它鎖
-
-
自動檢測丟失更新
- PostgreSQL的可重複讀,Oracle的可序列化和SQL Server的快照隔離級別,都會自動檢測到丟失更新,並中止惹麻煩的事務。但是,MySQL/InnoDB的可重複讀並不會檢測丟失更新
-
Compare and Set
-
衝突解決和複製
- 允許寫入多個衝突值,並在應用層進行合併
-
寫入偏差與幻讀
-
場景
- 一個SELECT查詢找出符合條件的行,並檢查是否符合一些要求。
- 按照第一個查詢的結果,應用程式碼決定是否繼續。
- 如果應用決定繼續操作,就執行寫入(插入、更新或刪除),並提交事務。
- 如兩個人同時搶購1件商品,二人都先查到還要一件庫存,在應用程式當中,二者均向資料庫發出減庫存的寫入請求,此時會出現庫存異常
-
快照隔離解決了讀事務中出現的幻讀情況,但會出現寫入偏差
-
-
可序列化
背景:
- 很難檢查程式碼在特定隔離級別下是否安全
- 併發測試困難
真的序列執行
-
單執行緒執行併發事務
-
產生背景
- RAM足夠便宜,可將事務需要的活躍的資料集放在RAM當中,而不是之前放在磁碟上,執行效率提高了
- OLTP事務通常執行速度很快,只進行少量的讀寫操作
-
例項:VoltDB/H-Store,Redis和Datomic
-
在儲存過程中封裝事務
-
儲存過程的優缺點
-
分割槽
- 對於可以把資料劃分為多個分割槽,並且每個事務只在一個分割槽上進行,即可實現事務隨分割槽數量增加而線性擴充套件。對於跨分割槽的事務,則不能通過增加機器提升效能
二階段鎖定
-
相比較快照隔離,二階段鎖定基於讀不阻塞寫,寫阻塞讀的方案,解決了寫入偏差與丟失更新
-
實現2PL
-
分為共享鎖與排它鎖
- 多個事務可同時持有共享鎖
- 有一個寫入事件則升級共享鎖為排它鎖,該鎖阻塞其它讀取與寫入,直到事務完成
- 效能相比較弱隔離級別差得多
-
-
謂詞鎖
-
為了防止範圍查詢出現幻讀的情況,需要一種範圍鎖(非指定物件,比如表中某一行的鎖),需要實現以下兩點
- 排它阻塞讀–任一讀取事務必須等到持有排它鎖的事務結束後才可進行讀取操作
- 任意一種鎖(排它與共享)阻塞排它鎖
-
-
索引範圍鎖(間隙鎖)
-
謂詞鎖由於效能不佳(鎖粒度過小,檢查鎖匹配非常耗時),多數資料庫實現了索引範圍鎖
- 如房間預訂請求,查詢條件為某時間段與房間號,如果表分別在時間段和房間號建立了索引,則索引範圍鎖會將其中一個索引(時間段或房間號)上附加共享鎖,如果其它事務的寫入操作需要修改該索引,則它會等到上述共享鎖釋放
-
可序列化快照隔離
-
在快照隔離的基礎上,SSI添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。(樂觀鎖)
- 如果出現了大併發寫入事務爭搶同一寫入物件,會導致一大部分寫入事務需要終止,如果系統已經接近最大吞吐,則重試事務可能會導致效能變差
-
快照隔離會出現寫入偏差,檢測方法有兩種
-
檢測對舊MVCC物件版本的讀取
-
在提交事務時檢測是否有任何被忽略的寫入現在已經被提交,如果有則終止事務並重試;如果其它提交的事務只包含查詢,由於其不會產生寫入偏差,則不中止事務
-
-
檢測影響先前讀取的寫入
-
藉助索引,如果事務中的UPDATE語句的WHERE條件列有索引,則事務提交時記錄下該修改值,在其它事務也基於該WHERE條件進行更新時,則通知該事務所讀取的資料已經不是最新的
-
-
-
效能
- 在SSI中,一個事務不需要阻塞等待另一個事務所持有的鎖,與兩階段鎖定相比,其查詢延遲可預測更好;與序列執行相比,其效能不侷限於單個CPU核的吞吐量