高性能MySQL(第3版)筆記 1.3 事務

1.3 事務

事務:一組原子性的SQL查詢,如果數據庫引擎能夠成功地對數據庫應用改組查詢呢的全部語句,那麼就執行該查詢,如果其中任何一條執行失敗,那麼其他語句都不會執行。
事務內的語句,要麼全部執行成功,要麼全部執行失敗

事務的四大特性:
原子性(Atomicity):事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
首先創建兩張表並插入數據

DROP TABLE
IF
	EXISTS checking;
CREATE TABLE checking(
customer_id int(10) UNSIGNED NOT NULL,
balance DOUBLE UNSIGNED NOT NULL DEFAULT 0 CHECK(balance >= 0),
-- 使用CHECK時注意mysql版本,有些版本雖然能解析CHECK語法,但是並不生效
PRIMARY KEY (customer_id)
) ENGINE = INNODB DEFAULT CHARSET = utf8;

INSERT INTO checking (customer_id, balance)
VALUES
		(10233276, 20),
		(10233277, 19);

在這裏插入圖片描述

DROP TABLE
IF
	EXISTS savings;
CREATE TABLE savings(
customer_id int(10) UNSIGNED NOT NULL,
balance DOUBLE UNSIGNED NOT NULL DEFAULT 0 CHECK(balance >= 0),
PRIMARY KEY (customer_id)
) ENGINE = INNODB DEFAULT CHARSET = utf8;

INSERT INTO savings (customer_id, balance)
VALUES
		(10233276, 80),
		(10233277, 81);

在這裏插入圖片描述
在這裏插入圖片描述
如果Jane(10233276)想要從自己的支票餘額中轉移20美元到自己的儲蓄賬戶
這個過程包含兩個步驟
checking: 20 - 20 = 0
savings: 80 + 20 = 100

START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 20 WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 20 WHERE customer_id = 10233276;
COMMIT;

結果:
checkings
在這裏插入圖片描述
savings
在這裏插入圖片描述
這兩個步驟必須同時執行成功,否則同時執行失敗,例如支票餘額不足20美元,那麼儲蓄賬戶中不會增加20美元(創建表的時候用check約束)

使用另一個用戶(10233277)進行測試

START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233277;
UPDATE savings SET balance = balance + 20 WHERE customer_id = 10233277;
UPDATE checking SET balance = balance - 20 WHERE customer_id = 10233277;
COMMIT;

信息
在這裏插入圖片描述

可以看到checkings表由於餘額不足沒有向下執行,再查看錶中的數據發現沒有發生變化
結果:
checkings
在這裏插入圖片描述
savings
在這裏插入圖片描述

一致性(Consistency):事務前後數據的完整性必須保持一致。
在這裏插入圖片描述
同上:

狀態 checking savings
操作前 20 80
操作後 0 100

即操作前後賬戶數字複合邏輯運算,即操作前後checking和savings中餘額的總數爲100
隔離性(Isolation):事務的隔離性是併發訪問數據庫時,數據庫爲每一個訪問開啓的事務,不能被其他事務的操作數據鎖干擾,多個併發事務之前要互相隔離。
持久性(Durability):一個事務一點被提交,它對數據庫中數據的改變是永久性的,此時即使系統崩潰,修改的數據也不會丟失

一個實現了ACID的數據庫,相比沒有實現ACID的數據庫,通常需要更強的CPU處理能力、更大內存和磁盤空間

1.3.1 隔離級別

在SQL標準中爲隔離性定義了四種級別

未提交讀(Read Uncommitted):

在Read Uncommitted級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。例如在上例中,用戶10233277如果在執行第3行sql

UPDATE savings SET balance = balance + 20 WHERE customer_id = 10233277;

時,事務沒有提交,這時候恰好另外一個事務B去讀取savings表中用戶10233277的餘額時,發現餘額已經變成了100,但是這個事務會因爲checkings表中用戶10233277的餘額不足導致無法向下執行後又進行了回滾,這樣事務B就讀取了此次的髒數據,稱爲髒讀

髒讀(Dirty Read):事務可以讀取未提交的數據
這個級別會導致很多問題,並且性能不會比其他級別好太多,實際應用很少

提交讀(Read Committed)

一個事務開始時,只能讀取已經提交的事務所做的修改。
Read Committed是大多數數據庫系統的默認隔離級別(但MySQL不是)
Read Committed也叫Nonrepeatable Read(不可重複讀),因爲兩次執行同樣的查詢,可能會得到不一樣的結果
例如上例中,事務A比較複雜,前後讀取同一條數據需要經歷很長時間,事務A第一次讀取到用戶10233276的checkings表中的支票餘額爲0,這時候事務B對用戶10233276的checkings表中的支票餘額增加了100,那麼事務A再次去讀checkings表中用戶10233276的支票餘額,發現餘額變成了100,這時候前後數據就不一致了,稱爲不可重複讀

不可重複讀和髒讀的區別是:髒讀是某一事務讀取了另一個事務未提交的髒數據,而不可重複讀則是讀取了前一事務提交的數據

可重複讀(Repeatable Read)

MySQL默認事務隔離級別
可重複讀解決了髒讀和不可重複讀的問題。保證了在同一個事務中多次讀取同樣記錄的結果是一致的。但是還是無法解決幻讀的問題。

幻讀(Phantom Read):例如又另外一張表records記錄這每個每一條從checkings表和savings表的轉賬記錄,事務A很複雜需要很長的時間,第一次讀取records中用戶10233276的轉賬記錄,發現是一條,這時候另外一個事務B增加了轉賬1元的操作,並順利提交,這時候事務A再次去讀表records記錄,發現用戶10233276又多了一條記錄(幻行(Phantom Row))

在對於數據庫中的某個數據,一個事務範圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。

幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點和髒讀不同),所不同的是不可重複讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。

可串行化(Serializable)

隔離的最高級別,它通過強制事務串行執行,避免了前面說的幻讀的問題
Serializable會在讀取的每一行數據上都加上鎖,所以可能導致大量的超時和鎖爭用的問題。實際很少應用。只有在非常需要確保數據一致性而且可以接受沒有併發的情況下,才考慮該級別。

查看當前會話隔離級別:

select @@transaction_isolation;

(上述sql是mysql8.0以後的版本用的,如果版本小於8.0則把sql中的transaction_isolation替換成tx_isolation
在這裏插入圖片描述

SHOW VARIABLES LIKE 'transaction_isolation';

在這裏插入圖片描述
查看系統當前隔離級別

SELECT @@GLOBAL.TRANSACTION_ISOLATION;

在這裏插入圖片描述
設置當前會話隔離級別:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
隔離級別 髒讀可能性 不可重複讀可能性 幻讀可能性 加鎖讀
Read Uncommitted ✔️ ✔️ ✔️
Read Committed ✔️ ✔️
Repeatable Read ✔️
Serializable ✔️

1.3.2 死鎖

死鎖:兩個或者多個事務在同一資源上互相佔用,並請求鎖定對方佔用的資源,從而導致惡性循環的現象

爲了解決這種問題,數據庫系統實現了各種死鎖檢測和死鎖超時機制。比較複雜的存儲引擎InnnoDB,遇到死鎖的循環依賴可以立即返回一個錯誤。還有一種解決方式當查詢時間達到鎖等待超時的設定後放棄鎖請求,這種方式不太好。InnoDB目前處理死鎖的方法是,將持有最少行級排他鎖的事務進行回滾

死鎖發生以後,只有部分或者完全回滾其中一個事務,才能打破死鎖

1.3.3 事務日誌

事務日誌可以提高事務的效率
使用事務日誌時,存儲引擎在修改表的數據時只需要修改其內存拷貝,再把修改行行爲記錄持久在硬盤上的事務日誌中,而不需要每次都將修改的數據本身持久到硬盤。事務日誌持久化以後,內存中被修改的數據在後臺可以慢慢地刷回磁盤。目前大多數存儲引擎都是這樣實現的,我們通常稱之爲預寫式日誌(Write-Ahead Logging),修改數據需要寫兩次磁盤
如果數據的修改已經記錄到事務日誌並持久化,但數據本身還沒有寫回磁盤,此時系統崩潰,存儲引擎在重啓時能夠自動恢復這部分修改的數據。具體的恢復方案是則視存儲引擎而定。

1.3.4 MySQL中的事務

MySQL共提供了兩種事務型存儲引擎:InnnoDBNDB Cluster。還有一些第三方存儲引擎也支持事務,比較知名的有XtraDBPBXT

自動提交(AUTOCOMMIT)

MySQL默認採用自動提交。如果不是顯式地開始一個事務,每個查詢都被當作一個事務執行提交操作。在當前連接中,可以通過設置AUTOCOMMIT變量來啓用或者禁用自動提交模式:

SHOW VARIABLES LIKE 'AUTOCOMMIT';

在這裏插入圖片描述
可以臨時設置當前連接的AUTOCOMMIT

SET AUTOCOMMIT = 0;

在這裏插入圖片描述
如果關閉當前連接後重新連接,那麼臨時設置就失效了。

AUTOCOMMIT = 0或OFF時,所有的查詢都是在一個事務中,直到顯式地執行COMMITROLLBACK,該事務結束,同時又開始一個新事務。AUTOCOMMIT對非事務型的表沒有任何影響,例如MyISAM或者內存表,這類表沒有COMMITROLLBACK的概念,也可以說相當於一直處於AUTOCOMMIT啓用的模式。

另外還有一些命令,在執行之前會強制執行COMMIT提交當前的活動事務。例如數據定義語言(DDL)中,如果是會導致大量數據改變的操作,比如ALTER TABLELOCK TABLES

在事務中混合使用存儲引擎

MySQL服務器層不管理事務,事務是由下層的存儲引擎實現的,所以在同一個事務中,使用多種存儲引擎是不可靠的。
如果在事務中混合使用了事務型和非事務型的表(例如InnoDBMyISAM),正常提交的情況下不會有問題。但是如果該事務需要回滾,非事務型的表上的變更就無法撤銷,這會導致數據庫處於不一致的狀態,很難修復。
如上例中,把savings表的存儲引擎換爲MyISAM
在這裏插入圖片描述
savings表中的數據:
在這裏插入圖片描述
checking表中的數據:
在這裏插入圖片描述
執行事務:

START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233277;
UPDATE savings SET balance = balance + 20 WHERE customer_id = 10233277;
UPDATE checking SET balance = balance - 20 WHERE customer_id = 10233277;
COMMIT;

執行信息:
在這裏插入圖片描述
checking表中的數據:
在這裏插入圖片描述
可以發現並沒有發生變化
savings表中的數據:
在這裏插入圖片描述
可以發現用戶10233277餘額多了20,這樣數據就不一致了。
所以,爲每張表選擇合適的引擎非常重要。

顯式和隱式鎖定

InnoDB採用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行過程中,隨時都可以執行鎖定,鎖只喲歐在執行COMMIT或者ROOLBACK的時候纔會釋放,並且所有的鎖是在同一時刻被釋放。前面描述的鎖都是隱形鎖定InnoDB會根據隔離級別在需要的時候自動加鎖

InnoDB也支持通過特定的語句進行顯式鎖定

SELECT ... LOCK IN SHARE
SELECT ... FOR UPDATE

實例:
會話1開啓事務,並且用FOR UPDATE進行查詢:

START TRANSACTION;
SELECT
	* 
FROM
	savings 
WHERE
	customer_id = 10233277 FOR UPDATE;

此時再去開啓會話2,先用普通不加FOR UPDATE進行查詢:

SELECT
	* 
FROM
	savings 
WHERE
	customer_id = 10233277;

結果沒有任何問題:
在這裏插入圖片描述
再在會話2中使用FOR UPDATE進行查詢:

SELECT
	* 
FROM
	savings 
WHERE
	customer_id = 10233277 FOR UPDATE;

等待一段事件後會顯示超時:
在這裏插入圖片描述
也就是說在事務中使用,需要兩個查詢都用FOR UPDATE進行查詢才能阻塞另一個去讀。需要注意的是,where中的條件裏必須是主鍵,否則會鎖住整張表。

LOCK TABLES 、UNLOCK TABLES

LOCK TABLES命令是爲當前線程鎖定表。這裏有2種類型的鎖定

鎖定類型 命令
讀鎖定 LOCK TABLES tablename READ
寫鎖定 LOCK TABLES tablename WRITE

讀鎖定

如果一個線程獲得在一個表上的READ鎖,那麼該線程和所有其他線程都只能從表中讀數據,不能進行任何寫操作。
實例:
會話A:

LOCK TABLES savings READ;
SELECT
	* 
FROM
	savings;
UPDATE savings 

SET balance = 200 
WHERE
	customer_id = 10233277;

結果信息:
在這裏插入圖片描述
可以看到查詢完全沒有問題,只有寫操作UPDATE被讀鎖阻塞住了

線程B

SELECT
	* 
FROM
	savings;
UPDATE savings 
SET balance = 200 WHERE customer_id = 10233277;

結果信息:
在這裏插入圖片描述
可以看到線程B查詢也完全沒有問題,同樣寫操作UPDATE被讀鎖阻塞住了
再測試一下INSERT

LOCK TABLES savings READ;
SELECT
	* 
FROM
	savings;
UPDATE savings -- SET balance = 200 
-- WHERE
-- 	customer_id = 10233277;
INSERT INTO savings ( customer_id, balance )
VALUES (10233278, 400);

在這裏插入圖片描述
可以看到是同樣的結果。

線程A釋放鎖並執行更新操作

UNLOCK TABLES;
UPDATE savings 
SET balance = 300 
WHERE
	customer_id = 10233277;

結果信息:
在這裏插入圖片描述
同時一直等待讀鎖的線程B也執行完成,結果信息:
在這裏插入圖片描述
同時再測試用LOCAL修飾讀鎖:
線程A

LOCK TABLES savings READ LOCAL;
SELECT
	* 
FROM
	savings;
UPDATE savings 
SET balance = 200 
WHERE
	customer_id = 10233277;

結果信息:
在這裏插入圖片描述
可以看到寫操作同樣被阻塞住了
再用線程B去執行讀寫(SELECT UPDATE)操作:

SELECT
	* 
FROM
	savings;
UPDATE savings 
SET balance = 400 WHERE customer_id = 10233277;

結果信息:
在這裏插入圖片描述
在測試一下INSERT

LOCK TABLES savings READ LOCAL;
SELECT
	* 
FROM
	savings;
INSERT INTO savings ( customer_id, balance )
VALUES (10233278, 400);

結果
在這裏插入圖片描述
在線程B中進行INSERT

INSERT INTO savings ( customer_id, balance )
VALUES
	( 10233278, 400 );

在這裏插入圖片描述
同樣被阻塞住了

因爲數據庫引擎是InnoDB,可以發現效果和上面不加LOCAL修飾是相同的不能UPDATEINISERT,把表引擎更換爲MyISAM注意要刪除表重新創建

DROP TABLE
IF
	EXISTS savings;
	
CREATE TABLE savings(
customer_id int(10) UNSIGNED NOT NULL,
balance DOUBLE UNSIGNED NOT NULL DEFAULT 0 CHECK(balance >= 0),
PRIMARY KEY (customer_id)
) ENGINE = MyISAM DEFAULT CHARSET = utf8;

INSERT INTO savings (customer_id, balance)
VALUES
		(10233276, 80),
		(10233277, 81);

更換完後,再次進行上面的測試
線程A

LOCK TABLES savings READ LOCAL;
SELECT
	* 
FROM
	savings;
INSERT INTO savings ( customer_id, balance )
VALUES (10233278, 400);

結果:
在這裏插入圖片描述
線程B

INSERT INTO savings ( customer_id, balance )
VALUES
	( 10233278, 400 );

結果:
在這裏插入圖片描述

奇怪的是當我不重新創建表再次去執行上述操作時:
線程A

UNLOCK TABLES;
LOCK TABLES savings READ LOCAL;
SELECT
	* 
FROM
	savings;
INSERT INTO savings ( customer_id, balance )
VALUES (10233278, 400);

結果:
在這裏插入圖片描述
線程B

INSERT INTO savings ( customer_id, balance )
VALUES
	( 10233278, 400 );

結果在這裏插入圖片描述
竟然又被鎖住了,也就是說LOCAL失效了,這個問題還沒搞明白。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章