聲明測試表,供文章案例使用 CREATE TABLE `cs` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `num` int(10) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoD ...
聲明測試表,供文章案例使用
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 是否用於外鍵約束。