糟糕,线上库存竟然变成负500。。。

数据库关系型数据库NoSQL数据库

picture.image

苏三的免费八股文网站:www.susan.net.cn

大家好,我是苏三,又跟大家见面了。

前言

"快看我们的秒杀系统!库存显示-500了!"

3年前的这个电话让我记忆犹新。

当时某电商大促,我们自认为完美的分布式架构,在0点整瞬间被击穿。

数据库连接池耗尽,库存表出现负数,客服电话被打爆...

今天这篇文章跟大家一起聊聊商品超卖的问题,希望对你会有所帮助。

1 为什么会发生超卖?

首先我们一起看看为什么会发送超卖?

1.1 数据库的"最后防线"漏洞

我们用下面的列子,给大家介绍一下商品超卖是如何发生的。

  
public boolean buy(int goodsId) {  
    // 1. 查询库存  
    int stock = getStockFromDatabase(goodsId);  
    if (stock > 0) {  
        // 2. 扣减库存  
        updateStock(goodsId, stock - 1);  
        return true;  
    }  
    return false;  
}  

在并发场景下可能变成下图这样的:

picture.image

请求1和请求2都将库存更新成9。

根本原因 :数据库的查询和更新操作,不是原子性校验,多个事务可能同时通过stock>0的条件检查。

1.2 超卖的本质

商品超卖的本质是 :多个请求同时穿透缓存,同一时刻读取到相同库存值,最终在数据库层发生覆盖。

就像100个人同时看上一件衣服,都去试衣间前看了眼牌子,出来时都觉得自己应该拿到那件衣服。

2 防止超卖的方案

2.1 数据库乐观锁

数据库乐观锁的核心原理是通过版本号控制并发。

例如下面这样的:

  
UPDATE product   
SET stock = stock -1, version=version+1   
WHERE id=123 AND version=#{currentVersion};  

Java的实现代码如下:

  
@Transactional  
public boolean deductStock(Long productId) {  
    Product product = productDao.selectForUpdate(productId);  
    if (product.getStock() <= 0) return false;  
      
    int affected = productDao.updateWithVersion(  
        productId,   
        product.getVersion(),  
        product.getStock()-1  
    );  
    return affected > 0;  
}  

基于数据库乐观锁方案的架构图如下:

picture.image

优缺点分析

优点缺点
无需额外中间件
高并发时DB压力大
实现简单
可能出现大量更新失败

适用场景 :日订单量1万以下的中小系统。

2.2 Redis原子操作

Redis原子操作的核心原理是使用:Redis + Lua脚本。

核心代码如下:

  
// Lua脚本保证原子性  
String lua = "if redis.call('get', KEYS >= ARGV[1] then " +  
             "return redis.call('decrby', KEYS[1], ARGV " +  
             "else return -1 end";  
  
public boolean preDeduct(String itemId, int count) {  
    RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);  
    Long result = redisTemplate.execute(script,   
        Collections.singletonList(itemId), count);  
    return result != null && result >= 0;  
}  

该方案的架构图如下:

picture.image

性能对比

  • 单节点QPS:数据库方案500 vs Redis方案8万
  • 响应时间:<1ms vs 50ms+

2.3 分布式锁

目前最常用的分布式锁的方案是Redisson。

下面是Redisson的实现:

  
RLock lock = redisson.getLock("stock\_lock:"+productId);  
try {  
    if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {  
        // 执行库存操作  
    }  
} finally {  
    lock.unlock();  
}  

注意事项

  1. 1.锁粒度要细化到商品级别
  2. 2.必须设置等待时间和自动释放
  3. 3.配合异步队列使用效果更佳

该方案的架构图如下:

picture.image

2.4 消息队列削峰

可以使用 RocketMQ的事务消息。

核心代码如下:

  
// RocketMQ事务消息示例  
TransactionMQProducer producer = new TransactionMQProducer("stock\_group");  
producer.setExecutor(new TransactionListener() {  
    @Override  
    public LocalTransactionState executeLocalTransaction(Message msg) {  
        // 扣减数据库库存  
        return LocalTransactionState.COMMIT\_MESSAGE;  
    }  
});  

该方案的架构图如下:

picture.image

技术指标

  • 削峰能力:10万QPS → 2万TPS
  • 订单处理延迟:<1秒(正常时段)

2.5 预扣库存

预扣库存是防止商品超卖的终极方案。

核心算法如下:

  
// Guava RateLimiter限流  
RateLimiter limiter = RateLimiter.create(1000); // 每秒1000个令牌  
  
public boolean preDeduct(Long itemId) {  
    if (!limiter.tryAcquire()) return false;  
      
    // 写入预扣库存表  
    preStockDao.insert(itemId, userId);  
    return true;  
}  

该方案的架构图如下:

picture.image

性能数据

  • 百万级并发支撑能力
  • 库存准确率99.999%
  • 订单处理耗时200ms内

3 避坑指南

3.1 缓存与数据库不一致

某次大促因缓存未及时失效,导致超卖1.2万单。

错误示例如下:

  
// 错误示例:先删缓存再写库  
redisTemplate.delete("stock:"+productId);  
productDao.updateStock(productId, newStock); // 存在并发写入窗口  

3.2 未考虑库存回滚

秒杀取消后,忘记恢复库存,引发后续超卖。

正确做法是使用事务补偿。

例如下面这样的:

  
@Transactional  
public void cancelOrder(Order order) {  
    stockDao.restock(order.getItemId(), order.getCount());  
    orderDao.delete(order.getId());  
}  

库存回滚和订单删除,在同一个事务中。

3.3 锁粒度过大

锁粒度过大,全局限流导致10%的请求被误杀。

错误示例如下:

  
// 错误示例:全局限锁  
RLock globalLock = redisson.getLock("global\_stock\_lock");  

总结

其实在很多大厂中,一般会将防止商品超卖的多种方案组合使用。

架构图如下:picture.image

通过组合使用:

  1. Redis做第一道防线(承受80%流量)
  2. 分布式锁控制核心业务逻辑
  3. 预扣库存+消息队列保证最终一致性

实战经验 :某电商在2023年双11中:

  • Redis集群承载98%请求
  • 分布式锁拦截异常流量
  • 预扣库存保证最终准确性

系统平稳支撑了每秒12万次秒杀请求,0超卖事故发生!

记住:没有银弹方案,只有适合场景的组合拳!

最后欢迎加入苏三的星球,你将获得:AI开发项目课程、苏三AI项目、商城微服务实战、秒杀系统实战、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

扫描下方二维码,即可加入星球:

picture.image

目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1600+小伙伴加入学习。

picture.image

苏三的免费八股文网站:www.susan.net.cn

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
云原生数据库 veDB 核心技术剖析与展望
veDB 是一款分布式数据库,采用了云原生计算存储分离架构。本次演讲将为大家介绍火山引擎这款云原生数据库的核心技术原理,并对未来进行展望。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论