全局鎖和悲觀鎖的異常處理

悲觀鎖一般用在事務中“獨佔的”持有一個資源,這樣是爲了在併發操作中保護數據一致性。悲觀鎖的使用非常常見,但是我們代碼中對悲觀鎖的認識上存在不足,導致對異常的處理其實是不到位的,比如以下代碼:

    

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了:

 

 

發佈了28 篇原創文章 · 獲贊 9 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章