資料庫鎖分類和總結
鎖
鎖是網路資料庫中的一個非常重要的概念,當多個使用者同時對資料庫併發操作時,會帶來資料不一致的問題,所以,鎖主要用於多使用者環境下保證資料庫完整性和一致性。
幫助理解:以商場的試衣間為例,每個試衣間都可供多個消費者使用,因此,可能出現多個消費者同時需要使用試衣間試衣服。為了避免衝突,試衣間裝了鎖,某一個試衣服的人在試衣間裡把鎖鎖住了,其他顧客就不能從外面打開了,只能等待裡面的顧客試完衣服,從裡面把鎖開啟,外面的人才能進去。
資料庫鎖出現的目的:處理併發問題
併發控制的主要採用的技術手段:樂觀鎖、悲觀鎖和時間戳。
鎖分類
從資料庫系統角度分為三種:排他鎖、共享鎖、更新鎖。
從程式設計師角度
悲觀鎖(Pessimistic Lock)
顧名思義,很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人拿這個資料就會block(阻塞),直到它拿鎖。
悲觀鎖(Pessimistic Lock):正如其名,具有強烈的獨佔和排他特性。它指的是對資料被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)。
傳統的關係資料庫裡用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖。
悲觀鎖按使用性質劃分
共享鎖(Share Lock)
S鎖,也叫讀鎖,用於所有的只讀資料操作。共享鎖是非獨佔的,允許多個併發事務讀取其鎖定的資源。
性質
1. 多個事務可封鎖同一個共享頁;
2. 任何事務都不能修改該頁;
3. 通常是該頁被讀取完畢,S鎖立即被釋放。
在SQL Server中,預設情況下,資料被讀取後,立即釋放共享鎖。
例如,執行查詢語句“SELECT * FROM my_table”時,首先鎖定第一頁,讀取之後,釋放對第一頁的鎖定,然後鎖定第二頁。這樣,就允許在讀操作過程中,修改未被鎖定的第一頁。
例如,語句“SELECT * FROM my_table HOLDLOCK”就要求在整個查詢過程中,保持對錶的鎖定,直到查詢完成才釋放鎖定。
排他鎖(Exclusive Lock)
X鎖,也叫寫鎖,表示對資料進行寫操作。如果一個事務對物件加了排他鎖,其他事務就不能再給它加任何鎖了。(某個顧客把試衣間從裡面反鎖了,其他顧客想要使用這個試衣間,就只有等待鎖從裡面打開了。)
性質
1. 僅允許一個事務封鎖此頁;
2. 其他任何事務必須等到X鎖被釋放才能對該頁進行訪問;
3. X鎖一直到事務結束才能被釋放。
產生排他鎖的SQL語句如下:select * from ad_plan for update;
更新鎖
U鎖,在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象。
因為當使用共享鎖時,修改資料的操作分為兩步:
1. 首先獲得一個共享鎖,讀取資料,
2. 然後將共享鎖升級為排他鎖,再執行修改操作。
這樣如果有兩個或多個事務同時對一個事務申請了共享鎖,在修改資料時,這些事務都要將共享鎖升級為排他鎖。這時,這些事務都不會釋放共享鎖,而是一直等待對方釋放,這樣就造成了死鎖。
如果一個數據在修改前直接申請更新鎖,在資料修改時再升級為排他鎖,就可以避免死鎖。
性質
1. 用來預定要對此頁施加X鎖,它允許其他事務讀,但不允許再施加U鎖或X鎖;
2. 當被讀取的頁要被更新時,則升級為X鎖;
3. U鎖一直到事務結束時才能被釋放。
行鎖
鎖的作用範圍是行級別。
表鎖
鎖的作用範圍是整張表。
資料庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。
舉個例子,一個使用者表user,有主鍵id和使用者生日birthday。
當你使用update … where id=?這樣的語句時,資料庫明確知道會影響哪一行,它就會使用行鎖;
當你使用update … where birthday=?這樣的的語句時,因為事先不知道會影響哪些行就可能會使用表鎖。
樂觀鎖(Optimistic Lock)
顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以,不會上鎖。但是在更新的時候會判斷一下在此期間別人有沒有更新這個資料,可以使用版本號等機制。
樂觀鎖( Optimistic Locking ): 相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。
悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。
樂觀鎖,大多是基於資料版本( Version )記錄機制實現。
資料版本:為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。
樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。
- 版本號(version)
版本號(記為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就是這種思想。
- 所有欄位
所有欄位:和待更新欄位類似,只是使用所有欄位做版本控制資訊,只有所有欄位都沒變化才會執行更新。
- 活鎖
- 死鎖
併發控制會造成活鎖和死鎖,就像作業系統那樣,會因為互相等待而導致。
活鎖
定義:指的是T1封鎖了資料R,T2同時也請求封鎖資料R,T3也請求封鎖資料R,當T1釋放了鎖之後,T3會鎖住R,T4也請求封鎖R,則T2就會一直等待下去。
解決方法:採用“先來先服務”策略可以避免。
死鎖
定義:就是我等你,你又等我,雙方就會一直等待下去。比如:T1封鎖了資料R1,正請求對R2封鎖,而T2封住了R2,正請求封鎖R1,這樣就會導致死鎖,死鎖這種沒有完全解決的方法,只能儘量預防。
預防方法:
1. 一次封鎖法,指的是一次性把所需要的資料全部封鎖住,但是這樣會擴大了封鎖的範圍,降低系統的併發度;
2. 順序封鎖法,指的是事先對資料物件指定一個封鎖順序,要對資料進行封鎖,只能按照規定的順序來封鎖,但是這個一般不大可能的。
系統判定死鎖的方法:
- 超時法:如果某個事物的等待時間超過指定時限,則判定為出現死鎖;
- 等待圖法:如果事務等待圖中出現了迴路,則判斷出現了死鎖。
對於解決死鎖的方法,只能是撤銷一個處理死鎖代價最小的事務,釋放此事務持有的所有鎖,同時對撤銷的事務所執行的資料修改操作必須加以恢復。
參考資料
- 《Java程式設計師面試筆試真題與分析》 猿媛之家 編著
- 百度百科