1. 为什么我们需要Redis+Lua的秒杀方案
去年双十一,我负责的一个电商项目在秒杀活动开始后30秒内崩溃了。当时用的是纯数据库方案,每秒3000的并发直接打穿了MySQL。这次惨痛经历让我意识到,面对瞬时高并发场景,传统架构必须重构。
秒杀系统的核心矛盾在于:海量用户瞬间涌入与有限库存之间的对抗。假设某商品库存100件,却有10万人同时点击购买。传统方案会遇到几个致命问题:
- 超卖问题:多个线程同时查询库存>0,都通过校验导致实际卖出数量超过库存
- 数据库压力:每次请求都产生至少4次DB操作(查询库存→创建订单→扣库存→支付)
- 性能瓶颈:MySQL单机QPS通常不超过5000,而秒杀场景轻松突破10万+
Redis+Lua的组合拳能完美解决这些问题:
- Redis单机QPS可达10万级,完全hold住瞬时流量
- Lua脚本的原子性执行杜绝超卖
- 内存操作比磁盘IO快几个数量级
关键认知:秒杀不是"快速购物",而是"库存预扣"。系统只需要确保不超卖,后续订单处理可以异步进行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础环境搭建与配置
2.1 SpringBoot项目初始化
使用IDEA创建项目时重点注意这些配置:
bash复制# 选择依赖时务必包含
- Spring Web
- Spring Data Redis
- Lombok
application.yml中Redis连接配置示例:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
lettuce:
pool:
max-active: 20 # 根据压测调整
max-wait: 200ms
2.2 Redis特殊配置
redis.conf中必须调整的参数:
conf复制# 关闭持久化以提升性能
save ""
appendonly no
# 内存限制和淘汰策略
maxmemory 2gb
maxmemory-policy allkeys-lru
生产环境务必搭建Redis集群,建议3主3从。可以使用Docker快速部署:
bash复制docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
3. 核心秒杀逻辑实现
3.1 商品预热方案
活动开始前1小时执行预热:
java复制// 商品初始化示例
public void initSeckillProduct(long productId, int stock) {
String key = "seckill:stock:" + productId;
redisTemplate.opsForValue().set(key, stock);
// 布隆过滤器预热(防刷)
stringRedisTemplate.opsForValue()
.setBit("seckill:bloom", productId, true);
}
3.2 Lua脚本精讲
stock_check.lua脚本内容:
lua复制local stock_key = KEYS[1]
local user_key = KEYS[2]
local user_id = ARGV[1]
local quantity = tonumber(ARGV[2])
-- 检查是否重复购买
if redis.call('sismember', user_key, user_id) == 1 then
return 2
end
-- 检查库存
local stock = tonumber(redis.call('get', stock_key))
if stock <= 0 then
return 0
end
-- 扣减库存
redis.call('decrby', stock_key, quantity)
redis.call('sadd', user_key, user_id)
return 1
脚本执行Java代码:
java复制public Result seckill(long productId, long userId) {
String script = // 加载上面的lua脚本
List<String> keys = Arrays.asList(
"seckill:stock:" + productId,
"seckill:users:" + productId
);
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
keys,
String.valueOf(userId),
"1" // 购买数量
);
if (result == 0) {
return Result.fail("库存不足");
} else if (result == 2) {
return Result.fail("请勿重复购买");
}
return Result.success();
}
4. 高并发优化实战技巧
4.1 流量削峰方案
- 前端层:
- 静态化活动页,CDN加速
- 按钮点击后禁用+倒计时
- 随机延迟提交(人类行为模拟)
- 网关层:
java复制// 基于Guava的令牌桶限流
RateLimiter limiter = RateLimiter.create(10000); // QPS限制
@GetMapping("/seckill")
public Result seckill(SeckillRequest request) {
if (!limiter.tryAcquire()) {
return Result.fail("活动太火爆,请稍后");
}
// 后续处理
}
- 队列缓冲:
java复制// RocketMQ异步处理订单
public void afterSeckill(long userId, long productId) {
SeckillMessage message = new SeckillMessage(userId, productId);
rocketMQTemplate.asyncSend("seckill_queue", message,
new SendCallback() {
// 回调处理
});
}
4.2 缓存策略优化
多级缓存架构:
- JVM缓存(Caffeine)→ 2. Redis集群 → 3. 数据库
缓存击穿解决方案:
java复制public Object getProductInfo(long productId) {
// 1. 先查本地缓存
Object value = caffeineCache.getIfPresent(productId);
if (value != null) return value;
// 2. 查Redis(使用Redisson分布式锁)
RLock lock = redisson.getLock("lock:" + productId);
try {
lock.lock(3, TimeUnit.SECONDS);
// 双重检查
value = redisTemplate.opsForValue().get(productId);
if (value == null) {
// 3. 查数据库并重建缓存
value = dbQuery(productId);
redisTemplate.opsForValue().set(productId, value, 5, TimeUnit.MINUTES);
}
caffeineCache.put(productId, value);
} finally {
lock.unlock();
}
return value;
}
5. 生产环境踩坑实录
5.1 Lua脚本调试技巧
常见问题排查步骤:
- 使用redis-cli --eval 直接测试脚本
- 在脚本中加入日志输出:
lua复制redis.log(redis.LOG_NOTICE, "Debug stock:", stock)
- 使用Redis的SCRIPT DEBUG命令
我曾遇到一个诡异问题:Lua脚本在本地测试正常,上生产后报错。最终发现是Redis版本差异导致语法兼容性问题。教训:务必保证测试环境与生产环境的Redis版本一致。
5.2 分布式锁的注意事项
错误示范:
java复制// 错误!锁过期时间小于业务执行时间会导致锁失效
redisTemplate.opsForValue().setIfAbsent("lock", "1", 10, TimeUnit.SECONDS);
正确方案(Redisson实现):
java复制RLock lock = redisson.getLock("seckillLock");
try {
// 自动续期,默认30秒过期
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
5.3 库存回滚机制
当订单创建失败时需要恢复库存:
java复制@Transactional
public void handleOrder(Order order) {
try {
orderDao.create(order);
// 其他业务操作...
} catch (Exception e) {
// 异步恢复库存
mqTemplate.send("stock_recover",
new StockMessage(order.getProductId(), 1));
throw e;
}
}
6. 性能压测数据对比
使用JMeter进行10万并发测试:
| 方案 | QPS | 平均响应时间 | 超卖率 |
|---|---|---|---|
| 纯数据库方案 | 1,200 | 2.3s | 8.7% |
| Redis乐观锁 | 28,000 | 156ms | 0% |
| Lua脚本方案(本文) | 85,000 | 32ms | 0% |
关键发现:
- Lua方案比纯DB方案性能提升70倍
- 网络带宽可能成为新瓶颈(建议使用Redis Pipeline)
- 适当增加Redis连接池大小可提升吞吐量
7. 扩展优化方向
7.1 热点数据隔离
发现热点商品后,采用特殊处理:
java复制// 在Redis中为热点商品建立独立分片
public String getHotKey(long productId) {
if (isHotProduct(productId)) {
return "hot_seckill:" + productId;
}
return "seckill:" + productId;
}
7.2 虚拟线程优化
JDK21+可使用虚拟线程提升性能:
java复制ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<Result> future = executor.submit(() -> {
return seckillService.doSecKill(productId, userId);
});
7.3 库存预热分片
将库存拆分为多个key减轻单个key压力:
java复制// 初始化时将库存拆分为10份
public void initShardedStock(long productId, int total) {
for (int i = 0; i < 10; i++) {
String key = "seckill:stock:" + productId + ":" + i;
redisTemplate.opsForValue().set(key, total / 10);
}
}
在秒杀场景中,技术方案永远需要根据实际业务特点调整。最近我在处理一个海外电商项目时,发现时区问题导致缓存失效时间计算错误。这个经历再次提醒我们:任何技术方案都需要经过真实流量的检验。
