SQL 事務與鎖 詳解

本篇博客旨在記錄數據庫中事務與鎖機制的必要性,記錄瞭如何在數據庫中使用事務與鎖機制實現數據庫的一致性以及併發性。

1. 事務機制

1.1. 事務是什麼

事務通常包含一系列更新操作,這些更新操作是一個不可分割的邏輯工作單元。如果事務成功執行,那麼該事務中所有的更新操作都會成功執行、並將執行結果提交到數據庫文件中,成爲數據庫永久的組成部分。如果事務中某條更新操作執行失敗,那麼事務中的所有操作均被撤銷。

這個性質叫做事務的原子性,即事務操作是打包執行的,一個不成功就全部不成功,要成功就全部成功。

1.2. 事務的必要性

舉一個容易理解的例子,對於銀行系統而言,轉賬業務是銀行最基本的、且最常用的業務,轉賬通常是在兩個賬戶之間進行的,如果一個人把錢轉出去了,另一個人收款卻失敗了,就會導致錢白白丟失,這肯定是不能接收的,因此有必要將轉賬業務封裝成存儲過程,該存儲過程作爲一個整體要麼轉賬和收款都成功,要麼轉賬和收款都失敗,調用該存儲過程後即可真正實現兩個銀行賬戶間的轉賬而不會造成損失。

1.3. 在MySql中關閉自動提交 autocommit

關閉自動提交的方法有兩種: 一種是顯式地關閉自動提交,一種種是隱式地關閉自動提交。

顯式關閉自動提交
使用MySQL命令set autocommit=0;,即可顯示地關閉MySQL自動提交。
如圖,
在這裏插入圖片描述

隱式關閉自動提交
使用MySQL命令start transaction;可以隱式地關閉自動提交。隱式地關閉自動提交,不會修改系統會話變量@@autocommit的值。
例如下例SQL語句:

-- 查看autocommit的值
select @@autocommit;

-- 開啓事務
start transaction;
	-- sql語句
	insert into prot_user values ('111', 'AAA');
-- 提交
commit;

-- 查看autocommit的值
select @@autocommit;

執行的結果是,事務中的sql語句被成功執行,而且兩次查詢autocommit的值都是1。這就是隱式關閉自動查詢,不會修改autocommit的值。

1.4. 回滾 rollback 與 保存點 savepoint

關閉MySQL自動提交後,就可以使用rollback關鍵字根據需要回滾(撤銷)整個事務,或者回滾到保存點,也就是恢復數據庫之前的狀態。

事務的原子性,就是通過保存點(也稱爲檢查點)實現的。使用MySQL命令savepoint [保存點名];可以在事務中設置一個保存點,使用MySQL命令rollback to savepoint [保存點名];可以將事務回滾到保存點狀態,例如。

create procedure test()
begin
	start transaction;
		set @temp = '0';
		savepoint sp1; -- 定義保存點
		-- sql語句
		select prot_user.user_name into @temp from prot_user where prot_user.user_name="bais";
		if @temp='0' then
			rollback to savepoint sp1; -- 回滾到保存點
		else 
			select @temp;
		end if;
	-- 提交
	commit;
end

需要說明的是,假設有一個保存點B,rollback to savepoint B僅僅是讓數據庫回到事務中的某個"一致性狀態B",而"一致性狀態B"僅僅是一個"臨時狀態",該"臨時狀態"並沒有將更新回滾,也沒有將更新提交,如圖所示。
在這裏插入圖片描述

事務回滾必須藉助於rollback;(而不是rollback to savepoint B;),事務的提交需藉助於commit;

使用MySQL命令release savepoint [保存點名];可以刪除一個事務的保存點。如果該保存點不存在,該命令將出現錯誤信息: ERROR 1305 (42000): SAVEPOINT does not exist。如果當前的事務中先後定義了兩個相同名字的保存點,舊保存點將被自動丟棄。

1.5. 提交 commit

MySQL自動提交一旦關閉,要是SQL語句正在能操作數據庫就需要手動“提交”更新語句,才能將更新結果提交到數據庫文件中,成爲數據庫永久的組成部分。

MySQL的手動提交方式也分爲顯式提交與隱式提交。

顯式提交:
MySQL自動提交關閉後,使用MySQL命令commit;可以顯示地提交更新語句。
例如:

-- 將自動提交關閉
set autocommit = 0;

insert into prot_user values ('12141', 'AAA');
-- 顯式提交
commit;

執行sql語句後,數據成功插入數據庫中。

隱式提交:
MySQL自動提交關閉後,使用下面的MySQL語句,搭配上數據定義語句,可以隱式地提交更新語句。

begin;
set autocommit = 1;
start transaction;
rename table;
truncate table;

-- 數據定義語句 create alter drop
create database ...
create table ...
create index ...
create function ...
create procedure ...
alter table ...
alter function ...
alter procedure ...
drop database ...
drop table ...
drop function ...
drop index ...
drop procedure ...

1.6. 開啓事務

使用MySQL命令start transaction;可以開啓一個事務,該命令開啓事務的同時,會隱式地關閉MySQL自動交。
在這裏插入圖片描述

2. 鎖機制

2.1. 鎖機制的必要性

同一時刻,多個併發用戶同時訪問同一個數據時,僅僅通過事務機制,無法保證多用戶同時訪問同一數據的數據一致性,有必要引入鎖機制實現MySQL的併發訪問,鎖機制是實現多用戶併發訪問的基石。

也就是說,併發用戶訪問同一數據,鎖機制可以避免數據不一致問題的發生。以場景描述爲例。
在這裏插入圖片描述

2.2. 鎖基礎

1、鎖的粒度
鎖的粒度是指鎖的作用範圍。InnoDB存儲引擎支持表鎖以及行級鎖

2、隱式鎖與顯式鎖
MySQL鎖分爲隱式鎖以及顯式鎖。

MySQL自動加鎖稱爲隱式鎖;數據庫開發人員手動加鎖稱爲顯式鎖。

3、鎖的類型
鎖的類型包括讀鎖(read lock)和寫鎖(write lock)其中讀鎖也稱爲**(表級)共享鎖**,寫鎖也稱爲**(表級)排他鎖或者獨佔鎖**。

-- 用共享鎖鎖表,會阻礙其他事務修改表數據
lock table [表名] read;

-- 用排他鎖鎖表,會阻礙其他事務查詢和修改
lock table [表名] write;

加讀鎖和寫鎖的處理過程,如下兩圖所示:
在這裏插入圖片描述
4、鎖的鑰匙
多個MySQL客戶機併發訪問同一個數據時,如果MySQL客戶機A對該數據成功地施加了鎖,那麼只有MySQL客戶機A擁有這把鎖的"鑰匙”,也就是說:只有MySQL客戶機A能夠對該鎖進行解鎖操作。

解鎖有兩種方式:
第一種是kill鎖的進程,可以用如下代碼實現:

-- 查找鎖進程
-- 如果有SUPER權限可以看到所有進程,否則只能看到自己的進程
show processlist;

-- 解鎖鎖進程
kill [鎖進程的id];

第二種是解鎖表

-- 查詢是否有鎖表
show open tables where in_use > 0;

-- 解鎖表
unlock [表名];

5、鎖的生命週期
鎖的生命週期是指在同一個MySQL服務器連接內,對數據加鎖到解鎖之間的時間間隔。

2.2. 行級鎖

2.2.1. 共享鎖與排他鎖

InnoDB提供了兩種類型的行級鎖,分別是 (行級)共享鎖(S) 以及 (行級)排他鎖(X),其中共享鎖也叫讀鎖,排他鎖也叫寫鎖。在查詢(select) 語句或者更新(insert、update以及delete)語句中,爲受影響的記錄施加行級鎖的方法也非常簡單。

由於共享鎖和排他鎖的自身特性相互矛盾,因此不能在同一數據上同時加上共享鎖和排他鎖。

使用下面的語句可以添加共享鎖:

[sql語句] lock in share mode;

例如:

-- 對test_table表中id=1的行進行共享查詢
-- 在解鎖前其他事務不能對數據進行修改
select * from test_table where id=1 lock in share mode;

寫鎖(s)也稱排它鎖,同一時刻只能有一個事務擁有排它鎖,其它事務不能擁有共享鎖和排它鎖。

需要注意的是:InnoDB引擎insert、update、 delete會自動給涉及的數據加排他鎖(X),這樣的排他鎖叫做隱式排他鎖。而對於一般的select語句,不會加任何鎖,因此一般情況下只對select語句加排他鎖。

使用下面的語句可以添加排他鎖:

[sql語句] for update;

例如:

-- 對test_table表中username="a"的行進行排他查詢
-- 在解鎖前其他事務不能對數據進行查詢和修改
select * from test_table where username="a" for update;

行級鎖與索引之間的關係
InnoDB表的行級鎖是通過對"索引"施加鎖的方式實現的,這就意味着:只有通過索引字段檢索數據的查詢(select)語句或者更新(insert、update、 delete)語句,纔可能施加行級鎖;否則InnoDB將使用表級鎖,使用表級鎖勢必會降低InnoDB表的併發訪問性能。

2.2.2. 意向鎖

意向鎖主要是運用在如下場景:MySQL客戶機A獲得了某個InnoDB表中若干條記錄的行級鎖,此時MySQL客戶機B出於某種原因需要向該表顯式地施加表級鎖(使用lock tables命令),MySQL客戶機B爲了獲得該表的表級鎖,需要逐行檢測表中的行級鎖是否與表級鎖兼容,而這種檢測需要耗費大量的服務器資源。

如果MySQL客戶機A獲得該表若干條記錄的行級鎖之前,MySQL客戶機A直接向該表施加一個"表級鎖” (這個表級鎖是隱式的,也叫意向鎖),MySQL客戶機B僅僅需要檢測自己的表級鎖與該意向鎖是否兼容,無需逐行檢測該表是否存在行級鎖,就會節省不少服務器資源。

在這裏插入圖片描述
爲此,MySQL提供了兩種意向鎖:意向共享鎖(IS)意向排它鎖(IX)

  1. 意向共享鎖(IS)
    向InnoDB表的某些記錄施加行級共享鎖時,InnoDB存儲引擎會自動地向該表施加意向共享鎖(IS)。也就是說,執行select * from [表名] where [條件] lock in share mode;後,InnoDB存儲引擎在爲表中符合[條件]的記錄施加共享鎖前,自動地爲該表施加意向共享鎖(IS);
  2. 意向排他鎖(IX)
    向InnoDB表的某些記錄施加行級排它鎖時,InnoDB存儲引擎會自動地向該表施加意向排它鎖(IX)。也就是說:執行更新語句(例如insert、 update或者delete語句)或者select * from [表名] where [條件] for update;時,InnoDB存儲引擎在爲表中符合[條件]的記錄施加排他鎖前,自動地爲該表施加意向排它鎖(lX)。
2.2.3. 死鎖與等待鎖

默認情況下,InnoDB存儲引擎一旦出現鎖等待超時異常,便不會自動提交事務,也不會自動回滾事務,而這是十分危險的。爲了避免鎖等待超時異常,應該自定義錯誤處理程序,由程序開發人員選擇進一步提交事務,還是回滾事務。

2.2.4. 悲觀鎖與樂觀鎖

悲觀鎖:就是單獨使用排它鎖鎖住記錄select * from [表名] where [條件] for update;,這樣的事務就不能修改這條記錄了。

加了悲觀鎖後,一定要等到一個數據操作結束,纔可以進行下一個數據操作,不支持併發,會造成鎖等待導致數據庫效率降低。

樂觀鎖:樂觀鎖與悲觀鎖相反,它可以支持"併發"操作——樂觀鎖在這條記錄上加一個version字段,更新的時候就+1,select的時候帶出這個字段,當實際更新的時候判斷當前version是不是等於記錄中的version,是則表示執行成功,反之失敗並回退。

begin;
	set @ver = 0;
	select version into @ver from test where id = 1 ;
	select @ver;
	update test set name='b',version=version+1 where id=1 and version=@ver;
commit;

3. 事務的ACID特性

事務的任務是保證一系列更新語句的原子性,鎖的任務是解決併發訪問可能導致的數據不一致問題。如果事務
與事務之間存在併發操作,此時可以通過隔離級別實現事務的隔離性,從而實現數據的併發訪問。

3.1. ACID特性

所謂的ACID特性,就是

  1. 原子性(Atomicity)
    原子性是指事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生;
  2. 一致性(Consistency)
    事務必須便數據庫從一個一致性狀態變換到另外;
  3. 隔離性(Isolation)
    事務的隔離性是多個用戶併發訪問數據庫時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作數據所幹擾,多個併發事務之間要相互隔離,通過鎖機制實現;
  4. 持久性(Durability)
    持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即使數據庫發生故障也不應該對其有任何影響。

3.2. 事務的隔離級別與併發問題

多個線程開啓各自事務操作數據庫中數據時,數據庫系統要負責隔離操作,以保證各個線程在獲取數據時的準確性。如果不考慮隔離性,可能會引發如下問題:

  1. 更新丟失(Lost Update)
    當多個事務選擇同一行操作,並且都是基於最初選定的值,由於每個事務都不知道其他事務的存在,就會發生更新覆蓋的問題,類比github的提交衝突。
  2. 髒讀(Dirty Reads )
    髒讀就是指當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,外一個事務也訪問這個數據,然後使用了這個數據。
    舉一個生動的例子:公司發工資了,領導把5000元打到小白的賬號上,但是該事務並未提交,而小白正好去查看賬戶,發現工資已經到賬,是5000元整,非常高興。可是不幸的是,領導發現發給小白的工資金額不對,是2000元,於是迅速回滾了事務,修改金額後,再將事務提交,最後小白實際的工資只有2000元,小白空歡喜一場。
  3. 不可重複讀(Non-Repeatable Reads)
    是指在一個事務內, 多次讀詞-數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那麼,在第一個事務中的兩次讀數據之間,由於第二個事務的修改, 那麼第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不-樣的,因此稱爲不可重複讀。(即不能讀到相同的數據內容)。
    舉個例子:小白拿着工資卡去消費,系統讀取到卡里確實有2000元,而此時她的老婆也正好在網上轉賬,把小白工資卡的2000元轉到另一賬戶,並在小白之前提交了事務,當小白扣款時, 系統檢查到小白的工資卡已經沒有錢,扣款失敗。
  4. 幻讀( Phantom Reads )
    是指在一個事務內讀取到了別的事務插入的數據,導致前後讀取不一致。
    舉個例子:小白的老婆工作在銀行部門,她時常通過銀行內部系統查看小白的信用卡消費記錄。有一天,她正在查詢到小白當月信用卡的總消費金額select sum(amount) from transaction where month="本月";爲80元,而小白此時正好在外面胡吃海塞後在收銀臺買單,消費1000元,即新增了一條1000元的消費記錄insert transaction;,並提交了事務,隨後小白的老婆將小白當月信用卡消費的明細打印到A4紙上,卻發現消費總額爲1080元,小白的老婆很詫異,以爲出現了幻覺,幻讀就這樣產生了。

爲了區分避免上述問題,SQL標準定義了四種隔離級別:

  1. 0 Read Uncommitted (讀取未提交的數據)
    在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。該隔離級別很少用於實際應用,並且它的性能也不比其他隔離級別好多少,會導致髒讀。
  2. 1 Read Committed (讀取提交的數據)
    這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。只有事物A提交了數據,事物B才能讀取到。它滿足了隔離的簡單定義:一個事務只能看見已提交事務所做的改變。當隔離級別設置爲Readcommitted時,避免了髒讀,但是可能會造成不可重複讀。
  3. 2 Repeatable Read (可重複讀)
    這是MySQL默認的事務隔離級別,它確保同一事務內相同的查詢語句,執行結果一致。當一個事務開始操作某個數據時,該數據就不可被其他事務修改,但這個級別可能會出現幻讀現象。
  4. 3 Serializable (串行化)
    該級別不允許讀寫併發操作,寫執行時,讀必須等待。這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突。換言之,它會在每條select語句後自動加上lock in share mode,爲每個查詢操作施加一個共享鎖。在這個級別,可能導致大量的鎖等待現象。該隔離級別主要用於InnoDB存儲引擎的分佈式事務。

四種隔離級別逐漸增強,其中Read Uncommitted的隔離級別最低,Serializable的隔離級別最高。讀未提交、讀已提交、可重複讀和串行化也可以用數字0、1、2和3來表示。

用一張表來表示如下:

隔離級別
(從上到下依次增強)
髒讀
(Drity Read)
不可重複讀
(Non-repeatable read)
幻讀
(Phantom Read)
read uncommi tted
(讀取未提交的數據)
read commi tted
(讀取提交的數據)
×
repeatable read
(可重讀)
× ×
serial izable
(串行化)
× × ×

四個隔離級別可以通過下面的代碼進行查看和設置

-- 查看隔離級別
show [global] variables like 'transaction_isolation';
-- 或
select @@transaction_isolation;

-- 設置隔離級別
set [session|global] transaction_isolation=[0|1|2|3];

例如

-- 查看隔離級別
show global variables like 'transaction_isolation';
-- 或
select @@transaction_isolation;

-- 設置隔離級別爲0級
-- 此時事務還未提交,查詢就能將數據讀取出
set global transaction_isolation=0;

3.3. 不可重複讀與幻讀的區別

很多人容易搞混不可重複讀和幻讀,確實這兩者有些相似。但不可重複讀重點在於update,而幻讀的重點在於insertdelete

如果使用鎖機制來實現這兩種隔離級別,在可重複讀中,該sql第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這麼做可以有效的避免幻讀、不可重複讀、髒讀等問題,但會極大的降低數據庫的併發能力。

所以說不可重複讀和幻讀最大的區別,就在於如何通過鎖機制來解決他們產生的問題。

3.4. 如何設置事務的隔離級別

合理地設置事務的隔離級別,可以有效避免髒讀、不可重複讀、幻讀等併發問題。

出於性能考慮,都是使用了以樂觀鎖爲理論基礎的MVCC(多版本併發控制)來避免這兩種問題。

在MySQL中不可重複讀和幻讀的解決辦法是使用MVCC(多版本併發控制)保證了數據的可重複讀,也保證了不會讀到幻讀數據(即使是有新的符合條件的數據產生,在同一個事務的下次查詢中也查不到,矇蔽自己的雙眼假裝看不到)。

在MySQL的InnoDB存儲引擎中,MVCC中普通方式select * from table查詢數據是不加任何鎖的,數據的篩選除了通過查詢條件之外,還要根據數據行的隱藏字段(兩個版本號)來和事務的版本號來進行比較後過濾。這樣做的好處是支持的併發量更高(因爲不加鎖),根據版本號來過濾數據也解決了不可重複讀的問題,也能保證不會讀到幻讀數據。

record lock(記錄鎖)和gap lock(間隙鎖)保證了幻讀數據不會產生(在讀取數據的時候加鎖,防止在讀取時有其他事務對讀取條件內的數據做增刪改操作)也就是說如果要完全解決幻讀問題,還要在查詢語句中使用像lock in share modefor update這樣顯式的加鎖語句

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