MySQL事務(4種事務隔離級別、髒寫、髒讀、不可重複讀、幻讀、當前讀、快照讀、MVCC、事務指標監控)

小松聊PHP进阶發表於2024-03-10

宣告測試表,供文章案例使用

CREATE TABLE `cs` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `num` int(10) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

事務的分類

  • 顯示事務:
    • read write:讀寫事務,預設模式,表示當前事務可以讀寫資料。
    • read only:只讀事務,很少用,表示當前事務不能修改資料。
    • with consistent snapshot:一致性快照,在資料庫事務中確保事務在執行過程中能看到一個事務開始時的一致資料庫狀態,避免被其他併發操作影響。
  • 隱式事務:不需要顯示宣告事務相關語句,autocommit是開啟狀態(預設值),每條DML和DDL的SQL語句都是一個獨立的事務。

MySQL事務的4個特性:

  • 原子性(Atomicity):當前事務中的執行結果,要麼全部執行成功,要麼全部執行失敗。
  • 一致性(Consistency):事務執行前後,資料庫從一個合法(指符合業務預期)狀態轉換成另一個一合法狀態。
  • 隔離性(Isolation):多個事務可以併發執行,各個事務之間的操作互相隔離互不干擾。
  • 永續性(Durability):無論事務提交還是回滾,都會持久化到磁碟中。

自動提交

自動提交(auto commit),指的是SQL語句執行完畢後自動將資料持久化到磁碟(刷盤)中。
顯式的宣告事務,或者宣告set autocommit = 0;都可以關閉自動提交。

savepoint

  • 俗稱儲存點,是用於實現部分事務回滾的一種機制,需要確定從哪裡開始回滾,就需要savepoint的標識來定位。
  • 回滾範圍:從儲存點開始到事務最後一條SQL,都會被回滾。
  • 適用場景:用於複雜的業務邏輯中,給出靈活可控的後悔藥,降低事務回滾影響範圍。
  • 注意:rollback to之後,不代表事務流程走完,還需要再次commit提交其它未回滾的事務。
  • 用法:
    savepoint 儲存點名:建立一個 Savepoint,併為其指定一個名稱。
    rollbackto savepoint 儲存點名;:將事務回滾到指定的 Savepoint。
    release savepoint 儲存點名;:釋放指定的 Savepoint。
    示例:
start transaction;
insert into cs(num) values(1);
savepoint insert_1;
insert into cs(num) values(2);
savepoint insert_2;
insert into cs(num) values(3);
savepoint insert_3;
insert into cs(num) values(4);
savepoint insert_4;
rollback to insert_2;
commit;
發現1,2資料被插入。

事務的隱式提交

在上一個事務沒提交或回滾時,執行下一個事務,則上一個事務自動提交。

start transaction;
insert into cs(num) values(1);
insert into cs(num) values(2);
start transaction;
insert into cs(num) values(3);
insert into cs(num) values(4);
commit;
成功插入1,2,3,4。

4種隔離級別

  • 讀未提交(Read Uncommitted):最低階別的隔離,事務中的修改即使未提交也能被其他事務看到,可能導致髒讀、不可重複讀和幻讀問題。
  • 讀已提交(Read Committed):保證一個事務提交後對其他事務可見,避免了髒讀,但可能會導致不可重複讀和幻讀問題。
  • 可重複讀(Repeatable Read):保證在同一事務內多次讀取資料時,資料保持一致,避免了不可重複讀問題,但仍可能出現幻讀。
  • 序列化(Serializable):最高階別的隔離,透過對讀取的資料新增共享鎖或排他鎖來確保事務之間的隔離性,避免了髒讀、不可重複讀和幻讀問題,但可能會影響併發效能。

表格從上到下,越來越高可用,但是效能越來越低。

隔離級別 是否解決髒讀 是否解決不可重複讀 是否解決幻讀 是否加鎖
讀未提交
讀已提交
可重複讀
序列化

檢視或設定MySQL隔離級別

  • 檢視:select @@transaction_isolation; 或者 show variables like 'transaction_isolation;'
  • 設定:set session transaction_isolation = 'read-uncommitted/read-committed/repeatable-read/serializable';
    注意隔離級別是回話級別的,所以無法set glboal。

MySQL會發生什麼讀?

因為mysql預設隔離級別是可重複讀(Repeatable Read),所以只會發生幻讀情況,髒讀和可重複度不會發生,除非改事務隔離級別。

髒寫(不允許發生)

  • 簡介:一個事務修改某些資料時,另一個事務在未提交的情況下也修改了這些資料,引起的導致資料的不一致性。
  • 危害:造成資料在併發情況下嚴重不一致。
  • 演示:試不出來,髒寫這麼嚴重的bug,是不允許發生的情況。

髒讀(讀未提交隔離級別會發生)

  • 簡介:一個事務尚未commit(提交,刷盤,持久化),卻讀取了事務修改後的值,引起資料讀取不準確的情況。
  • 危害:事務還未提交就被讀取了,該事務成功提交還好,要是回滾了,會造成讀取資料不一致的問題。
  • 演示:因為髒讀是讀未提交(Read Uncommitted)才會發生的情況,所以要降低MySQL的隔離級別。
步驟 會話A 會話B 備註
1 set session transaction_isolation = 'read-committed'; set session transaction_isolation = 'read-committed'; 設定事務的隔離級別為讀未提交
2 select @@transaction_isolation; select @@transaction_isolation; 檢查隔離級別是否設定成功
3 select num from cs where id = 20; #20 start transaction;
update cs set num = 20 where id = 40;
會話A num的初始值為20
4 select num from cs where id = 20; #40 / 會話B並未commit,此時會話A中num的值為40,發生髒讀現象
5 / rollback 結束本次事務
6 select num from cs where id = 20; #20 / num恢復為20

不可重複讀(讀未提交、讀已提交隔離級別會發生)

  • 簡介:在事務A中讀取某些資料,然後在事務B中修改這些資料,此時事務A讀取這些資料還未發生變化,但是事務B提交後,並在事務A在未結束事務的前提下,那些資料發生了變化,不可重複讀不是禁止讀動作,而是重複讀資料不一致。
    一句話概括,在同一個事務中,受其它事務提交的影響,讀取同一資料兩次得到的結果不一致的現象。
  • 危害:破壞了事務內資料的準確性,例如事務內的SQL有自增自減的邏輯,如果事務內的初始值受其他事物提交從而發生變化,那麼這是個巨大的問題。
  • 演示:因為不可重複讀是讀已提交(Read Committed)才會發生的情況,所以要降低MySQL的隔離級別。
步驟 會話A 會話B 備註
1 set session transaction_isolation = 'read-uncommitted'; set session transaction_isolation = 'read-uncommitted'; 設定事務隔離級別為讀已經提交
2 select @@transaction_isolation; select @@transaction_isolation; 檢查隔離級別是否設定成功
3 start transaction; start transaction; 雙方開啟事務
4 select num from cs where id = 20; #20 select num from cs where id = 20; #20 兩個會話中num的值為20
5 update cs set num = 40 where id = 20; select num from cs where id = 20; #20 會話A將資料更新為40,此時會話B查詢的值仍為20
6 commit select num from cs where id = 40; #40 會話A提交事務,會話B仍在事務中,但是得到的值變成了40,發生了不可重複讀
7 / commit 結束事務

幻讀(讀未提交、讀已提交、可重複讀隔離級別會發生)

  • 簡介:同一個事務裡前後查詢兩次相同範圍的資料,後一次查詢查詢到了前一次看不到的東西,就好像出現了"幻影"一樣。(注意,如果把會話B的insert改為delete導致的資料減少,不算幻讀,算不可重複讀)。
  • 危害:沒有充分的做好資料隔離,資料一致性存在問題。
  • 演示:mysql 的預設隔離級別為REPEATABLE-READ,所以大機率不用調整隔離級別。
步驟 會話A 會話B 備註
1 select @@transaction_isolation; select @@transaction_isolation; 檢查隔離級別是否是REPEATABLE-READ
2 start transaction; start transaction; 雙方開啟事務
3 select * from cs; select * from cs; 兩個事務檢視,都只有id為20的一條資料
4 insert into cs (id,num) values(21,21); select * from cs; 會話B查詢,仍舊只有id為20的一條資料
5 commit / 會話A提交事務
6 / select * from cs; 即使會話A提交了事務,會話B查詢仍舊無法搜尋到會話A插入的資料,起始這一步已經幻讀了,但是mysql不表明是幻讀,所以到第7步測試
7 / insert into cs (id,num) values(21,21); 因為會話B select查不到id為21的資料,所以插入id相同的資料,但是報錯1062 - Duplicate entry '21' for key 'PRIMARY'
8 / rollback; 回滾以結束事務流程

如何解決幻讀?

  • 或者使用序列化的隔離級別。在序列化隔離級別下,也會隱式的新增行(X)鎖。
  • 新增間隙鎖,可以避免幻讀。
  • mysql 的預設隔離級別為REPEATABLE-READ,又稱為RR,透過MVCC的機制,如果對資料進行快照讀,正因為讀取的不一定第最新的資料,所以可以防止幻讀(注意不是解決幻讀),如果是當前讀(最近資料),那麼仍舊會發生幻讀現象。

當前讀

當前讀讀的就是資料最新的記錄,需要保證當前讀的資料不能被修改,修改了就不是最新的記錄了(髒寫),因此需要加鎖,select for update、select lock in share mode以及DML(insert、update、delete)獲取的資料都是當前讀的資料。

快照讀

快照讀顧名思義,讀取的就是由MVCC Read View控制的undo log的資料,不加鎖,所以是讀取是非阻塞的。不加鎖的select都屬於快照讀。如果當前事務的隔離級別是序列化,那麼快照讀也變成了當前讀。
舉個例子:常用的navicat,檢視一個表,事務提交前的insert或update語句,表格內仍舊顯示的原資料,則用的快照讀。

MVCC

MVCC(Multi-Version Concurrency Control)是 MySQL 中一種實現事務隔離的機制,用於處理資料庫事務併發訪問時可能出現的讀寫衝突。事務的四種隔離級別,就是透過MVCC機制提供的底層支撐。
MVCC三板斧:隱藏欄位、Undo log(存放歷史版本)、Read view(版本控制)

MVCC解決的是讀已提交和可重複讀級別的併發控制。
因為讀未提交,就算事務未提交,可以直接讀取最新的資料(髒讀),相當於當前讀,那就不分快照讀和當前讀了。
序列化的隔離級別,強制事務序列執行,也不存在快照讀和當前讀的區分,因為讀取的都是事務執行過後的最新資料。

事務各項指標監控

檢視 InnoDB 儲存引擎中當前活動的事務資訊。

SELECT * FROM information_schema.innodb_trx;

trx_id                         事務的唯一識別符號。
trx_state                      事務的狀態,如 RUNNING、LOCK WAIT、ROLLING BACK 等。
trx_started                    事務啟動的時間。
trx_requested_lock_id          請求的鎖的識別符號。
trx_wait_started               等待鎖的開始時間。
trx_weight                     事務的權重,用於死鎖檢測。
trx_mysql_thread_id            MySQL 執行緒 ID。
trx_query                      與事務相關的 SQL 查詢語句。
trx_operation_state            事務內部操作的狀態。
trx_tables_in_use              事務使用的表的數量。
trx_tables_locked              事務鎖定的表的數量。
trx_lock_structs               事務內部使用的鎖結構數量。
trx_lock_memory_bytes          用於事務鎖定的記憶體位元組數。
trx_rows_locked                事務鎖定的行數。
trx_rows_modified              事務修改的行數。
trx_concurrency_tickets        用於事務併發控制的票數。
trx_isolation_level            事務的隔離級別。
trx_unique_checks              是否啟用了唯一性檢查。
trx_foreign_key_checks         是否啟用了外來鍵約束檢查。
trx_last_foreign_key_error     最後一個外來鍵錯誤資訊。
trx_adaptive_hash_latched      是否適應性雜湊被鎖定。
trx_adaptive_hash_timeout      適應性雜湊鎖定超時次數。
trx_is_foreign_key_with_check  是否用於外來鍵約束檢查。
trx_is_foreign_key             是否用於外來鍵約束。

相關文章