基于SpringBoot + Redis + Lua 实现高并发秒杀系统实战
大家好,我是服务端技术精选的作者。今天咱们聊聊一个在电商领域极其重要的话题:高并发秒杀系统。
秒杀系统的挑战
在我们的日常开发工作中,经常会遇到这样的场景:
- 限量商品开售瞬间,成千上万的用户同时访问
- 系统在秒杀开始时直接崩溃,用户无法下单
- 超卖现象频发,库存被抢购一空
- 黑产机器人恶意刷单,正常用户买不到商品
传统的库存扣减方式在高并发场景下根本无法胜任,今天我们就来聊聊如何用Redis + Lua构建一个高并发的秒杀系统。
为什么选择Redis + Lua
相比传统的数据库事务方案,Redis + Lua有以下优势:
- 高性能:内存操作,响应速度极快
- 原子性:Lua脚本在Redis中是原子执行的
- 低延迟:避免数据库的网络IO开销
- 高并发:Redis单机可支撑10万+ QPS
系统架构设计
1. 整体架构
用户请求 → API网关 → 限流过滤 → Redis Lua脚本 → 库存扣减 → 订单创建
2. 核心组件
- Redis:存储商品库存和用户限购信息
- Lua脚本:原子性执行库存扣减逻辑
- 消息队列:异步处理订单创建
- 限流组件:防止恶意刷单
Lua脚本实现
1. 库存扣减脚本
-- 秒杀库存扣减脚本
-- KEYS[1]: 商品库存key
-- KEYS[2]: 用户限购key
-- ARGV[1]: 购买数量
-- ARGV[2]: 用户ID
-- ARGV[3]: 限购数量
local stock_key = KEYS[1]
local user_limit_key = KEYS[2]
local buy_num = tonumber(ARGV[1])
local user_id = ARGV[2]
local limit_num = tonumber(ARGV[3])
-- 检查库存是否充足
local current_stock = redis.call('GET', stock_key)
if not current_stock then
return {code = 1, msg = '商品不存在'}
end
current_stock = tonumber(current_stock)
if current_stock < buy_num then
return {code = 2, msg = '库存不足'}
end
-- 检查用户是否已购买过(限购)
local user_bought = redis.call('GET', user_limit_key)
if user_bought then
user_bought = tonumber(user_bought)
if user_bought >= limit_num then
return {code = 3, msg = '已达到购买上限'}
end
-- 检查加上本次购买是否会超过限制
if user_bought + buy_num > limit_num then
return {code = 4, msg = '购买数量超过限制'}
end
end
-- 扣减库存
redis.call('DECRBY', stock_key, buy_num)
-- 记录用户购买数量
if user_bought then
redis.call('INCRBY', user_limit_key, buy_num)
else
redis.call('SET', user_limit_key, buy_num)
end
-- 设置用户限购key过期时间(防止永久占用)
redis.call('EXPIRE', user_limit_key, 86400) -- 24小时
return {code = 0, msg = '秒杀成功', remaining_stock = current_stock - buy_num}
2. 库存初始化脚本
-- 商品库存初始化脚本
-- KEYS[1]: 商品库存key
-- ARGV[1]: 库存数量
-- ARGV[2]: 过期时间
local stock_key = KEYS[1]
local stock_num = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])
-- 设置库存
redis.call('SET', stock_key, stock_num)
-- 设置过期时间
if expire_time > 0 then
redis.call('EXPIRE', stock_key, expire_time)
end
return stock_num
SpringBoot集成实现
1. Redis配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public DefaultRedisScript<SeckillResult> seckillScript() {
DefaultRedisScript<SeckillResult> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/seckill.lua"));
script.setResultType(SeckillResult.class);
return script;
}
}
2. 秒杀服务实现
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisScript<List> seckillScript;
@Autowired
private OrderService orderService;
/**
* 执行秒杀
*/
public SeckillResult executeSeckill(Long productId, Long userId, Integer num) {
try {
// 构建Redis key
String stockKey = "seckill:stock:" + productId;
String userLimitKey = "seckill:user:" + productId + ":user:" + userId;
// 执行Lua脚本
List<String> keys = Arrays.asList(stockKey, userLimitKey);
List<String> args = Arrays.asList(
String.valueOf(num), // 购买数量
String.valueOf(userId), // 用户ID
"1" // 限购数量
);
List<Object> result = (List<Object>) redisTemplate.execute(
seckillScript,
new DefaultRedisScript<>("return redis.call('EVALSHA', '...', 2, KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3])"),
keys,
args.toArray(new String[0])
);
if (result != null && result.size() >= 2) {
int code = Integer.parseInt(result.get(0).toString());
String msg = result.get(1).toString();
SeckillResult seckillResult = new SeckillResult();
seckillResult.setCode(code);
seckillResult.setMsg(msg);
if (code == 0) {
// 秒杀成功,异步创建订单
asyncCreateOrder(productId, userId, num);
}
return seckillResult;
}
return SeckillResult.fail("秒杀失败");
} catch (Exception e) {
log.error("秒杀异常", e);
return SeckillResult.fail("系统异常,请稍后重试");
}
}
/**
* 异步创建订单
*/
@Async
public void asyncCreateOrder(Long productId, Long userId, Integer num) {
try {
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(num);
order.setStatus("CREATED");
order.setCreateTime(new Date());
orderService.createOrder(order);
log.info("订单创建成功: userId={}, productId={}, quantity={}", userId, productId, num);
} catch (Exception e) {
log.error("订单创建失败", e);
// 可以考虑补偿机制
}
}
}
限流防护机制
1. 接口限流
@Component
public class RateLimiter {
private final RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个令牌
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}
@RestController
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private RateLimiter rateLimiter;
@PostMapping("/seckill/{productId}")
public Result seckill(@PathVariable Long productId,
@RequestParam Long userId,
@RequestParam(defaultValue = "1") Integer num) {
// 接口限流
if (!rateLimiter.tryAcquire()) {
return Result.fail("请求过于频繁,请稍后再试");
}
// 参数校验
if (num <= 0 || num > 5) { // 限制单次购买数量
return Result.fail("购买数量不合法");
}
// 执行秒杀
SeckillResult result = seckillService.executeSeckill(productId, userId, num);
return Result.success(result);
}
}
2. 黑产防护
@Component
public class AntiCrawlerService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean isSuspiciousRequest(String ip, Long userId) {
String ipKey = "anti_crawler:ip:" + ip;
String userKey = "anti_crawler:user:" + userId;
// 检查IP请求频率
Long ipCount = redisTemplate.opsForValue().increment(ipKey);
if (ipCount == 1) {
redisTemplate.expire(ipKey, Duration.ofMinutes(1));
}
if (ipCount > 10) { // 1分钟内超过10次请求
return true;
}
// 检查用户请求频率
Long userCount = redisTemplate.opsForValue().increment(userKey);
if (userCount == 1) {
redisTemplate.expire(userKey, Duration.ofMinutes(1));
}
if (userCount > 5) { // 1分钟内超过5次请求
return true;
}
return false;
}
}
库存预热与管理
1. 库存预热
@Component
public class StockWarmUpService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@EventListener
public void handleSeckillStart(SeckillStartEvent event) {
// 预热秒杀库存
warmUpStock(event.getProductId(), event.getStockNum());
}
private void warmUpStock(Long productId, Integer stockNum) {
String luaScript = """
local stock_key = KEYS[1]
local stock_num = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])
redis.call('SET', stock_key, stock_num)
if expire_time > 0 then
redis.call('EXPIRE', stock_key, expire_time)
end
return stock_num
""";
String stockKey = "seckill:stock:" + productId;
List<String> keys = Arrays.asList(stockKey);
List<String> args = Arrays.asList(
String.valueOf(stockNum),
"7200" // 2小时过期
);
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
keys,
args.toArray(new String[0])
);
log.info("库存预热完成: productId={}, stock={}", productId, stockNum);
}
}
2. 库存监控
@Component
public class StockMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(fixedRate = 30000) // 每30秒检查一次
public void monitorStock() {
// 获取所有秒杀商品的库存
Set<String> keys = redisTemplate.keys("seckill:stock:*");
if (keys != null) {
for (String key : keys) {
String stockStr = (String) redisTemplate.opsForValue().get(key);
if (stockStr != null) {
int stock = Integer.parseInt(stockStr);
// 库存不足告警
if (stock < 10) {
log.warn("库存不足告警: key={}, stock={}", key, stock);
// 发送告警通知
}
}
}
}
}
}
超卖防护
1. 库存扣减验证
@Service
public class InventoryService {
public boolean validateAndDeductInventory(Long productId, Integer num) {
String luaScript = """
local stock_key = KEYS[1]
local buy_num = tonumber(ARGV[1])
local current_stock = redis.call('GET', stock_key)
if not current_stock then
return {0, '商品不存在'}
end
current_stock = tonumber(current_stock)
if current_stock < buy_num then
return {0, '库存不足'}
end
-- 使用CAS模式扣减库存
local new_stock = redis.call('DECRBY', stock_key, buy_num)
if new_stock < 0 then
-- 库存不够,回滚
redis.call('INCRBY', stock_key, buy_num)
return {0, '库存不足'}
end
return {1, '扣减成功', new_stock}
""";
String stockKey = "seckill:stock:" + productId;
List<String> keys = Arrays.asList(stockKey);
List<String> args = Arrays.asList(String.valueOf(num));
List<Object> result = (List<Object>) redisTemplate.execute(
new DefaultRedisScript<>(luaScript, List.class),
keys,
args.toArray(new String[0])
);
if (result != null && result.size() > 0) {
int code = Integer.parseInt(result.get(0).toString());
return code == 1;
}
return false;
}
}
性能优化策略
1. 连接池优化
spring:
redis:
lettuce:
pool:
max-active: 200 # 最大连接数
max-idle: 50 # 最大空闲连接
min-idle: 20 # 最小空闲连接
max-wait: 2000ms # 最大等待时间
timeout: 1000ms # 连接超时时间
2. 批量操作优化
@Service
public class BatchSeckillService {
public List<SeckillResult> batchSeckill(List<SeckillRequest> requests) {
// 使用Redis Pipeline批量执行
List<Object> results = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (SeckillRequest request : requests) {
String stockKey = "seckill:stock:" + request.getProductId();
String userKey = "seckill:user:" + request.getProductId() + ":user:" + request.getUserId();
// 执行Lua脚本
connection.eval(
seckillScript.getSha1().getBytes(),
2,
stockKey.getBytes(),
userKey.getBytes(),
String.valueOf(request.getNum()).getBytes(),
String.valueOf(request.getUserId()).getBytes(),
"1".getBytes()
);
}
return null;
}
);
// 处理结果
return processResults(results);
}
}
异常处理与补偿
1. 订单补偿机制
@Component
public class OrderCompensationService {
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkAndCompensate() {
// 检查Redis中成功但订单创建失败的秒杀记录
// 实现补偿逻辑
}
public void compensateFailedOrder(Long productId, Long userId, Integer num) {
// 补偿逻辑:回滚库存等
String stockKey = "seckill:stock:" + productId;
redisTemplate.opsForValue().increment(stockKey, num);
log.warn("补偿库存: productId={}, userId={}, num={}", productId, userId, num);
}
}
实际应用效果
通过Redis + Lua的秒杀系统,我们可以实现:
- 高并发处理:单机可支撑数万QPS
- 精准库存控制:防止超卖和重复购买
- 快速响应:毫秒级响应时间
- 可靠保障:原子性操作,数据一致性
注意事项
在实现秒杀系统时,需要注意以下几点:
- Redis集群:单机Redis存在单点故障,建议使用集群模式
- 内存管理:合理设置Redis内存限制,避免内存溢出
- 网络优化:确保应用服务器与Redis在同一机房
- 监控告警:建立完善的监控体系
- 安全防护:防止恶意刷单和攻击
最佳实践
- 预热库存:秒杀开始前预热Redis库存
- 分层防护:网关限流→应用限流→Redis限流
- 异步处理:秒杀成功后异步创建订单
- 降级策略:系统压力过大时快速降级
- 灰度发布:新功能先小范围验证
总结
Redis + Lua是实现高并发秒杀系统的绝佳方案,通过原子性的Lua脚本保证了数据的一致性,同时利用Redis的高性能特性支撑了高并发访问。
记住,秒杀系统的设计需要从多个维度考虑,既要保证高性能,也要确保数据准确,还要有完善的防护机制。
希望这篇文章对你有所帮助!如果你觉得有用,欢迎关注【服务端技术精选】公众号,获取更多后端技术干货。
标题:基于SpringBoot + Redis + Lua 实现高并发秒杀系统实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/23/1769148262044.html
0 评论