資料庫事務與鎖詳解
什麼是事務(Transaction)?
是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行。 事務處理可以確保除非事務性單元內的所有操作都成功完成,否則不會永久更新面向資料的資源。通過將一組相關操作組合為一個要麼全部成功要麼全部失敗的單元,可以簡化錯誤恢復並使應用程式更加可靠。一個邏輯工作單元要成為事務,必須滿足所謂的ACID(原子性、一致性、隔離性和永續性)屬性。事務是資料庫執行中的一個邏輯工作單位,由DBMS中的事務管理子系統負責事務的處理。
舉個例子加深一下理解:同一個銀行轉賬,A轉1000塊錢給B,這裡存在兩個操作,一個是A賬戶扣款1000元,兩一個操作是B賬戶增加1000元,兩者就構成了轉賬這個事務。
- 兩個操作都成功,A賬戶扣款1000元,B賬戶增加1000元,事務成功
- 兩個操作都失敗,A賬戶和B賬戶金額都沒變,事務失敗
最後思考一下,怎麼樣會出現A賬戶扣款1000元,B賬戶金額不變?如果你是把兩個操作放在一個事務裡面,並且是資料庫提供的內在事務支援,那就不會有問題,但是開發人員把兩個操作放在兩個事務裡面,而第二個事務失敗就會出現中間狀態。現實中自己實現的分散式事務處理不當也會出現中間狀態,這並不是事務的錯,事務本身就是規定不會出現中間狀態,是事務實現者做出來的方案有問題。
事務的4個特性
原子性(Atomic):事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行。通常,與某個事務關聯的操作具有共同的目標,並且是相互依賴的。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。原子性消除了系統處理操作子集的可能性。
一致性(Consistency):事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。這種特性稱為事務的一致性。假如資料庫的狀態滿足所有的完整性約束,就說該資料庫是一致的。
隔離性(Isolation):由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。事務檢視資料時資料所處的狀態,到底是另一個事務執行之前的狀態還是中間某個狀態,相互之間存在什麼影響,是可以通過隔離級別的設定來控制的。
永續性(Durability):事務結束後,事務處理的結果必須能夠得到固化,即寫入資料庫檔案中即使機器宕機資料也不會丟失,它對於系統的影響是永久性的。
事務併發控制
我們從另外一個方向來說說,如果不對事務進行併發控制,我們看看資料庫併發操作是會有那些異常情形,有些使我們可以接受的,有些是不能接受的,注意這裡的異常就是特定語境下的,並不一定就是錯誤什麼的。假設有一個order表,有個欄位叫count,作為計數用,當前值為100
第一類丟失更新(Update Lost):此種更新丟失是因為回滾的原因,所以也叫回滾丟失。此時兩個事務同時更新count,兩個事務都讀取到100,事務一更新成功並提交,count=100+1=101,事務二出於某種原因更新失敗了,然後回滾,事務二就把count還原為它一開始讀到的100,此時事務一的更新就這樣丟失了。
髒讀(Dirty Read):此種異常時因為一個事務讀取了另一個事務修改了但是未提交的資料。舉個例子,事務一更新了count=101,但是沒有提交,事務二此時讀取count,值為101而不是100,然後事務一出於某種原因回滾了,然後第二個事務讀取的這個值就是噩夢的開始。
不可重複讀(Not Repeatable Read):此種異常是一個事務對同一行資料執行了兩次或更多次查詢,但是卻得到了不同的結果,也就是在一個事務裡面你不能重複(即多次)讀取一行資料,如果你這麼做了,不能保證每次讀取的結果是一樣的,有可能一樣有可能不一樣。造成這個結果是在兩次查詢之間有別的事務對該行資料做了更新操作。舉個例子,事務一先查詢了count,值為100,此時事務二更新了count=101,事務一再次讀取count,值就會變成101,兩次讀取結果不一樣。
第二類丟失更新(Second Update Lost):此種更新丟失是因為更新被其他事務給覆蓋了,也可以叫覆蓋丟失。舉個例子,兩個事務同時更新count,都讀取100這個初始值,事務一先更新成功並提交,count=100+1=101,事務二後更新成功並提交,count=100+1=101,由於事務二count還是從100開始增加,事務一的更新就這樣丟失了。
幻讀(Phantom Read):幻讀和不可重複讀有點像,只是針對的不是資料的值而是資料的數量。此種異常是一個事務在兩次查詢的過程中資料的數量不同,讓人以為發生幻覺,幻讀大概就是這麼得來的吧。舉個例子,事務一查詢order表有多少條記錄,事務二新增了一條記錄,然後事務一查了一下order表有多少記錄,發現和第一次不一樣,這就是幻讀。
資料庫事務隔離級別
看到上面提到的幾種問題,你可能會想,我擦,這麼多坑怎麼辦啊。其實上面幾種情況並不是一定都要避免的,具體看你的業務要求,包括你資料庫的負載都會影響你的決定。不知道大家發現沒有,上面各種異常情況都是多個事務之間相互影響造成的,這說明兩個事務之間需要某種方式將他們從某種程度上分開,降低直至避免相互影響。這時候資料庫事務隔離級別就粉墨登場了,而資料庫的隔離級別實現一般是通過資料庫鎖實現的。
讀未提交(Read Uncommitted):該隔離級別指即使一個事務的更新語句沒有提交,但是別的事務可以讀到這個改變,幾種異常情況都可能出現。極易出錯,沒有安全性可言,基本不會使用。
讀已提交(Read Committed):該隔離級別指一個事務只能看到其他事務的已經提交的更新,看不到未提交的更新,消除了髒讀和第一類丟失更新,這是大多數資料庫的預設隔離級別,如Oracle,Sqlserver。
可重複讀(Repeatable Read):該隔離級別指一個事務中進行兩次或多次同樣的對於資料內容的查詢,得到的結果是一樣的,但不保證對於資料條數的查詢是一樣的,只要存在讀改行資料就禁止寫,消除了不可重複讀和第二類更新丟失,這是Mysql資料庫的預設隔離級別。
序列化(Serializable):意思是說這個事務執行的時候不允許別的事務併發執行.完全序列化的讀,只要存在讀就禁止寫,但可以同時讀,消除了幻讀。這是事務隔離的最高級別,雖然最安全最省心,但是效率太低,一般不會用。
下面是各種隔離級別對各異常的控制能力:
級別\異常 | 第一類更新丟失 | 髒讀 | 不可重複讀 | 第二類丟失更新 | 幻讀 |
---|---|---|---|---|---|
讀未提交 | Y | Y | Y | Y | Y |
讀已提交 | N | N | Y | Y | Y |
可重複讀 | N | N | N | N | Y |
序列化 | N | N | N | N | N |
資料庫鎖分類
一般可以分為兩類,一個是悲觀鎖,一個是樂觀鎖,悲觀鎖一般就是我們通常說的資料庫鎖機制,樂觀鎖一般是指使用者自己實現的一種鎖機制,比如hibernate實現的樂觀鎖甚至程式語言也有樂觀鎖的思想的應用。
悲觀鎖:顧名思義,就是很悲觀,它對於資料被外界修改持保守態度,認為資料隨時會修改,所以整個資料處理中需要將資料加鎖。悲觀鎖一般都是依靠關係資料庫提供的鎖機制,事實上關係資料庫中的行鎖,表鎖不論是讀寫鎖都是悲觀鎖。
悲觀鎖按照使用性質劃分:
共享鎖(Share locks簡記為S鎖):也稱讀鎖,事務A對物件T加s鎖,其他事務也只能對T加S,多個事務可以同時讀,但不能有寫操作,直到A釋放S鎖。
排它鎖(Exclusivelocks簡記為X鎖):也稱寫鎖,事務A對物件T加X鎖以後,其他事務不能對T加任何鎖,只有事務A可以讀寫物件T直到A釋放X鎖。
更新鎖(簡記為U鎖):用來預定要對此物件施加X鎖,它允許其他事務讀,但不允許再施加U鎖或X鎖;當被讀取的物件將要被更新時,則升級為X鎖,主要是用來防止死鎖的。因為使用共享鎖時,修改資料的操作分為兩步,首先獲得一個共享鎖,讀取資料,然後將共享鎖升級為排它鎖,然後再執行修改操作。這樣如果同時有兩個或多個事務同時對一個物件申請了共享鎖,在修改資料的時候,這些事務都要將共享鎖升級為排它鎖。這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請更新鎖,在資料修改的時候再升級為排它鎖,就可以避免死鎖。
悲觀鎖按照作用範圍劃分:
- 行鎖:鎖的作用範圍是行級別,資料庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。舉個例子,一個使用者表user,有主鍵id和使用者生日birthday當你使用update … where id=?這樣的語句資料庫明確知道會影響哪一行,它就會使用行鎖,當你使用update … where birthday=?這樣的的語句的時候因為事先不知道會影響哪些行就可能會使用表鎖。
- 表鎖:鎖的作用範圍是整張表。
樂觀鎖:顧名思義,就是很樂觀,每次自己操作資料的時候認為沒有人回來修改它,所以不去加鎖,但是在更新的時候會去判斷在此期間資料有沒有被修改,需要使用者自己去實現。既然都有資料庫提供的悲觀鎖可以方便使用為什麼要使用樂觀鎖呢?對於讀操作遠多於寫操作的時候,大多數都是讀取,這時候一個更新操作加鎖會阻塞所有讀取,降低了吞吐量。最後還要釋放鎖,鎖是需要一些開銷的,我們只要想辦法解決極少量的更新操作的同步問題。換句話說,如果是讀寫比例差距不是非常大或者你的系統沒有響應不及時,吞吐量瓶頸問題,那就不要去使用樂觀鎖,它增加了複雜度,也帶來了額外的風險。
樂觀鎖實現方式:
- 版本號(記為version):就是給資料增加一個版本標識,在資料庫上就是表中增加一個version欄位,每次更新把這個欄位加1,讀取資料的時候把version讀出來,更新的時候比較version,如果還是開始讀取的version就可以更新了,如果現在的version比老的version大,說明有其他事務更新了該資料,並增加了版本號,這時候得到一個無法更新的通知,使用者自行根據這個通知來決定怎麼處理,比如重新開始一遍。這裡的關鍵是判斷version和更新兩個動作需要作為一個原子單元執行,否則在你判斷可以更新以後正式更新之前有別的事務修改了version,這個時候你再去更新就可能會覆蓋前一個事務做的更新,造成第二類丟失更新,所以你可以使用update … where … and version=”old version”這樣的語句,根據返回結果是0還是非0來得到通知,如果是0說明更新沒有成功,因為version被改了,如果返回非0說明更新成功。
- 時間戳(timestamp):和版本號基本一樣,只是通過時間戳來判斷而已,注意時間戳要使用資料庫伺服器的時間戳不能是業務系統的時間。
- 待更新欄位:和版本號方式相似,只是不增加額外欄位,直接使用有效資料欄位做版本控制資訊,因為有時候我們可能無法改變舊系統的資料庫表結構。假設有個待更新欄位叫count,先去讀取這個count,更新的時候去比較資料庫中count的值是不是我期望的值(即開始讀的值),如果是就把我修改的count的值更新到該欄位,否則更新失敗。java的基本型別的原子型別物件如AtomicInteger就是這種思想。
所有欄位:和待更新欄位類似,只是使用所有欄位做版本控制資訊,只有所有欄位都沒變化才會執行更新。
樂觀鎖幾種方式的區別:
新系統設計可以使用version方式和timestamp方式,需要增加欄位,應用範圍是整條資料,不論那個欄位修改都會更新version,也就是說兩個事務更新同一條記錄的兩個不相關欄位也是互斥的,不能同步進行。舊系統不能修改資料庫表結構的時候使用資料欄位作為版本控制資訊,不需要新增欄位,待更新欄位方式只要其他事務修改的欄位和當前事務修改的欄位沒有重疊就可以同步進行,併發性更高。
mysql事務隔離級別實戰
實踐是檢驗真理的唯一標準,掌握上面的理論之後,我們在資料庫上實戰一番家裡更好地掌握也加深理解,同時有助於解決實際問題。不同資料庫很多實現可能不同,這裡以mysql為例講解各種隔離級別下的情況,測試表為user(id,name,gender,passwd,email)。
隔離級別:read-uncommitted
髒讀測試流程:
1. A設定隔離級別為read-uncommitted(注意這裡未宣告都是session級別,而非全域性的),開啟事務,查詢id=1的記錄
2. B設定隔離級別為read-uncommitted,開啟事務,修改id=1的記錄,但不提交
3. A再次查詢id=1的記錄,和第一次查詢的比較一下
4. B事務回滾,A事務回滾。
A:
B:
結論:A讀到了B沒有提交的內容,隔離級別為read-uncommitted的時候出現髒讀。
第一類更新丟失測試流程:
1. A設定隔離級別為read-uncommitted,開啟事務,查詢id=1的記錄
2. B設定隔離級別為read-uncommitted,開啟事務,查詢id=1的記錄
3. A修改id=1的記錄
4. B修改id=1的記錄
5. A提交
6. B回滾
7. A在查詢一次id=1的記錄,看看自己的修改是否成功
結論:結果不如我所想的,A的更新成功了,為什麼呢?A執行update語句的時候對該條記錄加鎖了,B這時候根本無法修改直至超時,也就是至少在mysql中在read-uncommitted隔離級別下驗證第一類丟失更新,據瞭解有的資料庫好像可以設定不加鎖,如果能夠不加鎖的話則可以實現,也貼一下圖吧。
A:
B:
不可重複讀測試流程(省略):
結論:流程和測試髒讀一樣,其實在第一次測試髒讀的時候就可以發現會出現不可重複讀,A兩次讀取id=1的資料內容不同。
第二類丟失更新流程:
1. A開啟事務,查詢order_id=1的記錄
2. B開啟事務,查詢order_id=1的記錄
3. A把查出來的count加1後更新
4. B把查出來的count加1更新
5. A提交,B也提交
A:
B:
結論:A的更新丟失,我們希望的結果是3,而實際結果是2,跟java的多執行緒很像對不對,read-uncommitted隔離模式下會出現第二類丟失更新。
幻讀測試流程:
1. A開啟事務,查詢user表所有資料
2. B開啟事務,新增一條記錄
3. A再次查詢user表所有記錄,和第一次作比對
4. A回滾,B回滾
A:
B:
結論:A兩次查詢全表資料結果不同,read-uncommitted隔離模式下會出現幻讀。
注:因為後面對這幾種異常情況的測試流程基本和上面一樣,個別有些差別讀者自己注意,另外注意更改隔離級別即可,就能看到對應結果,後面的我只給出進一步能解決的異常測試截圖,結論可以參照前面的對照表。
隔離級別:read-committed
髒讀測試截圖
A:
B:
結論:A沒有讀到B沒有提交的內容,隔離級別為read-committed的時候不會出現髒讀。
隔離級別:repeatable-read
不可重複讀測試截圖
A:
B:
結論:A兩次讀取id=1的資料內容相同,repeatable-read隔離模式下不會出現不可重複讀。
隔離級別:Serializable
幻讀測試截圖
A:
B:
結論:因為A事務未提交之前,B事務插入操作無法獲得鎖而超時,Serializable隔離模式下不會出現幻讀。