【问题标题】:Spring @Transactional and concurrencySpring @Transactional 和并发
【发布时间】:2020-05-24 13:13:28
【问题描述】:

我有一个带有以下方法的 Spring 控制器:

    @PostMapping("/tables")
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public ResponseEntity<Table> addTable(@RequestBody Table table) {
        if (table.getTableNumber() == 0) {
            table.setTableNumber(tableRepository.count() + 1);
        }
        return new ResponseEntity<>(tableRepository.save(table), HttpStatus.CREATED);
    }

我希望多个调用按顺序发生,以便在每次调用时count 方法将返回正确的行数(目前它不会发生,因为多个调用可以同时运行)。

我试图找出最好的方法,我得出了以下结论(如果我错了,请纠正我):

  1. 使用@Transactional 没有帮助,因为多个事务仍然可以同时运行
  2. 无论如何,@Transactional 将阻止在多次调用 save 时仅保留部分数据的复杂方法(不是这种情况)
  3. isolation = Isolation.SERIALIZABLE 仅确定新事务与已完成事务的关系(在这种情况下,新事务将以最新形式查看数据库)

我读到我可以使用带有@Lock 的锁来独占访问表,但我只看到它应用于单个方法(如findById),而在我的情况下,锁必须跨越多个操作(countsave)。有没有办法任意锁定表(使用注释或以编程方式)?锁定整个表可能看起来有点激烈,但至少在不同表上操作的方法仍然可以同时运行(这不能仅通过在方法级别使用synchronized 来实现,而通过锁定存储库可以实现,但是这绝对不是一个干净的工作)。

听起来很奇怪,Spring 没有提供一种简单的方式来表达“ehi,我希望在这个存储库上运行的这个方法一次只能从一个线程运行”。我错过了什么吗?

【问题讨论】:

  • 对不起,我忘了说我简化了问题的方法。据我了解,即使在并发修改的情况下,我也应该依靠数据库提供的功能(例如约束)以及事务来防止发生令人讨厌的事情。我也明白事务性和并发性不是一回事(这就是为什么我在问题中写了到目前为止我所理解的内容),但我认为这两个问题有些相关,这就是我提到两者的原因。我仍然想了解如何对数据库进行顺序操作。
  • 我知道在这种特殊情况下没有必要使调用顺序,因为我可以让它以其他方式工作(如你所说),我只是想展示一个涉及更多的例子而不是在同一张表上的一个操作来显示@Transactional 如何在防止并发访问方面没有帮助(并确保我正确理解它)并了解如何在不改变该代码逻辑的情况下使其工作。
  • 事务不是锁,但它们旨在保证数据完整性(在各个级别,取决于隔离)。我建议你阅读不同的隔离级别,看看SERIALIZABLE 在冲突情况下的表现。这实际上与 Spring、JPA 甚至 Java 无关。
  • @NathanHughes 似乎 @Lock 不适用于 count (我认为这是因为锁定仅在返回实体时发生)。但是,它适用于findAllfindById,我可以使用它们来获得我想要的东西。主要缺点是需要在存储库中重新定义所有这些方法,最重要的是,需要在事务中调用这些方法以使它们真正锁定表。即使在交易之外,我也想这样做。我找到了一些方法(例如,here),但看起来很hacky。

标签: java spring jpa concurrency transactions


【解决方案1】:

您对@Transactional 的看法是正确的,因为它涉及单个请求而不是多个请求。

这里有几个选项可供您选择:

  1. 通过编写原子查询将此“锁定”移至数据库层。例如:update tbl set tableNumber = tableNumber + 1 where id = X 将确保多个并发请求将返回正确的值。在这种情况下,DB 正在执行行级 WRITE 锁以确保数据一致性。

  2. 使用锁。这就像在数据库中的表中设置标志一样简单。在任何线程开始此操作流之前,它会检查标志是否设置/取消设置。如果未设置,它会设置标志并继续。如果未设置,线程将等待并轮询该值,直到它读取未设置的值。使用 Redis 之类的东西对于锁定实现来说是一个更好的主意,因为会在锁定到期时进行回调。

使用锁会更好,因为这样您就可以让线程在需要的时间内保持锁(记住要有max_timeout),并在此时间段内根据需要接触尽可能多的实体。

【讨论】:

  • 感谢您的回答。获得锁的最佳方法是什么?显然我可以通过手动编写查询来做到这一点,但这似乎不是一个优雅的解决方案。
猜你喜欢
  • 1970-01-01
  • 2022-01-12
  • 1970-01-01
  • 2023-03-10
  • 2023-03-07
  • 2010-12-07
  • 2018-08-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多