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

来源:互联网 发布:电力数据库 编辑:程序博客网 时间:2024/06/05 05:04

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

    

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

 

 

原创粉丝点击