【问题标题】:Spring transaction rollback on exception continues to execute code after throw异常的Spring事务回滚在抛出后继续执行代码
【发布时间】:2017-02-21 13:26:16
【问题描述】:

我有一项服务可以安排会议并发送确认电子邮件,我注意到如果我多次单击提交按钮,将发送多封电子邮件。

服务是这样的:

@Service
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = { Exception.class })
public class MeetingService {
    public void scheduleAndInvite(int meetingId) {
        try {
            Meeting meeting = meetingDao.loadById(meetingId);

            // Validations.
            if (meeting.getMeetingStatus() != MeetingStatus.Draft) {
                throw new FmcUserException("not draft");
            }

            // Persist entity
            meeting.setMeetingStatus(MeetingStatus.Scheduled);

            meetingDao.persistMyEntity(meeting);

            // This eventually calls JavaMailSender. Uses the Meeting hibernate entity
            sendInvitations(meeting);
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            throw new FmcSystemException(ex); // This class extends RuntimeException.
        }
    }

当(在浏览器中)多次单击提交时,我希望第一次测试(状态!= 草稿)足以评估此会议是否已安排。在这种情况下,异常被抛出,被 catch 块捕获,从而跳过了 sendInvitations() 调用。

它在日志中正确生成了大量异常:

12:45:01,117 ERROR [my.framework.mvc.BaseController] (default task-55) could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement: org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
    at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:232)
    at org.springframework.orm.hibernate5.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:755)

Caused by: org.hibernate.exception.LockAcquisitionException: could not execute statement
    at org.hibernate.dialect.MySQLDialect$3.convert(MySQLDialect.java:522)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)

我无法理解这到底是如何发送多封电子邮件的。我在这里读到 spring 继续执行直到方法结束但是为什么呢?!抛出异常后,为什么要在 throw() 语句之后继续执行?

我知道这个问题可以通过包装 sendEmail 调用来解决:

    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            sendInvitations(meeting);
        }
    });

但话又说回来了。为什么?

非常感谢!

【问题讨论】:

  • Spring 在异常之后肯定不进行执行。发送多封电子邮件的原因是存在竞争条件。如果您多次按下提交,则会并行执行相同的请求,因此meetingDao.loadById 可以在您到达代码中完成更新的点之前执行多次。
  • 我明白了。是因为提交只在事务结束时执行吗?如果是这样,有没有办法在更新后立即强制提交?还是使用 TransactionSynchronizationManager 的更好方法?
  • 您可以强制提交,但您仍然会有竞争条件,因为在提交事务之前,多个参与者可以进入读取部分。一个“简单”的解决方法是在提交请求后禁用提交按钮。不过,这不会阻止任何人直接调用端点。对于这样的系统问题,我通常会建议您的请求具有幂等性;但是,对于您正在做的事情,这可能完全是矫枉过正。
  • 明白。我现在将使用 TransactionSynchronizationManager 在提交后执行非事务性方法。如果您想将您的 cmets 升级为完整答案(以帮助他人),我会接受。
  • @Leon 请升级您的评论以回答,以便我接受!谢谢。

标签: java spring hibernate spring-transactions


【解决方案1】:

抛出异常后,Spring 不执行代码。这是不可能的,这就是java的工作原理。

在我看来,这是因为通过多次推送提交,您会多次向您的服务器发送请求,这些请求会并行执行。在某些时候,不同的线程会互相阻碍。

如果您想正确解决此问题,您将需要同步此代码,或获取数据库锁以防止其他事务通过。 (使用 SELECT .... FOR UPDATE 或其他方式)

【讨论】:

    猜你喜欢
    • 2022-07-22
    • 1970-01-01
    • 2015-10-03
    • 2011-03-22
    • 2012-04-07
    • 2015-10-31
    • 1970-01-01
    • 2021-05-13
    • 2014-07-15
    相关资源
    最近更新 更多