【问题标题】:Spring Data JPA : A transaction with isolation READ_COMMITTED does not see data committed in another transactionSpring Data JPA:具有隔离 READ_COMMITTED 的事务看不到另一个事务中提交的数据
【发布时间】:2019-05-01 00:37:45
【问题描述】:

所以,我有一个非常简单的基于 Spring Boot 的 Web 应用程序。

数据库有一个表user,列idusername和一个记录:(1, 'Joe')

我还有以下课程:

User - 映射到表user的实体

UserRepository - 一个 Spring Data JPA 存储库

UserService + DefaultUserService - 具有 CRUD 方法的服务层

UserController - 具有两种方法的控制器:getupdate

Application - 主类(带有@EnableTransactionManagement注解 就可以了)

所以,我要做的是测试事务隔离级别READ_COMMITTED。我同时发送两个请求:

  1. /update,它更新用户,将其名称设置为Jack,然后将当前线程休眠5s,然后提交事务。
  2. /get,它会重复读取同一个用户10次,每次尝试后都会小睡1秒。

问题在于,即使 (1) 的事务已提交,(2) 仍会继续返回旧值 - Joe。如果我尝试在此之后向/get 发送另一个请求,它会按预期返回Jack,因此只有在事务(2) 在事务(1) 提交对数据库的更改之前开始时才会出现问题。

您可以在下面找到一些代码供参考。

服务:

@Service
public class DefaultUserService implements UserService {

    ... //fields & constructor

    @Override
    @SneakyThrows
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public User read(Long id) {
        User user = userRepository.findById(id).get();
        Thread.sleep(1000);
        return user;
    }

    @Override
    @SneakyThrows
    @Transactional
    public User update(Long id, User update) {
        log.info("Entering update method for user {}", id);
        User user = read(id);

        user.setUsername("Jack");
        user = userRepository.save(user);

        log.info("User {} updated, falling asleep for 5s", id);
        Thread.sleep(5000);

        return user;
    }
}

控制器:

@RestController
public class UserController {

    ... //fields & constructor

    @RequestMapping("/update")
    public User update() {
        User user = userService.update(1L, new User("Jack"));
        log.info("UPDATED: {}", user);
        return user;
    }

    @RequestMapping("/get")
    public User get() {
        User user = userService.read(1L);
        for (int i = 0; i < 10; i++) {
            log.info("READ: {}", user);
            user = userService.read(1L);
        }
        return user;
    }
}    

日志输出:

04:37:52.915 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:53.151 [nio-8781-exec-1] Entering update method for user 1
04:37:53.919 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:54.152 [nio-8781-exec-1] User 1 updated, falling asleep for 5s
04:37:54.922 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:55.926 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:56.932 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:57.937 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:58.943 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:59.222 [nio-8781-exec-1] UPDATED: ID: 1 :: Jack
04:37:59.947 [io-8781-exec-10] READ: ID: 1 :: Joe
04:38:00.950 [io-8781-exec-10] READ: ID: 1 :: Joe
04:38:01.956 [io-8781-exec-10] READ: ID: 1 :: Joe

控制器返回的响应也不同。 /update 是:

{"id":1,"username":"Jack"}

/get

{"id":1,"username":"Joe"}

我正在使用 MySQL 5.7.18 和 Spring Boot 2.1.0。

关于我可能做错/遗漏的事情有什么想法吗? 提前致谢。

【问题讨论】:

  • 好问题。我想知道它是否可能与 Spring Boot 中默认启用的 OpenEntityMangerInViewFilter 有关。您可以设置以下启动属性并重试 spring.jpa.open-in-view=false。进一步参见 stackoverflow.com/q/30549489/1356423 和 static.javadoc.io/org.springframework/spring-orm/4.0.1.RELEASE/…
  • 从下面的 cmets 中,您假设您的问题出在数据库级别而不是 JPA 级别。启用 SQL 日志记录将是确定问题所在的良好开端。 stackoverflow.com/a/19299769/1356423
  • 非常感谢禁用视图中打开的想法,我一直怀疑控制器开始出现问题。我能够使 REPEATABLE_READ 按预期工作。但是,现在我遇到了另一个问题:我不能强制读取事务来确保可重复读取。因此,当我为它设置 REPEATABLE_READ 甚至 SERIALIZABLE 隔离时,它只会返回“Joe”,直到第二个事务提交,然后它开始返回“Jack”(即使我没有离开那个隔离事务的范围!)
  • @AlanHay 首先,感谢这个精彩的日志库,它很棒。它向我展示了每次findById 方法在返回值后被调用并提交时都会打开一个新事务。我知道 CrudRepository 方法默认是事务性的,但是没有定义传播,所以它应该使用默认的 - REQUIRED,我猜?无论如何,即使在使用 @Transactional(propagation = Propagation.SUPPORTS) 注释存储库方法之后(以确保没有创建新的 trx),我仍然无法获得有效的可重复读取场景

标签: java spring hibernate transactions isolation-level


【解决方案1】:

您的代码在Oracle 下可以正常工作,但在MySQL 下它有点不同。在此处阅读有关 MySql 中的隔离级别的信息

https://blog.pythian.com/understanding-mysql-isolation-levels-repeatable-read/

我相信,如果您将方法的 isolation level 更改为 Read committed,它应该可以正常工作,因为默认情况下 MySql 具有 Repeatable-read,但处理方式有所不同。

   @Transactional(Isolation.READ_COMMITTED)
    public User update(Long id, User update) 

【讨论】:

  • 感谢分享文章。这让我考虑迁移到另一个具有更可预测的隔离机制的 DBMS。另外,我尝试将隔离规则应用于更新方法而不是获取但没有效果,结果是一样的
  • 我的意思是你应该为这两种方法保留Isolation.READ_COMMITTED。它应该可以工作
  • 不幸的是,它没有
【解决方案2】:

设置:

@Override
@SneakyThrows
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public User read(Long id) {
    User user = userRepository.findById(id).get();
    Thread.sleep(1000);
    return user;
}

【讨论】:

  • 你想解决什么问题?请添加更多信息,而不是在这里扔一堆快乐的代码行。
猜你喜欢
  • 2021-08-28
  • 2018-05-04
  • 2016-09-11
  • 2015-12-17
  • 1970-01-01
  • 2017-06-17
  • 1970-01-01
  • 2015-05-04
  • 2016-10-23
相关资源
最近更新 更多