简体   繁体   中英

Spring Boot - Concurrency Control with Optimistic Lock

I've been developing an application in Spring Boot using Spring Web, Spring Data JPA and MySQL where an Item is up for sale and multiple Users can bid on it. Each Item might have an E-bay like Buy-Now price with the same functionality.

Soon I realised that some concurrency issues might come up like a User out bidding another on an Item or 2 Users bidding the Buy-Now price of an Item at the same time. After some research I came across the concept of Optimistic Lock and I tried to implement a solution that follows this pattern:

Entities:

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:

@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");
    }
}

Repository:

@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);

Logic:

  1. In Service Layer store the conditions X under which an Item was retrieved
  2. In Repository Layer in both update methods attempt to update the Item that meets the conditions X under the assumption that the highestBid field might have increased and under the restriction that the amount offered is strictly higher than the current highestBid.

Questions:

  1. Is my code actually concurrency - safe?
  2. Is there a simpler way to achieve the same result?
  1. I don't know if your code is concurrency-safe, maybe, but you could test it to be sure

  2. You're using JPA but you're ignoring a lot of features it offers you. You can configure an optimistick-locking strategy on the entities you want to protect, and you will not need to re-implement it yourself like you're doing. It will simplify all your SQL queries because JPA will take care of stale entities and will throw an exception if the entity was updated in another transaction.

    Moreover, you can execute your business method without relying on SQL since it only a simple entity mutation:

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

    The entity state will be flushed at the end of your transaction.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM