1. 程式人生 > >3、資料庫的事務、併發和鎖機制

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