为什么在使用 Spring Data JPA 更新实体时 @Transactional 隔离级别无效?

Why does @Transactional isolation level have no effect when updating entities with Spring Data JPA?

对于这个基于spring-boot-starter-data-jpa依赖和H2内存数据库的实验项目,我定义了一个User实体,有两个字段(idfirstName) 并通过扩展 CrudRepository 接口声明一个 UsersRepository

现在,考虑一个提供两个端点的简单控制器:/print-user 读取同一个用户两次,间隔一段时间打印出其名字,/update-user 用于更改用户的名字在这两次阅读之间。请注意,我特意设置了 Isolation.READ_COMMITTED 级别,并预计在第一次交易过程中,同一个 id 检索到两次的用户将具有不同的名称。但是,第一笔交易两次打印出相同的值。为了更清楚,这是完整的操作顺序:

  1. 最初,jeremy 的名字设置为 Jeremy
  2. 然后我调用 /print-user 打印出 Jeremy 并进入睡眠状态。
  3. 接下来,我从另一个会话调用 /update-user,它将 jeremy 的名字更改为 Bob
  4. 最后,当第一个事务在睡眠后被唤醒并重新读取 jeremy 用户时,它再次打印出 Jeremy 作为他的名字,即使名字已经更改为Bob(如果我们打开数据库控制台,它现在确实存储为 Bob,而不是 Jeremy)。

似乎设置隔离级别在这里没有效果,我很好奇为什么会这样。

@RestController
@RequestMapping
public class UsersController {

    private final UsersRepository usersRepository;

    @Autowired
    public UsersController(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    @GetMapping("/print-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional (isolation = Isolation.READ_COMMITTED)
    public void printName() throws InterruptedException {
        User user1 = usersRepository.findById("jeremy"); 
        System.out.println(user1.getFirstName());
        
        // allow changing user's name from another 
        // session by calling /update-user endpoint
        Thread.sleep(5000);
        
        User user2 = usersRepository.findById("jeremy");
        System.out.println(user2.getFirstName());
    }


    @GetMapping("/update-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public User changeName() {
        User user = usersRepository.findById("jeremy"); 
        user.setFirstName("Bob");
        return user;
    }
    
}

您更新 @GetMapping("/update-user") 的方法设置了隔离级别 @Transactional(isolation = Isolation.READ_COMMITTED),因此此方法永远不会达到 commit() 步骤。

您必须更改隔离级别或读取事务中的值以提交更改:) user.setFirstName("Bob"); 不保证您的数据会被提交

线程摘要将如下所示:

A: Read => "Jeremy"
B: Write "Bob" (not committed)
A: Read => "Jeremy"
Commit B : "Bob"

// Now returning "Bob"

您的代码有两个问题。

您在同一个事务中执行了两次 usersRepository.findById("jeremy");,您的第二次读取很可能是从 Cache 中检索记录。第二次读取记录时需要刷新缓存。我更新了使用 entityManager 的代码,请检查如何使用 JpaRepository

User user1 = usersRepository.findById("jeremy"); 
Thread.sleep(5000);
entityManager.refresh(user1);    
User user2 = usersRepository.findById("jeremy");

这是我的测试用例的日志,请检查 SQL 查询:

  • 第一次读取操作完成。线程正在等待超时。

Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

  • 触发对 Bob 的更新,它选择并更新记录。

Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

Hibernate: update person set city=?, name=? where id=?

  • 现在线程从休眠中醒来,触发第二次读取。我看不到任何触发的数据库查询,即第二次读取来自缓存。

第二个可能的问题是 /update-user 端点处理程序逻辑。您正在更改用户名但没有将其保留回来,仅调用 setter 方法不会更新数据库。因此,当其他端点的 Thread 醒来时,它会打印 Jeremy.

因此改名后需要调用userRepository.saveAndFlush(user)

@GetMapping("/update-user")
@ResponseStatus(HttpStatus.OK)
@Transactional(isolation = Isolation.READ_COMMITTED)
public User changeName() {
    User user = usersRepository.findById("jeremy"); 
    user.setFirstName("Bob");
    userRepository.saveAndFlush(user); // call saveAndFlush
    return user;
}

此外,您需要检查数据库是否支持所需的隔离级别。可以参考H2 Transaction Isolation Levels