画秒杀系统流程图
秒杀系统流程图
秒杀系统关键点
- 高并发处理:
- 使用网关(如 Nginx)进行流量限流,避免过载。
- 分布式锁或 Redis 原子操作控制并发。
- 活动状态检查:
- Redis 存储活动状态(如 seckill:activity:1:status),快速判断活动是否进行中。
- 用户资格校验:
- Redis Set 记录参与用户(如 seckill:activity:1:users),检查是否重复参与。
- 示例: SADD seckill:activity:1:users user123 和 SISMEMBER。
- 库存扣减(Redis Lua 脚本):
- 为什么用 Lua 脚本?
- 保证原子性,避免并发超卖。
- 减少网络往返,提高性能。
- Redis Key: seckill:activity:1:stock(库存)。
- Lua 脚本示例:
local stock_key = KEYS[1]
local current_stock = tonumber(redis.call('GET', stock_key) or 0)
if current_stock <= 0 then
return -1 -- 库存不足
end
redis.call('DECR', stock_key)
return current_stock - 1 -- 返回剩余库存
- Java 调用 Lua 脚本(Spring Boot + Redis):
@Autowired
private StringRedisTemplate redisTemplate;
public boolean deductStock(String activityId) {
String stockKey = "seckill:activity:" + activityId + ":stock";
String script = "local stock_key = KEYS[1] " +
"local current_stock = tonumber(redis.call('GET', stock_key) or 0) " +
"if current_stock <= 0 then return -1 end " +
"redis.call('DECR', stock_key) " +
"return current_stock - 1";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(stockKey)
);
return result != null && result >= 0;
}
- 订单生成:
- 异步队列(如 RabbitMQ、Kafka)处理订单生成,减轻数据库压力。
- 示例: 将 {userId, activityId, timestamp} 发送到队列。
- 数据库写入:
- 异步任务消费队列,批量插入订单到 MySQL。
- 避免实时写库导致瓶颈。
- 防超卖:
- Redis Lua 脚本确保库存不减为负。
- 数据库加乐观锁(如 UPDATE stock SET count = count - 1 WHERE id = ? AND count > 0)。
- 返回响应:
- 扣减成功后立即返回“秒杀成功”,后续操作异步完成。
完整流程伪代码
@RestController
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/seckill/{activityId}")
public String seckill(@PathVariable String activityId, @RequestParam String userId) {
// 1. 检查活动状态
String status = redisTemplate.opsForValue().get("seckill:activity:" + activityId + ":status");
if (!"ongoing".equals(status)) {
return "活动未开始或已结束";
}
// 2. 检查用户资格
if (redisTemplate.opsForSet().isMember("seckill:activity:" + activityId + ":users", userId)) {
return "已参与秒杀";
}
// 3. 扣减库存 (Lua 脚本)
if (!deductStock(activityId)) {
return "库存不足";
}
// 4. 标记用户参与
redisTemplate.opsForSet().add("seckill:activity:" + activityId + ":users", userId);
// 5. 异步生成订单
rabbitTemplate.convertAndSend("seckill-queue",
new OrderMessage(userId, activityId, System.currentTimeMillis()));
return "秒杀成功";
}
}
补充:
redis减扣后 减扣 MySQL 库存方案
1. 异步减扣 MySQL 库存(推荐)
- 时机
- Redis 减库存成功后,将任务发送到异步队列(如 RabbitMQ、Kafka),由后台消费者异步更新 MySQL 库存。
- 流程
- 用户发起秒杀请求。
- Redis Lua 脚本扣减库存(原子操作)。
- 扣减成功后:
- 发送消息到队列(如 {activityId, userId, timestamp})。
- 返回“秒杀成功”给前端。
- 队列消费者异步处理:
- 更新 MySQL 库存表。
- 生成订单记录。
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/seckill/{activityId}")
public String seckill(@PathVariable String activityId, @RequestParam String userId) {
// Redis 减库存
if (!deductStock(activityId)) {
return "库存不足";
}
// 异步更新 MySQL
rabbitTemplate.convertAndSend("seckill-queue",
new OrderMessage(activityId, userId, System.currentTimeMillis()));
return "秒杀成功";
}
// Lua 脚本扣库存
private boolean deductStock(String activityId) {
String stockKey = "seckill:stock:" + activityId;
String script = "local stock = tonumber(redis.call('GET', KEYS[1]) or 0) " +
"if stock <= 0 then return 0 end " +
"redis.call('DECR', KEYS[1]) " +
"return 1";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(stockKey)
);
return result != null && result == 1;
}
// 队列消费者
@Component
@RabbitListener(queues = "seckill-queue")
public class SeckillConsumer {
@Autowired
private JdbcTemplate jdbcTemplate;
@RabbitHandler
public void process(OrderMessage msg) {
// 更新 MySQL 库存
String sql = "UPDATE seckill_stock SET stock = stock - 1 WHERE activity_id = ? AND stock > 0";
int updated = jdbcTemplate.update(sql, msg.getActivityId());
if (updated > 0) {
// 插入订单
jdbcTemplate.update("INSERT INTO seckill_order (activity_id, user_id, create_time) VALUES (?, ?, ?)",
msg.getActivityId(), msg.getUserId(), msg.getTimestamp());
}
}
}
优点
- 高性能: Redis 减库存后立即返回,MySQL 异步处理,避免实时写库瓶颈。
- 高并发: 适合秒杀场景,减少数据库压力。
缺点
- 数据一致性: Redis 和 MySQL 可能短暂不一致(最终一致性)。
- 失败处理: 队列消费失败需重试或补偿。
- 适用场景
- 高并发秒杀,优先保证响应速度。
2. 同步减扣 MySQL 库存
- 时机
- Redis 减库存成功后,在同一事务中同步更新 MySQL 库存。
- 流程
- 用户发起秒杀请求。
- Redis Lua 脚本扣减库存。
- 扣减成功后:
- 立即更新 MySQL 库存。
- 生成订单。
- 返回“秒杀成功”。
@PostMapping("/seckill/{activityId}")
@Transactional
public String seckill(@PathVariable String activityId, @RequestParam String userId) {
// Redis 减库存
if (!deductStock(activityId)) {
return "库存不足";
}
// 同步更新 MySQL
int updated = jdbcTemplate.update(
"UPDATE seckill_stock SET stock = stock - 1 WHERE activity_id = ? AND stock > 0",
activityId
);
if (updated == 0) {
// 回滚 Redis(可选)
redisTemplate.opsForValue().increment("seckill:stock:" + activityId);
return "库存不足";
}
// 插入订单
jdbcTemplate.update("INSERT INTO seckill_order (activity_id, user_id, create_time) VALUES (?, ?, ?)",
activityId, userId, System.currentTimeMillis());
return "秒杀成功";
}
优点
- 强一致性: Redis 和 MySQL 库存保持同步。
- 简单: 无需异步队列。
缺点
- 性能瓶颈: MySQL 写操作耗时,影响并发能力。
- 回滚复杂: 如果 MySQL 更新失败,需回滚 Redis。
- 适用场景
- 低并发场景,或对数据一致性要求极高。
3. 延迟减扣 MySQL 库存(定时同步)
- 时机
- Redis 减库存后,通过定时任务(如每分钟)批量同步 MySQL 库存。
- 流程
- Redis 减库存。
- 记录每次扣减的日志(如 Redis List seckill:stock:log)。
- 定时任务读取日志,批量更新 MySQL。
- 实现示例
// 秒杀接口
@PostMapping("/seckill/{activityId}")
public String seckill(@PathVariable String activityId, @RequestParam String userId) {
if (!deductStock(activityId)) {
return "库存不足";
}
// 记录日志
redisTemplate.opsForList().leftPush("seckill:stock:log",
activityId + "," + userId + "," + System.currentTimeMillis());
return "秒杀成功";
}
// 定时任务
@Component
@EnableScheduling
public class StockSyncTask {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@Scheduled(fixedRate = 60000) // 每分钟
public void syncStock() {
List<String> logs = redisTemplate.opsForList().range("seckill:stock:log", 0, -1);
if (logs != null && !logs.isEmpty()) {
Map<String, Integer> stockUpdates = new HashMap<>();
for (String log : logs) {
String[] parts = log.split(",");
String activityId = parts[0];
stockUpdates.merge(activityId, 1, Integer::sum);
}
// 批量更新 MySQL
for (Map.Entry<String, Integer> entry : stockUpdates.entrySet()) {
jdbcTemplate.update(
"UPDATE seckill_stock SET stock = stock - ? WHERE activity_id = ?",
entry.getValue(), entry.getKey()
);
}
redisTemplate.opsForList().trim("seckill:stock:log", logs.size(), -1); // 清空已处理日志
}
}
}
优点
- 性能优化: 批量处理,减少 MySQL 频繁写。
- 容错: 日志记录便于排查。
缺点
- 一致性延迟: MySQL 库存更新有延迟。
- 复杂性: 需维护日志和定时任务。
- 适用场景
- 中等并发,允许短暂不一致。
选择依据
方案 | MySQL减库存时机 | 一致性 | 性能 | 复杂度 | 适用场景 |
---|---|---|---|---|---|
异步减扣 | Redis 后异步队列 | 最终一致 | 高 | 中 | 高并发秒杀 |
同步减扣 | Redis 后立即同步 | 强一致 | 低 | 低 | 低并发强一致性 |
延迟减扣 | Redis 后定时批量 | 延迟一致 | 中 | 高 | 中等并发可接受延迟 |
推荐方案
- 高并发秒杀: 采用异步减扣。
- Redis 负责实时库存控制,MySQL 异步更新。
- 通过队列解耦,确保高吞吐量。
- 关键点:
- Redis Lua 脚本保证原子性。
- 异步任务失败时,需重试或补偿(如记录失败日志)。