一、秒杀场景的技术挑战
跨境电商独立站在大促期间(黑五、双十一)经常推出限时秒杀活动。Taocarts的1688代购秒杀场景中,同一商品可能被数千用户同时抢购,库存扣减面临瞬时高并发冲击。
三大核心挑战:
瞬时高并发:秒杀开始瞬间,QPS可达日常的50倍以上
超卖风险:库存扣减不是原子操作时,可能出现超卖
响应延迟:数据库行锁导致接口响应时间从毫秒级退化到秒级
二、Redis+Lua原子扣减方案
传统数据库行锁扣减库存的方式在高并发下性能极差。我们采用Redis Lua脚本实现原子库存扣减,避免竞态条件。
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)
-- 库存不足返回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
-- 设置过期时间(防止库存Key永久占用内存)
redis.call('expire', stock_key, timeout)
return new_stock
Java调用实现:
java
@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;
}
}
三、分布式锁防止重复下单
同一用户可能因网络延迟多次点击下单按钮。我们需要防止同一用户对同一商品重复下单。
java
@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);
}
}
}
四、消息队列削峰填谷
秒杀订单创建后,后续的采购、仓储、物流操作通过消息队列异步处理,避免阻塞主线程。
java
@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跨境电商独立站系统的大促场景中稳定运行,确保了大促期间零超卖事故