高并发抢券系统如何解决超卖问题?一文带你彻底搞懂!

在双十一、618、秒杀、优惠券发放等电商高峰场景中,**“高并发 + 库存限制”**常常导致后端系统承压严重,甚至出现“超卖”、“重复下单”、“系统雪崩”等问题。

本文以“抢券”业务为例,深入分析如何从架构层面、并发控制、数据库设计等多维度解决“高并发 + 超卖”这一经典难题,内容通俗易懂,适合有一定后端开发经验的读者阅读。


一、抢券业务特点与挑战

1. 抢券业务场景分析

抢券和秒杀业务的典型特征:

  • 并发极高:可能在1秒内涌入几万甚至几十万请求

  • 库存有限:只有固定数量的优惠券或商品

  • 写操作密集:每次请求都可能修改库存、生成订单等

  • 数据一致性要求高:不能发出超过库存数量的券(不能超卖)

2. 抢券系统的核心挑战

挑战点描述
高并发瞬间大量请求进入,数据库或应用服务器承压
数据一致性多线程同时读取库存,可能导致超卖
性能瓶颈数据库写入能力有限,QPS不高
可扩展性单机处理能力有限,必须支持横向扩展

二、系统吞吐量指标:QPS 与 TPS

系统优化的第一步,是理解性能指标。

1. QPS:每秒查询请求数(Queries Per Second)

  • 常用于衡量接口的处理能力。

  • 包括读请求,如查询用户状态、查库存等。

示例计算:

一台服务器 10 秒内处理了 20,000 个接口请求,那么它的 QPS = 2000。

静态资源(如图片、JS)常常由 CDN 分担,QPS 可以非常高(>10,000)。但动态接口尤其是数据库写入类接口(如“扣库存”)QPS 会显著降低。

2. TPS:每秒事务处理数(Transactions Per Second)

  • 更偏向于数据库或交易系统的指标。

  • 关注“写操作”的处理能力,如订单生成、扣库存等。

一般 TPS < QPS,因为写操作代价更高,需保证事务、持久化等。


三、抢券系统中的超卖问题

1. 超卖现象回顾

场景:假设库存为 1,同时来了两个用户请求,读取库存都为 1,判断都通过,最终都成功扣减库存。结果超卖了 1 个。

2. 为什么会超卖?

根本原因在于多个请求“并发读取库存”时,读取到的都是相同的旧值,造成多个请求都判断库存充足,进而并发写入。

这是典型的并发冲突问题


四、常见解决方案及优劣分析

接下来我们逐一分析解决并发超卖的几种常见方案。


1. 使用 Java synchronized 同步锁

public synchronized void抢券() {
    if (库存 > 0) {
        库存--;
        // 下单逻辑
    }
}

优点:

  • 实现简单,适合单机测试

  • JVM 层加锁,线程安全

缺点:

  • 只在单个JVM内生效,多实例部署无法共享锁

  • 串行处理,请求吞吐量极低,不适合高并发


2. 数据库悲观锁:SELECT ... FOR UPDATE

START TRANSACTION;
SELECT * FROM coupon WHERE id = 1 FOR UPDATE;
-- 判断库存 > 0
UPDATE coupon SET stock = stock - 1 WHERE id = 1;
COMMIT;

优点:

  • 能保证事务强一致性

  • 适合金融、电商核心业务(要求高一致性)

缺点:

  • 锁表或锁行,并发高时阻塞严重

  • 性能差,不适合 QPS > 100 的场景


3. 数据库乐观锁(CAS机制)

表结构新增 version 字段,每次更新时加上版本号条件。

UPDATE coupon 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 当前版本号;

优点:

  • 不加锁,提升并发性能

  • 支持多线程并发执行

缺点:

  • 成功率随并发升高而降低,需要加重试逻辑

  • 编码复杂,需额外字段和逻辑控制


4. Redis 分布式锁控制并发访问

使用 SETNX 或 Redisson 实现分布式锁机制:

// 获取锁
Boolean success = redis.setIfAbsent("lock_key", "UUID", 5秒过期);
if (success) {
    try {
        // 查询库存、扣减逻辑
    } finally {
        // 释放锁
        redis.del("lock_key");
    }
}

优点:

  • 支持分布式部署

  • 可以精准控制访问

缺点:

  • 性能瓶颈:每个请求都要走 Redis 一次 SETNX

  • 存在死锁、锁丢失等问题(需设置过期+唯一标识)


✅5. Redis 原子操作(最终推荐)

Redis 提供天然的原子命令,比如 DECR

DECR coupon_stock

原理: Redis 是单线程的,所有命令天然串行执行,保证操作原子性。

优势非常明显:

优点说明
性能极高单线程执行命令,无锁实现,处理百万 QPS 不是问题
支持并发安全不需要加锁,自带原子性
网络请求少不需要读-判断-写三步,只需要一次写操作
易于扩展可以搭配 Lua 脚本,进一步实现逻辑判断

典型逻辑:

  1. 将库存预热到 Redis 中,key: coupon:stock:123

  2. 抢券请求使用 DECR 扣减库存

  3. DECR 结果 >= 0,表示抢券成功,异步入库


五、方案总结与选型建议

方案性能并发安全成本可扩展性推荐等级
synchronized
悲观锁⭐⭐
乐观锁一般⭐⭐⭐
分布式锁⭐⭐⭐
Redis原子扣减✅高✅低⭐⭐⭐⭐⭐

六、最佳实践建议

  • 小型项目 / 教学实验:可使用数据库悲观锁或乐观锁

  • 中型项目 / 单机部署:考虑 Redisson 分布式锁 + 本地队列

  • 大型项目 / 高并发抢购:使用 Redis 缓存 + 原子扣减 + 异步削峰(MQ)


七、写在最后

高并发场景的核心思路是“加锁避免冲突、缓存抵抗压力、异步削峰填谷”。任何一个优化点的背后,都是对底层原理的深入理解与工程实践的不断打磨。

希望这篇文章能帮你理清抢券系统背后的技术细节,也欢迎你评论区分享你遇到的坑,我们一起交流进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值