悲觀鎖一般用在事務中“獨佔的”持有一個資源,這樣是爲了在併發操作中保護數據一致性。悲觀鎖的使用非常常見,但是我們代碼中對悲觀鎖的認識上存在不足,導致對異常的處理其實是不到位的,比如以下代碼:
protected void lockInvoiceAndSet(ApInvoiceOperateContext context) {
ApInvoice invoice = apInvoiceRepository.lockByInvoiceId(context.getInvoiceId());
if (invoice == null) {
LoggerUtil.warn(logger, "鎖定發票失敗,", "發票id:", context.getInvoiceId());
throw new GFCenterException(GFCenterErrorCodeEnum.AP_INVOICE_LOCK_ERROR);
}
context.setOrigInvoice(invoice);
}
這種代碼很常見。思路無外乎是:1. 鎖, 2.判, 3.決策。
1. “鎖”,很直觀,就是直接調用lock方法,這個方法要麼是對select for update或者select for updat nowait/waitxxx的包裝。
2. “判”,判什麼?我覺得需要判兩種可能:
· 鎖失敗:數據是存在於數據庫中的,只是因爲已經被其他資源佔用無法獲取獨佔鎖。
· 鎖不到:數據不存在於數據庫中,select都select不到,更別說for update了。
1. “決策”,根據“判”出來的不同場景做不同的決策。根據實際需要或返回失敗,或包裝業務異常拋出去,或返回獲取的業務資源,等等。
現在再看最初的代碼有沒有什麼問題?我直接一點說結果,就不賣關子了。
· 問題1:沒有判“鎖失敗”的場景。
· 問題2:對於“鎖不到”的場景處理,是錯誤的。
爲什麼說沒有判“鎖失敗”的場景?
因爲這段代碼根本沒有搞清楚“鎖失敗”會發生什麼,所以導致了在“鎖不到”的場景中,做了“鎖失敗”的決策。——以爲invoice == null就是“鎖失敗”,其實這是“鎖不到”。
來看看真正鎖失敗的時候是什麼樣子:
對於“鎖失敗”真正的表現是會拋出CannotAcquireLockException(Springframework),無論是嘗試拿鎖(select for update)失敗,還是等待鎖超時(select for updatenowait/wait xxx)失敗,都會返回這個異常。
我們來看看Spring怎麼解釋這個異常的:
/**
* Exception thrown on failure to aquire a lock during an update,
* for example during a "select for update" statement.
*
* @author Rod Johnson
*/
public class CannotAcquireLockException extends PessimisticLockingFailureException {
/**
* Constructor for CannotAcquireLockException.
* @param msg the detail message
*/
public CannotAcquireLockException(String msg) {
super(msg);
}
/**
* Constructor for CannotAcquireLockException.
* @param msg the detail message
* @param cause the root cause from the data access API in use
*/
public CannotAcquireLockException(String msg, Throwable cause) {
super(msg, cause);
}
}
“當拿鎖(selectfor update)失敗時拋出此異常”——簡單明瞭。
我們注意到CannotAcquireLockException繼承自PessimisticLockingFailureException,中文名叫:“悲觀鎖失敗異常”,好像很屌的樣子,爲啥同樣是悲觀鎖失敗,拋CannotAcquireLockException而不是PessimisticLockingFailureException呢?
來看一下代碼:
/**
* Exception thrown on a pessimistic locking violation.
* Thrown by Spring's SQLException translation mechanism
* if a corresponding database error is encountered.
*
* <p>Serves as superclass for more specific exceptions, like
* CannotAcquireLockException and DeadlockLoserDataAccessException.
*
* @author Thomas Risberg
* @since 1.2
* @see CannotAcquireLockException
* @see DeadlockLoserDataAccessException
* @see OptimisticLockingFailureException
*/
public class PessimisticLockingFailureException extends ConcurrencyFailureException {
/**
* Constructor for PessimisticLockingFailureException.
* @param msg the detail message
*/
public PessimisticLockingFailureException(String msg) {
super(msg);
}
/**
* Constructor for PessimisticLockingFailureException.
* @param msg the detail message
* @param cause the root cause from the data access API in use
*/
public PessimisticLockingFailureException(String msg, Throwable cause) {
super(msg, cause);
}
}
代碼裏發現PessimisticLockingFailureException有三個子類:
· CannotAcquireLockException
· CannotSerializeTransactionException
· DeadlockLoserDataAccessException
CannotSerializeTransactionException:這種是在串行(serialized)的事務隔離級別中,由於update競爭失敗拋出來的異常(Exception thrown on failure to complete a transaction in serializedmode due to update conflicts)
DeadlockLoserDataAccessException:這種是在當前線程由於死鎖失敗,且事務已經被回滾的情況下拋出來的異常(Generic exception thrown when the current process was a deadlockloser, and its transaction rolled back)
所以總結下來“悲觀鎖失敗異常”之下還有三個子類,他們分別代表着在不同的場景下悲觀鎖失敗的異常。顯然CannotAcquireLockException更加常見且通用;因爲CannotSerializeTransactionException要求數據庫的隔離級別要在“串行”(serialized),這種隔離級別是數據庫的最高事務隔離級別,以犧牲性能爲代價完全避免了“髒讀”、“幻讀”和“不可重複讀”,由於代價太高,實際應用場景中幾乎不會選擇這種。而一般較爲常用的隔離級別僅僅是“讀提交”(read committed),也就是我們現在生產庫中的事務隔離級別。
有點扯遠了,回到原來的那段代碼,看看如何完善:
/**
* 鎖定發票放到上下文中
*
*a @param context AP發票處理上下文
*/
protected void lockInvoiceAndSet(ApInvoiceOperateContext context) {
ApInvoice invoice = null;
try {
invoice = apInvoiceRepository.lockByInvoiceId(context.getInvoiceId());
} catch (CannotAcquireLockException e) {
LoggerUtil.warn(logger, "鎖定發票失敗[併發鎖失敗]");
// do something else
}
if (invoice == null) {
LoggerUtil.warn(logger, "鎖定發票失敗[根據發票ID找不到發票], 發票id:", context.getInvoiceId());
throw new GFCenterException(GFCenterErrorCodeEnum.AP_INVOICE_LOCK_ERROR);
}
context.setOrigInvoice(invoice);
}
以上代碼修復了幾個問題:
· 識別並處理“鎖失敗”
· 識別並正確處理“鎖不到”
· 使用合理的日誌級別,並修改了日記記錄信息,表達更明確。
另外,我寫完這篇總結,這裏就可以大大方方的catch對應的異常,不用這麼hack了: