我正在使用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%)。
我收到了大量与死锁相关的异常。这是为什么?这样简单的代码怎么会导致死锁。我的意思是,它只是一个选择,然后是插入。
有什么建议吗?
答案 0 :(得分:2)
应用程序不应检查事务中的重复键本身。将此保留为具有唯一索引的数据库引擎,如果发生异常则捕获异常,并再次使用其他标识符。
如果您真的想在应用程序级别解决此问题,也许您应该在打开事务后手动锁定表。隔离级别可以自动为您执行此操作,但性能成本较高(您可能不需要)。
另一个解决方案是使用@Version
注释进行乐观锁定,但是您将无法保证标识符的唯一性。
很难诊断您的死锁问题,但通常会在您有递归事务(在另一个事务中打开一个事务)时出现。检查您的bean @Scope
,他们可以创建此类问题。另外,请确保您只有一个TransactionManager
和一个EntityManager
bean。
答案 1 :(得分:1)
我认为与死锁相关的异常是由我测试演示应用程序的方式引起的。更确切地说,测试代码是用JavaScript / Node.js编写的,在启动I / O任务时,它非常非常。几乎同时请求的所有交易(并且可能同时自动重试。)。
通过在每个请求之间添加非常短的等待(例如10 ms),我获得了合理的吞吐量和非常少的与死锁相关的异常。
我的猜测是没有死锁。只是非常高的锁争用,在数据库级别的某种启发式解释为可能的死锁。实际上,通过从MySQL CLI禁用死锁检测,我完全消除了那些与死锁相关的异常(虽然它们被锁等待超时所取代)。