大家好,我是《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 的数据结构实现原理、开发技巧、运维技术和高阶使用,让人轻松愉快地学习。
关注我能学到什么?
我不仅提供高质量的编程知识,还会结合实际项目和常见面试问题,带给你全面的学习路径。如果你正在学习编程、准备面试,或者在工作中想要提升技能,这里绝对是你提升自我的好帮手。
怎么关注我的公众号?
非常简单!扫描下方二维码即可一键关注。
也可以点击下方卡片关注
另外,码哥最近新创建了一个技术交流群,大家如果在阅读的过程中有遇到问题或者有不理解的地方,欢迎大家加群询问或者评论区询问,我能解决的都尽可能给大家回复。
扫一扫码哥的个人微信,备注「加群」即可。
往期推荐