3、數據庫的事務、併發和鎖機制

1、事務

概念:

用戶定義的一個數據庫操作序列,這些操作要麼全做、要麼全不做,是不可分割的工作單位,同時事務也是恢復和併發控制的基本單位。

定義事務語句:

begin transaction;開始事務。

commit;提交,即提交事務所有操作,將事務中所有的對數據庫的更新寫回到磁盤中去。

rollback;回滾,即撤銷對事務的所有操作,回滾到事務的開始狀態。

事務特性:ACID

原子性:

事務中包括的操作要麼都做要麼都不做。

一致性

事務執行的結果必須使事務從一個一致狀態轉變到另一個一致狀態。數據庫中只包含成功事務提交的結果時,就說數據庫處於一致性狀態。A賬戶給B賬戶轉一萬塊錢,這個事務涉及兩個操作,A賬戶減1萬,B賬戶加1萬,如果這兩個操作都做那麼就是一致的,如果只做其中一個那麼肯定是不一致的。

隔離性:

併發事務之間是互不影響,即我這個事務內部操作和使用的數據和別的事務沒有半毛錢關係。

持續性:

一旦提交,那麼我這個事務對數據庫的改變就應該是永久的。

 

2、併發

分類:

交叉併發:單處理機事務交叉執行,假並行,只是減少處理機空閒時間。(後續討論基於此情況)

同時併發:多處理機事務同時執行,真並行。

 

併發的異常情型:

第一類丟失更新:A count=count+1 commit;B count=count-1 rollback;A事務的操作被丟失。

髒讀:A 100->100+1 沒提交 B開始讀到101,A rollback; B讀到的數就是髒讀數據。

不可重複讀:第一次:A讀count 100  後B改count=count+1  第二次:A再讀,兩次結果不一樣。

第二類丟失更新:A count=count+1 commit;B count=count+2 commit;A事務被丟失。

幻讀:A 第一次查一個表中的數據行數count;B 向表中添加一行數據,A再次讀count,A兩次讀到的數據不一致。

 

3、事務的隔離級別:

讀未提交(Read Uncommitted):該隔離級別指即使一個事務的更新語句沒有提交,但是別的事務可以讀到這個改變,幾種異常情況都可能出現。極易出錯,沒有安全性可言,基本不會使用。

讀已提交(Read Committed):該隔離級別指一個事務只能看到其他事務的已經提交的更新,看不到未提交的更新,消除了髒讀和第一類丟失更新,這是大多數數據庫的默認隔離級別,如Oracle,Sqlserver

可重複讀(Repeatable Read):該隔離級別指一個事務中進行兩次或多次同樣的對於數據內容的查詢,得到的結果是一樣的,但不保證對於數據條數的查詢是一樣的,只要存在讀改行數據就禁止寫,消除了不可重複讀和第二類更新丟失,這是Mysql數據庫的默認隔離級別。

串行化(Serializable):意思是說這個事務執行的時候不允許別的事務併發執行.完全串行化的讀,只要存在讀就禁止寫,但可以同時讀,消除了幻讀。這是事務隔離的最高級別,雖然最安全最省心,但是效率太低,一般不會用。


 

4、鎖機制:數據庫的隔離級別實現一般是通過數據庫鎖實現的。

總分類:

樂觀鎖:

一般是指用戶自己實現的一種鎖機制,比如hibernate實現的樂觀鎖甚至編程語言也有樂觀鎖的思想的應用。

悲觀鎖:

認爲數據隨時會修改,所以整個數據處理中需要將數據加鎖。悲觀鎖一般都是依靠關係數據庫提供的鎖機制,事實上關係數據庫中的行鎖,表鎖不論是讀寫鎖都是悲觀鎖。

 

悲觀鎖按作用範圍再分:

行鎖:

鎖的作用範圍是行級別,數據庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。舉個例子,一個用戶表user,有主鍵id和用戶生日birthday當你使用update … where id=?這樣的語句數據庫明確知道會影響哪一行,它就會使用行鎖,當你使用update … where birthday=?這樣的的語句的時候因爲事先不知道會影響哪些行就可能會使用表鎖。

表鎖:

鎖的作用範圍是整張表。

 

悲觀鎖按使用性質再分:

共享鎖(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鎖,主要是用來防止死鎖的。因爲使用共享鎖時,修改數據的操作分爲兩步,首先獲得一個共享鎖,讀取數據,然後將共享鎖升級爲排它鎖,然後再執行修改操作。這樣如果同時有兩個或多個事務同時對一個對象申請了共享鎖,在修改數據的時候,這些事務都要將共享鎖升級爲排它鎖。這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請更新鎖,在數據修改的時候再升級爲排它鎖,就可以避免死鎖。
 

樂觀鎖:

顧名思義,就是很樂觀,每次自己操作數據的時候認爲沒有人回來修改它,所以不去加鎖,但是在更新的時候會去判斷在此期間數據有沒有被修改,需要用戶自己去實現。既然都有數據庫提供的悲觀鎖可以方便使用爲什麼要使用樂觀鎖呢?對於讀操作遠多於寫操作的時候,大多數都是讀取,這時候一個更新操作加鎖會阻塞所有讀取,降低了吞吐量。最後還要釋放鎖,鎖是需要一些開銷的,我們只要想辦法解決極少量的更新操作的同步問題。換句話說,如果是讀寫比例差距不是非常大或者你的系統沒有響應不及時,吞吐量瓶頸問題,那就不要去使用樂觀鎖,它增加了複雜度,也帶來了額外的風險。
 

樂觀鎖實現方式:

版本號(記爲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,也就是說兩個事務更新同一條記錄的兩個不相關字段也是互斥的,不能同步進行。舊系統不能修改數據庫表結構的時候使用數據字段作爲版本控制信息,不需要新增字段,待更新字段方式只要其他事務修改的字段和當前事務修改的字段沒有重疊就可以同步進行,併發性更高。
 

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