写点什么

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

作者:Geek_e3e86e
  • 2025-03-11
    湖南
  • 本文字数:2301 字

    阅读完需:约 8 分钟

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

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

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

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


数据库扣减

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

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

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

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

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

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

UPDATE product_inventorySET stock = stock - :quantity,    version = version + 1WHERE 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  -- 如果没有找到库存,返回nilend
if tonumber(stock) >= tonumber(ARGV[1]) then  -- 如果库存足够    redis.call('decrby', KEYS[1], ARGV[1])  -- 扣减库存    return tonumber(stock) - tonumber(ARGV[1])  -- 返回扣减后的库存else    return nil  -- 库存不足,返回nilend

复制代码

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

如果要调增库存,为了防止多个线程同时调整库存出现并发问题,这里要加分布式锁,可以通过 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]。

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

用户头像

Geek_e3e86e

关注

还未添加个人签名 2025-03-07 加入

还未添加个人简介

评论

发布
暂无评论
拼多多二面:高并发场景扣减商品库存如何防止超卖?_Java_Geek_e3e86e_InfoQ写作社区