大家對 樂觀鎖 這三個字眼應該不陌生吧?
為什麼今天我想談談樂觀鎖的設計呢?
關於資料庫的樂觀鎖使用, 是不是很多人一看到樂觀鎖就會想到 Version 欄位 (版本標識)。
ps: 其實不是非要新增版本欄位
樂觀鎖 , Optimistic Concurrency Control (樂觀並行控制),簡稱 OCC 。
樂觀鎖不是一種真正的 ‘ 鎖 ’,而是一種實現鎖效果的 設計思想:
樂觀地 認為 並行的操作對資料 不會產生衝突,所以沒有使用 真正的 ‘鎖’ 去對資料加鎖;
而是選擇在提交資料的時候,去檢測資料是否衝突了?
發現衝突就採取 處理操作,例如報錯、重試、停止等等。
基於資料庫使用展開介紹
也就是在 表內 增加一個欄位 version 。
每次寫操作如果時成功的,都需要 將 version 版本值 +1 ,
例如原來 某條資料的 version值為 1, 如果修改了,那麼 version就需要變成 version+ 1 , 也就是 2.
然而在並行場景,大量的寫操作不免會發生衝突。
所以當我們 讀取 資料, 需要做更新操作。 我們的 設計流程時這樣的:
1. 讀取資料,把資料裡的version值 取出作為 更新前標識 值 version-before。
2.做業務邏輯計算等等 ....
3. 更新資料操作 ,更新時, 將之前的 標識 值 version-before 與 資料庫裡面的 version值 做匹對, 檢測是否一致。
如果一致, 那麼意味著 這時段內,沒有其他寫操作修改過資料, 那麼我們可以提交成功。
如果不一致,那麼意味著 發生了寫寫衝突, 也就是我們此刻需要更新的資料,已經被修改過了。那麼我們可以根據業務場景,採取處理措施 (報錯記錄、重試流程、停止等等)。
ps: 注意了,這裡的讀取,檢測,更新 這些操作都是務必保證 操作的原子性 ,連貫執行,也就是處於同個事務內。
mysql語句的寫法舉例:
update proinfo set proNum = proNum + 10 , version = version + 1 where version = #{version} and proId = #{proId}
只要where 後的 version 條件不成立,那麼就是更新不成功,也就是 檢測到了 ‘衝突’ 。
那麼前言裡,我提到 使用樂觀鎖,不一定非要新增版本 version欄位。
我們還可以使用 updateTimestamp 這種欄位值。
我想,大家接觸過很多專案,是不是看到很多老專案的表內都會有個 更新時間(時間戳)的欄位,
但是好像業務裡又沒有用。
其實,這種欄位,可以用來實現 樂觀鎖。
精確到毫秒或者更細, 每次操作,讀取資料前把 時間戳的值 儲存,然後更新提交的時候, 將這時間戳和資料庫內的時間戳 值做匹對,原理也是一樣的。時間戳欄位可以自己傳入,也可以是通過mysql函數預設獲取更新。
update proinfo set proNum = proNum + 10 , updateTimestamp = unix_timestamp(now()) where updateTimestamp = #{updateTimestamp} and proId = #{proId}
(可能有人會反駁,如果時間戳一模一樣呢? 我不多說、)
為什麼需要提這個呢。 因為我想傳達的是, 樂觀鎖,要理解這種 樂觀控制的設計思想,靈活去運用。
而不是固化,看到千篇一律地加欄位 version,就跟著加。
有時候有些老專案,不是說加個欄位那麼回事,也許會引發一些雜七雜八的問題。
那麼我們大可去根據實際情況去設計樂觀鎖。
那麼最後也簡單地說下這個資料庫使用樂觀鎖設計,
1.最好是 讀多寫少的場景下使用,因為寫的操作少了,也就更樂觀了。
2. 在發現衝突時, 咱們的處理操作要慎重設計, 特別是寫操作並行特別多的情況,採取 無限制地重試? 短時間會不會適得其反?
思想很重要,不是隻顧著去套模式,因為掌握了這種思想,也許你不單單在使用樂觀鎖的時候你才用得上。
最後給大家留個小話題, CAS 無鎖演演算法 大家瞭解過麼?
可以去了解下,再回來 看看 文中說的 資料庫裡樂觀鎖的設計思想。
該篇淺談就到這,神神叨叨習慣了,說的東西可能沒營養可能有營養,就到這吧。