1 秒杀场景
电商秒杀场景具有瞬时高并发、资源竞争激烈和数据一致性要求高三大特征。当数万用户同时抢购少量商品时(如1000件商品承受10万QPS),系统面临多重挑战:
- 超卖风险:多个并发请求同时查询库存并扣减,导致库存扣减数超过实际库存量
- 数据库压力:传统数据库难以承受瞬时高并发读写
- 用户体验:响应延迟或服务崩溃导致用户请求失败
// 典型超卖代码示例
@GetMapping("/reduce")
public String reduce() {
int stock = (Integer) redisTemplate.opsForValue().get("stock");
if (stock > 0) {
redisTemplate.opsForValue().set("stock", stock - 1);
return "秒杀成功";
}
return "库存不足";
}
上述代码在并发场景下必然出现超卖问题。当多个请求同时通过stock > 0
检查后,每个请求都会执行减库存操作,导致库存变为负数。
2 超卖问题根源
2.1 并发冲突的本质
超卖问题的本质是非原子性操作在分布式环境下的并发冲突:
sequenceDiagram
参与者 用户A as 用户A
参与者 用户B as 用户B
参与者 Redis as Redis服务
用户A->>Redis: 查询库存(10)
用户B->>Redis: 查询库存(10)
用户A->>Redis: 扣减5个库存(10-5=5)
用户B->>Redis: 扣减6个库存(5-6=-1)
当用户A和用户B同时查询库存(均为10),用户A先扣减5个库存成功,用户B随后扣减6个库存导致库存变为-1,即发生了超卖。
2.2 传统解决方案的局限
- 数据库悲观锁:
SELECT FOR UPDATE
导致性能瓶颈 - 应用层锁:仅限单机有效,无法解决分布式问题
- 乐观锁:高并发下重试率高,用户体验差
3 Redis分布式锁深度优化
3.1 基础分布式锁实现
基于Redis的SETNX
命令实现分布式锁:
public class RedisLock {
private RedisTemplate<String, String> redisTemplate;
public boolean tryLock(String key, String value, long expireTime) {
return redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
}
public boolean releaseLock(String key, String value) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
return result != null && result == 1;
}
}
此实现解决了锁释放安全性问题,确保只有锁持有者才能释放锁。
3.2 锁续命机制
为解决长任务执行时锁过期问题,需实现锁续命机制:
sequenceDiagram
参与者 Client as 客户端
参与者 Thread as 守护线程
参与者 Redis as Redis
Client->>Redis: 获取锁(设置30秒过期)
Client->>Thread: 启动守护线程
loop 每10秒执行
Thread->>Redis: 检查锁是否存在
Redis-->>Thread: 存在
Thread->>Redis: 重置过期时间(续命)
end
Client->>Redis: 任务完成,释放锁
Client->>Thread: 停止守护线程
具体实现:
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public void renewLock(String key, String value, long renewInterval) {
scheduler.scheduleAtFixedRate(() -> {
String currentValue = redisTemplate.opsForValue().get(key);
if (value.equals(currentValue)) {
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
}
}, 0, renewInterval, TimeUnit.SECONDS);
}
3.3 集群环境下的RedLock算法
在Redis集群环境下,为避免主从切换导致锁丢失,需采用RedLock算法:
实现要点:
- 同时向5个独立Redis实例请求锁
- 当获取超过半数的锁(≥3)且总耗时小于锁有效期时视为成功
- 锁的实际有效时间 = 初始有效时间 - 获取锁耗时
4 基于Lua的原子库存扣减
4.1 Lua脚本原子性原理
Redis执行Lua脚本具有原子性特性:
- 脚本作为一个整体执行
- 执行期间不会插入其他命令
- 无需显式加锁即可实现事务
4.2 库存扣减Lua脚本
-- KEYS[1]: 商品库存key
-- ARGV[1]: 扣减数量
local key = KEYS[1]
local val = tonumber(ARGV[1])
-- 获取当前库存
local stock = tonumber(redis.call('get', key))
if stock <= 0 then
return -1 -- 库存不足
end
if stock < val then
return -2 -- 库存不足指定数量
end
-- 扣减库存
local newStock = redis.call('decrby', key, val)
if newStock >= 0 then
return newStock -- 扣减成功
else
-- 回滚操作
redis.call('incrby', key, val)
return -3 -- 扣减后库存为负,操作回滚
end
脚本关键点:
- 使用
tonumber()
转换字符串为数字 - 在扣减前进行库存充足性检查
- 扣减后出现负库存时自动回滚
4.3 Spring Boot集成Lua脚本
@Configuration
public class LuaScriptConfig {
@Bean
public DefaultRedisScript<Long> stockDeductScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/stock_deduct.lua"));
script.setResultType(Long.class);
return script;
}
}
@Service
public class SecKillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private DefaultRedisScript<Long> stockDeductScript;
public String handleSecKill(String productId, int quantity) {
List<String> keys = Collections.singletonList("stock:" + productId);
Long result = redisTemplate.execute(
stockDeductScript,
keys,
String.valueOf(quantity));
if (result == -1 || result == -2) {
return "库存不足";
} else if (result == -3) {
return "扣减失败,库存不足";
}
// 扣减成功,创建订单
createOrder(productId, quantity);
return "秒杀成功";
}
}
5 库存扣减优化策略
5.1 缓存与数据库双写一致性
操作要点:
- 先操作缓存再操作数据库
- 数据库操作异步化(通过消息队列)
- 设置库存操作流水表用于对账
5.2 库存分段策略
借鉴ConcurrentHashMap的分段锁思想,适用于超高并发场景:
// 初始化分段库存
public void initSegmentStock(String productId, int totalStock, int segment) {
int base = totalStock / segment;
for (int i = 0; i < segment; i++) {
int segmentStock = (i == segment - 1) ?
base + totalStock % segment : base;
redisTemplate.opsForValue().set(
"stock:" + productId + ":" + i,
segmentStock);
}
}
// 分段扣减
public boolean segmentDeduct(String productId, int quantity) {
int segment = getSegmentCount(productId);
for (int i = 0; i < segment; i++) {
String key = "stock:" + productId + ":" + i;
Long result = redisTemplate.execute(
stockDeductScript,
Collections.singletonList(key),
String.valueOf(quantity));
if (result != null && result >= 0) {
return true;
}
}
return false;
}
适用场景:
- 库存量较大(≥1000)
- 允许少量库存碎片(最后一段可能有剩余库存)
6 限流与降级机制
6.1 多级限流策略
网关层限流:基于令牌桶算法控制全局流量
http { limit_req_zone $binary_remote_addr zone=secKill:10m rate=100r/s; server { location /seckill { limit_req zone=secKill burst=50; proxy_pass http://backend_server; } } }
服务层限流:基于Redis实现分布式限流
public boolean tryAcquire(String key, int max, int timeout) { String luaScript = "local current = redis.call('incr', KEYS[1])\n" + "if current == 1 then\n" + " redis.call('expire', KEYS[1], ARGV[1])\n" + "end\n" + "if current > tonumber(ARGV[2]) then\n" + " return 0\n" + "else\n" + " return 1\n" + "end"; DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(key), String.valueOf(timeout), String.valueOf(max)); return result != null && result == 1; }
商品维度限流:针对热点商品设置独立阈值
6.2 服务降级策略
根据系统负载动态调整服务能力:
降级措施:
- 关闭商品详情页的推荐模块
- 简化购物车逻辑
- 关闭评论功能
- 仅保留核心交易链路
7 性能测试与优化效果
7.1 测试环境配置
组件 | 配置 |
---|---|
应用服务器 | 4核8G × 3台(负载均衡) |
Redis | 6核16G 主从哨兵模式 |
数据库 | MySQL 8.0 读写分离 |
压测工具 | JMeter 1000并发线程 |
7.2 不同方案性能对比
方案 | 吞吐量(QPS) | 平均响应时间(ms) | 超卖率 |
---|---|---|---|
数据库悲观锁 | 120 | 450 | 0% |
应用层锁 | 980 | 105 | 单机0% |
Redis+Lua原子操作 | 5,600 | 18 | 0% |
库存分段+Redis Lua | 8,200 | 12 | 0% |
7.3 资源消耗对比
bar
title 资源消耗对比
x-axis 方案
y-axis CPU利用率(%)
series 数据库悲观锁: 85
series 应用层锁: 65
series Redis+Lua: 42
series 库存分段: 38
测试结果表明:
- Redis+Lua方案相比传统方案吞吐量提升46倍
- 响应时间从450ms降至18ms,提升25倍
- CPU利用率降低50%以上
8 总结与最佳实践
经过实战验证的秒杀系统最佳实践:
分层防控
- 前端:按钮置灰、验证码、请求间隔限制
- 网关:请求过滤、全局限流
- 服务:分布式锁、Lua原子操作
- 数据层:缓存预热、异步写库
库存扣减黄金法则
容灾设计
- Redis宕机时降级到数据库乐观锁
- 设置库存操作开关
- 实现自动对账机制
持续优化方向
- 库存碎片回收(定期合并分段库存)
- 基于机器学习的动态限流
- 区域化库存分配
经验总结:在高并发秒杀系统中,原子性操作和分层限流是两大核心支柱。Redis结合Lua脚本提供了高效的原子操作能力,而合理设计的分布式限流策略则是系统稳定的保障。实际生产中,没有银弹解决方案,需要根据业务特性和流量规模灵活组合多种技术手段。