數據庫事務
概念
數據庫事務( transaction)是訪問並可能操作各種數據項的一個數據庫操作序列,這些操作要么全部執行,要么全部不執行,是一個不可分割的工作單位
作用
一個數據庫事務通常包含了一個序列的對數據庫的讀/寫操作。它的存在包含有以下兩個目的:
- 為數據庫操作序列提供了一個從失敗中恢復到正常狀態的方法,同時提供了數據庫即使在異常狀態下仍能保持一致性的方法。
- 當多個應用程序在並發訪問數據庫時,可以在這些應用程序之間提供一個隔離方法,以防止彼此的操作互相干擾。
當事務被提交給了DBMS(數據庫管理系統),則DBMS(數據庫管理系統)需要確保該事務中的所有操作都成功完成且其結果被永久保存在數據庫中,如果事務中有的操作沒有成功完成,則事務中的所有操作都需要被回滾,回到事務執行前的狀態;同時,該事務對數據庫或者其他事務的執行無影響,所有的事務都好像在獨立的運行。
但在現實情況下,失敗的風險很高。在一個數據庫事務的執行過程中,有可能會遇上事務操作失敗、數據庫系統/操作系統失敗,甚至是存儲介質失敗等情況。這便需要DBMS對一個執行失敗的事務執行恢復操作,將其數據庫狀態恢復到一致狀態(數據的一致性得到保證的狀態)。為了實現將數據庫狀態恢復到一致狀態的功能,DBMS通常需要維護事務日志以追蹤事務中所有影響數據庫數據的操作。
事務(Transaction)ACID屬性
事務是由一組SQL語句組成的邏輯處理單元,事務具有4屬性,通常稱為事務的ACID屬性。
- 原子性(Actomicity):事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行。
- 一致性(Consistent):
- 幾個並行執行的事務,其執行結果必須與按某一順序串行執行的結果相一致 (百度百科)
- 一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之后都必須處於一致性狀態。
如:轉賬,假設用戶A和用戶B兩者的錢加起來一共是5000,那么不管A和B之間如何轉賬,轉幾次賬,事務結束后兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。
- 隔離性(Isolation):
- 數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然。
- 隔離性是當多個用戶並發訪問數據庫時,比如操作同一張表時,數據庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個並發事務之間要相互隔離。
- 即要達到這么一種效果:對於任意兩個並發的事務T1和T2,在事務T1看來,T2要么在T1開始之前就已經結束,要么在T1結束之后才開始,這樣每個事務都感覺不到有其他事務在並發地執行。
- 持久性(Durable):事務完成之后,它對於數據的修改是永久性的,即使出現系統故障(數據庫系統、操作系統和計算機硬件故障)也能夠保持。
並發事務帶來的問題
相對於串行處理來說,並發事務處理能大大增加數據庫資源的利用率,提高數據庫系統的事務吞吐量,從而可以支持可以支持更多的用戶。但並發事務處理也會帶來一些問題,主要包括以下幾種情況。:
- 更新丟失(Lost update) :兩個事務T1和T2讀入同一個數據並修改,T2提交的結果覆蓋了T1提交的結果,導致T1的修改被丟失。
- 臟讀(Dirty Reads) :
- 事務T1對數據進行修改,但是還沒有提交時,事務T2讀取數據進行修改,此時T2讀取的是T1修改了的值。但是突然由於某種原因T1進行了回滾,這時候數據恢復了原來的值,而T2取得的數據依然是T1修改的值,這就導致了數據庫中的值與事務獲取的值不同的現象。也就是讀到了不正確的值,這就叫臟讀。
- 比如張三工資為5000元,事務A將他的工資改為8000元,在事務A還未提交時,事務B正在讀取張三的工資,讀取的值為8000.這時候突然由於事務A發生了異常進行了回滾,張三的工資恢復成了5000元,而事務B讀取的工資卻是8000元,讀取的值與數據庫中的值不同,這就是臟讀。
- 非重復讀(Non-repeatable Reads)
- 是指在一個事務內,多次讀取同一數據。在這個事務還沒結束時,另外一個事務也訪問該同一數據,並對該數據進行了修改。此時第一個事務再去讀此數據時讀到的結果與之前的結果不同。這就導致了在一個事務內兩次相同的查詢讀到的數據是不一樣的,因此稱為是不可重復讀。
- 比如:在事務A中讀到的張三的工資是5000元,操作還沒有完成,事務還沒提交。與此同時,事務B把張三的工資改為了8000,並提交了事務。隨后,在事務A中,再次讀取張三的工資,此時工資變為8000元。在A事務這一個事務內前后兩次讀取的結果不一致,導致了不可重復讀。
- 幻讀(Phantom Reads)
- 例如:目前工資為5000元的員工有10個人,事務A讀取所有工資為5000元的員工的人數為10人。此時事務B插入一條工資也為5000的記錄。這時,事務A再次讀取工資為5000元的員工,記錄為11人。此時產生了幻讀。
臟讀和幻讀的區別
- 不可重復讀和幻讀的情況很類似,但是不可重復讀的重點是修改:同樣條件下,你讀取過的數據,再次讀取出來發現值不一樣了。
- 幻讀的重點在於新增或者刪除:同樣條件下,第一次和第二次讀出來的記錄數不一樣。
“臟讀”、“不可重復讀”和“幻讀”,其實都是數據庫讀一致性問題,避免不一致的方法和技術就是進行並發控制。最常用的就是封鎖技術,但封鎖技術的缺點是會造成死鎖和性能下降。為了兼顧並發效率與異常控制,定義了4中隔離級別,接下來將介紹隔離級別。
數據庫事務的隔離級別
在並發事務處理帶來的問題中,“更新丟失”通常應該是完全避免的。但防止更新丟失,並不能單靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖來解決,因此,防止更新丟失應該是應用的責任。
隔離級別越高,越能保證數據的完整性和一致性,但是對並發性能的影響也越大。
大多數數據庫的默認級別就是Read committed,比如Sql Server , Oracle。
Mysql的默認隔離級別就是可重復讀取。
1、Read uncommitted(讀未提交數據):
如果一個事務已經開始寫數據,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據。該隔離級別可以通過“排他寫鎖”實現。
避免了更新丟失,卻可能出現臟讀、不可重復讀、幻讀的情況。
2、Read committed(讀已提交數據):
讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。
該隔離級別避免了臟讀,但是卻可能出現不可重復讀、幻讀的情況。
3、Repeatable read(可重復讀):
讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。
避免了不可重復讀取和臟讀,但是有時可能出現幻讀。這中隔離級別可以通過“共享讀鎖”和“排他寫鎖”實現。
4、Serializable(可串行化):
提供嚴格的事務隔離。它要求事務串行化執行,事務只能一個接着一個地執行,但不能並發執行。如果僅僅通過“行級鎖”是無法實現事務串行化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。
串行化是最高的事務隔離級別,同時代價也花費最高,性能很低,一般很少使用,在該級別下,事務順序執行,不僅可以避免臟讀、不可重復讀,還避免了幻像讀。
對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設為Read Committed。它能夠避免臟讀取,而且具有較好的並發性能。盡管它會導致不可重復讀、幻讀和第二類丟失更新這些並發問題,在可能出現這類問題的個別場合,可以由應用程序采用悲觀鎖或樂觀鎖來控制。
傳播行為
在我們用SSH開發項目的時候,我們一般都是將事務設置在Service層,那么當我們調用Service層的一個方法的時候它能夠保證我們的這個方法中執行的所有的對數據庫的更新操作保持在一個事務中,在事務層里面調用的這些方法要么全部成功,要么全部失敗。那么事務的傳播特性也是從這里說起的。
如果你在你的Service層的這個方法中,除了調用了Dao層的方法之外,還調用了本類的其他的Service方法,那么在調用其他的Service方法的時候,這個事務是怎么規定的呢,我必須保證我在我方法里掉用的這個方法與我本身的方法處在同一個事務中,否則如果保證事物的一致性。事務的傳播特性就是解決這個問題的,“事務是會傳播的”在Spring中有針對傳播特性的多種配置我們大多數情況下只用其中的一種:PROPGATION_REQUIRED:這個配置項的意思是說當我調用service層的方法的時候開啟一個事務(具體調用那一層的方法開始創建事務,要看你的aop的配置),那么在調用這個service層里面的其他的方法的時候,如果當前方法產生了事務就用當前方法產生的事務,否則就創建一個新的事務。
- PROPAGATION_REQUIRED:如果當前沒有事務,就創建一個新事務,如果當前存在事務,就加入該事務,該設置是最常用的設置。
- PROPAGATION_SUPPORTS:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就以非事務執行。‘
- PROPAGATION_MANDATORY:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就拋出異常。
- PROPAGATION_REQUIRES_NEW:創建新事務,無論當前存不存在事務,都創建新事務。
- PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則拋出異常。
- PROPAGATION_NESTED:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。
鎖機制
鎖是計算機協調多個進程或線程並發訪問某一資源的機制。在數據庫中,除傳統的 計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供許多用戶共享的資源。如何保證數據並發訪問的一致性、有效性是所有數據庫必須解決的一 個問題,鎖沖突也是影響數據庫並發訪問性能的一個重要因素。
- 按鎖類型划分,可分為共享鎖、排他鎖
- 按鎖的粒度划分,可分為表級鎖、行級鎖、頁級鎖
- 按使用機制划分,可分為樂觀鎖、悲觀鎖
按鎖類型划分,可分為共享鎖、排他鎖
共享鎖(也叫寫鎖、S鎖):多個事務可封鎖一個共享頁;任何事務都不能修改該頁;通常是該頁讀取完畢,S鎖立即被釋放。在執行select語句的時候需要給操作對象(表或者一些記錄 )加上共享鎖,但加鎖之前需要檢查是否有排他鎖,如果沒有,則可以加共享鎖(一個對象上可以加n個共享鎖),否則不行。共享鎖通常在執行完select語句之后被釋放,當然也有可能是在事務結束(包括正常結束和異常結束)的時候被釋放,主要取決於數據庫所設置的事務隔離級別。
排他鎖(也叫寫鎖、X鎖):僅允許一個事務封鎖此頁;其他任何事務必須等到X鎖被釋放才能對該鎖頁進行訪問;X鎖一直到事務結束才能被釋放。執行insert、update、delete語句的時候需要給操作的對象加排他鎖,在加排他鎖之前必須確認該對象上沒有其他任何鎖,一旦加上排他鎖之后,就不能再給這個對象加其他任何鎖。排他鎖的釋放通常是在事務結束的時候(當然也有例外,就是在數據庫事務隔離級別被設置成Read Uncommitted(讀未提交數據)的時候,這種情況下排他鎖會在執行完更新操作之后被釋放,而不是在事務結束的時候)。
按鎖的粒度划分,可分為表級鎖、行級鎖、頁級鎖
表級鎖:直接鎖定整張表,在鎖定期間,其他進程無法對該表進行寫操作。如果你是寫鎖,則其他進程讀也不允許。特點是:開銷小、加鎖快,不會出現死鎖。鎖定粒度最大,發生鎖沖突的概率最高,並發度最低。
行級鎖:僅對指定的記錄進行加鎖,這樣其他進程還是可以對同一個表中的其他記錄進行操作。特點:開銷大,加鎖慢,會出現死鎖。鎖定的粒度最小,發生鎖沖突的概率最低,並發度也最高。
頁級鎖:一次鎖定相鄰的一組記錄。開銷和加鎖時間介於表級鎖和行級鎖之間;會出現死鎖;鎖定粒度也介於表級鎖和行級鎖之間,並發度一般。
按使用機制划分,可分為樂觀鎖、悲觀鎖
實現並發控制的主要手段大致可以分為樂觀並發控制和悲觀並發控制兩種。
在開始介紹之前要明確一下:無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可以認為是一種思想。其實不僅僅是關系型數據庫系統中有樂觀鎖和悲觀鎖的概念,像hibernate、tair、memcache等都有類似的概念。所以,不應該拿樂觀鎖、悲觀鎖和其他的數據庫鎖等進行對比。
並發控制
當程序中可能出現並發的情況時,我們就需要通過一定的手段來保證在並發情況下數據的准確性,通過這種手段保證了當前用戶和其他用戶一起操作時,所得到的結果和他單獨操作時的結果是一樣的。這種手段就叫做並發控制。並發控制的目的是保證一個用戶的工作不會對另一個用戶的工作產生不合理的影響。
沒有做好並發控制,就可能導致臟讀、幻讀和不可重復讀等問題。
我們常說的並發控制,一般都和數據庫管理系統(DBMS)有關。在DBMS中的並發控制的任務,是確保在多個事務同時存取數據庫中同一數據時,不破壞事務的隔離性和統一性以及數據庫的統一性。
悲觀鎖(Pessimistic Lock)
當我們要對一個數據庫中的一條數據進行修改的時候,為了避免同時被其他人修改,最好的辦法就是直接對該數據進行加鎖以防止並發。這種借助數據庫鎖機制,在修改數據之前先鎖定,再修改的方式被稱之為悲觀並發控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”)。
百度百科:
悲觀鎖,正如其名,具有強烈的獨占和排他特性。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度。因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
之所以叫做悲觀鎖,是因為這是一種對數據的修改抱有悲觀態度的並發控制方式。我們一般認為數據被並發修改的概率比較大,所以需要在修改之前先加鎖。
悲觀鎖主要是共享鎖或排他鎖
- 共享鎖又稱為讀鎖,簡稱S鎖。顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改。
- 排他鎖又稱為寫鎖,簡稱X鎖。顧名思義,排他鎖就是不能與其他鎖並存,如果一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對數據行讀取和修改。
悲觀並發控制實際上是“先取鎖再訪問”的保守策略,為數據處理的安全提供了保證。
但是在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會。另外還會降低並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數據。
樂觀鎖( Optimistic Locking )
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則返回給用戶錯誤的信息,讓用戶決定如何去做。
百度百科:
樂觀鎖機制采取了更加寬松的加鎖機制。樂觀鎖是相對悲觀鎖而言,也是為了避免數據庫幻讀、業務處理時間過長等原因引起數據處理錯誤的一種機制,但樂觀鎖不會刻意使用數據庫本身的鎖機制,而是依據數據本身來保證數據的正確性。
相對於悲觀鎖,在對數據庫進行處理的時候,樂觀鎖並不會使用數據庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄數據版本。
樂觀並發控制相信事務之間的數據競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。
實現方式
悲觀鎖實現方式
悲觀鎖的實現,往往依靠數據庫提供的鎖機制。在數據庫中,悲觀鎖的流程如下:
- 在對記錄進行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)。
- 如果加鎖失敗,說明該記錄正在被修改,那么當前查詢可能要等待或者拋出異常。具體響應方式由開發者根據實際需要決定。
- 如果成功加鎖,那么就可以對記錄做修改,事務完成后就會解鎖了。
- 期間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。
拿比較常用的MySql Innodb引擎舉例,來說明一下在SQL中如何使用悲觀鎖。
要使用悲觀鎖,我們必須關閉MySQL數據庫的自動提交屬性。因為MySQL默認使用autocommit模式,也就是說,當我們執行一個更新操作后,MySQL會立刻將結果進行提交。(sql語句:set autocommit=0)
以淘寶下單過程中扣減庫存的需求說明一下悲觀鎖的使用:
以上,在對id = 1的記錄修改前,先通過for update的方式進行加鎖,然后再進行修改。這就是比較典型的悲觀鎖策略。
如果以上修改庫存的代碼發生並發,同一時間只有一個線程可以開啟事務並獲得id=1的鎖,其它的事務必須等本次事務提交之后才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
上面我們提到,使用select…for update會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認行級鎖。行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住,這點需要注意。
樂觀鎖實現方式
使用樂觀鎖就不需要借助數據庫的鎖機制了。
樂觀鎖的概念中其實已經闡述了它的具體實現細節。主要就是兩個步驟:沖突檢測和數據更新。其實現方式有一種比較典型的就是CAS(Compare and Swap)。
CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
比如前面的扣減庫存問題,通過樂觀鎖可以實現如下:

以上,我們在更新之前,先查詢一下庫存表中當前庫存數(quantity),然后在做update的時候,以庫存數作為一個修改條件。當我們提交更新的時候,判斷數據庫表對應記錄的當前庫存數與第一次取出來的庫存數進行比對,如果數據庫表當前庫存數與第一次取出來的庫存數相等,則予以更新,否則認為是過期數據。
以上更新語句存在一個比較重要的問題,即傳說中的ABA問題。
比如說一個線程one從數據庫中取出庫存數3,這時候另一個線程two也從數據庫中取出庫存數3,並且two進行了一些操作變成了2,然后two又將庫存數變成3,這時候線程one進行CAS操作發現數據庫中仍然是3,然后one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。

有一個比較好的辦法可以解決ABA問題,那就是通過一個單獨的可以順序遞增的version字段。改為以下方式即可:

樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現ABA問題,因為版本號只會增加不會減少。
除了version以外,還可以使用時間戳,因為時間戳天然具有順序遞增性。
以上SQL其實還是有一定的問題的,就是一旦遇上高並發的時候,就只有一個線程可以修改成功,那么就會存在大量的失敗。
對於像淘寶這樣的電商網站,高並發是常有的事,總讓用戶感知到失敗顯然是不合理的。所以,還是要想辦法減少樂觀鎖的粒度的。
有一條比較好的建議,可以減小樂觀鎖力度,最大程度的提升吞吐率,提高並發能力!如下:

以上SQL語句中,如果用戶下單數為1,則通過quantity - 1 > 0的方式進行樂觀鎖控制。
以上update語句,在執行過程中,會在一次原子操作中自己查詢一遍quantity的值,並將其扣減掉1。
高並發環境下鎖粒度把控是一門重要的學問,選擇一個好的鎖,在保證數據安全的情況下,可以大大提升吞吐率,進而提升性能。
如何選擇
在樂觀鎖與悲觀鎖的選擇上面,主要看下兩者的區別以及適用場景就可以了。
-
樂觀鎖並未真正加鎖,效率高。一旦鎖的粒度掌握不好,更新失敗的概率就會比較高,容易發生業務失敗。
-
悲觀鎖依賴數據庫鎖,效率低。更新失敗的概率比較低。
隨着互聯網三高架構(高並發、高性能、高可用)的提出,悲觀鎖已經越來越少的被使用到生產環境中了,尤其是並發量比較大的業務場景。
MySQL鎖概述
相對其他數據庫而言,MySQL的鎖機制比較簡單,其最 顯著的特點是不同的存儲引擎支持不同的鎖機制。比如,MyISAM和MEMORY存儲引擎采用的是表級鎖(table-level locking);BDB存儲引擎采用的是頁面鎖(page-level locking),但也支持表級鎖;InnoDB存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般
從上述特點可見,很難籠統地說哪種鎖更好,只能就具體應用的特點來說哪種鎖更合適!僅從鎖的角度 來說:表級鎖更適合於以查詢為主,只有少量按索引條件更新數據的應用,如Web應用;而行級鎖則更適合於有大量按索引條件並發更新少量不同數據,同時又有 並發查詢的應用,如一些在線事務處理(OLTP)系統。
MyISAM表鎖
MySQL的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨占寫鎖(Table Write Lock)。
對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;對 MyISAM表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的!根據如表20-2所示的 例子可以知道,當一個線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。
MyISAM存儲引擎的寫鎖阻塞讀例子:
當一個線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。
MyISAM存儲引擎的讀鎖阻塞寫例子:
一個session使用LOCK TABLE命令給表film_text加了讀鎖,這個session可以查詢鎖定表中的記錄,但更新或訪問其他表都會提示錯誤;同時,另外一個session可以查詢表中的記錄,但更新就會出現鎖等待。
如何加表鎖
MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作 (UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預,因此,用戶一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。在示例中,顯式加鎖基本上都是為了演示而已,並非必須如此。
給MyISAM表顯示加鎖,一般是為了在一定程度模擬事務操作,實現對某一時間點多個表的一致性讀取。例如, 有一個訂單表orders,其中記錄有各訂單的總金額total,同時還有一個訂單明細表order_detail,其中記錄有各訂單每一產品的金額小計 subtotal,假設我們需要檢查這兩個表的金額合計是否相符,可能就需要執行如下兩條SQL:
Select sum(total) from orders; Select sum(subtotal) from order_detail;
這時,如果不先給兩個表加鎖,就可能產生錯誤的結果,因為第一條語句執行過程中,order_detail表可能已經發生了改變。因此,正確的方法應該是:
Lock tables orders read local, order_detail read local; Select sum(total) from orders; Select sum(subtotal) from order_detail; Unlock tables;
要特別說明以下兩點內容:
1、上面的例子在LOCK TABLES時加了“local”選項,其作用就是在滿足MyISAM表並發插入條件的情況下,允許其他用戶在表尾並發插入記錄,有關MyISAM表的並發插入問題,在后面還會進一步介紹。
2、在用LOCK TABLES給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,並且MySQL不支持鎖升級。也就是說,在執行LOCK TABLES后,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那么只能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的 情況下也基本如此,MyISAM總是一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因。
當使用LOCK TABLES時,不僅需要一次鎖定用到的所有表,而且,同一個表在SQL語句中出現多少次,就要通過與SQL語句中相同的別名鎖定多少次,否則也會出錯!舉例說明如下。
(1)對actor表獲得讀鎖:
mysql> lock table actor read;
Query OK, 0 rows affected (0.00 sec)
(2)但是通過別名訪問會提示錯誤:ERROR 1100 (HY000): Table ‘a’ was not locked with LOCK TABLES
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' anda.last_name = 'Tom' and a.last_name <> b.last_name;
(3)需要對別名分別鎖定:
mysql> lock table actor as a read,actor as b read; Query OK, 0 rows affected (0.00 sec)
(4)按照別名的查詢可以正確執行:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' anda.last_name = 'Tom' and a.last_name <> b.last_name;
+————+———–+————+———–+
| first_name | last_name | first_name | last_name |
+————+———–+————+———–+
| Lisa | Tom | LISA | MONROE |
+————+———–+————+———–+
1 row in set (0.00 sec)
查詢表級鎖爭用情況
可以通過檢查table_locks_waited和table_locks_immediate狀態變量來分析系統上的表鎖定爭奪:
mysql> show status like 'table%';
Variable_name | Value
Table_locks_immediate | 2979
Table_locks_waited | 0
2 rows in set (0.00 sec))
如果Table_locks_waited的值比較高,則說明存在着較嚴重的表級鎖爭用情況。
並發插入(Concurrent Inserts)
上文提到過MyISAM表的讀和寫是串行的,但這是就總體而言的。在一定條件下,MyISAM表也支持查詢和插入操作的並發進行。
MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其並發插入的行為,其值分別可以為0、1或2。
- 當concurrent_insert設置為0時,不允許並發插入。
- 當concurrent_insert設置為1時,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個進程讀表的同時,另一個進程從表尾插入記錄。這也是MySQL的默認設置。
- 當concurrent_insert設置為2時,無論MyISAM表中有沒有空洞,都允許在表尾並發插入記錄。
在下面的例子中,session_1獲得了一個表的READ LOCAL鎖,該線程可以對表進行查詢操作,但不能對表進行更新操作;其他的線程(session_2),雖然不能對表進行刪除和更新操作,但卻可以對該表進行並發插入操作,這里假設該表中間不存在空洞。
MyISAM存儲引擎的讀寫(INSERT)並發例子:
可以利用MyISAM存儲引擎的並發插入特性,來解決應 用中對同一表查詢和插入的鎖爭用。例如,將concurrent_insert系統變量設為2,總是允許並發插入;同時,通過定期在系統空閑時段執行 OPTIMIZE TABLE語句來整理空間碎片,收回因刪除記錄而產生的中間空洞。
MyISAM的鎖調度
前面講過,MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。那么,一個進程請求某個 MyISAM表的讀鎖,同時另一個進程也請求同一表的寫鎖,MySQL如何處理呢?答案是寫進程先獲得鎖。不僅如此,即使讀請求先到鎖等待隊列,寫請求后 到,寫鎖也會插到讀鎖請求之前!這是因為MySQL認為寫請求一般比讀請求要重要。這也正是MyISAM表不太適合於有大量更新操作和查詢操作應用的原 因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。這種情況有時可能會變得非常糟糕!幸好我們可以通過一些設置來調節MyISAM 的調度行為。
- 通過指定啟動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。
- 通過執行命令
SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低。 - 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。
雖然上面3種方法都是要么更新優先,要么查詢優先的方法,但還是可以用其來解決查詢相對重要的應用(如用戶登錄系統)中,讀鎖等待嚴重的問題。
另外,MySQL也提供了一種折中的辦法來調節讀寫沖突,即給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值后,MySQL就暫時將寫請求的優先級降低,給讀進程一定獲得鎖的機會。
上面已經討論了寫優先調度機制帶來的問題和解決辦法。這 里還要強調一點:一些需要長時間運行的查詢操作,也會使寫進程“餓死”!因此,應用中應盡量避免出現長時間運行的查詢操作,不要總想用一條SELECT語 句來解決問題,因為這種看似巧妙的SQL語句,往往比較復雜,執行時間較長,在可能的情況下可以通過使用中間表等措施對SQL語句做一定的“分解”,使每 一步查詢都能在較短時間完成,從而減少鎖沖突。如果復雜查詢不可避免,應盡量安排在數據庫空閑時段執行,比如一些定期統計可以安排在夜間執行。
InnoDB鎖
InnoDB與MyISAM的最大不同有兩點:一是支持事務(TRANSACTION);二是采用了行級鎖。行級鎖與表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。
3、事務隔離級別
在並發事務處理帶來的問題中,“更新丟失”通常應該是完全避免的。但防止更新丟失,並不能單靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖來解決,因此,防止更新丟失應該是應用的責任。
“臟讀”、“不可重復讀”和“幻讀”,其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。數據庫實現事務隔離的方式,基本可以分為以下兩種。
- 一種是在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。
- 另一種是不用加任何鎖,通過一定機制生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從用戶的角度,好像是數據庫可以提供同一數據的多個版本,因此,這種技術叫做數據多版本並發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也經常稱為多版本數據庫。
在MVCC並發控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,並且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再並發修改這條記錄。
在一個支持MVCC並發控制的系統中,哪些讀操作是快照讀?哪些操作又是當前讀呢?以MySQL InnoDB為例:
- 快照讀:簡單的select操作,屬於快照讀,不加鎖。(當然,也有例外)
select * from table where ?;
- 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,需要加鎖。
下面語句都屬於當前讀,讀取記錄的最新版本。並且,讀取之后,還需要保證其他並發事務不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。
select * from table where ? lock in share mode; select * from table where ? for update; insert into table values (…); update table set ? where ?; delete from table where ?;
數據庫的事務隔離越嚴格,並發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上 “串行化”進行,這顯然與“並發”是矛盾的。同時,不同的應用對讀一致性和事務隔離程度的要求也是不同的,比如許多應用對“不可重復讀”和“幻讀”並不敏 感,可能更關心數據並發訪問的能力。
為了解決“隔離”與“並發”的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別,每個級別的隔離程度不同,允許出現的副作用也不同,應用可以根據自己的業務邏輯要求,通過選擇不同的隔離級別來平衡 “隔離”與“並發”的矛盾。下表很好地概括了這4個隔離級別的特性。
獲取InonoD行鎖爭用情況
可以通過檢查InnoDB_row_lock狀態變量來分析系統上的行鎖的爭奪情況:
mysql> show status like 'innodb_row_lock%';
如果發現鎖爭用比較嚴重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比較高,還可以通過設置InnoDB Monitors來進一步觀察發生鎖沖突的表、數據行等,並分析鎖爭用的原因。
InnoDB的行鎖模式及加鎖方法
InnoDB實現了以下兩種類型的行鎖。
- 共享鎖(s):又稱讀鎖。允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
- 排他鎖(X):又稱寫鎖。允許獲取排他鎖的事務更新數據,阻止其他事務取得相同的數據集共享讀鎖和排他寫鎖。若事務T對數據對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。
對於共享鎖大家可能很好理解,就是多個事務只能讀數據不能改數據。
對於排他鎖大家的理解可能就有些差別,我當初就犯了一個錯誤,以為排他鎖鎖住一行數據后,其他事務就不能讀取和修改該行數據,其實不是這樣的。排他鎖指的是一個事務在一行數據加上排他鎖后,其他事務不能再在其上加其他的鎖。mysql InnoDB引擎默認的修改數據語句:update,delete,insert都會自動給涉及到的數據加上排他鎖,select語句默認不會加任何鎖類型,如果加排他鎖可以使用select …for update語句,加共享鎖可以使用select … lock in share mode語句。所以加過排他鎖的數據行在其他事務種是不能修改數據的,也不能通過for update和lock in share mode鎖的方式查詢數據,但可以直接通過select …from…查詢數據,因為普通查詢沒有任何鎖機制。
另外,為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
- 意向共享鎖(IS):事務打算給數據行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
- 意向排他鎖(IX):事務打算給數據行加排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
InnoDB行鎖模式兼容性列表:
如果一個事務請求的鎖模式與當前的鎖兼容,InnoDB就請求的鎖授予該事務;反之,如果兩者兩者不兼容,該事務就要等待鎖釋放。
意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖。
事務可以通過以下語句顯式給記錄集加共享鎖或排他鎖:
- 共享鎖(S):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 - 排他鎖(X):
SELECT * FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE獲得共享鎖,主要用在需要數據依存關系時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操作。但是如果當前事務也需要對該記錄進行更新操作,則很有可能造成死鎖,對於鎖定行記錄后需要進行更新操作的應用,應該使用SELECT… FOR UPDATE方式獲得排他鎖。
InnoDB行鎖實現方式
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味着:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖沖突,從而影響並發性能。下面通過一些實際例子來加以說明。
(1)在不通過索引條件查詢的時候,InnoDB確實使用的是表鎖,而不是行鎖。
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4'); Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0
在上面的例子中,看起來session_1只給一行加了排他鎖,但session_2在請求其他行的排他鎖時,卻出現了鎖等待!原因就是在沒有索引的情況下,InnoDB只能使用表鎖。當我們給其增加一個索引后,InnoDB就只鎖定了符合條件的行,如下例所示:
創建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb; mysql> alter table tab_with_index add index id(id);
(2)由於MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。應用設計的時候要注意這一點。
在下面的例子中,表tab_with_index的id字段有索引,name字段沒有索引:
mysql> alter table tab_with_index drop index name; Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> insert into tab_with_index values(1,'4'); Query OK, 1 row affected (0.00 sec) mysql> select * from tab_with_index where id = 1;
InnoDB存儲引擎使用相同索引鍵的阻塞例子
(3)當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。
在下面的例子中,表tab_with_index的id字段有主鍵索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name); Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0 Warnings: 0
InnoDB存儲引擎的表使用不同索引的阻塞例子
(4)即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計划的代價來決 定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。因此,在分析鎖沖突 時,別忘了檢查SQL的執行計划,以確認是否真正使用了索引。
比如,在tab_with_index表里的name字段有索引,但是name字段是varchar類型的,檢索值的數據類型與索引字段不同,雖然MySQL能夠進行數據類型轉換,但卻不會使用索引,從而導致InnoDB使用表鎖。通過用explain檢查兩條SQL的執行計划,我們可以清楚地看到了這一點。
mysql> explain select * from tab_with_index where name = 1 \G mysql> explain select * from tab_with_index where name = '1' \G
間隙鎖(Next-Key鎖)
當我們用范圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的 索引項加鎖;對於鍵值在條件范圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖 (Next-Key鎖)。
舉例來說,假如emp表中只有101條記錄,其empid的值分別是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一個范圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖。
InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求,對於上面的例子,要是不使 用間隙鎖,如果其他事務插入了empid大於100的任何記錄,那么本事務如果再次執行上述語句,就會發生幻讀;另外一方面,是為了滿足其恢復和復制的需 要。有關其恢復和復制對鎖機制的影響,以及不同隔離級別下InnoDB使用間隙鎖的情況,在后續的章節中會做進一步介紹。
很顯然,在使用范圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件范圍內鍵值的並發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是並發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用范圍條件。
還要特別說明的是,InnoDB除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!下面這個例子假設emp表中只有101條記錄,其empid的值分別是1,2,……,100,101。
InnoDB存儲引擎的間隙鎖阻塞例子
小結
本文重點介紹了MySQL中MyISAM表級鎖和InnoDB行級鎖的實現特點,並討論了兩種存儲引擎經常遇到的鎖問題和解決辦法。
對於MyISAM的表鎖,主要討論了以下幾點:
(1)共享讀鎖(S)之間是兼容的,但共享讀鎖(S)與排他寫鎖(X)之間,以及排他寫鎖(X)之間是互斥的,也就是說讀和寫是串行的。
(2)在一定條件下,MyISAM允許查詢和插入並發執行,我們可以利用這一點來解決應用中對同一表查詢和插入的鎖爭用問題。
(3)MyISAM默認的鎖調度機制是寫優先,這並不一定適合所有應用,用戶可以通過設置LOW_PRIORITY_UPDATES參數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的爭用。
(4)由於表鎖的鎖定粒度大,讀寫之間又是串行的,因此,如果更新操作較多,MyISAM表可能會出現嚴重的鎖等待,可以考慮采用InnoDB表來減少鎖沖突。
對於InnoDB表,本文主要討論了以下幾項內容:
(1)InnoDB的行鎖是基於索引實現的,如果不通過索引訪問數據,InnoDB會使用表鎖。
(2)介紹了InnoDB間隙鎖(Next-key)機制,以及InnoDB使用間隙鎖的原因。
在不同的隔離級別下,InnoDB的鎖機制和一致性讀策略不同。
在了解InnoDB鎖特性后,用戶可以通過設計和SQL調整等措施減少鎖沖突和死鎖,包括:
- 盡量使用較低的隔離級別; 精心設計索引,並盡量使用索引訪問數據,使加鎖更精確,從而減少鎖沖突的機會;
- 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小;
- 給記錄集顯式加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;
- 不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;
- 盡量用相等條件訪問數據,這樣可以避免間隙鎖對並發插入的影響; 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;
- 對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能。
問題探討
在程序里執行事務,想要預防斷網問題,但是事務執行到中間的時候,強行斷網,會造成此表死鎖,就算重連后通過catch捕獲異常,也無法使用回滾
所以要用存儲過程,不過若服務器斷電,又無UPS,也是麻煩事
參考地址
事務四大特性:http://www.cnblogs.com/fjdingsd/p/5273008.html
mysql共享鎖與排他鎖:https://www.cnblogs.com/boblogsbo/p/5602122.html
數據庫事務隔離級別:https://www.cnblogs.com/WJ-163/p/6023054.html
Mysql中的鎖機制 http://blog.csdn.net/soonfly/article/details/70238902
https://blog.csdn.net/lishenglong666/article/details/53913126
悲觀鎖和樂觀鎖:https://www.jianshu.com/p/d2ac26ca6525
