1. 程式人生 > >5.偏頭痛楊的mysql教學系列之innodb事務篇

5.偏頭痛楊的mysql教學系列之innodb事務篇

前戲

mysql事務的水較深,是一塊非常龐大的知識體系,需要花費大量的時間去學習和實踐。

除了事務自有的ACID特性,還要掌握底層資料庫的事務機制(例如mysql事務),

以及上層的spring事務處理以及事務的隔離級別,傳播級別,事務的各種屬性等等,

並且事務要結合索引,表引擎,鎖機制(鎖機制是深坑)等知識配合使用。

如果事務使用不當,會造成鎖表,事務死鎖,事務超時,髒資料等等重大事故,

尤其是在大併發的情況下,更是滅頂之災。(寶寶好怕~)

正因為水深,所以各大名企的java面試題都會圍繞著事務來提問,弄的有些小夥伴一臉懵逼。

換句話說,你掌握了事務,你進入名企的障礙就少了一個,因此:

事務已經是你java職業生涯發展上一個必須通關的遊戲了。

注意:如果底層資料庫使用mysql,則需要選擇支援事務的表引擎,例如innodb。

事務這些到底是個啥?我不喜歡用書上難以理解的語言來描述,取而代之的是用通俗易懂的話來講述。

建議大家從網上or書上看到的知識,都自己實踐一下,把別人的知識變成自己的知識,注重總結,

不斷提高自己,離開舒適區。

先來個例子:你去銀行轉賬給你的麻麻,轉了100塊錢,程式要至少幹三件事情:

第一件事:檢查你的餘額是否大於等於100。

第二件事:將你的帳號裡的餘額扣除100。

第三件事:將你麻麻的帳號裡的餘額增加100。

這三件事需要要麼全部成功(事務提交),要麼全部失敗(事務回滾),否則:

剛扣完你的錢,然後出bug了,機房斷電了,網路通訊中斷了,或者什麼奇葩情況,你麻麻的賬戶裡的錢沒有增加,

這事兒,放誰身上誰也不能幹對吧?

扣題:

事務是一步或幾步基本操作組成的邏輯執行單元,這些基本操作作為一個整體執行單元,他們要麼全部執行,

要麼全部取消,絕對不能僅僅執行部分,否則資料庫中會出現大量的“髒資料”等等。

本文所有場景均在innodb引擎下。

什麼是事務

簡單的說,事務是由一組SQL語句組成的邏輯執行單元,預設情況下mysql會自動管理事務,

一條SQL語句獨佔一個事務。

事務具有以下4個屬性,通常簡稱為事務的ACID屬性&特性&原則。

  • 原子性Atomicity[ˌætəˈmɪsɪti]

事務是一個原子操作單元,最小執行單位,就像原子是最小的顆粒,具有邏輯上不可再分的特性。

  • 一致性Consistency[kənˈsɪstənsi]

事務執行的結果,必須使資料庫從一個一致性狀態,變成另一個一致性狀態。無論事務執行成功或失敗,

資料都要保證一致性的狀態,保證資料的完整性,要麼需要一起變化要麼一起回滾。

如果系統執行發生中斷,某個事務尚未完成而被迫中斷,而該未完成的事務對資料庫所做的修改已被寫入資料庫,

此時,資料庫就處於不一致的狀態。

例如:A給B轉賬,從A中扣除的金額必須與B中存入的金額一致。

  • 隔離性Isolation[ˌaɪsəˈleɪʃn]

資料庫系統提供一定的隔離機制,各個事務的執行互不干擾,保證事務在不受併發事務操作影響的“獨立”環境執行。

任意一個事務的內部操作對其他併發的事務,都是隔離的,看不到的。併發執行的事務之間不能互相影響。

(當然隔離性越高,效能會越差,反之亦然,例如:當前事務能否看到其他事務未提交的寫資料。

後面會衍生出事務隔離級別和鎖機制等概念,用於併發事務。)

  • 永續性durability[ˌdjʊrəˈbɪlətɪ]

事務一旦提交,對資料庫所做的任何改變,都要記錄到永久儲存器中,也就是儲存進物理資料庫。

常見事務命令

  • 開始、提交、回滾(當前會話下)

#手動開始一個事務(如果有自動提交的話,會把自動提交掛起,在使用COMMIT命令後恢復自動提交

BEGIN;或START TRANSACTION;

#事務回滾

ROLLBACK;

#手動事務提交(事務未提交之前,在本事務內可以看到資料變化,但並未真正入庫。

COMMIT;

#注意:在提交和回滾後,AUTOCOMMIT引數又會變成預設值。

  • 設定提交模式(當前會話下)

#禁止自動提交(只有遇到COMMIT才會提交,預設自動執行BEGIN或START TRANSACTION,自動開啟一個新事務

SET AUTOCOMMIT=0;

#開啟自動提交(每條sql執行完之後自動提交,預設把每條sql當作一個新的事務處理,一條普通查詢也是一個事務)

SET AUTOCOMMIT=1;

#查詢自動提交模式

SELECT @@autocommit;

SHOW VARIABLES LIKE 'autocommit';

~不管autocommit 是1還是0

START TRANSACTION 後,只有當commit資料才會生效,ROLLBACK後就會回滾。

~當autocommit 為 0 時

不管有沒有START TRANSACTION。只有當commit資料才會生效,ROLLBACK後就會回滾。

~如果autocommit 為1並且沒有START TRANSACTION時,呼叫ROLLBACK是沒有用的。

關於autocommit=1的情況,網上有很多文章說autocommit=1會新建事務,只不過事務自動提交了。

對於這個言論我做了2個小實驗(新人可以PASS掉)。

實驗1:預設autocommit=1,插入n條資料,在插入的過程中,去查詢系統表information_schema.`INNODB_TRX`,

會查詢出正在執行的事務,我發現確實能查詢出正在執行的insert事務,證明確實是會新建事務。

實驗2:使用2個序列化隔離級別的事務,事務A設定autocommit=0,事務B設定autocommit=1,

事務A執行:UPDATE t_user_transaction SET user_age = 3 WHERE key_id = 'alex3'

事務B1執行:SELECT * FROM t_user_transaction WHERE key_id = 'alex3'

事務B2執行:SELECT * FROM t_user_transaction WHERE key_id = 'alex3' FOR UPDATE 

事務B3執行:UPDATE t_user_transaction SET user_age = 3 WHERE key_id = 'alex3'

在序列化級別下,正常情況下事務B1、B2、B3應該是會被阻塞的,因為A持有這條資料的排它鎖,

而B1會獲取這條資料的共享鎖,在序列化級別下,select ... where ...=會走一個共享鎖。

B2、B3會獲取這條資料的排它鎖,造成互斥,因此阻塞。

但實驗結果是,事務B1不阻塞,事務B2、B3阻塞,而將事務B1設定autocommit=0,事務B1就會阻塞,

理論上來說事務B1就應該是阻塞的。

那就奇怪了,為什麼會造成這種奇葩的情況,應該是mysql還有什麼隱藏的知識點我沒有掌握,慚愧。。。

暫時的結論:autocommit=1確實會為每條執行的語句都自動開啟一個新的事務,

但在某些隔離級別下,例如序列化級別,會出現加鎖機制混亂的問題,如實驗2所示。

  • 設定事務隔離級別

#設定當前會話事務隔離級別

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

#設定全域性事務隔離級別

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

事務隔離級別詳見後面的講解

  • 查詢事務隔離級別

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

  • 查詢正在執行的事務

SELECT * FROM information_schema.INNODB_TRX;

併發事務處理帶來的問題&缺陷

相對於序列處理來說,併發事務處理能大大增加資料庫資源的利用率,提高資料庫系統的事務吞吐量,

從而可以支援更多的使用者。(如果不明白什麼是序列&並行請留言)

在一個應用程式中,可能有多個事務同時在進行,這些事務應當彼此之間互不知道另一個事務的存在,

由於事務彼此之間的獨立性,若讀取的是同一個資料的話,就容易發生問題。

隨便舉個常見的問題,例如:2個事務同時去修改一條資料的某列,初始值為0,讓事務分別做+1操作,

我們期待的結果是2(因為有2次+1),但如果出現1的情況,就代表出現了問題。

學習完下面的知識再看看這個問題是什麼缺陷?

  • 髒讀(dirty reads)

一個事務讀到了另一個事務未提交的更新資料。

例如:事務B執行過程中修改了資料X,在未提交前,事務A讀取了X(此時X變動了),而事務B卻回滾了,

這樣事務A就形成了髒讀。

  • 不可重複讀(non-repeatable reads)

在同一事務中,多次讀取同一資料但返回的結果不同,後續讀取可以讀到另一事務已提交的更新資料。

例如:事務A首先讀取了一條資料,然後事務A在執行邏輯的時候,事務B將這條資料改變並提交,

然後事務A再次讀取的時候,發現數據變了(被修改&刪除),造成了事務A的資料混亂。

相反,可重複讀則為多次讀到的資料是一樣的,也就是不能讀取到其他事務已經提交的更新資料。

  • 幻讀 &虛讀(phantom reads)

一個事務讀到另一個事務已提交的insert資料,導致前後讀取不一致。

小的時候數手指,第一次數是10個,第二次數是11個,怎麼回事?產生幻覺了?

例如:

事務A首先根據條件檢索得到10條資料,然後事務B改變了資料庫一條資料或新增了一條資料並提交,

導致這條資料也符合事務A當時的搜尋條件,此時事務A再次搜尋發現還是10條,

但是如果事務A修改了符合條件的事務B的那條資料(update set...),再查詢則有11條資料了,就產生了幻讀。

注意事項

~幻讀與不可重複讀有點類似,都是同一個事務中多次讀不一致的問題。

~不可重複讀的重點在於修改,同樣的條件,你讀取過的資料,再次讀取出來發現值不一樣 。

~幻讀的重點在於新增,同樣的條件,你讀取過的資料,再次讀取出來發現記錄數不一樣 ,

幻讀的前提是有過符合條件資料的update操作。

  • 第一類丟失更新(lost update)

在沒有事務隔離的情況下,兩個事務都同時更新一行資料,但是第二個事務卻中途失敗退出, 

導致對資料的兩個修改都失效了。

例如:張三的工資為5000,事務A中獲取工資為5000,事務B獲取工資為5000,匯入100,並提交資料庫,

工資變為5100,隨後事務A發生異常,回滾了,恢復張三的工資為5000,這樣就導致事務B的更新丟失了。

這種情況一般不會發生。

  • 第二類丟失更新(second lost update)

不可重複讀的特例,有兩個併發事務同時讀取同一行資料,然後其中一個對它進行修改提交,

而另一個也進行了修改提交。這就會造成第一次寫操作失效。

例如:在事務A中,讀取到張三的存款為5000,操作沒有完成,事務還沒提交。

與此同時,事務B,儲存1000,把張三的存款改為6000,並提交了事務。

隨後,在事務A中,儲存500,把張三的存款改為5500,並提交了事務,這樣事務A的更新覆蓋了事務B的更新。

  • 這些缺陷的解決辦法

為了避免上述問題,需要在某個事務的進行過程中鎖定正在更新或者查詢的資料,直到目前的事務完成,

然而如果是完全鎖定,則另一個事務來查詢同一份資料就必須等待,直到前一個事務完成並解除鎖定位置,

這會造成效能問題。

在現實場景中,根據需求的不同,並不用完全鎖定,可以設定不同的隔離級別來滿需求,以及後續文件會講的樂觀鎖。

目前我們可以使用事務隔離級別來有效的解決此類問題。

事務隔離級別

各大關係型資料庫廠商都會提供四種資料庫隔離級別以應對事務的隔離性,這四種隔離級別對應前面提到的五種缺陷。

隔離性越高,效能越低,因此需要權衡使用場景再做決策。

讓我們先來看這張對應表:

dirty reads

non-repeatable reads

phantom reads

lost update

second lost update

SERIALIZABLE

no

no

no

no

no

REPEATABLE READ

no

no

yes

no

no

READ COMMITTED

no

yes

yes

no

yes

READ UNCOMMITTED

yes

yes

yes

no

yes

alex總結:

在兩個事務執行時,事務A執行update一條資料,此時事務A沒有提交,而事務B再執行update相同的資料,

SERIALIZABLE與REPEATABLE READ級別下,事務B會進入等待狀態。

區別是一個鎖住了插入,一個沒有鎖插入,效能不高。

READ COMMITTED,READ UNCOMMITTED則沒有把資料鎖上,效能提升。

我們知道並行可以提高資料庫的吞吐量和效率,但是並不是所有的併發事務都可以併發執行。

我們要思考,魚和熊掌不可兼得,是要高併發還是要高一致性。

為了下面的示例,我們需要建立一張表:

CREATE TABLE `t_user_transaction` (

  `key_id` varchar(32) NOT NULL DEFAULT '' COMMENT '主鍵',

  `user_name` varchar(50) DEFAULT '' COMMENT '名稱(索引列)',

  `user_content` varchar(50) NOT NULL DEFAULT '' COMMENT '內容(非索引列)',

  `is_used` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '是否刪除',

  PRIMARY KEY (`key_id`),

  KEY `idx_user_name` (`user_name`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  • SERIALIZABLE,序列化

最高的隔離級別,它通過強制事務排序,使之不可能相互衝突。

一旦一個事務處理開始了之後,其他的事務則按順序的排在後面,一個個排成一個序列的形式,序列執行。

一個事務沒有處理完,下面的事務無法處理。

可以防止三個缺陷 。簡言之,它是在每個讀的資料行上加上共享鎖(後續介紹)。

在這個級別,事務被處理為序列執,可能導致大量的超時現象和鎖競爭,速度最慢,影響效能。

完全序列化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞。

事務A

事務B

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

START TRANSACTION;

SELECT * FROM t_user_transaction;

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

START TRANSACTION;

SELECT * FROM t_user_transaction;

#查詢並不會被阻塞。

INSERT INTO t_user_transaction(key_id,user_name)

VALUES ('alex2','alex2');

#處於等待狀態,事務B沒有提交則會阻塞事務A。

#此時增刪改操作全部會被阻塞,查詢則不會。

#如果事務B一直沒有提交則事務A會超時。

#Lock wait timeout exceeded; try restarting transaction

COMMIT;

#事務提交

#1 row(s) affected

#插入成功

COMMIT;

#事務提交

SELECT * FROM t_user_transaction;

#此時事務B可以查詢到該條資料,

#如果事務A一直不提交,則會阻塞事務B。

COMMIT;

  • REPEATABLE READ,可重複讀(mysql的預設隔離級別)

在安全上會有所妥協,只有一個缺陷,就是會產生幻讀,但是在效能上會比序列化提高很多。

除非事務自身更改了資料,否則事務多次讀取的資料相同,避免了“髒讀”和“不可重複讀取”。

事務A

事務B

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

START TRANSACTION;

SELECT * FROM t_user_transaction;

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

START TRANSACTION;

SELECT * FROM t_user_transaction;

#此時和連線A查詢到的資料一樣

INSERT INTO t_user_transaction(key_id,user_name)

VALUES ('alex3','alex3');

#插入一條資料,此時不會阻塞。

SELECT * FROM t_user_transaction;

#在當前事務中可以查詢到這條新增資料

COMMIT;

#事務提交,無論提交與否,事務A都看不到這條資料,

#這就是可重複讀,但是沒有像序列化那樣鎖住insert。

SELECT * FROM t_user_transaction;

#查詢不到連線B提交的資料哇~

#遮蔽了不可重複讀+賍讀的缺陷。

UPDATE t_user_transaction SET is_used  = 0;

#將符合條件的資料全部更新,

#也把剛才事務B的資料更新了。

SELECT * FROM t_user_transaction;

#把連線B提交的資料也查詢出來了,出現了幻覺嗎?

#剛才還沒有呢。。。

#更新之前是不可重複讀,更新之後出現了幻讀。

SELECT * FROM t_user_transaction;

看不到事務A的變化,因為事務A還沒有提交事務,

規避了賍讀。

COMMIT;

事務提交,連線B也能看到變化了。

  • READ COMMITTED,提交讀(oracle,sql server的預設隔離級別)

允許讀取其他事務已經提交的更新,避免了“髒讀”,但是有缺陷:會造成不可重複讀, 幻讀。

事務A

事務B

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

SET SESSION binlog_format = 'ROW';

#需要把binlog的格式調成ROW,不然會報錯

SELECT @@GLOBAL.binlog_format,@@session.binlog_format;

START TRANSACTION;

SELECT * FROM t_user_transaction;

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

SET SESSION binlog_format = 'ROW';

#需要把binlog的格式調成ROW,不然會報錯

SELECT @@GLOBAL.binlog_format,@@session.binlog_format;

START TRANSACTION;

SELECT * FROM t_user_transaction;

UPDATE t_user_transaction SET user_name = 'alex4' WHERE key_id = 'alex3';

SELECT * FROM t_user_transaction;

#在沒提交之前,事務A無法查詢到事務B的改動。規避了賍讀的缺陷

COMMIT;

SELECT * FROM t_user_transaction;

#在同一個事務中再次查詢便會查詢出不一樣的資料,為不可重複讀。

COMMIT;

其他缺陷的例子略。。。

  • READ UNCOMMITED,未提交讀

可以讀取到其他事務未提交的修改,如果事務回滾,則會出現賍讀。

會導致四個缺陷發生。執行速度最快(最不安全,效能最好),如果想要效能,直接不加事務不就行了?

事務A

事務B

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

SET SESSION binlog_format = 'ROW';

#需要把binlog的格式調成ROW,不然會報錯

SELECT @@GLOBAL.binlog_format,@@session.binlog_format;

START TRANSACTION;

SELECT * FROM t_user_transaction;

SET AUTOCOMMIT=0;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT @@GLOBAL.tx_isolation,@@session.tx_isolation;

SET SESSION binlog_format = 'ROW';

#需要把binlog的格式調成ROW,不然會報錯

SELECT @@GLOBAL.binlog_format,@@session.binlog_format;

START TRANSACTION;

SELECT * FROM t_user_transaction;

UPDATE t_user_transaction SET user_name = 'alex5' WHERE key_id = 'alex2';

#修改資料,但事務還沒有提交

SELECT * FROM t_user_transaction;

#事務B自己能檢視到修改後的樣子。

SELECT * FROM t_user_transaction;

#在同一個事務中再次查詢便會查詢出另一個事務未提交的資料,為賍讀。

ROLLBACK;

#事務回滾

SELECT * FROM t_user_transaction;

#資料又變更了,給連線A的資料造成了很大的混亂。

COMMIT;

其他缺陷的例子略。。。

總結

mysql事務是鎖機制以及後面的spring事務管理的基礎,也是面試題的重要考點,請大家務必要消化理解。

後續的文章會講解實用性更高&更復雜的mysql鎖機制。建議使用小事務,一個事務不要幹特別多的事情。

事務較大,會增加事務死鎖風險,高併發下效能較低,死鎖我們會在鎖機制的文章中涉獵。