資料的一致性是資料準確的重要指標,那如何實現資料的一致性呢?本文從事務特性和事務級別的角度和大家一起學習如何實現資料的讀寫一致性。
1.資料的一致性:通常指關聯資料之間的邏輯關係是否正確和完整。
舉個例子:某系統實現讀寫分離,讀資料庫是寫資料庫的備份庫,小李在系統中之前錄入的學歷資訊是高中,經過小李努力學習,成功獲得了本科學位。小李及時把資訊變成成了本科,可是由於今天系統備份時間較長,小李變更資訊時,資料已經開始備份。公司的HR通過系統查詢小李資訊時,發現還是本科,小李的申請被駁回。這就是資料不一致問題。
2.資料庫的一致性:是指資料庫從一個一致性狀態變到另一個一致性狀態。這是事務的一致性的定義。
舉個例子:倉庫中商品A有100件,門店中商品A有10件。上午10點,倉庫傳送商品A50件到門店,最後倉庫中有商品A50件,門店有商品A60件,這樣商品的總是是不變的。不能門店收到貨後,倉庫的商品A還是100件,這樣就出現資料庫不一致問題。倉庫和門店商品A的總數是110才是正確的,這就是資料庫的一致性。
資料庫事務( transaction)是存取並可能操作各種資料項的一個資料庫操作序列,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部資料庫操作組成。
事務的性質:
資料庫在並行環境下會出現髒讀、重複讀和幻讀問題。
事務A讀取了事務B未提交的資料,如果事務B回滾了,事務A讀取的資料就是髒的。
舉例:訂單A需要商品A20件,訂單B需要商品A10件。倉庫中有商品A庫存是20件。訂單B先查詢,發現庫存夠,進行扣減。在扣減的過程中,訂單A進行查詢,發現庫存只有10個不夠訂單數量,丟擲異常。這時候訂單B提交失敗了。庫存數量又變成20了。這時候,倉庫人員去查庫存,發現數量是20,可是訂單A卻說庫存不足,這就讓人很奇怪。
復讀指的是在一個事務內,最開始讀到的資料和事務結束前的任意時刻讀到的同一批資料出現不一致的情況。
舉例:庫房管理員查詢商品A的數量,讀取結果是20件。這是訂單A出庫,扣減了商品10件。這時管理員再去查商品A時,發現商品A的數量時10件和第一此查詢的結果不同了。
事務A在執行讀取操作,需要兩次統計資料的總量,前一次查詢資料總量後,此時事務B執行了新增資料的操作並提交後,這個時候事務A讀取的資料總量和之前統計的不一樣,就像產生了幻覺一樣,平白無故的多了幾條資料,成為幻讀。
舉例:操作員查詢可生產單量10個,呼叫介面下發10個訂單,事務A增加10個訂單。操作員獲取10個訂單落庫,查詢 發現變成30個訂單。
Read Uncommitted(未提交讀)
一個事務可以讀取到其他事務未提交的資料,會出現髒讀,所以叫做 RU,它沒有解決任何的問題。
Read Committed(已提交讀)
一個事務只能讀取到其他事務已提交的資料,不能讀取到其他事務未提交的資料,它解決了髒讀的問題,但是會出現不可重複讀的問題。
Repeatable Read(可重複讀)
它解決了不可重複讀的問題,也就是在同一個事務裡面多次讀取同樣的資料結果是一樣的,但是在這個級別下,沒有定義解決幻讀的問題。
Serializable(序列化)
在這個隔離級別裡面,所有的事務都是序列執行的,也就是對資料的操作需要排隊,已經不存在事務的並行操作了,所以它解決了所有的問題。
有兩個方案可以解決讀一致性問題:基於鎖的並行操作(LBCC)和基於多版本的並行操作(MVCC)
既然要保證前後兩次讀取資料一致,那麼讀取資料的時候,鎖定我要操作的資料,不允許其他的事務修改就行了。這種方案叫做基於鎖的並行控制 Lock Based Concurrency Control(LBCC)。
LBCC是通過悲觀鎖來實現並行控制的。
如果事務A對資料進行加鎖,在鎖釋放前,其他事務就不能對資料進行讀寫操作。這樣並行呼叫,改成了順序呼叫。對目前的大多數系統來說,效能完全不能滿足要求。
要讓一個事務前後兩次讀取的資料保持一致,那麼我們可以在修改資料的時候給它建立一個備份或者叫快照,後面再來讀取這個快照就行了。不管事務執行多長時間,事務內部看到的資料是不受其它事務影響的,根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的資料可能是不一樣的。這種方案我們叫做多版本的並行控制 Multi Version Concurrency Control(MVCC)。
MVCC是基於樂觀鎖的。
在 InnoDB 中,MVCC 是通過Undo log中的版本鏈和Read-View一致性檢視來實現的。
undo log是innodb引擎的一種紀錄檔,在事務的修改記錄之前,會把該記錄的原值先儲存起來再做修改,以便修改過程中出錯能夠恢復原值或者其他的事務讀取。undo log是一種用於復原回退的紀錄檔,在事務沒提交之前,MySQL會先記錄更新前的資料到 undo log紀錄檔檔案裡面,當事務回滾時或者資料庫崩潰時,可以利用 undo log來進行回退。
對資料變更的操作不同,undo log記錄的內容也不同:
undo log版本鏈
每條資料有兩個隱藏欄位,trx_id 和 roll_pointer,trx_id表示最近一次事務的id,roll_pointer表示指向你更新這個事務之前生成的undo log。
事務ID:MySQL維護一個全域性變數,當需要為某個事務分配事務ID時,將該變數的值作為事務id分配給事務,然後將變數自增1。
舉例:
所以當多個事務序列執行的時候,每個事務修改了一行資料,都會更新隱藏欄位trx_id 和 roll_pointer,同時多個事務的undo log會通過roll_pointer指標串聯起來,形成undo log版本鏈。
InnoDB為每個事務維護了一個陣列,這個陣列用來儲存這個事務啟動的瞬間,當前活躍的事務ID。這個陣列裡有兩個水位值: 低水位(事務ID 最小值)和 高水位(事務ID 最大值 + 1);這兩個水位值就構成了當前事務的一致性檢視(Read-View)
ReadView中主要包含4個比較重要的內容:
有了這些資訊,這樣在存取某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:
快照讀又叫一致性讀,讀取的是歷史版本的資料。不加鎖的簡單的SELECT都屬於快照讀,即不加鎖的非阻塞讀,只能查詢建立時間小於等於當前事務ID的資料或者刪除時間大於當前事務ID的行(或未刪除)。
當前讀查詢的是記錄的最新資料。加鎖的SELECT、對資料進行增刪改都會進行當前讀。
如圖所示:
事務A id =1 初始化了資料
事務B id=2 進行了查詢操作(MVCC唯讀取建立時間小於當前事務ID的資料或者刪除時間大於當前事務ID的行)
事務B的結果是 (商品A:10,商品B:5)
事務C id =3 插入了商品C
事務B id=2 進行了查詢操作(MVCC唯讀取建立時間小於當前事務ID的資料或者刪除時間大於當前事務ID的行)
事務B的結果是 (商品A:10,商品B:5)
事務D id =4 刪除商品B
事務B id=2 進行了查詢操作(MVCC唯讀取建立時間小於當前事務ID的資料或者刪除時間大於當前事務ID的行)
事務B的結果是 (商品A:10,商品B:5)
事務E id =4 修改商品A的數量
事務B id=2 進行了查詢操作(MVCC唯讀取建立時間小於當前事務ID的資料或者刪除時間大於當前事務ID的行)
事務B的結果是 (商品A:10,商品B:5)
所以當事務E提交後,當前讀獲取的資料和事務B讀取的快照資料明顯不同。
MVCC可以很好的解決讀一致問題,只能看到這個時間點之前事務提交更新的結果,而不能看到這個時間點之後事務提交的更新結果。而且降低了死鎖的概率和解決讀寫之間堵塞問題。
LBCC和MVCC都可以解決讀一致問題,具體使用哪種方式,要結合業務場景選擇最合適的方式,MVCC和鎖也可以結合使用,沒有最好只有更好。