暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

拼多多二面:高并发场景扣减商品库存如何防止超卖?

码哥跳动 2025-03-11
11

大家好,我是《Redis 高手心法》作者,可以叫我靓仔。

相信大家都参与过某某电商的抢购活动,那么大家有没有思考过,在高并发场景下,如何防止商品超卖?这里需要注意哪些问题?

下面,让我们来一步步看下。

首先,我们先看下正常的下单流程(简易版)。

那么,针对库存扣减的场景,要如何实现呢

进入正文前,介绍下我的点击查看详细介绍 -> Java 面试高手心法 58 讲专栏内容涵盖 Java 基础、Java 高级进阶、Redis、MySQL、消息中间件、微服务架构设计等面试必考点、面试高频点。

丢掉你收藏的那些所谓的「面试宝典」,因为它们大多数深度不够,甚至内容还有错误,你只会看完就忘,还浪费时间。这也是为何每次面试你都回答不好的原因,找不到好工作的原因。

正文开始......

数据库扣减

我们先来看下通过数据库方式去实现。

因为要防止它超卖,所以要先把库存锁住,避免库存还剩最后一个时,多个线程同时去扣减成负数了。

但是这种方式显而易见效率非常低下,因为这里加的悲观锁,读请求也被阻塞了,我们知道大部分场景下都是读多写少,所以如何优化呢?

很快小白想到了,可以通过乐观锁的方式实现。

乐观锁:事务不会在读取数据时加锁,而是继续执行后续操作,只有在提交数据时才会检查数据是否已经被其他事务修改,通常通过 版本号或时间戳来实现。

我们可以给库存这条记录加一个版本号字段 version,在更新库存时判断版本号是否一致,这样也不会阻塞读请求。

UPDATE product_inventory
SET stock = stock - :quantity,
    version = version + 1
WHERE product_id = :productId
  AND version = :version;

这种方式能满足一般场景,但是假设在高并发的抢购活动下,当你压测时发现 TPS 怎么也提不上来。

高并发场景下使用乐观锁,一是其他请求拿不到版本号导致线程一直自旋等待中,甚至会降低系统的性能。二是数据库的性能瓶颈。

这时,你在想有没有其他更好的方式呢?

Redis 扣减

既然数据库无法满足高并发性能,我们知道 Redis 单节点理论能支持几万级 TPS,而且我们还可以部署集群多节点,这样肯定能满足了吧。

Redis 如何实现库存扣减呢?

很快,你想到了,Redis 不是有一个 INCRBY 的命令吗?可以通过这个实现呀。

INCRBY product:1001:stock -10

但很快,测试时你又发现了问题,在场景下,这个库存会被扣成负数,这显然是不能接受的。

那再加上锁不就好了吗,因为是是节点操作,我们想到通过加分布式锁的方式。

同一时刻只有一个线程能获取到锁去执行扣减,这样肯定不会超卖了,但这种方式因为只有一个线程能去扣减这个商品的库存,显然并发性能还有待提升。

我们可以不加锁吗?但判断库存是否大于 0 和扣减库存是两个指令,如何保证一致性呢?

Redis Lua 扣减

Lua:Redis 支持在服务器端执行 Lua 脚本时,脚本的所有操作都是原子执行的,即脚本中的所有命令要么全部成功,要么全部失败。

我们可以通过 Lua 的原子性来实现,避免加锁。

先获取当前库存,判断是否足够,如果足够再进行扣减。


local stock = redis.call('get', KEYS[1])  -- 获取当前库存
if not stock then
    return nil  -- 如果没有找到库存,返回nil
end

if tonumber(stock) >= tonumber(ARGV[1]) then  -- 如果库存足够
    redis.call('decrby', KEYS[1], ARGV[1])  -- 扣减库存
    return tonumber(stock) - tonumber(ARGV[1])  -- 返回扣减后的库存
else
    return nil  -- 库存不足,返回nil
end


如果这时老板看商品卖的很好,要后台调增库存怎么办?

如果要调增库存,为了防止多个线程同时调整库存出现并发问题,这里要加分布式锁,可以通过 SETNX 实现。

/**
     * 增加库存,使用分布式锁确保并发安全
     * @param productId 商品ID
     * @param quantity 增加的数量
     * @param lockValue 锁的值,用于解锁时进行验证
     * @param lockTimeout 锁的超时时间
     * @return 是否成功增加库存
     */

    public boolean increaseInventoryWithLock(String productId, int quantity, String lockValue, int lockTimeout) {
        try (Jedis jedis = jedisPool.getResource()) {
            // 获取分布式锁
            String lockKey = "product_lock:" + productId;
            boolean lockAcquired = acquireLock(jedis, lockKey, lockValue, lockTimeout);
            if (lockAcquired) {
                try {
                    // 增加库存
                    jedis.incrBy("product:" + productId + ":stock", quantity);
                    return true;
                } finally {
                    // 释放锁
                    releaseLock(jedis, lockKey, lockValue);
                }
            } else {
                // 如果获取不到锁,可以返回 false 或进行重试等操作
                return false;
            }
        }
    }

这样,你想应该就万无一失了吧。

但是,如果你的商品卖得非常好,Redis 单节点也扛不住了,针对这种热点商品怎么办呢?

Redis 库存分片

莫慌,别忘了我们 Redis 是多节点集群部署的,我们如果把这个热点商品库存拆分到每个节点上不就解决了吗。

怎么拆分呢?

假设我们 Redis 有 12 个节点,我们可以把商品库存缓存 Key 再加个后缀 0,1,2....12 分布到每一个节点上,扣减时如果发现当前节点没库存了,再扣除下个缓存 key。

当然,如果每次都从节点 1 开始,热点问题并没有解决,我们可以设置一个随机数组把顺序打散,比如[1,2,......,12],[2,12......,1]。

这样避免了该热点商品的所有请求都打到同一个节点上的问题了。

最后,也向大家介绍下我的新书《Redis 高手心法》本书基于 Redis 7.0 版本,复杂的概念与实际案例相结合,以简洁、诙谐、幽默的方式揭示了Redis的精髓。


从 Redis 的第一人称视角出发,拟人故事化方式和诙谐幽默的言语与各路“神仙”对话,配合 158 张图,由浅入深循序渐进的讲解 Redis 的数据结构实现原理、开发技巧、运维技术和高阶使用,让人轻松愉快地学习。




关注我能学到什么?

我不仅提供高质量的编程知识,还会结合实际项目和常见面试问题,带给你全面的学习路径。如果你正在学习编程、准备面试,或者在工作中想要提升技能,这里绝对是你提升自我的好帮手。

怎么关注我的公众号?

非常简单!扫描下方二维码即可一键关注。

也可以点击下方卡片关注


另外,码哥最近新创建了一个技术交流群,大家如果在阅读的过程中有遇到问题或者有不理解的地方,欢迎大家加群询问或者评论区询问,我能解决的都尽可能给大家回复。

扫一扫码哥的个人微信,备注「加群」即可。



往期推荐



云原生时代的JVM调优:从被K8s暴打到优雅躺平

高并发系统必看!G1如何让亿级JVM吞吐量提升300%?

性能提升300%!JVM分配优化三板斧,JVM 的内存区域划分、对象内存布局、百万 QPS 优化实践

从 12s 到 200ms,MySQL 两千万订单数据 6 种深度分页优化全解析

你真的懂 Redis 哨兵集群吗?一主二从三哨兵架构如何扛住百万级并发?

找工作三个月面试 20 家,一份offer都没收到,怎么办?



文章转载自码哥跳动,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论