資料庫的鎖策略 – 樂觀鎖 Optimistic Locking 、悲觀鎖 Pessimistic Locking

設計 DB Lock 的目的

Web Application 大部分都是提供給多個使用者使用;也就是說,會產生多個使用者,同時讀寫同一張資料表 (Table) 或是同一列 (Row) 資料的狀況。反之,如果今天你的資料庫同時只會有一個使用者在讀寫,那 Database 是不需要提供 Lock 機制的。

鎖的目的,就是在多個使用者在讀寫資料庫的狀況,還是需要保持資料庫的 ACID。

沒有(正確) 使用 DB Lock 可能會產生的問題

Lost Update

假設資料表中有一筆資料結構如下: (蘋果庫存剩下兩個)

idnamequantity
1Apple2

這個問題產生的順序如下:

Step1. 倉庫人員,讀取該筆資料,得到蘋果庫存 2 。

Step2. 銷售人員,讀取該筆資料,得到蘋果庫存2。

Step3. 倉庫人員,進貨 10 個蘋果,寫入系統,蘋果總共有 12 個。

Step4. 銷售人員,賣出 2 個蘋果,寫入系統,蘋果總共有 0 個。

所謂 Lost Update 指的就是 Step3 的 12 個蘋果消失了。

兩種不同的鎖策略

Pessimistic Locking (悲觀鎖)

悲觀鎖的悲觀,指的是一種保守策略,資料庫是一個相當悲觀的狀況,早早就假設可能會出現 Lost Update 的狀況。

舉例來說,當 Step1 倉庫人員抓取該資料的時候,鎖就會鎖上 id 為 1 的資料,這時候 Step2 就無法讀取該筆資料,因為該筆資料被鎖上了,需要等 Step3 執行完並 commited ,Step 2 銷售人員才能再讀取該筆資料。

悲觀鎖在 Client Sever 的情境蠻有用的,大部分的 client 都會直接連結 database 進行修改,到了近期的 Web Application 是比較不適用的,因為大部分的 Web App 可能都是抓取完資料,connection 就會先斷開,也就是 stateless 。

悲觀鎖的 SQL

Step1. 抓取資料

Select id, name, quantity from product where id = 1;

Step2. 存取目前的值,把 product name , quantity 記下來

Step3. 上鎖

select id, name, quantity from product where id = 1 and name = 'Apple' and quantity = 2 for update nowait;

Step4. 進行資料的修改

update product set name='Apple' and quantity = 12 where id = 1;

Step5. Commit ,流程結束

commit;

這樣子確保不會產生 Lost Updates.

Optimistic Locking (樂觀鎖)

樂觀鎖的樂觀,指的是一種相對比較有風險的策略,假設不會出現 lost update 的狀況。

以悲觀鎖的例子說明,不會在 Step3 對資料加上鎖,而且把值記錄下來以後,直接更新,當更新不到資料的時候,就代表資料已經被改變,需要請使用者重新做一次。

這種方式在大多情況下都是適用的,但也產生使用者可能會需要重新執行更新的可能。

樂觀鎖的 SQL

Step1. 抓取資料

Select id, name, quantity from product where id = 1;

Step2. 存取目前的值,把 product name , quantity 記下來

Step3. 進行資料的修改

update product set name='Apple' and quantity = 12 where id = 1;

Step4. 確認是否有更新到 “一筆” 資料,如果沒有需要重新執行 Step1 的抓取,再透過畫面的呈現,再請使用修改一次。

樂觀鎖其它常見的實作方式
  • 透過新增一個 version 欄位來識別,取代需要記下所有的值,只需要記下這個 version. 也可以使用修改時間來當這個 version 欄位。
idnamequantitylast_modified
1Apple22024/01/18 13:00:59
update product set name='Apple' and quantity = 12  last_modified = systimestamp where last_modified = to_timestamp_tz(:last_mod, 'DD-MON-YYYY HH.MI.SSXFF AM TZR');
  • 透過資料庫提供的 checksum 來檢查資料。
select name, quantity, ora_hash(name || '/' || quantity ) hash
     into :name, :quantity, :hash
     from product
     where id = 1;


update product set quantity = 12
where name = :name
and ora_hash(name || '/' || quantity) = :hash
Scroll to Top