某生鲜平台面试题:如何保证库存在高并发的场景下是安全的?

文摘   2024-11-29 12:04   山西  
天咱们来聊聊一个困扰电商平台的老大难问题:如何在高并发情况下保证库存的安全?
想象一下,你正在做一个大促销,库存刚刚上线100个商品,结果10秒钟内,1000个用户蜂拥而至,想要抢这100个库存,你的后台能撑住吗?
如果没有搞定高并发和库存安全问题,那么在用户下单的过程中,可能会出现超卖、漏单,甚至出现“抢购成功”后,发货时却发现库存已经不足的尴尬局面。

一、问题描述

在电商场景中,我们最常见的库存问题通常是这样的:
  • 商品库存为100个,但用户数量有1000或者更多。
  • 每个用户购买的数量不一样,所以得确保库存既不能多发,也不能少发。
核心目标
  • 不多发,即不会出现库存已经卖完了,但还是有用户能够购买。
  • 不少发,即避免库存数量大于实际商品数量的情况发生。
这些问题背后,其实就是并发控制的难题。让我们从下单流程一步一步分析下如何解决。

二、下单步骤

一个典型的电商下单流程是这样的:
  1. 下单:用户选中商品,提交订单。
  2. 预占库存:系统在用户下单时,就会预占库存,以确保库存不会在支付过程中被其他用户抢走。
  3. 支付:用户完成支付,系统确认支付成功。
  4. 减扣库存:支付成功后,系统才会真正减库存。
  5. 取消订单:用户若取消订单,库存需要回退。
  6. 回退预占库存:若订单未支付,库存需要及时回退。
那么问题来了,如何保证预占库存的安全性和有效性呢?

三、预占库存的时机

常见的预占库存时机有三种,我们来分析一下它们的优缺点:

1. 方案一:加入购物车时预占库存

这种方式看起来很合理,但其实并不适合高并发场景。因为用户添加商品到购物车时,并不意味着他们一定会购买。结果可能是,库存被占用了,但用户最后并没有完成购买,导致库存被浪费。

2. 方案二:下单时预占库存

这种方式比较靠谱。因为用户已经明确表示了购买意图,且下单时就能确认购买数量,可以合理地在下单时就预占库存。如果用户长时间未支付,订单超时自动取消,库存及时回退。

3. 方案三:支付时预占库存

虽然这种方式看起来很“安全”,但实际上存在很大问题。支付过程往往涉及到第三方支付平台,而且并发量非常高。此时预占库存可能导致长时间的等待,降低用户体验,且系统负担重,容易出现超时或死锁等问题。
结论:
方案二,即下单时预占库存,是最合适的选择。这时用户已经有了明确的购买意图,且下单后订单会有时效性,超时未支付的订单会被取消,库存得到回退。

四、重复下单问题

在高并发的环境下,重复下单是另一个需要解决的大问题。这个问题通常会因为以下原因发生:
  1. 用户点击过快,重复提交请求。
  2. 网络延时,用户刷新页面或重复点击。
  3. 网络框架重复请求。
  4. 用户恶意行为,故意发起重复请求。
解决方案:
  • UI拦截:比如在提交订单后,把按钮设置为置灰,避免用户重复点击。
  • Token校验:每次提交订单时生成一个唯一的Token,防止重复提交。可以通过Redis来存储Token,利用自增功能来判断是否已经使用过。
// 创建Token
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("order_token_" + token, token);

// 校验Token有效性
String storedToken = redisTemplate.opsForValue().get("order_token_" + token);
if(storedToken == null || !storedToken.equals(token)) {
    // Token无效,重复提交
    throw new IllegalStateException("订单已提交,不能重复提交");
}
通过这种方式,可以有效避免重复下单的问题,提高系统的稳定性和用户体验。

五、如何安全减扣库存

减扣库存是一个高并发场景下的痛点,因为我们需要保证库存的准确性,避免出现超卖现象。常见的减库存方式有几种,下面我一一给大家拆解:

1. 方法1:不安全的库存操作

比如直接用数据库的 UPDATE 语句来更新库存:
UPDATE product SET stock = stock - 1 WHERE product_id = 100 AND stock > 0;
这种方法的风险在于,如果并发量非常高,就有可能出现库存超卖的问题。因为多个线程可能会同时看到库存为1,结果两个用户都下单了,这时候库存就会出现负数。

2. 方法2:使用乐观锁

乐观锁通过在数据库表中增加版本号或时间戳字段,来避免并发冲突。在减库存时,通过SQL语句校验库存是否充足,然后再进行更新。
UPDATE product SET stock = stock - 1version = version + 1 
WHERE product_id = 100 AND stock > 0 AND version = 1;
这种方式能有效避免超卖问题,但如果并发量非常高,可能会导致一定的性能问题。

3. 方法3:使用Redis分布式锁

Redis分布式锁可以通过锁机制来确保只有一个请求能够成功更新库存。比如在更新库存之前,先通过 SETNX 来尝试获取锁,获取成功的请求才能进行库存操作。
boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent("lock:product:100""locked");
if (lockAcquired) {
    // 执行库存更新操作
    redisTemplate.opsForValue().increment("product:100:stock", -1);
}
这种方式可以减少数据库的压力,但由于涉及到锁机制,可能导致用户体验差,比如等待时间较长。

4. 方法4:使用Redis存储库存信息

使用Redis来存储商品的库存信息,并通过Redis的 INCRBY 原子操作来确保库存的安全:
long stock = redisTemplate.opsForValue().decrement("product:100:stock"1);
if (stock < 0) {
    // 库存不足,回滚操作
    redisTemplate.opsForValue().increment("product:100:stock"1);
    throw new IllegalStateException("库存不足");
}
通过这种方式,我们可以确保库存操作的原子性,且性能比数据库操作要高很多。Redis作为缓存系统,可以很好地支持高并发请求。

六、订单时效与库存回退

最后,咱们说说订单时效与库存回退。订单一旦超时未支付,必须自动取消并回退库存。对于已经支付的订单,如果用户取消,也要回退库存。使用消息队列(如Kafka或RabbitMQ)来处理这些异步操作是比较常见的做法。
// 订单超时未支付
@Scheduled(fixedDelay = 60000)
public void checkOrderTimeout() {
    List<Order> orders = orderRepository.findTimeoutOrders();
    for (Order order : orders) {
        // 取消订单并回退库存
        cancelOrderAndRestoreStock(order);
    }
}

结论

高并发下的库存安全问题,确实需要我们精心设计。通过合理的预占库存时机、有效的重复下单处理、以及合理的库存减扣方式,我们可以保证库存的安全性,不多发、不少发。Redis的应用,也为我们提供了高效的解决方案。
你知道的,当系统流量激增时,所有这些技术点都能有效地保护库存,避免出现超卖或者库存不足的情况,确保用户体验不受影响。
总的来说,解决高并发库存问题,最重要的还是要关注业务流程,结合合适的技术手段,提升系统的稳定性和可靠性。

-END-


ok,今天先说到这,老规矩,给大家分享一份不错的副业资料,感兴趣的同学找我领取。

以上,就是今天的分享了,看完文章记得右下角给何老师点赞,也欢迎在评论区写下你的留言

程序媛山楂
5年+程序媛,专注于AI编程普及。本号主要分享AI编程、Chat账号、Chat教程、Sora教程、Suno AI、Sora账号、Sora提示词、AI换脸、AI绘画等,帮助解决各种AI疑难杂症。
 最新文章