ヾ( ̄▽ ̄) Hi,欢迎来到『业务杂谈』专栏!不定期收录业务逻辑相关内容,期待您的关注
☕ 前言:面试必问 - 秒杀怎么解决库存超卖?
『库存超卖』是一个比较常见的问题,并且这个问题很容易出现在秒杀系统中,如果做个电商相关的项目,这个问题基本是面试必问的问题。
秒杀场景之所以容易出现库存超卖的情况,是因为在秒杀活动中,商品数量通常很少,而参与的用户却非常多,用户同时抢购的压力非常大。由于实时更新库存需要花费一定的时间,因此,在短时间内可能会存在多个用户同时购买同一件商品的情况,从而导致库存不足的情况。
解决库存超卖的问题采取很多种方案,例如:可以通过限流措施来控制并发量和请求频率;也可以在下单时使用分布式锁等机制来保证只有一个用户能够成功下单;此外,还可以对订单进行逐一审核,以便及时发现并处理超卖情况…
一种比较流行的解决库存超卖问题的办法就是『预扣库存』,实现起来相对简单。其基本思想是在销售订单被创建时,系统会检查库存数量是否足够满足该订单,并将相应的库存数量从可用库存中预扣除,然后将该订单存储到待处理订单列表中。
步骤如下:
检查库存:当一个客户提交一个订单时,系统首先检查库存以决定是否可以完成该订单。如果库存数量不足则无法完成订单。
预扣库存:如果库存数量足够,则系统会将相应的库存数量从可用库存中预扣除。这样做可以避免出现库存超卖的情况。
创建订单:系统会将该订单存储到待处理订单列表中。此时,该订单的状态为待处理状态。
完成订单:如果有足够的库存,系统将准备好订单并将其标记为已完成状态。此时,已经完成的订单将从待处理订单列表中移除,并且库存数量将更新以反映这次销售操作。
取消订单:如果需要取消订单,该订单的预扣库存会被释放回可用库存中。这样可以确保已取消订单的库存不会被浪费或错误地计算进库存数量。
@Service
public class OrderService {
@Autowired
private ProductRepository productRepo;
// 创建订单方法,返回已创建订单的信息
public Order createOrder(OrderRequest orderReq) {
// 检查库存是否足够
Product product = productRepo.findProductById(orderReq.getProductId());
if (product.getStock() < orderReq.getQuantity()) {
throw new RuntimeException("库存不足");
}
// 预扣库存
int reservedStock = product.getReservedStock();
product.setReservedStock(reservedStock + orderReq.getQuantity());
productRepo.save(product);
// 创建订单
Order order = new Order();
order.setProductId(orderReq.getProductId());
order.setQuantity(orderReq.getQuantity());
order.setStatus(Order.Status.PENDING);
order.setCreatedAt(new Date());
orderRepo.save(order);
return order;
}
// 完成订单方法,返回已完成订单的信息
public Order completeOrder(Long orderId) {
Order order = orderRepo.findById(orderId);
if (order == null || order.getStatus() != Order.Status.PENDING) {
throw new RuntimeException("无效订单");
}
// 更新库存
Product product = productRepo.findProductById(order.getProductId());
int reservedStock = product.getReservedStock();
product.setReservedStock(reservedStock - order.getQuantity());
product.setStock(product.getStock() - order.getQuantity());
productRepo.save(product);
// 更新订单状态为已完成
order.setStatus(Order.Status.COMPLETED);
order.setCompletedAt(new Date());
orderRepo.save(order);
return order;
}
// 取消订单方法,返回已取消订单的信息
public Order cancelOrder(Long orderId) {
Order order = orderRepo.findById(orderId);
if (order == null || order.getStatus() != Order.Status.PENDING) {
throw new RuntimeException("无效订单");
}
// 释放预扣库存
Product product = productRepo.findProductById(order.getProductId());
int reservedStock = product.getReservedStock();
product.setReservedStock(reservedStock - order.getQuantity());
productRepo.save(product);
// 更新订单状态为已取消
order.setStatus(Order.Status.CANCELLED);
order.setCancelledAt(new Date());
orderRepo.save(order);
return order;
}
}
排队系统:当用户请求访问或购买某个商品或服务时,将其加入一个队列中,并按照先来先服务的原则依次处理。
限制访问频率:限制用户访问或购买某个商品或服务的频率,例如设定每分钟或每小时只允许用户进行一定数量的交易。
购买配额:将商品或服务的总供应量平均分配给每个用户,每个用户只能购买一定数量的商品或服务。
预约系统:让用户提前预约需要购买的商品或服务,并安排合理的发货时间,以避免库存超卖和物流拥堵。
当用户下单后,先将其订单状态设置为待审核,同时锁定对应商品的库存。这样可以避免多个用户同时购买同一件商品而导致库存超卖。
审核订单时,需要检查用户下单的商品数量是否超过了实际库存数量。如果超卖,就需要取消订单并释放已经锁定的库存。
如果审核通过,就将订单状态修改为已支付,并扣除用户对应的账户余额。同时,需要将实际库存数量减去用户下单的商品数量,并记录交易明细。
如果审核不通过,就需要取消订单,并释放已经锁定的库存。如果是因为用户恶意下单导致审核不通过,可以考虑对该用户采取封禁等措施。
进行订单审核时,可以设置一个时间阈值,如果在规定时间内没有审核通过,系统将自动取消订单并释放库存。这样可以避免订单长时间处于待审核状态而导致商品无法及时出售的情况。
当用户下单时,先尝试获取对应商品的分布式锁。如果锁已经被其他线程或者进程持有,则表示该商品正在被处理,此时需要等待一段时间后再次尝试获取锁。
获取到锁之后,在进行订单状态修改和扣除库存数量操作前,需要再次检查当前商品库存是否充足,以避免并发情况下库存超卖的问题。
进行完订单状态修改和扣除库存数量操作后,需要释放对应商品的分布式锁,以便其他用户可以继续购买该商品。
如果在加锁期间出现异常,例如网络中断或系统崩溃等情况,需要及时释放分布式锁,避免锁资源被一直占用而导致系统崩溃或无法继续处理其他用户请求。