具有同步关键字的春季@Transactional不起作用

2022-09-02 08:53:47

假设我有一个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");
        }
    }
}

假设这本书被删除了,然后用新的作者或其他微小的更改重新添加,所以这种方法可能会从另一个系统快速调用两次,首先是删除这本书,然后添加回同一本书(有一些新的细节)。

为了解决这个问题,我们可以尝试(就像我一样)添加上面的@Transactional代码,然后在@Transactional不起作用时“同步”。但奇怪的是,它在第二次通话中失败了。

“无法添加书籍 - 书籍已存在”。

我花了很多时间试图弄清楚这个问题,所以我想我会分享答案。


答案 1

当删除并立即添加回一本书时,如果我们没有“@Transactional”或“同步”,我们将从以下线程执行开始:

T1:|-----删除书籍----->

T2:|-------添加书籍------->

关键字确保该方法一次只能由一个线程运行。这意味着执行变为:synchronized

T1:|-----删除书-----> T2:|--------添加书籍------>

注释是一个方面,它的作用是围绕你的类创建一个代理java类,在方法调用之前向它添加一些代码(开始事务),调用该方法,然后调用一些其他代码(提交事务)。所以第一个线程现在看起来像这样:@Transactional

T1:|--Spring开始交易--|-----删除账簿----- |--Spring提交事务--->

或更短:T1:|-B-|-R-|-C-->

和第二个线程,如下所示:

T2:|--Spring开始交易--|-------添加书------- |--Spring提交交易--->

T2: |-B-|-A-|-C-->

请注意,批注仅锁定数据库中的同一实体,使其无法同时被修改。由于我们要添加一个不同的实体(但具有相同的书名),因此它没有多大好处。但它仍然不应该伤害对吧?@Transactional

好吧,这是有趣的部分:

Spring 添加的事务代码不是同步方法的一部分,因此 T2 线程实际上可以在“提交”代码完成运行之前,即第一个方法调用完成后启动其方法。喜欢这个:

T1: |-B-|-R-|-C--|-->

T2: |-B------|-A-|-C-->

因此,当“add”方法读取数据库时,删除代码已经运行,但不是提交代码,因此它仍然在数据库中找到对象并引发错误。几毫秒后,它将从数据库中消失。

删除注释将使关键字按预期工作,尽管这不是其他人提到的好解决方案。删除并修复批注是更好的解决方案。@Transactionalsynchronizedsynchronized@Transactional


答案 2

您需要设置事务隔离级别以防止从数据库中进行脏读,而不必担心线程安全。

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

以下是事务传播和隔离的出色说明。

弹簧@Transactional - 隔离、传播


推荐