面试官问我:库存预扣减之后,用户订单超时之后怎么补偿库存?我的方案让他满意...
引言
在后端开发和面试中,如果是做类似秒杀和商城等业务时,我们难免会遇到关于订单超时了,但是Redis里面的库存已经预扣减了,那么如何把预扣减的库存进行补偿呢?下面和大家讨论几种比较常见常用的方案。
为什么要补偿预扣减的库存?😵
古人常说,要做一件事之前,先要清楚为什么要做这件事,再去想解决这件事的方案。我认为这种说法也是对的,只有先清楚为什么要补偿预扣减的库存,才能获得解决方案的灵感。
在一个秒杀业务中,大家一起来抢购这种商品,但是商品数额有限。一位用户率先抢到这个商品,已经成功下单(即已经有了订单号),但是尚未结款,也就代表数据库还没有真正落库扣减这个商品,只是在Redis进行预扣减了这件库存。该用户在后来的一段时间因为某事忘记了付款,即订单超时,那么我们就要把这个Redis中预扣减的库存补偿回Redis中,否则就会造成后来的用户无法继续抢购,也就是少卖现象,会给商家利益造成损失。
方案一:Redis的过期监听🤤
在redis里面中,key可以设过期时间,这就给我们提供了一个思路:我们可以给预扣减的库存键设一个过期时间,使用redis的发布订阅机制来监听键过期事件,从而自动触发回增库存的逻辑
功能目标:
- 监听 Redis 中所有以
product:
开头的商品 key 的过期事件。 - 一旦某个商品 key 过期(比如用户超时未支付),自动执行库存回补(
INCR
)操作。
1 |
|
这个方案的优点是实时性强,对要求实时性高的业务比较友好,如抢票等。缺点是过于依靠Redis,风险不能分摊。且容易造成大量过期Key问题,影响性能。
方案二:Redis的延时队列😉
我们可以基于Redis的Sorted Set数据类型来做一个延时队列,通过score来进行排序,我们服务端下单时在当前时间加上超时时间,将订单ID放入进去,通过定时任务来定量拿取超时订单进行取消订单+补偿库存。
添加订单到延时队列
- 当用户下单但未支付时,计算出订单的超时时间(例如:当前时间 + 30 分钟),然后将订单 ID 以这个时间为 score 添加到 Redis Sorted Set 中。
1 |
|
定时任务检查并处理超时订单
定期运行一个后台任务,使用
ZRANGEBYSCORE
获取所有已经到期的订单(即 score 小于等于当前时间戳的所有订单)。对于每个找到的订单,执行取消订单和补偿库存的操作,并从 Sorted Set 中移除这些订单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17long currentTime = System.currentTimeMillis();
Collection<String> expiredOrders = delayedQueue.valueRange(0, true, currentTime, false);
for (String orderId : expiredOrders) {
try {
// 执行取消订单逻辑
cancelOrder(orderId);
// 补偿库存逻辑
restoreStock(orderId);
// 从延迟队列中删除该订单
delayedQueue.remove(orderId);
} catch (Exception e) {
// 异常处理
logger.error("处理超时订单失败: {}", orderId, e);
}
}
这样的方案优点和方案一差不多,实时性好延迟小,但是就是过于依靠Redis,有数据丢失的风险。而且Redis是基于内存的,如果超时订单过多对Redis压力过大,容易挂机。
方案三:MQ的延时消息🤩
我们通过RocketMQ来讲这个例子,用户下单之后,生产者将设置好时间的消息发送给broker,等到规定的消息之后,该消息才对消费者可见,broker发送相应消息给消费者,消费者执行订单取消+库存补偿逻辑即可(消息重试机制保证消费者一定可以消费)
生产者:用户下单后发送延时消息
1 |
|
消费者:监听并处理超时订单
1 |
|
如果你使用的RocketMQ是5.x的版本就可以支持任意时刻的延时消息了,否则就是对应级别的延时消息。
这种方案处理高并发性能好,而且处理消息丢失等方案成熟。缺点就是系统架构更加复杂了,需要处理MQ的其他问题,维护成本变高了。
方案四:分布式定时任务调度框架😈
使用分布式定时任务调度框架,比如xxl-job 或 Quartz是企业里面非常常见的方案。用户下单之后,我们在数据库里面记录订单状态为“待支付”,然后通过xxl-job定期扫描过期的订单,筛选出超时未支付的订单,进行取消订单+补偿库存。
1 |
|
笔者更加推荐xxl-job来实现分布式定时任务,因为它自带可视化界面,兼顾性能与精准,更适合微服务架构下的分布式任务调度
这种方案对实时性支持不是很好,且框架学习成本高。
重复补偿库存问题😗
由于网络延迟或者网络分区,又或者是Redis服务器问题,还有并发问题等,有可能会导致重复补偿库存的情况,那么该怎么解决呢?
1.使用分布式锁
在执行库存补偿之前,尝试获取一个针对特定商品 ID 的分布式锁。只有成功获取锁的进程才能执行补偿操作。这样可以防止多个实例同时处理同一个过期事件。
示例代码片段:
1
2
3
4
5
6
7
8RLock lock = redisson.getLock("lock:product:" + productId);
if (lock.tryLock()) {
try {
// 执行库存补偿逻辑
} finally {
lock.unlock();
}
}
2.状态标记
在补偿库存后,给对应的 key 添加一个特殊的标识(如
compensated
标记)。下次再遇到该 key 过期时,先检查是否存在这个标记。如果存在,则跳过补偿步骤。示例代码片段:
1
2
3
4if (!redissonMap.containsKey(productId + ":compensated")) {
// 执行库存补偿逻辑
redissonMap.put(productId + ":compensated", true);
}
3.延迟队列结合定时任务
刚才方案二就可以解决这个问题,将需要补偿的订单信息放入延迟队列中,并设置适当的延迟时间。通过后台定时任务统一处理这些订单,确保每个订单只被处理一次
4.数据库唯一约束
通过建立补偿库存表,业务和订单id作为唯一索引,每次补偿之前尝试是否可以插入该表,可以则补偿,反之则跳过。
总结❤️
实际业务不一定要按照上面的方案执行,可以根据实际来组合使用(●’◡’●)。如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer