mysql事務、鎖、MVCC

事務特性(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類信息:

  1. m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。
  2. min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
  3. max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。
  4. 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就可以判斷當前事務可以看到哪些數據:

  1. 如果某行的版本號小於min_trx_id,表示ReadView創建時,該行已經被提交,可以被訪問。
  2. 如果某行的版本號大於max_trx_id,表示ReadView創建時,修改該行數據的事務還未創建,不可被訪問。
  3. 如果某行的版本號大於min_trx_id並小於max_trx_id,則看m_ids是否存在該版本號,如果存在(如上面的事務2),表示ReadView創建時,事務還未提交,不可訪問;如果不存在(如上面的事務3),表示ReadView創建時,事務已提交,可以被訪問。
  4. 如果某行的版本號等於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)。
關係如圖
在這裏插入圖片描述

  1. 從磁盤中讀取數據到內存拷貝
  2. 修改數據的內存拷貝
  3. 內存中記錄redo log
  4. 提交事務
  5. 將內存中的redo log持久化,寫入到磁盤中
  6. 後臺線程同步,非實時的
    內存中的修改是先修改數據,再生成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.

  1. 事務開始
  2. 記錄A=1到undo log
  3. 修改A=3
  4. 記錄A=3到 redo log
  5. 記錄B=2到 undo log
  6. 修改B=4
  7. 記錄B=4到redo log
  8. 將redo log寫入磁盤
  9. 事務提交

注意:undo log不是redo log的逆向,redo log注重數據上的修改,undo log注重邏輯上的修改

參考

  1. 《高性能Mysql》
  2. MySQL事務
  3. 淺析MySQL事務中的redo與undo
  4. mysql版本鏈和readView原理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章