如何在 Spring 中处理具有并发访问的事务

How to handle transactions with concurrent access in Spring

我有一种方法的服务:

    @Service
    public class DefaultTestService implements TestService {
        private static final Logger LOGGER = Logger.getLogger(DefaultTestService.class);
        @Autowired
        private TestRepository testRepository;

        @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
        @Override
        public void incrementAndGet(Long testModelId) {
            LOGGER.debug("Transaction is active: " + TransactionSynchronizationManager.isActualTransactionActive());
            final TestModel tm = testRepository.findOne(testModelId);
            if (tm != null) {
                LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
                tm.setValue(tm.getValue() + 1);
                testRepository.save(tm);
            } else {
                LOGGER.debug("Saved with id: " + testModelId);
                final TestModel ntm = new TestModel();
                ntm.setId(testModelId);
                testRepository.save(ntm);
            }
       }
    }

而且我 运行 Gatling 有 2 个并行调用配置 testModelId = 1L 参数。 由于这些调用,我收到错误消息:

org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "test_model_pkey"

从日志中可以看出,有两次调用同时进入了这个方法,并且每次都打印了日志

"Saved with id: 1"
"Saved with id: 1"

我假设在此方法上添加事务注释会阻止行 testRepository.findOne(testModelId) 中的一个调用,直到其他调用完成其执行,但正如我从日志中看到的那样,它以不同的方式工作。

所以我的问题是,在出现并发访问的情况下,事务是如何工作的?我该如何处理这种并发访问的情况?

事务意味着在事务边界内执行的持久对象的所有修改将:

  • 在事务结束时提交(即 所有 修改都保存在数据库中)
  • 在事务结束时回滚(即 none 的修改保留在数据库中) 就这些了。

在这种情况下交易如何运作?

2 个线程之一到达事务末尾并成功提交。另一个线程到达事务末尾,由于违反约束而未能提交,因此第二个事务终止于 "rollback" 状态。

为什么 findOne 在第二笔交易中没有被阻止?

仅仅是因为,尽管 SERIALIZABLE 事务级别,但没有要锁定的行。 findOne return 两个事务都没有结果,也没有任何东西被锁定(当然,如果第一个事务在第二个事务执行之前提交 findOne:这是另一回事)。

如何在您的特定情况下处理并发事务(即插入新行时违反 PK 约束)?

最常见的策略是让数据库将 id 分配给新行 - 在序列的帮助下 -

(作为实验,您可以尝试将隔离级别设置为 READ_UNCOMMITED,以便第二个事务可以从第一个事务读取未提交的更改。我不确定您是否注意到任何差异,因为如果 findOne 在第二笔交易中,在第一笔交易 testRepository.save(ntm); 之前执行,它仍然 return 没有结果)

一般如何处理并发修改引起的事务回滚?

这真的取决于您的用例。基本上你可以选择:

  • 捕获异常并"retry"操作。
  • 向调用者抛出异常(可能向用户显示一条温和的错误消息)。

请注意,如果事务以回滚状态终止:事务期间修改的持久对象图不会恢复到其原始状态。

请注意,使用隔离级别 SERIALIZABLE 会导致巨大的性能问题,通常仅用于关键和偶尔的事务。

我在并行调用中遇到了类似的问题,我使用 ReentrantLock 解决了它。

这是一个使用您的代码的示例:

@Service
public class DefaultTestService {

  private final ReentrantLock lock = new ReentrantLock();

  @Autowired
  private TestRepository testRepository;

  @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
  public void incrementAndGet(Long testModelId) {
    lock.lock();
    try {
      final TestModel tm = testRepository.findOne(testModelId);
      if (tm != null) {
        LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
        tm.setValue(tm.getValue() + 1);
        testRepository.save(tm);
      } else {
        LOGGER.debug("Saved with id: " + testModelId);
        final TestModel ntm = new TestModel();
        ntm.setId(testModelId);
        testRepository.save(ntm);
      }
    } finally {
      lock.unlock();
    }
  }
}