【问题标题】:Spring JPA: Locking parent row when inserting one to many child recordSpring JPA:插入一对多子记录时锁定父行
【发布时间】:2018-07-01 09:45:27
【问题描述】:

我们有两个具有一对多关系的表。当我们跨多个线程(更具体地说是跨多个 REST Web 请求)将多条记录插入到子表中时,由于竞争条件,我们会遇到丢失更新的问题。

我们需要做的是让 JPA 在插入子记录之前识别出实体已在其他地方更新。我尝试使用 @Version 注释方法,但这似乎并没有解决问题,因为更新/插入(我猜......)正在另一个表上发生。我尝试在每次更新时更新的父表上添加一个版本时间戳列,但这似乎也没有奏效。

我认为我真正需要做的是直接获取对 EntityManager 的引用,以便在调用save() 之前在记录上发出lock() 命令。我对 Spring 太陌生了,不知道是否

A)这确实是正确的方法,

B)如果有更好/更简单的方法来完成我们想要完成的工作,以及

C) 如何真正做到这一点。

另外,我知道@OneToMany 注释,但这似乎没有任何作用。

为了简洁起见,我已经截断了下面的代码,我还 created a trimmed down version of the code 演示了这个问题,希望可以更容易地看到我正在尝试做什么。在测试中如果将线程池号更改为 1 可以看到测试通过。

参与类:

@Entity
public class Engagement implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @ElementCollection(fetch = EAGER)
  private List<String> assignedUsers;

  @Version
  private Long version;

  private LocalDateTime updatedOn;

  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }

  public Long getVersion(){return version;}
  public void setVersion(Long version){this.version = version;}

  public LocalDateTime getUpdatedOn(){
    return updatedOn;
  }
  public void setUpdatedOn(LocalDateTime updatedOn) {
    this.updatedOn = updatedOn;
  }

  public List<String> getAssignedUsers() {
    return assignedUsers;
  }
  public void setAssignedUsers(List<String> assignedUsers) {
    this.assignedUsers = assignedUsers;
  }

  public Engagement() {
  }
}

用户类别:

public final class User {
  private final String           name;
  private final String           email;
  private final String           userId;
  private final List<Engagement> engagements;

  @ConstructorProperties({"roles", "name", "email", "userId", "engagements"})
  User(String name, String email, String userId, List<Engagement> engagements) {
    this.name = name;
    this.email = email;
    this.userId = userId;
    this.engagements = engagements;
  }

  public static User.UserBuilder builder() {
    return new User.UserBuilder();
  }

  public String getName() {
    return this.name;
  }

  public String getEmail() {
    return this.email;
  }

  public String getUserId() {
    return this.userId;
  }

  public List<Engagement> getEngagements() {
    return this.engagements;
  }

  public static final class UserBuilder {
    private String           name;
    private String           email;
    private String           userId;
    private List<Engagement> engagements;

    UserBuilder() {
    }

    public User.UserBuilder name(String name) {
      this.name = name;
      return this;
    }

    public User.UserBuilder email(String email) {
      this.email = email;
      return this;
    }

    public User.UserBuilder userId(String userId) {
      this.userId = userId;
      return this;
    }

    public User.UserBuilder engagements(List<Engagement> engagements) {
      this.engagements = engagements;
      return this;
    }

    public User build() {
      return new User(this.name, this.email, this.userId, this.engagements);
    }

    public String toString() {
      return "User.UserBuilder(name=" + this.name + ", email=" + this.email + ", userId=" + this.userId + ", engagements=" + this.engagements + ")";
    }
  }
}

线程测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class EngagementTest {
  @Mock
  UsersAuthService usersService;

  @Autowired
  EngagementsRepository engagementsRepository;

  UsersAuthService authService;

  @Before
  public void init() {
    MockitoAnnotations.initMocks(this);
    authService = new UsersAuthServiceImpl(usersService, engagementsRepository);
  }

  @Test
  public void addingMultipleUsersAtOnceSucceeds() throws InterruptedException {
    Long engagementId = 1L;
    String userId1 = "user1";
    String userId2 = "user2";
    String userId3 = "user3";
    String userId4 = "user4";
    String userId5 = "user5";
    String auth = "asdf";
    User adminUser = User.builder()
                         .userId("adminUser")
                         .email("user@user.com")
                         .name("Admin User")
                         .build();
    Engagement engagement = new Engagement();
    engagement.setAssignedUsers(new ArrayList<>());
    engagement.getAssignedUsers().add(adminUser.getUserId());

    engagementsRepository.save(engagement);

    ExecutorService executorService = Executors.newFixedThreadPool(5);//change this to 1 to see the test pass
    List<Callable<Engagement>> callableList = Arrays.asList(
        addUserThread(engagementId, userId1, auth, adminUser),
        addUserThread(engagementId, userId2, auth, adminUser),
        addUserThread(engagementId, userId3, auth, adminUser),
        addUserThread(engagementId, userId4, auth, adminUser),
        addUserThread(engagementId, userId5, auth, adminUser));

    executorService.invokeAll(callableList);

    Engagement after = engagementsRepository.findById(engagementId);
    assertEquals(6, after.getAssignedUsers().size());
  }

  private Callable<Engagement> addUserThread(Long engagementId, String userId1, String auth, User adminUser) {
    return () -> authService.addUserTo(engagementId, userId1, auth, adminUser);
  }
}

【问题讨论】:

    标签: spring jpa spring-boot spring-data spring-data-jpa


    【解决方案1】:

    我认为我真正需要做的是直接获取对 EntityManager 的引用,以便在调用 save() 之前对记录发出 lock() 命令。我对 Spring 太陌生了,不知道是否

    A) 这确实是正确的方法,

    如果我理解正确,您基本上是想强制实体上的版本递增,这样如果多个线程执行此操作就会失败。

    您确实可以通过使用LockModeType.PESSIMISTIC_FORCE_INCREMENTLockModeType.OPTIMISTIC_FORCE_INCREMENT 锁定相关实体来实现。

    B) 如果有更好/更简单的方法来完成我们想要完成的工作,以及

    C) 如何实际做到这一点。

    使用 Spring Data 可能最好的方法是在用于加载实体的方法上使用 @Lock 注释。

    【讨论】:

      【解决方案2】:

      这里发生的情况是,您提交回调以供执行,但在检查结果之前从未真正等待它们完成。您需要使用List&lt;Future&lt;Engagement&gt;&gt; 实际等待结果完成后再继续。

      这样的事情可以解决问题:

      executorService.invokeAll(callableList).forEach(it -> {
        try {
          it.get(500, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
          e.printStackTrace();
        }
      });
      

      请注意,这不是处理异常情况的正确方法,但它会导致代码等待完成。如果你有这个,你会看到线程正确地拒绝了带有ObjectOptimisticLockingFailureException的一些更新:

      java.util.concurrent.ExecutionException: org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.example.racecondition.engagement.Engagement] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.example.racecondition.engagement.Engagement#1]
        at java.util.concurrent.FutureTask.report(FutureTask.java:122)
        at java.util.concurrent.FutureTask.get(FutureTask.java:206)
        at com.example.racecondition.EngagementTest.lambda$0(EngagementTest.java:68)
        at java.util.ArrayList.forEach(ArrayList.java:1257)
        at com.example.racecondition.EngagementTest.addingMultipleUsersAtOnceSucceeds(EngagementTest.java:66)
      

      除此之外,测试用例的奇怪之处在于UsersAuthServiceImpl 带有@Transactional,但测试用例手动实例化了该类,因此已经没有事务代理。这会导致从addToUser(…) 中对findById(…)save(…) 的调用在两个事务中运行。调整不会改变输出。

      【讨论】:

      • 感谢您的回复,但这不是我要解决的问题。测试用例不是问题,它只是一个穷人试图将多个用户发出的 REST 调用复制到调用示例应用程序中代码的控制器。当客户端进行此调用时,也会发生丢失更新行为。
      • 我更新了 repo 中的代码,以便自动装配 UsersAuthServiceImpl。没有变化。
      • 正如我所写,设置很奇怪,但是“调整并不会改变输出。”我想您需要提出一个新问题,然后描述实际问题、您看到的行为和您期望的行为。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-04
      • 2021-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多