MySQL中的事務
前置文章:
MySQL存儲引擎
詳解MySQL中的鎖
爲什麼需要事務
現在的很多軟件都是多用戶,多程序,多線程的,對同一個表可能同時有很多人在用,爲保持數據的一致性,所以提出了事務的概念。
A 給B 要劃錢,A 的賬戶-1000元, B 的賬戶就要+1000元,這兩個 update 語句必須作爲一個整體來執行,不然A 扣錢了,B 沒有加錢這種情況很難處理。
什麼存儲引擎支持事務
- 查看數據庫下面是否支持事務
show engines;
- 查看mysql當前默認的存儲引擎
show variables like '%storage_engine%';
- 查看某張表的存儲引擎
show create table 表名 ;
- 對於表的存儲結構的修改
Alter table 表名 ENGINE = InnoDB;
事務特性
事務具有四個特徵:原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和持久性( Durability )。這四個特性簡稱爲 ACID 特性。
原子性
事務是數據庫的邏輯工作單位,事務中包含的各操作要麼都做,要麼都不做
一致性
事務執行的結果必須是使數據庫從一個一致性狀態變到另一個一致性狀態。如果數據庫系統運行中發生故障,有些事務尚未完成就被迫中斷,這些未完成事務對數據庫所做的修改有一部分已寫入物理數據庫,這時數據庫就處於一種不正確的狀態,或者說是 不一致的狀態。
隔離性 ☆
一個事務的執行不能其它事務干擾。即一個事務內部的操作及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。
四種隔離級別
SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級一般支持更高的併發處理,並擁有更低的系統開銷。
查看隔離級別:
show variables like '%tx_isolation%';
select @@tx_isolation;
示例:
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
`balance` int(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_balance` (`balance`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO `account` VALUES ('1', 'lilei', '900');
INSERT INTO `account` VALUES ('2', 'hanmei', '100');
INSERT INTO `account` VALUES ('3', 'lucy', '250');
INSERT INTO `account` VALUES ('5', 'tom', '0');
Read Uncommitted(讀取未提交內容)
在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因爲它的性能也不比其他級別好多少。
會導致髒讀(Dirty Read):某個事務已更新一份數據但未提交,另一個事務在此時讀取了同一份數據。
例如,事務A讀取到了事務B修改後但未提交的數據,這就是髒讀了。如果事務B回滾,事務A讀取到的就是錯誤的數據。
sessionA:
set SESSION TRANSACTION ISOLATION LEVEL read UNCOMMITTED;
start TRANSACTION;
sessionB:
set SESSION TRANSACTION ISOLATION LEVEL read UNCOMMITTED;
start TRANSACTION;
sessionA:
update account set balance = balance -50 where id = 1;
sessionB:
select * from account;
讀取到了未提交的數據,髒讀
sessionA:
ROLLBACK;
sessionB:
select * from account;
之前sessionA讀取到了錯誤的數據。
Read Committed(讀取提交內容)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。
解決了髒讀,但會導致不可重複讀(Non-repeatable read):在一個事務的兩次查詢之中數據不一致。
例如,事務A第一次查詢的是原有數據,第二次查詢的是事務B提交後的數據,兩次查詢不一致,但事務A不會讀取未提交的數據。
示例:
sessionA:
set SESSION TRANSACTION ISOLATION LEVEL read committed;
start TRANSACTION;
sessionB:
set SESSION TRANSACTION ISOLATION LEVEL read committed;
start TRANSACTION;
sessionA:
update account set balance = balance -50 where id = 1;
sessionB:
select * from account;
沒有讀取到未提交的數據,解決了髒讀
sessionA:
commit;
sessionB:
select * from account;
前後兩次讀取的數據不一樣,出現了不可重複讀
Repeatable Read(可重複讀)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。
解決了髒讀、不可重複讀,但會導致幻讀 (Phantom Read):在一個事務的兩次查詢中數據筆數不一致。
例如,有一個事務查詢了幾行(Row)數據,而另一個事務卻在此時插入了新的幾行數據,先前的事務在接下來的查詢中,就有幾行數據是未查詢出來的,如果此時插入和另外一個事務插入的一樣的數據,就會報錯。
不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。
InnoDB存儲引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control,多版本併發控制。讀不加鎖,讀寫不衝突)機制解決了該問題。
示例:
sessionA:
set SESSION TRANSACTION ISOLATION LEVEL repeatable read;
start TRANSACTION;
sessionB:
set SESSION TRANSACTION ISOLATION LEVEL repeatable read;
start TRANSACTION;
sessionA:
update account set balance = balance -50 where id = 1;
sessionB:
select * from account;
沒有讀取到未提交的數據,解決了髒讀
sessionA:
commit;
sessionB:
select * from account;
前後兩次讀取的數據一樣,解決了不可重複讀
sessionB:
commit;
select * from account;
當前事務結束之後就可查詢到新數據了
sessionA:
start TRANSACTION;
sessionB:
start TRANSACTION;
sessionA:
insert into account value(4,'aa',123);
commit;
sessionB:
select * from account;
insert into account value(4,'aa',123);
查詢的時候並沒有 key=4 的數據,卻不能新增 key=4 的數據,就好像有 key=4 的數據的幻影在那裏。這就是幻讀
Serializable(可串行化)
這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。
解決了髒讀、不可重複讀、幻讀。
示例:
sessionA:
set SESSION TRANSACTION ISOLATION LEVEL serializable;
start TRANSACTION;
sessionB:
set SESSION TRANSACTION ISOLATION LEVEL serializable;
start TRANSACTION;
sessionA:
select * from account;
sessionB:
select * from account;
兩個事務都可以對錶進行查詢
insert into account VALUES(6,'cc',500);
A進行過了操作之後,B進行寫操作會進入了等待狀態。
這裏十分合理,假如此時sessionB可以寫,豈不是sessionA讀的數據就不是最新的了。在未超時之前:
sessionA:
commit;
sessionB:
select * from account;
間隙鎖(gap鎖)
其實在mysql中,可重複讀已經解決了幻讀問題,藉助的就是間隙鎖,隔離級別RR(Repeatable Read)。
間隙鎖,是在索引的間隙之間加上鎖。
示例1(判斷條件爲主鍵大小):
create table t_lock_1 (a int primary key);
insert into t_lock_1 values(10),(11),(13),(20),(40);
sessionA:
begin;
select * from t_lock_1 where a <= 13 for update;
sessionB:
begin;
insert into t_lock_1 values(21);
insert into t_lock_1 values(19) ;
表中有數據10 11 13 20 40,sessionA查詢了小於等於13的數據,sessionB無法插入小於等於20 的數據,就好像在20到21之間的間隙裏上了一道鎖,這就是間隙鎖。
爲什麼是在20這裏上了間隙鎖,而不是13這裏呢?這是MySQL的實現方式,掃描到條件邊界值的下一個表中數據再加鎖。這個示例中,掃描到第一個大於 13 的數 20 就加鎖了。
再問,爲什麼要這麼設計呢?思考,假如判斷的是 where a <= 15,那你怎麼加鎖呢 。所以上面這個 13 作爲條件邊界具有迷惑性,因爲它恰好是表中數據。所以,給條件邊界的下一個表中數據加鎖是合理的。
示例2(判斷條件爲一般鍵大小):
create table t_lock_2 (a int primary key,b int, key (b));
insert into t_lock_2 values(1,1),(3,1),(5,3),(8,6),(10,8);
sessionA:
BEGIN;
select * from t_lock_2 where b=3 for update;
sessionB:
begin;
select * from t_lock_2 where a = 5 lock in share mode;
等待,因爲數據 (5,3)上有一把排它鎖
insert into t_lock_2 values(4, 2);
等待,因爲 b=2 在(1, 6)內,a=4 在(3,8)內
insert into t_lock_2 values(6, 5);
等待,因爲 b=5 在(1, 6)內,a=6 在(3,8)內
insert into t_lock_2 values(4, 0);
成功,因爲 b=0 在(1,6)外,即使 a=4 在(3,8)內也沒關係
insert into t_lock_2 values(6, 7);
成功,因爲 b=7 在(1,6)外,即使 a=6 在(3,8)內也沒關係
insert into t_lock_2 values(9, 6);
成功,因爲 b=6 在(1,6)的邊界上,但 a=9 在(3,8)外
insert into t_lock_2 values(7, 6);
等待,因爲 b=6 在(1, 6)的邊界上,但 a=6 在(3,8)內
update t_lock_2 set b = 5 where a = 8;
等待,因爲 b=5 在(1, 6)內,a = 8 在 (3,8) 的邊界上
update t_lock_2 set b = 9 where a = 8;
成功,因爲 b=9 在(1, 6)外,a = 8 在 (3,8) 的邊界上
update t_lock_2 set b = 1 where a = 8;
等待,因爲 b = 1 在(1, 6)邊界上,a = 8 在 (3,8) 的邊界上
總結:
假設有數據(主鍵A,一般鍵B)鎖住的範圍在(A1,B1)與(A2,B2)之間。
那麼,另一事務寫成功的條件是:
- B 在範圍 (B1,B2) 之外
- B 在邊界 B1 或 B2 上,但是 A 在 (A1,A2) 之外
另一事務寫失敗(進入等待狀態)的條件是:
- B 在範圍 (B1,B2) 之內
- B 在邊界 B1 或 B2 上,但是 A 在 (A1,A2) 之內
- B 在邊界 B1 或 B2 上,A在邊界 B1 或 B2 上(如,將(A2,B2)修改爲(A1,B2)。只有這裏是改表中數據)
至於讀,只有(A1,B1)與(A2,B2)之間的數據不能讀。
爲什麼解決了幻讀?
因爲間隙鎖的粒度比行鎖大(但比表鎖小)。讓原來一個事務能插入(引起幻讀)的時候不讓它插入,不就解決了幻讀嗎
持久性
指一個事務一旦提交,它對數據庫中的數據的改變就應該是永久性的。接下來的其它操作或故障不應該對其執行結果有任何影響。
事務語法
開啓事務
- START TRANSACTION(推薦)
- begin
- begin work
事務回滾
- rollback
- rollback work
事務提交
- commit
- commit work
自動提交
查看自動提交事務是否開啓
show variables like '%autocommit%';
默認開啓(這也意味着,當你開啓一個事務並修改了數據而沒有提交或回滾後,再開啓一個事務,上一個事務會自動提交,而不是回滾)
關閉自動提交事務
set autocommit=0;
再查看:
保存點(瞭解)
作用:
使事務可以回滾到保存點
語法:
savepoint 保存點名字;
rollback to savepoint 保存點名字;
示例:
CREATE TABLE `testdemo` (
`id` int(255) NOT NULL,
`c1` varchar(300) CHARACTER SET utf8 DEFAULT NULL,
`c2` int(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_c2` (`c2`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
start TRANSACTION;
insert into testdemo values(1,1,1);
savepoint s1;
insert into testdemo values(2,2,2);
savepoint s2;
insert into testdemo values(3,3,3);
savepoint s3;
select * from testdemo;
rollback to savepoint s2;
select * from testdemo;
commit;
參考:Deer——mysql優化