在电商大促期间,每秒数万次的抢购请求让传统系统架构不堪重负。我曾负责一个类似"黑马点评"的优惠券秒杀模块开发,经历了从数据库崩溃到稳定支撑十万级QPS的完整演进过程。本文将分享如何用Redis+Lua+Redisson构建高可靠秒杀系统,以及那些只有踩过坑才知道的实战经验。
当用户疯狂点击"立即抢购"按钮时,系统实际上在经历一场没有硝烟的战争。我们首先需要明确秒杀场景下的三大核心问题:
性能基准测试数据对比:
| 方案 | 吞吐量(QPS) | 平均响应时间(ms) | 资源消耗 |
|---|---|---|---|
| 纯数据库方案 | 120 | 850 | 高 |
| Redis简单计数 | 2,800 | 35 | 中 |
| Lua+Redisson(最终方案) | 18,000 | 8 | 低 |
提示:实际压测需考虑网络延迟和业务逻辑复杂度,本数据基于4核8G云服务器测试环境
很多团队知道要把库存加载到Redis,但往往忽略了关键细节。这是我们最初版本的库存初始化代码:
java复制// 错误示范:简单设置库存值
stringRedisTemplate.opsForValue().set("stock:"+voucherId, "100");
这种写法存在两个致命缺陷:
改进后的库存预热方案:
java复制// 使用Lua脚本保证原子性初始化
String script =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('set', KEYS[1], ARGV[1]) " +
" redis.call('pexpire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
// 设置24小时过期
stringRedisTemplate.execute(redisScript,
Collections.singletonList("stock:"+voucherId),
"100",
"86400000");
单纯使用Redis的DECR命令无法解决所有并发问题。这是我们趟过坑后设计的Lua脚本:
lua复制-- KEYS[1]: 库存key
-- KEYS[2]: 订单集合key
-- ARGV[1]: 用户ID
-- ARGV[2]: 当前时间戳(用于订单去重)
-- 检查库存
local stock = tonumber(redis.call('get', KEYS[1]))
if not stock or stock <= 0 then
return 1 -- 库存不足
end
-- 检查是否重复下单
local exists = redis.call('zscore', KEYS[2], ARGV[1])
if exists then
return 2 -- 已存在订单
end
-- 扣减库存
redis.call('decr', KEYS[1])
-- 记录订单(带时间戳用于后续排查)
redis.call('zadd', KEYS[2], ARGV[2], ARGV[1])
return 0
这个脚本解决了三个关键问题:
很多人知道用RedissonLock,但往往忽略这些优化点:
锁配置的最佳实践:
java复制RLock lock = redissonClient.getLock("order:lock:" + userId);
// 尝试获取锁,最多等待100ms,锁自动释放时间30秒
boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
if (!locked) {
throw new BusinessException("操作太频繁,请稍后再试");
}
try {
// 业务逻辑
} finally {
// 只释放自己持有的锁
if(lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
锁的注意事项:
秒杀成功后的订单处理应该与抢购逻辑解耦。我们采用的方案是:
code复制[抢购核心流程]
↓
[Redis Stream队列]
↓
[消费者集群]
↓
[数据库分库]
生产者端代码示例:
java复制// 抢购成功后发送到消息队列
Map<String, String> message = new HashMap<>();
message.put("voucherId", voucherId.toString());
message.put("userId", userId.toString());
message.put("orderId", orderId.toString());
stringRedisTemplate.opsForStream().add("order:queue", message);
消费者端关键配置:
properties复制# 每个消费者线程处理的消息数
spring.redis.stream.consumer.batch-size=20
# 失败消息重试次数
spring.redis.stream.consumer.max-attempts=3
# 消费组名称
spring.redis.stream.consumer.group=order-group
在百万级压力测试中,我们总结出这些经验:
缓存预热策略:
限流方案对比:
| 方案 | 实现复杂度 | 精准度 | 性能影响 |
|---|---|---|---|
| Redis计数器 | 低 | 中 | 小 |
| 令牌桶算法 | 中 | 高 | 中 |
| 漏桶算法 | 高 | 高 | 大 |
熔断降级配置示例:
java复制@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new CircuitBreakerConfig.Builder()
.failureRateThreshold(50) // 失败率阈值
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断时间
.slidingWindowSize(100) // 滑动窗口大小
.build());
}
没有完善的监控,秒杀系统就像盲人摸象。我们采用的监控方案:
Redis关键指标监控:
bash复制# 监控内存使用
redis-cli info memory
# 监控命令统计
redis-cli info commandstats
订单异常检测脚本:
python复制# 检查订单与库存的一致性
import redis
r = redis.Redis()
stock = r.get('stock:1001')
orders = r.zcard('orders:1001')
print(f"理论剩余库存: {int(stock)}")
print(f"实际售出数量: {orders}")
慢查询分析:
sql复制-- MySQL慢查询分析
SELECT * FROM mysql.slow_log
WHERE start_time > NOW() - INTERVAL 1 HOUR
ORDER BY query_time DESC LIMIT 10;
在实施这套方案后,我们的系统在618大促期间平稳支撑了峰值15万QPS的秒杀请求,库存准确率达到100%,订单处理延迟控制在200ms以内。记住,高并发系统没有银弹,只有不断调优和演练才能保证万无一失。