一、概述
數據庫是一個多用戶使用的共享資源,當多個用戶並發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對並發操作不加控制就可能讀取或存取不正確的數據,破壞數據的不正確性(臟讀,不可重復讀,幻讀等),可能產生死鎖。鎖主要用於多用戶環境下保證數據庫完整性和一致性。
加鎖是實現數據庫並發控制的一個非常重要的技術。當事務在對某個數據對象進行操作前,先向系統發出請求,對其加鎖,加鎖后事務就對該數據對象有了一定的控制,在該事務釋放鎖之前,其他的事務不能對此數據對象進行更新操作。簡單來說,當執行SQL語句事務前,應向數據庫發出請求對你訪問的記錄加鎖,在這個事務釋放鎖之前,其他事物不能對這個數據進行修改更新操作。
數據庫鎖出現的目的:處理並發問題。
二、發生鎖的必要條件
數據庫鎖表的四個必要條件:
(1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用完釋放后其他進程才能使用。
(2)請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不釋放的狀態。
(3)不剝奪條件:指進程已獲得的資源,在未使用完之前不能被剝奪,只能在使用完時由自己釋放。
(4)環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。
三、鎖的分類
從數據庫系統角度分為三種:排他鎖、共享鎖、更新鎖。從程序員角度分為兩種:一種是悲觀鎖,一種是樂觀鎖。
樂觀並發控制和悲觀並發控制是並發控制采用的主要方法。樂觀鎖和悲觀鎖不僅在關系數據庫里應用,在Hibernate、Memcache等也有相關概念。
如下圖所示分類:
悲觀鎖:即悲觀並發控制(Pessimistic Concurrency Controller,縮寫PCC)。悲觀鎖是指在數據處理過程中,使數據處於鎖定狀態,一般使用數據庫的鎖機制實現。即每次去獲取數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人拿這個數據就會block(阻塞),直到它拿到鎖。傳統的關系數據庫里用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖。
注意:在MySQL中使用悲觀鎖,必須關閉MySQL的自動提交即set autocommit=0。MySQL默認使用自動提交autocommit模式,即執行一個更新操作,MySQL會自動將結果提交。
悲觀鎖按使用性質划分為共享鎖、排他鎖、更新鎖三種。
共享鎖(Share Lock)
S鎖,也叫讀鎖,用於所有的只讀數據操作。共享鎖是非獨占的,允許多個並發事務讀取其鎖定的資源。若事務T對數據對象A加上S鎖,則其它事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。X鎖和S鎖都是加在某一個數據對象上的。
共享鎖有如下性質:
- 多個事務可封鎖同一個共享頁;
- 任何事務都不能修改該頁;
- 通常是該頁被讀取完畢,S鎖立即被釋放。
-- 例如在執行sql顯示加上共享鎖
select * from t_user lock in share mode;
排他鎖(Exclusive Lock)
X鎖,也叫寫鎖,表示對數據進行寫操作。如果一個事務對對象加了排他鎖,其他事務就不能再給它加任何鎖。若事務T對數據對象A加上X鎖,則只允許T讀取和修改A,其它任何事務都不能再對A加任何類型的鎖,直到T釋放A上的鎖。原理類似試衣間(某個顧客把試衣間從里面反鎖了,其他顧客想要使用這個試衣間,就只有等待鎖從里面打開)。
排他鎖有如下性質:
- 僅允許一個事務封鎖此頁;
- 其他任何事務必須等到X鎖被釋放才能對該頁進行訪問;
- X鎖一直到事務結束才能被釋放。
SELECT … FOR UPDATE會鎖住行及任何關聯的索引條目,和你對那些行執行update語句相同。其他的事務會被阻塞在對這些行執行update操作。所有被共享鎖和排他鎖查詢所設置的鎖都會在事務提交或者回滾之后被釋放。(使用SELECT FOR UPDATE為update操作鎖定行,只適用於autocommit被禁用的情況下)。
-- 產生排他鎖的SQL語句如下:
select * from ad_plan for update;
更新鎖(Update Lock)也叫U鎖,在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象。
更新鎖有如下性質:
- 用來預定要對此頁施加X鎖,它允許其他事務讀,但不允許再施加U鎖或X鎖;
- 當被讀取的頁要被更新時,則升級為X鎖;
- U鎖一直到事務結束時才能被釋放。
悲觀鎖按作用范圍划分為:行級鎖、表級鎖。
行級鎖:鎖的作用范圍是行級別。行鎖表示對一條記錄加鎖,只影響一條記錄。通常用在DML語句中,如INSERT, UPDATE, DELETE等。InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味着:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則InnoDB將使用表鎖。
行級鎖是一種排他鎖,防止其他事務修改此行。在使用以下語句時,Oracle會自動應用行級鎖:INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
SELECT … FOR UPDATE語句允許用戶一次鎖定多條記錄進行更新。使用COMMIT或ROLLBACK語句釋放鎖。
行級鎖定最大的特點就是鎖定對象的顆粒度很小,也是目前各大數據庫管理軟件所實現的鎖定顆粒度最小的。由於鎖定顆粒度很小,所以發生鎖定資源爭用的概率也最小,能夠給予應用程序盡可能大的並發處理能力從而提高一些需要高並發應用系統的整體性能。雖然能夠在並發處理能力上面有較大的優勢,但行級鎖定也因此帶來了不少弊端。由於鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。
使用行級鎖定的主要是InnoDB存儲引擎。
表級鎖:鎖的作用范圍是整張表。表級別的鎖定是MySQL各存儲引擎中最大顆粒度的鎖定機制。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由於表級鎖一次會將整個表鎖定,所以可以很好的避免困擾我們的死鎖問題。當然,鎖定顆粒度大所帶來最大的負面影響就是出現鎖定資源爭用的概率也會最高,致使並發度大打折扣。
使用表級鎖定的主要是MyISAM,MEMORY,CSV等一些非事務性存儲引擎。
鎖表發生在insert、update 、delete中。 鎖表的原理是數據庫使用獨占式封鎖機制,當執行上面的語句時對表進行鎖住,直到發生commite或者回滾或者退出數據庫用戶。
鎖表的原因:
第一、 A程序執行了對tableA的insert但還未commite時,B程序也對tableA進行insert則此時會發生資源正忙的異常就是鎖表。
第二、鎖表常發生於並發而不是並行(並行時個線程操作數據庫時,另一個線程是不能操作數據庫的)。
如何減少鎖表的概率:減少insert 、update 、delete語句執行到commite之間的時間。也就是說將批量執行改為單個執行、優化sql自身的非執行速度。如果異常則對事務進行回滾。
mysql各存儲引擎支持的鎖:
BDB:支持頁級鎖和表級鎖,默認是頁級鎖。
InnoDB:支持行級鎖和表級鎖,默認是行級鎖。
MyISAM &Memory:這兩個存儲引擎都是采用表級鎖。
數據庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。
樂觀鎖(Optimistic Lock):
每次去拿數據的時候都認為別人不會修改,所以不會上鎖。但是在更新的時候會判斷一下在此期間別人有沒有更新這個數據,可以使用版本號、時間戳等機制去判斷是否被更新過。
(1)版本號(記為version)
就是給數據增加一個版本標識,在數據庫上就是表中增加一個version字段,每次更新把這個字段加1,讀取數據的時候把version讀出來,更新的時候比較version,如果還是開始讀取的version就可以更新了,如果現在的version比老的version大,說明有其他事務更新了該數據,並增加了版本號,這時候得到一個無法更新的通知,用戶自行根據這個通知來決定怎么處理,比如重新開始一遍。這里的關鍵是判斷version和更新兩個動作需要作為一個原子單元執行,否則在你判斷可以更新以后正式更新之前有別的事務修改了version,這個時候你再去更新就可能會覆蓋前一個事務做的更新,造成第二類丟失更新,所以你可以使用update … where … and version=”old version”這樣的語句,根據返回結果是0還是非0來得到通知,如果是0說明更新沒有成功,因為version被改了,如果返回非0說明更新成功。
(2)時間戳(timestamp,使用數據庫服務器的時間戳)
和版本號基本一樣,只是通過時間戳來判斷而已,注意時間戳要使用數據庫服務器的時間戳不能是業務系統的時間。
(3)待更新字段
和版本號方式相似,只是不增加額外字段,直接使用有效數據字段做版本控制信息,因為有時候我們可能無法改變舊系統的數據庫表結構。假設有個待更新字段叫count,先去讀取這個count,更新的時候去比較數據庫中count的值是不是我期望的值(即開始讀的值),如果是就把我修改的count的值更新到該字段,否則更新失敗。
(4)所有字段
和待更新字段類似,只是使用所有字段做版本控制信息,只有所有字段都沒變化才會執行更新。
四、發生鎖的原因
發生鎖表可能的原因有:
(1)字段不加索引:在執行事務的時候,如果表中沒有索引,會執行全表掃描,如果這時候有其他的事務過來,就會發生鎖表。
(2)事務處理時間長:事務處理時間較長,當越來越多事務堆積的時候,會發生鎖表。
(3)關聯操作太多:涉及到很多張表的修改等,在並發量大的時候,會造成大量表數據被鎖。
五、解決鎖出現的方法
出現鎖表時解決方法有如下幾種:
1、通過相關的sql語句可以查出是否被鎖定,和被鎖定的數據。
2、為加鎖進行時間限定,防止無限死鎖。
3、加索引,避免全表掃描。
4、盡量順序操作數據。
5、根據引擎選擇合理的鎖粒度。
6、事務中的處理時間盡量短。
生產中出現死鎖等問題是比較嚴重的問題,因為通常死鎖沒有明顯的錯誤日志,只有在發現錯誤的時候才能后知后覺的處理,所以,一定要盡力避免。
六、鎖定時間的長短
鎖保持的時間長度為保護所請求級別上的資源所需的時間長度。
用於保護讀取操作的共享鎖的保持時間取決於事務隔離級別。采用READ COMMITTED的默認事務隔離級別時,只在讀取頁的期間內控制共享鎖。在掃描中,直到在掃描內的下一頁上獲取鎖時才釋放鎖。如果指定HOLDLOCK提示或者將事務隔離級別設置為REPEATABLE READ或SERIALIZABLE,則直到事務結束才釋放鎖。
根據為游標設置的並發選項,游標可以獲取共享模式的滾動鎖以保護提取。當需要滾動鎖時,直到下一次提取或關閉游標(以先發生者為准)時才釋放滾動鎖。但是,如果指定HOLDLOCK,則直到事務結束才釋放滾動鎖。
用於保護更新的排它鎖將直到事務結束才釋放。
如果一個連接試圖獲取一個鎖,而該鎖與另一個連接所控制的鎖沖突,則試圖獲取鎖的連接將一直阻塞到:將沖突鎖釋放而且連接獲取了所請求的鎖。 連接的超時間隔已到期。默認情況下沒有超時間隔,但是一些應用程序設置超時間隔以防止無限期等待。
七、如何鎖表或鎖表的某一行
(1)鎖一個表的某一行
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED //設置事務隔離級別為讀未提交
SELECT * FROM table ROWLOCK WHERE id = 1
(2)鎖定數據庫的一個表
SELECT * FROM table WITH (HOLDLOCK)
HOLDLOCK持有共享鎖,直到整個事務完成,應該在被鎖對象不需要時立即釋放等於SERIALIZABLE事務隔離級別。
NOLOCK 語句執行時不發出共享鎖,允許臟讀 ,等於READ UNCOMMITTED事務隔離級別。
PAGLOCK 使用一個表鎖的地方用多個頁鎖。
ROWLOCK 強制使用行鎖。
TABLOCKX 強制使用獨占表級鎖,這個鎖在事務期間阻止任何其他事務使用這個表。
UPLOCK 強制在讀表時使用更新而不用共享鎖。
八、數據庫隔離級別
每一種隔離級別滿足不同的數據要求,使用不同程度的鎖。
Read Uncommitted,讀寫均不使用鎖,數據的一致性最差,也會出現許多邏輯錯誤。
Read Committed,使用寫鎖,但是讀會出現不一致,不可重復讀。
Repeatable Read, 使用讀鎖和寫鎖,解決不可重復讀的問題,但會有幻讀。
Serializable, 使用事務串形化調度,避免出現因為插入數據沒法加鎖導致的不一致的情況。
(1)丟失修改/丟失更新(lose update)
兩個事務T1和T2讀入同一數據並修改,T2的提交結果破壞了T1的提交結果,導致T1的修改被丟失。
(2)臟讀
臟讀是指事務T1讀取了事務T2更新后的數據(已寫入磁盤),然后T2回滾操作,那么T1讀取到的數據是臟數據。
例如:A用戶修改了數據,隨后B用戶又讀出該數據,但A用戶因為某些原因取消了對數據的修改,數據恢復原值,此時B得到的數據就與數據庫內的數據產生了不一致。
(3)不可重復讀(non-repeatable read)
A用戶讀取數據,隨后B用戶讀出該數據並修改,此時A用戶再讀取數據時發現前后兩次的值不一致。
不可重復讀是指事務T1讀取數據后,事務T2執行更新操作,使事務T1無法再現前一次讀取結果。主要分三種情況:
- 事務T1讀取某一數據后,事務T2對其進行了修改,當事務T1再次讀取該數據時,得到的值與前一次不同;
- 事務T1按照某一條件讀取某些數據后,事務T2刪除了其中部分記錄,當事務T1再次以相同條件讀取數據時,發現某些記錄神秘消失了;
- 事務T1按照某一條件讀取某些數據后,事務T2在符合該條件下插入了部分記錄,當事務T1再次以相同條件讀取數據時,發現多了一些記錄。
第二條和第三條好像發生幻覺一樣,數據莫名其妙多了或者少了,有時也被稱為幻影(phantom row) 或 幻讀,其注重插入和刪除操作,而第一種注重修改操作。
針對不可重復讀的解決方案如下:
- 對於第一種,對象是一條記錄或者多條記錄,解決此問題需要鎖行(row locks),其他的記錄不用關心;
- 對於幻讀,操作對象是整個表,所以需要鎖表(table locks)。
產生上面三種數據不一致的主要原因是破壞了事務的隔離性。
(1)讀不提交,造成臟讀(Read Uncommitted)
一個事務中的讀操作可能讀到另一個事務中未提交修改的數據,如果事務發生回滾就可能造成錯誤。
例子:A打100塊給B,B看賬戶,這是兩個操作,針對同一個數據庫,兩個事物,如果B讀到了A事務中的100塊,認為錢打過來了,但是A的事務最后回滾了,造成損失。避免這些事情的發生就需要我們在寫操作的時候加鎖,使讀寫分離,保證讀數據的時候,數據不被修改,寫數據的時候,數據不被讀取。從而保證寫的同時不能被另個事務寫和讀。
沒有解決臟讀問題,不建議使用。
(2)讀提交(Read Committed)
我們加了寫鎖,就可以保證不出現臟讀,也就是保證讀的都是提交之后的數據,但是會造成不可重復讀,即讀的時候不加鎖,一個讀的事務過程中,如果讀取數據兩次,在兩次之間有寫事務修改了數據,將會導致兩次讀取的結果不一致,從而導致邏輯錯誤。
在READ COMMITTED
級別下,除了唯一性的約束檢查(duplicate-key checking)和外鍵約束檢查(foreign-key constraint checking)需要使用gap lock外,其他情況InnoDB不會使用此算法。所以由於gap lock算法禁用,該級別下是會出現幻讀的。
只讀提交隔離級別僅支持基於行的二進制日志記錄。如果使用read committed with binlog_format=mixed
,服務器將自動使用基於行的日志記錄。
- 對於update或delete語句,innodb只對其更新或刪除的行持有鎖。在mysql評估where條件之后,將釋放非匹配行的記錄鎖。這大大降低了死鎖的可能性,但它們仍然可能發生。
- 對於update語句,如果一行已經被鎖定,innodb執行“semi-consistent”讀取,將最新提交的版本返回給mysql,這樣mysql就可以確定該行是否與更新的where條件匹配。如果行匹配(必須更新),mysql會再次讀取行,這次innodb要么鎖定它,要么等待對它的鎖定。
(3)可重讀(Repeatable Read)
解決不可重復讀問題,一個事務中如果有多次讀取操作,讀取結果需要一致(指的是固定一條數據的一致,幻讀指的是查詢出的數量不一致)。 這就牽涉到事務中是否加讀鎖,並且讀操作加鎖后是否在事務commit之前持有鎖的問題,如果不加讀鎖,必然出現不可重復讀,如果加鎖讀完立即釋放,不持有,那么就可能在其他事務中被修改,若其他事務已經執行完成,此時該事務中再次讀取就會出現不可重復讀,所以讀鎖在事務中持有可以保證不出現不可重復讀,寫的時候必須加鎖且持有,這是必須的了,不然就會出現臟讀。Repeatable Read(可重復讀)也是MySql的默認事務隔離級別,上面的意思是讀的時候需要加鎖並且保持。
在數據庫系統對REPEATABLE READ
的說明中,禁止臟讀和不可重復讀,但可能會出現幻讀。InnoDB存儲引擎與標准SQL有所不同,使用Next-Key 和 Gap-Key 鎖的算法,避免幻讀的產生。簡單來說,可以分為以下幾方面:
- 對於普通的(非鎖定的)SELECT語句,不會加鎖(S鎖也不加),而是讀取數據的快照(read the snapshot)
- 對於鎖定讀取(選擇with For UPDATE或For SHARE)、UPDATE和DELETE語句,鎖定取決於該語句是使用具有唯一搜索條件的惟一索引,還是使用范圍類型搜索條件。
- 對於具有唯一搜索條件的唯一索引,InnoDB只鎖定找到的索引記錄,而不鎖定它之前的空白。
- 對於其他搜索條件,InnoDB鎖定掃描的索引范圍,使用gap-lock鎖或next-key鎖來阻止其他會話插入到范圍覆蓋的間隙中。
(4)可串行化(Serializable)
解決幻讀問題,在同一個事務中,同一個查詢多次返回的結果不一致。事務A新增了一條記錄,事務B在事務A提交前后各執行了一次查詢操作,發現后一次比前一次多了一條記錄。幻讀是由於並發事務增加記錄導致的,這個不能像不可重復讀通過記錄加鎖解決,因為對於新增的記錄根本無法加鎖。需要將事務串行化,才能避免幻讀。這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。
解決上面說的所有事務並發問題,但事務是串行執行的,即一個一個的按照順序執行,不能並發執行。
九、MySQL 事務特性
事務是由一組SQL語句組成的邏輯處理單元,事務具有ACID屬性。
(1)原子性(Atomicity):事務是一個原子操作單元。在當時原子是不可分割的最小元素,其對數據的修改,要么全部成功,要么全部都不成功。
(2)一致性(Consistent):事務開始到結束的時間段內,數據都必須保持一致狀態。
(3)隔離性(Isolation):數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的”獨立”環境執行。
(4)持久性(Durable):事務完成后,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。
十、參考博文
(1) https://blog.csdn.net/AntdonYu/java/article/details/82496349
(2) https://zhidao.baidu.com/question/427336027.html (較為清楚)
(3) https://www.cnblogs.com/djs19/p/11507024.html (基礎概念,有圖比較直觀)
(4) https://www.cnblogs.com/wzxblog/p/5731799.html
(5) https://blog.csdn.net/user2041/article/details/80766135
(6) https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%AD%BB%E9%94%81/10015665?fr=aladdin
(7)https://zhuanlan.zhihu.com/p/63912473 (鎖,備份等)
(8) https://blog.csdn.net/xxy55895/java/article/details/105715458
(9) https://blog.csdn.net/a1920135141/article/details/100915289/
(10) https://blog.csdn.net/C_J33/java/article/details/79487941
(11) https://blog.csdn.net/u014427391/java/article/details/78951730
(12) https://www.cnblogs.com/panxuejun/p/5924252.html (案例解釋很仔細)
(13)https://mingshan.fun/2019/09/01/transaction-running/ (事務鎖等待解決方法)