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

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

数据库扣减

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

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

图片图片

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

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

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

我们可以给库存这条记录加一个版本号字段 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]。

图片图片

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

责任编辑:武晓燕 来源: 码哥跳动
相关推荐

2022-09-19 09:49:17

MCube网络引擎

2025-02-26 08:10:40

2024-07-25 09:05:35

2021-08-26 08:24:33

高并发秒杀系统

2024-09-10 10:42:27

2020-09-03 06:33:35

高并发场景分布式锁

2024-07-12 11:28:44

2024-11-27 00:20:32

2024-03-11 15:13:22

数据库高并发

2024-10-22 16:26:11

2021-04-12 08:02:12

分布式锁秒杀高并发

2024-10-17 16:58:43

2024-10-15 10:59:18

Spring MVCJava开发

2025-01-27 00:40:41

2025-02-26 03:00:00

2025-02-28 00:03:22

高并发TPS系统

2021-06-09 18:52:05

方案设计库存数

2023-11-29 20:03:03

2025-03-10 09:20:00

库存异常Redis架构

2024-08-06 08:13:26

点赞
收藏

51CTO技术栈公众号