全局锁和悲观锁的异常处理

悲观锁一般用在事务中“独占的”持有一个资源,这样是为了在并发操作中保护数据一致性。悲观锁的使用非常常见,但是我们代码中对悲观锁的认识上存在不足,导致对异常的处理其实是不到位的,比如以下代码:

    

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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章