Spring 引导 - 使用乐观锁的并发控制

Spring Boot - Concurrency Control with Optimistic Lock

我一直在使用 Spring Web、Spring Data JPA 和 MySQL 在 Spring Boot 中开发应用程序,其中 Item销售和多个 Users 可以出价。每个 Item 可能 都有类似 Buy-Now 价格且具有相同功能的电子海湾。

很快我意识到可能会出现一些并发问题,例如一个用户对另一个项目出价或 2 个用户同时对一个项目的 Buy-Now 价格出价.经过一些研究,我遇到了乐观锁的概念,并尝试实现一个遵循这种模式的解决方案:

实体:

public class User  {

    @Id
    @Column(name = "username", nullable = false, length = 45)
    private String username;

    @Column(name = "first_name", nullable = false, length = 45)
    private String firstName;

    @Column(name = "last_name", nullable = false, length = 45)
    private String lastName;
}


public class Item {

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

    @Column(name = "end_date", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date endDate;

    @Column(name = "buy_now")
    private Double buyNow;

    @Column(name = "highest_bid", nullable = false)
    private Double highestBid;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "seller_id", nullable = false)
    private User seller;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "buyer_id")
    private User buyer;
}

服务:

@Service
@AllArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;
    private final UserRepository userRepository;

    @Transactional
    public void bid(String username, Long itemID, Double amount) {
        
        Date seenDate = TimeManager.now();
        Optional<Item> optionalItem = this.itemRepository.findNonExpiredById(seenDate, itemID);

        if (optionalItem.isEmpty())
            throw new ItemNotFoundException(itemID);

        Item item = optionalItem.get();

        Double seenHighestBID = item.getHighestBid();

        User bidder = this.userRepository.getById(username);

        if (item.getBuyNow() != null && amount >= item.getBuyNow())
            if (this.itemRepository.updateHighestBidAndBuyerWithOptimisticLock(itemID, seenHighestBID, bidder, amount, seenDate) == 0)
                throw new InvalidBidException("TODO");
        
        else
            if (this.itemRepository.updateHighestBidWithOptimisticLock(itemID, seenHighestBID, amount, seenDate) == 0)
                throw new InvalidBidException("TODO");
    }
}

存储库:

@Transactional
@Query("SELECT i FROM Item a WHERE i.buyer is NULL AND ?1 < a.endDate AND a.ID = ?2")
Optional<Item> findNonExpiredById(Date now, Long itemID);


@Modifying
@Transactional
@Query("UPDATE Item i SET i.highestBid = ?4, a.buyer = ?3 WHERE " +
        "i.ID = ?1 AND " +
        "i.buyer is NULL AND " +
        "?5 < i.endDate AND " +
        "i.highestBid >= ?2 AND " +
        "i.highestBid < ?4")
Long updateHighestBidAndBuyerWithOptimisticLock(Long itemID ,Double seenHighestBID, User bidder,Double amount,Date seenDate);


@Modifying
@Transactional
@Query("UPDATE Item i SET i.highestBid = ?4 WHERE " +
        "i.ID = ?1 AND " +
        "i.buyer is NULL AND " +
        "?4 < i.endDate AND " +
        "i.highestBid >= ?2 AND " +
        "i.highestBid < ?3")
Long updateHighestBidWithOptimisticLock(Long itemID,Double seenHighestBID,Double amount,Date seenDate);

逻辑:

  1. 在服务层中存储检索项目的条件 X
  2. 在 Repository Layer 中,两种更新方法都尝试在 假设 highestBid 字段 可能 下更新满足条件 X 的项目已经增加并且在 限制 下提供的金额严格高于当前最高出价。

问题:

  1. 我的代码实际上并发安全吗?
  2. 是否有更简单的方法来达到相同的结果?
  1. 我不知道你的代码是不是concurrency-safe,也许吧,但你可以测试一下以确保

  2. 您正在使用 JPA,但您忽略了它为您提供的许多功能。您可以配置要保护的 optimistick-locking strategy on the entities,而无需像现在这样自己 re-implement。它将简化您的所有 SQL 查询,因为 JPA 会处理过时的实体,如果该实体在另一个事务中更新,则会抛出异常。

    此外,您可以在不依赖 SQL 的情况下执行您的业务方法,因为它只是一个简单的实体突变:

         if (item.getBuyNow() != null && amount >= item.getBuyNow()) {
             item.setHighestBid(amount)
             item.setBuyer(buyer);
         }
         else {
             item.setHighestBid(amount)
         }
    

    实体状态将在您的事务结束时刷新。