让我们说我有一个类似这样的方法的java类(只是一个例子)
@Transactional
public synchronized void onRequest(Request request) {
if (request.shouldAddBook()) {
if (database.getByName(request.getBook().getName()) == null) {
database.add(request.getBook());
} else {
throw new Exception("Cannot add book - book already exist");
}
} else if (request.shouldRemoveBook()) {
if (database.getByName(request.getBook().getName()) != null) {
removeBook();
} else {
throw new Exception("Cannot remove book - book doesn't exist");
}
}
}
假设这本书被删除,然后重新添加了一个新作者或其他小改动,所以这个方法可能会从另一个系统快速调用两次,首先删除Book,然后再添加相同的Book(与一些新的细节)。
为了解决这个问题,我们可以尝试(像我一样)添加上面的@Transactional代码,然后同步'同步'当@Transactional不起作用时。但奇怪的是,它在第二次通话中失败了
"无法添加书本已存在"。
我花了很多时间试图解决这个问题,所以我想我会分享答案。
答案 0 :(得分:9)
当删除并立即添加一本书时,如果我们没有“@Transactional”或“synchronized”,我们将从这个线程执行开始:
T1:| -----删除书籍----->
T2:| -------添加书------->
synchronized
关键字可确保方法一次只能由一个线程运行。这意味着执行变为:
T1:| -----删除书-----> T2:| --------添加书------>
@Transactional
注释是一个方面,它的作用是在类周围创建代理 java类,添加一些代码(开始交易 )在方法调用之前,调用该方法,然后调用其他一些代码(提交事务)。所以第一个线程现在看起来像这样:
T1:| - 春天开始交易 - | -----删除书----- | - 春天提交交易--->
或更短:T1:| -B- | -R- | -C - >
和第二个线程是这样的:
T2:| - 春天开始交易 - | -------添加书------- | - 春天提交交易--->
T2:| -B- | -A- | -C - >
请注意,@Transactional
注释仅锁定同时修改数据库中的同一实体。由于我们正在添加一个不同的实体(但具有相同的书名),因此它没有多大好处。但它仍然不应该是正确的吗?
这里是有趣的部分:
Spring添加 的事务代码不是synchronized方法的一部分 ,因此T2线程实际上可以在“commit”代码运行完毕之前启动其方法,在完成第一个方法调用之后。像这样:
T1:| -B- | -R- | -C-- | - >
T2:| -B ------ | -A- | -C - >
因此。当“add”方法读取数据库时,删除代码已经运行,但不是提交代码,所以它仍然在数据库中找到对象并抛出错误。几毫秒之后,它将从数据库中消失。
删除@Transactional
注释会使synchronized
关键字按预期工作,尽管这不是其他人提到的好解决方案。删除synchronized
并修复@Transactional
注释是一种更好的解决方案。
答案 1 :(得分:4)
您需要设置事务隔离级别以防止数据库中的脏读,而不必担心线程安全。
@Transactional(isolation = Isolation.SERIALIZABLE)
public void onRequest(Request request) {
if (request.shouldAddBook()) {
if (database.getByName(request.getBook().getName()) == null) {
database.add(request.getBook());
} else {
throw new Exception("Cannot add book - book already exist");
}
} else if (request.shouldRemoveBook()) {
if (database.getByName(request.getBook().getName()) != null) {
removeBook();
} else {
throw new Exception("Cannot remove book - book doesn't exist");
}
}
}
以下是对事务传播和隔离的出色解释。
答案 2 :(得分:1)
应该在@Transaction方法之前使用'synchronized'。 否则,在多线程时,对象已解锁,但未提交事务。
答案 3 :(得分:0)
我将“同步”添加到 onRequest 的调用方方法并将事务的隔离更改为 READ_UNCOMMITTED。这可以解决问题。但我很好奇为什么只将“同步”移动到调用者方法不起作用。因为这样做,方法执行过程如下:持有同步锁,持有事务锁,释放事务锁,释放同步锁。然而,第四步似乎发生在第三步之前。所以修改事务隔离是必要的。有人知道原因吗?