事務特性(ACID)
- 原子性 Atomicity。每個事務中的操作,要麼都成功,要麼都失敗
- 一致性 Consistency。事務執行前後,數據庫中的數據應該保持一致
- 隔離性 Isolation。事務之間應該是隔離的,事務之間互不影響、干擾
- 持久性 Durability。事務一旦提交,便會將修改持久化到數據庫
數據庫事務隔離級別
- 讀未提交:read uncommitted
- 讀已提交:read committed
- 可重複讀:repeatable read
- 串行化:serializable
事務帶來的問題
髒讀:讀取了其他事務未提交的數據
舉例:當前事務隔離級別read uncommitted
,開啓兩個事務A、B。
ID爲1的ACCOUNT爲100.
//事務A
start transaction;
update salary SET ACCOUNT = 300 WHERE ID = 1;
SELECT SLEEP(10);
ROLLBACK;
SELECT * FROM salary;
//事務B
start transaction;
SELECT * FROM salary;
COMMIT;
事務B讀取的ACCOUNT爲300,實際這是個A事務產生的髒數據。
由此可見,read uncommitted
會出現髒讀。
不可重複讀:同一事務內多次讀取同一數據出現不一致
舉例:當前事務隔離級別read committed
,開啓兩個事務A、B。
//事務A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
//事務B
start transaction;
SELECT * FROM salary;
update salary SET ACCOUNT = 300 WHERE ID = 1;
COMMIT;
事務A兩次查詢的結果不一致,第一次ACCOUNT爲100,第二次爲300.
那read committed
是否會出現髒讀的情況呢,執行前面示例中的語句,會發現事務B讀取的還是100,沒有讀到A產生的髒數據。
綜上,read committed
可以解決髒讀,但是不能解決不可重複讀。
幻讀:像是讀取了不存在的數據
舉例:當前事務隔離級別repeatable read
,開啓兩個事務A、B
//事務A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
//事務B
start transaction;
DELETE FROM salary WHERE ID = 1;
COMMIT;
事務A兩次查詢的結果一致,都有ID爲1的數據,說明repeatable read
解決了不可重複讀。但是實際上事務B中把ID爲1的數據已經刪除並提交了,事務A第二次查詢仍然能看到這個實際不存在的數據。
如果此時事務A在開啓事務後,並沒有立即查詢,即事務B的操作在事務A的第一次查詢前完成
//事務A
start transaction;
SELECT SLEEP(10);
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;
此時兩次查詢的結果和事務B執行後的結果一致,沒有出現幻讀。repeatable read採用快照讀解決不可重複讀的問題,即事務中第一次查詢時讀取一個快照,而不是開啓事務就讀取快照,所以看到兩次執行結果會有不同。
如果兩個事務同時插入一條ID爲2的數據
//事務A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;
//事務B
start transaction;
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;
事務B插入成功,事務A查詢的時候沒有ID爲2的數據,插入數據的時候提示重複主鍵。此時就是產生了幻讀。
不可重複讀VS幻讀
-
不可重複讀:針對已存在數據的多次讀取結果不一致,update產生的影響
-
幻讀:針對數據量的前後不一致,有新增和刪除數據,insert和delete產生的影響
鎖
Record Lock
作用於索引上的鎖,而不是行記錄本身。
Gap Lock
作用於索引之間的鎖,不鎖索引本身。
Next-Key Lock
repeatable read默認採用的鎖。Record Lock+record之前的Gap lock。
locking read:當前讀
select xxx for update
select xxx lock in share mode
locking read、update、delete操作時
- 當檢索條件爲非唯一索引時,需要獲取next-key鎖或gap鎖
- 當檢索條件爲主鍵或唯一索引,獲取record鎖
- 當檢索條件沒有任何索引,全表掃描
關於next-key、gap的詳細分析見理解innodb的鎖、mysql鎖詳解
讀鎖只能和讀鎖並行,讀鎖和寫鎖、寫鎖和寫鎖之間不能並行。比如兩個請求可以同時讀數據,但是不能一個讀一個寫或者兩個都寫。
MVCC
MVCC(Multi-Version Concurrency Control),多版本併發控制。通過版本控制消除一些不必要的加鎖操作,實現不加鎖也可以讀寫並行,提供了併發能力。
InnoDB的MVCC是通過在每行記錄的後面保存兩個隱藏的列來實現的,一個保存了行創建時間,一個保存了過期時間。其中保存的並不是實際時間,而是系統版本號。每開始一個新事務,系統版本號遞增。
MVCC只在read committed和repeatable read下工作。read uncommitted總是讀取最新行,serializable則是對所有讀取行加鎖。
repeatable read下的MVCC:
- SELECT
1、InnoDB只查找版本早於當前事務版本的數據行(行的系統版本號小於等於當前事務的系統版本號)。確保當前事務讀取的行都是在事務開始前已提交的,或者是事務本身修改的。
2、行的刪除版本號要麼未定義,要麼大於當前事務的版本號。確保事務讀取的數據都是在事務開始時存在的數據。
以上兩個條件均滿足,才能作爲查詢結果返回。
- INSERT
InnoDB新插入一行時,將當前系統版本號作爲行版本號,刪除版本號此時爲未定義。
- DELETE
InnoDB刪除一行時,將當前系統版本號作爲該行的刪除版本號。
- UPDATE
InnoDB插入一行數據,保存當前系統版本號作爲行版本號,同時將當前系統版本號保存到原來行的刪除版本號。
ReadView
所有開啓的事務都會被維護在一個事務鏈表中,當事務結束後,從事務鏈表中刪除。
ReadView就是當開啓事務時,對當前事務鏈表的一個快照。其中包含了4類信息:
- m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。
- min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。
- creator_trx_id:表示生成該ReadView的事務的事務id。
比如事務鏈中有1、2、3,3提交了,那麼此時一個新事務生成ReadView,m_ids就是1、2,min_trx_id是1,max_trx_id是4,creator_trx_id是4.
根據ReadView就可以判斷當前事務可以看到哪些數據:
- 如果某行的版本號小於min_trx_id,表示ReadView創建時,該行已經被提交,可以被訪問。
- 如果某行的版本號大於max_trx_id,表示ReadView創建時,修改該行數據的事務還未創建,不可被訪問。
- 如果某行的版本號大於min_trx_id並小於max_trx_id,則看m_ids是否存在該版本號,如果存在(如上面的事務2),表示ReadView創建時,事務還未提交,不可訪問;如果不存在(如上面的事務3),表示ReadView創建時,事務已提交,可以被訪問。
- 如果某行的版本號等於creator_trx_id,表示是當前事務自己修改的數據,可以被訪問。
read committed在每次讀取數據時生成ReadView
。這也就是read committed不可重複讀的原因,
repeatable read在開啓事務後的第一次讀取時生成ReadView
。所以上面的例子中開啓事務後立即讀和不立即讀結果會不一樣,就是因爲兩次生成的ReadView不一樣。由於只生成一次ReadView,所以可以實現重複讀。
Redo log和Undo log
事務隔離性由鎖來實現,原子性和、一致性和持久性由redo log和undo log保證。
Redo log
redo log記錄的是物理上的數據頁變化,而不是邏輯上的操作。
Redo log又可以分爲兩部分:內存中的日誌緩衝(redo log buffer)和磁盤中的重做日誌(redo log file)。
關係如圖
- 從磁盤中讀取數據到內存拷貝
- 修改數據的內存拷貝
- 內存中記錄redo log
- 提交事務
- 將內存中的redo log持久化,寫入到磁盤中
- 後臺線程同步,非實時的
內存中的修改是先修改數據,再生成redo log,而持久化時相反,先持久化redo log,再持久化數據。爲了性能,不可能每次數據修改完成後都實時持久化數據,所以採用了異步的方式。那麼當這個同步的過程中如果宕機了,由於先持久化了redo log,就可以根據redo log恢復數據。
Undo log
undo log記錄邏輯上的修改,比如一個insert操作生成一個delete的undo log,便於進行回滾數據,所以在修改數據前就要生成undo log。
redo log和undo log的生成過程:
假設有A、B兩個數據,值分別爲1,2.
- 事務開始
- 記錄A=1到undo log
- 修改A=3
- 記錄A=3到 redo log
- 記錄B=2到 undo log
- 修改B=4
- 記錄B=4到redo log
- 將redo log寫入磁盤
- 事務提交
注意:undo log不是redo log的逆向,redo log注重數據上的修改,undo log注重邏輯上的修改。
參考
- 《高性能Mysql》
- MySQL事務
- 淺析MySQL事務中的redo與undo
- mysql版本鏈和readView原理