一、問題引出
① 假設噹噹網上使用者下單買了本書,這時資料庫中有條訂單號為001的訂單,其中有個status欄位是’有效’,表示該訂單是有效;
② 後台管理員查詢到這條001的訂單,並且看到狀態是有效;
③ 使用者發現下單的時候下錯了,於是撤銷訂單,假設運行這樣一條SQL: update order_table set status = ‘取消’ where order_id = 001;
④ 後台管理員由於在②這步看到狀態有效,這時,雖然使用者在③這步已經撤銷了訂單,可是管理員並未重新整理介面,看到的訂單狀態還是有效,於是點擊”發貨”按鈕,將該訂單發到物流部門,同時運行類似如下SQL,將訂單狀態改成已發貨:update order_table set status = ‘已發貨’ where order_id = 001;
二、概念
為了得到最大的效能,一般資料庫都有並發機制,不過帶來的問題就是資料訪問的衝突。為瞭解決這個問題,大多數資料庫的方法就是資料的鎖定。
悲觀鎖:對資料的衝突採取一種悲觀的態度,也就是說假設資料肯定會衝突,所以在資料開始讀取的時候就把資料鎖定住。
樂觀鎖:認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓使用者返回錯誤的資訊,讓使用者決定如何去做。
髒讀:當一個事務讀取其他完成一半事務的記錄時,就會發生髒讀。例如:使用者A、B看到的值都是6,使用者B把值改為2,使用者A督導的值仍為6.
丟失更新:一個事務的更新覆蓋了其他事務的更新結果。例如:使用者A把值從6改為2,使用者B把值從2改為6,則使用者A丟失了他的更新。
上面情境就是典型的‘髒讀’, 資料庫的樂觀鎖做法和悲觀鎖做法主要就是解決上面情境並發的問題!!!
三、悲觀鎖
SqlServer:資料的鎖定通常採用頁級鎖的方式,也就是說對一張表內的資料是一種序列化的更新插入機制,在任何時間同一張表只會插1條資料,別的想插入的資料要等到這一條資料插完以後才能依次插入。帶來的後果就是效能的降低,在多使用者並發訪問的時候,當對一張表進行頻繁操作時,會發現響應效率很低,資料庫經常處於一種假死狀態。
Oracle:用的是行級鎖,只是對想鎖定的資料才進行鎖定,其餘的資料不相干,所以在對Oracle表中並發插資料的時候,基本上不會有任何影響。
Oracle的悲觀鎖需要利用一條現有的串連,分成兩種方式,從SQL語句的區別來看,就是一種是for update,一種是for update nowait的形式。
① 執行select xxx for update操作時,資料會被鎖定,只有執行comit或rollover才會釋放
② 執行select xxx for update nowait操作時,資料也會被鎖定,其他人訪問時或返回ORA-00054錯誤,內容是資源正忙,需要採取相應的業務措施進行處理。
Oracle中的悲觀鎖就是利用Oracle的Connection對資料進行鎖定。在Oracle中,用這種行級鎖帶來的效能損失是很小的,只是要注意程式邏輯,不要給你一不小心搞成死結了就好。而且由於資料的及時鎖定,在資料提交時候就不呼出現衝突,可以省去很多惱人的資料衝突處理。缺點就是你必須要始終有一條資料庫連接,就是說在整個鎖定到最後放開鎖的過程中,你的資料庫連接要始終保持住。
四、樂觀鎖
樂觀鎖就是一開始假設不會造成資料衝突,在最後提交的時候再進行資料衝突檢測。在樂觀鎖中,我們有3種常用的做法來實現:
①第一種就是在資料取得的時候把整個資料都copy到應用中,在進行提交的時候比對當前資料庫中的資料和開始的時候更新前取得的資料。當發現兩個資料一模一樣以後,就表示沒有衝突可以提交,否則則是並發衝突,需要去用商務邏輯進行解決。
②第二種樂觀鎖的做法就是採用版本戳,這個在Hibernate中得到了使用。採用版本戳的話,首先需要在你有樂觀鎖的資料庫table上建立一個新的column,比如為number型,當你資料每更新一次的時候,版本數就會往上增加1。比如同樣有2個session同樣對某條資料進行操作。兩者都取到當前的資料的版本號碼為1,當第一個session進行資料更新後,在提交的時候查看到當前資料的版本還為1,和自己一開始取到的版本相同。就正式提交,然後把版本號碼增加1,這個時候當前資料的版本為2。當第二個session也更新了資料提交的時候,探索資料庫中版本為2,和一開始這個session取到的版本號碼不一致,就知道別人更新過此條資料,這個時候再進行業務處理,比如整個Transaction都Rollback等等操作。在用版本戳的時候,可以在應用程式側使用版本戳的驗證,也可以在資料庫側採用Trigger(觸發器)來進行驗證。不過資料庫的Trigger的效能開銷還是比較的大,所以能在應用側進行驗證的話還是推薦不用Trigger。
③第三種做法和第二種做法有點類似,就是也新增一個Table的Column,不過這次這個column是採用timestamp型,儲存資料最後更新的時間。在Oracle9i以後可以採用新的資料類型,也就是timestamp with time zone類型來做時間戳記。這種Timestamp的資料精度在Oracle的時間類型中是最高的,精確到微秒(還沒與到納秒的層級),一般來說,加上資料庫處理時間和人的思考動作時間,微秒層級是非常非常夠了,其實只要精確到毫秒甚至秒都應該沒有什麼問題。和剛才的版本戳類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳記和自己更新前取到的時間戳記進行對比,如果一致則OK,否則就是版本衝突。如果不想把代碼寫在程式中或者由於別的原因無法把代碼寫在現有的程式中,也可以把這個時間戳記樂觀鎖邏輯寫在Trigger或者預存程序中。
五、結論
① 如果系統並發量不大且不允許髒讀,可以使用悲觀鎖解決並發問題。
② 如果系統並發非常大的話,悲觀鎖會帶來很大效能問題,所以一般採用樂觀鎖。
六、參考資料
[1] oracle的樂觀鎖和悲觀鎖 http://www.blogjava.net/cheneyfree/archive/2008/01/25/177773.html
[2] 樂觀鎖和悲觀鎖的區別 http://www.cnblogs.com/Bob-FD/p/3352216.html