一、問題引出
1. 假設噹噹網上使用者下單買了本書,這時資料庫中有條訂單號為001的訂單,其中有個status欄位是’有效’,表示該訂單是有效;
2. 後台管理員查詢到這條001的訂單,並且看到狀態是有效;
3. 使用者發現下單的時候下錯了,於是撤銷訂單,假設運行這樣一條SQL: update order_table set status = ‘取消’ where order_id = 001;
4. 後台管理員由於在②這步看到狀態有效,這時,雖然使用者在③這步已經撤銷了訂單,可是管理員並未重新整理介面,看到的訂單狀態還是有效,於是點擊”發貨”按鈕,將該訂單發到物流部門,同時運行類似如下SQL,將訂單狀態改成已發貨:update order_table set status = ‘已發貨’ where order_id = 001;
<<< 上面的問題應該如何解決呢??? 請見下文分解 >>>
二、悲觀鎖
所謂的悲觀鎖:它會假設一個事務可能會在讀資料與更新資料之間對資料變更。為了防止由於這種更改帶來的資料不一致現象,讀語句(read statement)便鎖住資料來阻止任何其他進程對該資料變更。
Oracle的悲觀鎖需要利用一條現有的Connection,它分成兩種方式,從SQL語句的區別來看,就是一種是select for update,一種是select for update nowait的形式。
1. 執行select xxx for update操作時,資料會被鎖定,只有執行comit或rollover才會釋放
2. 執行select xxx for update nowait操作時,資料也會被鎖定,其他人訪問時或返回ORA-00054錯誤,內容是資源正忙,需要採取相應的業務措施進行處理。
雖然悲觀鎖應用起來很簡單並且十分安全,與此同時卻有兩大問題:
1. 鎖定:應用的使用者選擇一個記錄進行更新,然後去吃午飯,但是沒有結束或者丟棄該事務。這樣其他所有需要更新該記錄的使用者就必須等待進行中實務操作的使用者回來並且完成該事務或者直到DBA殺掉該不愉快的事務並且釋放鎖。
2. 死結:使用者A和B同時更新資料庫。使用者A鎖定了一條記錄並且試圖請求使用者B持有的鎖,同時使用者B也在等待擷取使用者A持有的鎖。兩個事務同時進入了無限等待狀態即進入死結狀態。
三、樂觀鎖
所謂的樂觀鎖就是:當資料讀入的時候並不對那些記錄加鎖,並且假設在讀操作進行時,被更新的資料並沒有被修改,在最後提交的時候再進行資料衝突檢測。oracle預設使用樂觀鎖,例如:update... set...
在樂觀鎖中,我們有3種常用的做法來實現:
1. 第一種就是在資料取得的時候把整個資料都copy到應用中,在進行提交的時候比對當前資料庫中的資料和開始的時候更新前取得的資料。當發現兩個資料一模一樣以後,就表示沒有衝突可以提交,否則則是並發衝突,需要去用商務邏輯進行解決。
2. 第二種樂觀鎖的做法就是採用版本戳,這個在Hibernate中得到了使用。採用版本戳的話,首先需要在你有樂觀鎖的資料庫table上建立一個新的column,比如為number型,當你資料每更新一次的時候,版本數就會往上增加1。比如同樣有2個session同樣對某條資料進行操作。兩者都取到當前的資料的版本號碼為1,當第一個session進行資料更新後,在提交的時候查看到當前資料的版本還為1,和自己一開始取到的版本相同。就正式提交,然後把版本號碼增加1,這個時候當前資料的版本為2。當第二個session也更新了資料提交的時候,探索資料庫中版本為2,和一開始這個session取到的版本號碼不一致,就知道別人更新過此條資料,這個時候再進行業務處理,比如整個Transaction都Rollback等等操作。在用版本戳的時候,可以在應用程式側使用版本戳的驗證,也可以在資料庫側採用Trigger(觸發器)來進行驗證。不過資料庫的Trigger的效能開銷還是比較的大,所以能在應用側進行驗證的話還是推薦不用Trigger。
3. 第三種做法和第二種做法有點類似,就是也新增一個Table的Column,不過這次這個column是採用timestamp型,儲存資料最後更新的時間。在Oracle9i以後可以採用新的資料類型,也就是timestamp with time zone類型來做時間戳記。這種Timestamp的資料精度在Oracle的時間類型中是最高的,精確到微秒(還沒與到納秒的層級),一般來說,加上資料庫處理時間和人的思考動作時間,微秒層級是非常非常夠了,其實只要精確到毫秒甚至秒都應該沒有什麼問題。和剛才的版本戳類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳記和自己更新前取到的時間戳記進行對比,如果一致則OK,否則就是版本衝突。如果不想把代碼寫在程式中或者由於別的原因無法把代碼寫在現有的程式中,也可以把這個時間戳記樂觀鎖邏輯寫在Trigger或者預存程序中。
四、結論
1. 如果系統並發量不大且不允許髒讀,可以使用悲觀鎖解決並發問題。
2. 如果系統並發非常大的話,悲觀鎖會帶來很大效能問題,所以一般採用樂觀鎖。
本欄目更多精彩內容:http://www.bianceng.cnhttp://www.bianceng.cn/database/Oracle/