【问题标题】:How to handle transactions with concurrent access in Spring如何在 Spring 中处理并发访问的事务
【发布时间】:2016-01-16 22:16:43
【问题描述】:

我有一种方法的服务:

    @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);
            }
       }
    }

我正在运行带有 testModelId = 1L 参数的 2 个并行调用配置的 Gatling。 由于这些调用,我收到了错误:

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) 行上的一个调用,直到其他调用完成其执行,但正如我从日志中看到的那样,它以不同的方式工作。

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

【问题讨论】:

    标签: java spring spring-data spring-transactions


    【解决方案1】:

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

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

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

    2 个线程中的一个到达事务结束并成功提交。另一个线程到达事务的末尾,由于违反约束而无法提交,因此第二个事务以“回滚”状态终止。

    为什么findOne 在第二个事务中没有被阻塞?

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

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

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

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

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

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

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

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

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

    【讨论】:

    • “您期望 JTA 事务将在数据库级别执行某种锁定。但事实并非如此。”但为什么?据我所知,SERIALIZABLE 隔离级别确实会锁定写入和读取,因此应该在 testRepository.findOne(..); 上抛出异常;排。此外,正如您所解释的,我们可以重试事务,它可能会发生几次,因为我们可以有多个用户并发访问。
    • 如果我将更改我的示例并且 findOne 将始终返回一些结果(它将存在于数据库中)然后并发访问之一将抛出错误“由于并发更新而无法序列化访问”,但无论如何testRepository.save 方法调用将引发错误。所以和你描述的有点不一样。感谢您的帮助。
    【解决方案2】:

    我在并行调用中遇到了类似的问题,我使用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();
        }
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-14
      • 2018-02-21
      • 1970-01-01
      • 2016-12-26
      • 2021-09-15
      • 1970-01-01
      相关资源
      最近更新 更多