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共提供了兩種事務型存儲引擎:InnnoDB和NDB Cluster。還有一些第三方存儲引擎也支持事務,比較知名的有XtraDB和PBXT。
自動提交(AUTOCOMMIT)
MySQL默認採用自動提交。如果不是顯式地開始一個事務,每個查詢都被當作一個事務執行提交操作。在當前連接中,可以通過設置AUTOCOMMIT變量來啓用或者禁用自動提交模式:
SHOW VARIABLES LIKE 'AUTOCOMMIT';
可以臨時設置當前連接的AUTOCOMMIT:
SET AUTOCOMMIT = 0;
如果關閉當前連接後重新連接,那麼臨時設置就失效了。
當AUTOCOMMIT = 0或OFF時,所有的查詢都是在一個事務中,直到顯式地執行COMMIT或ROLLBACK,該事務結束,同時又開始一個新事務。AUTOCOMMIT對非事務型的表沒有任何影響,例如MyISAM或者內存表,這類表沒有COMMIT或ROLLBACK的概念,也可以說相當於一直處於AUTOCOMMIT啓用的模式。
另外還有一些命令,在執行之前會強制執行COMMIT提交當前的活動事務。例如數據定義語言(DDL)中,如果是會導致大量數據改變的操作,比如ALTER TABLE、 LOCK TABLES等
在事務中混合使用存儲引擎
MySQL服務器層不管理事務,事務是由下層的存儲引擎實現的,所以在同一個事務中,使用多種存儲引擎是不可靠的。
如果在事務中混合使用了事務型和非事務型的表(例如InnoDB和MyISAM),正常提交的情況下不會有問題。但是如果該事務需要回滾,非事務型的表上的變更就無法撤銷,這會導致數據庫處於不一致的狀態,很難修復。
如上例中,把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修飾是相同的不能UPDATE和INISERT,把表引擎更換爲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失效了,這個問題還沒搞明白。