Spring JPA:在插入一个到多个子记录时锁定父行

时间:2018-01-22 17:24:44

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

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

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

我认为我真正需要做的是直接获取对EntityManager的引用,这样我就可以在调用lock()之前在记录上发出save()命令。我对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);
  }
}

2 个答案:

答案 0 :(得分:0)

这里发生的是你提交回调以便执行但在检查结果之前从未真正等待完成。在继续之前,您需要使用List<Future<Engagement>>实际等待结果完成。

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

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,但测试用例手动实例化该类,因此没有事务代理到位已经。这会导致findById(…)save(…)addToUser(…)的调用在两个事务中运行。然而,调整并没有改变输出。

答案 1 :(得分:0)

  

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

     

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

如果我理解正确,你想基本上强制一个实体的版本增量,这样如果多个线程做了那个就失败了。

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

  

B)如果有更好/更简单的方法来完成我们想要完成的任务,

     

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

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