Spring JPA + MySQL和死锁

时间:2017-06-16 20:04:56

标签: java mysql spring jpa deadlock

我正在使用Spring Boot在Java中实现的REST API。我使用嵌入式内存数据库H2几周,但在某些时候我发现事务隔离有问题。

更准确地说,我有一张桌子,我需要跟踪"复制"记录。副本只是一个记录,对于表格列的明确定义的子集,它等于另一个记录。所以,基本上,当我插入新记录时,我首先检查它是否重复并相应地标记。一个布尔列"重复"为此目的服务。

例如,让我们说B和C是我检查的列,以便定义重复项。这是一个有效的状态:

| A | B | C | duplicate | | - | - | - | --------- | | x | y | z | false | | z | y | z | true | | x | y | y | false | | x | y | y | true | | y | y | y | true |

虽然是有效状态:

| A | B | C | duplicate | | - | - | - | --------- | | x | y | z | false | | z | y | z | true | | x | y | y | false | | x | y | y | true | | y | y | y | false |

...因为第3行和第5行对B和C都有相同的值,所以必须将其中一个标记为重复。

换句话说,我的要求是将碰巧已经使用过值的任何行标记为重复。对于给定的一组值,只允许一行duplicate == false

但是,我的基于Spring的实现没有按预期工作。例如,插入具有相同值的100行应该导致99个重复,并且只有一个非重复。但是,当我尝试并行执行这些插入时,未检测到大量重复项。

我尝试了几个修复程序,在某些时候我开始认为H2没有正确实现SERIALIZABLE隔离级别。我创建了一个小应用程序来演示它:

@RestController
public class NewFooCtrl {

  @Autowired
  private FooRepo repo;

  @RequestMapping(value = "/foo", method = RequestMethod.POST)
  @Transactional(isolation = Isolation.SERIALIZABLE)
  public void newFoo(@RequestBody Foo foo) {
    List<Foo> foos = repo.findByBar(foo.getBar());
    if (foos.isEmpty()) foo.setDuplicate(false);
    else foo.setDuplicate(true);
    repo.save(foo);
  }

}

注意:我省略了明显的代码,例如模型和存储库。 Foo模型具有标识符(类型UUID)和bar属性(类型String)和duplicate属性(类型为boolean)。重复检查基于bar属性。

使用H2我有很多错过的重复(通常是10%)。使用MySQL我总是有正确的结果(即标记为重复的行数正好 N - 1,其中N是插入行的数量)。唯一的问题是只有一小部分插入成功(最多1%到30%)。

我收到了大量与死锁相关的异常。这是为什么?这样简单的代码怎么会导致死锁。我的意思是,它只是一个选择,然后是插入。

有什么建议吗?

2 个答案:

答案 0 :(得分:2)

应用程序不应检查事务中的重复键本身。将此保留为具有唯一索引的数据库引擎,如果发生异常则捕获异常,并再次使用其他标识符。

如果您真的想在应用程序级别解决此问题,也许您应该在打开事务后手动锁定表。隔离级别可以自动为您执行此操作,但性能成本较高(您可能不需要)。

另一个解决方案是使用@Version注释进行乐观锁定,但是您将无法保证标识符的唯一性。

很难诊断您的死锁问题,但通常会在您有递归事务(在另一个事务中打开一个事务)时出现。检查您的bean @Scope,他们可以创建此类问题。另外,请确保您只有一个TransactionManager和一个EntityManager bean。

答案 1 :(得分:1)

我认为与死锁相关的异常是由我测试演示应用程序的方式引起的。更确切地说,测试代码是用JavaScript / Node.js编写的,在启动I / O任务时,它非常非常。几乎同时请求的所有交易(并且可能同时自动重试。)。

通过在每个请求之间添加非常短的等待(例如10 ms),我获得了合理的吞吐量和非常少的与死锁相关的异常。

我的猜测是没有死锁。只是非常高的锁争用,在数据库级别的某种启发式解释为可能的死锁。实际上,通过从MySQL CLI禁用死锁检测,我完全消除了那些与死锁相关的异常(虽然它们被锁等待超时所取代)。