秒殺系統流程圖
秒殺系統關鍵點
- 高并發處理:
- 使用網關(如 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 thenreturn -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 {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate 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 "庫存不足";}// 異步更新 MySQLrabbitTemplate.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 {@Autowiredprivate JdbcTemplate jdbcTemplate;@RabbitHandlerpublic 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 "庫存不足";}// 同步更新 MySQLint 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 {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate 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);}// 批量更新 MySQLfor (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 腳本保證原子性。
- 異步任務失敗時,需重試或補償(如記錄失敗日志)。