大促期间的秒杀活动,尤其是像黑五、双十一这类顶级购物节,对系统架构的挑战极为严峻。特别是面向1688代购的秒杀场景,问题更加集中——同一件商品在短短几秒内被数千人同时抢购,库存扣减环节的压力瞬间拉满。核心挑战其实非常明确:

首先,瞬时高并发是家常便饭。秒杀开启的那一瞬间,QPS可以飙升至日常的50倍以上。其次,最令人担忧的是超卖问题——如果库存扣减不是原子操作,很容易出现卖了100件但实际库存只有80件的尴尬局面。最后,响应延迟也是重大隐患。数据库的行锁一旦加锁,接口响应时间直接从毫秒级退化到秒级,用户点击后等待数秒无反馈,体验瞬间崩塌。
Redis Lua原子扣减方案
传统做法是直接利用数据库的行锁来扣减库存,但在高并发场景下性能极差。因此,我们换用了一套更优方案:通过Redis Lua脚本实现原子库存扣减,彻底杜绝竞态条件。本质上,就是让扣减动作成为一个不可分割的整体操作。
具体的Lua脚本逻辑如下:
-- stock_deduct.lua
-- KEYS[1]: 库存Key
-- ARGV[1]: 扣减数量
-- ARGV[2]: 超时时间(秒)
local stock_key = KEYS[1]
local deduct_qty = tonumber(ARGV[1])
local timeout = tonumber(ARGV[2])
local current = tonumber(redis.call('get', stock_key) or 0)
if current < deduct_qty then
return 0
end
local new_stock = redis.call('decrby', stock_key, deduct_qty)
if new_stock < 0 then
-- 极端情况:扣减后为负,回滚
redis.call('incrby', stock_key, deduct_qty)
return 0
end
redis.call('expire', stock_key, timeout)
return new_stock
这段脚本的核心思路是:先检查库存是否充足,若不足直接返回失败;充足则进行原子扣减,但如果扣减后发现库存变为负数(极端并发下可能发生),立即回滚以确保数据一致性。最后设置过期时间,避免库存Key永久占用Redis内存。
Ja va端的调用同样简洁直接:
@Service
public class StockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String STOCK_PREFIX = "taocarts:stock:";
private static final DefaultRedisScript DEDUCT_SCRIPT;
static {
DEDUCT_SCRIPT = new DefaultRedisScript<>();
DEDUCT_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/stock_deduct.lua")));
DEDUCT_SCRIPT.setResultType(Long.class);
}
public boolean deductStock(Long productId, Integer quantity) {
String stockKey = STOCK_PREFIX + productId;
Long result = redisTemplate.execute(DEDUCT_SCRIPT,
Collections.singletonList(stockKey),
String.valueOf(quantity),
String.valueOf(3600) // 1小时过期
);
return result != null && result >= 0;
}
}
分布式锁防止重复下单
库存扣减问题解决了,但还有一个隐患:用户网络延迟可能导致多次点击下单按钮。同一用户对同一商品下多个订单,这种情况必须拦截。这就要引入分布式锁了。
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private StockService stockService;
@Autowired
private OrderService orderService;
@PostMapping("/place")
public Result placeOrder(@RequestBody SeckillRequest request) {
Long userId = request.getUserId();
Long productId = request.getProductId();
// 1. 分布式锁:防止同一用户重复下单
String lockKey = "taocarts:seckill:lock:" + userId + ":" + productId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
if (!locked) {
return Result.fail("请勿重复提交");
}
try {
// 2. 原子扣减库存
boolean success = stockService.deductStock(productId, 1);
if (!success) {
return Result.fail("库存不足");
}
// 3. 创建订单(异步)
orderService.createSeckillOrder(userId, productId);
return Result.success("下单成功");
} finally {
redisTemplate.delete(lockKey);
}
}
}
设计思路是:用户发起下单请求时,先通过Redis分布式锁判断该用户ID与商品ID的组合是否已有请求正在处理。如果锁已被占用,则直接返回“请勿重复提交”。锁的过期时间设为3秒,足以覆盖正常流程。库存扣减与订单创建均在锁范围内,确保整体操作的原子性。
削峰填谷
订单创建成功后,后续的采购、仓储、物流等操作如果全部同步执行,主线程必定扛不住。这时,消息队列就派上了用场。秒杀订单创建后,立即通过消息队列异步处理,将耗时操作延后执行。这样一来,主线程的压力大大减轻,系统吞吐量得以提升。
@Component
public class SeckillOrderConsumer {
@Autowired
private PurchaseService purchaseService;
@RabbitListener(queues = "seckill.order.queue")
public void handleSeckillOrder(SeckillOrderMessage msg) {
// 执行采购、仓储、物流等耗时操作
purchaseService.purchaseFrom1688(msg.getOrderId());
}
}
效果评估
这套方案在真实黑五大促中经受住了严苛考验。QPS从原来的500提升到8000,直接翻了16倍。超卖率从2.3%直线降至0%,一次超卖都未发生。平均响应时间也从1200ms骤降到80ms,用户端几乎感觉不到延迟。目前,这套Redis Lua原子扣减方案已在Taocarts跨境电商独立站的秒杀场景中稳定运行,确保每一次大促都能平稳收官。
