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);
逻辑:
- 在服务层中存储检索项目的条件 X
- 在 Repository Layer 中,两种更新方法都尝试在 假设 highestBid 字段 可能 下更新满足条件 X 的项目已经增加并且在 限制 下提供的金额严格高于当前最高出价。
问题:
- 我的代码实际上并发安全吗?
- 是否有更简单的方法来达到相同的结果?
我不知道你的代码是不是concurrency-safe,也许吧,但你可以测试一下以确保
您正在使用 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)
}
实体状态将在您的事务结束时刷新。
我一直在使用 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);
逻辑:
- 在服务层中存储检索项目的条件 X
- 在 Repository Layer 中,两种更新方法都尝试在 假设 highestBid 字段 可能 下更新满足条件 X 的项目已经增加并且在 限制 下提供的金额严格高于当前最高出价。
问题:
- 我的代码实际上并发安全吗?
- 是否有更简单的方法来达到相同的结果?
我不知道你的代码是不是concurrency-safe,也许吧,但你可以测试一下以确保
您正在使用 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) }
实体状态将在您的事务结束时刷新。