From 74fc46df627ce32576b21a28e5c0927adad6a56b Mon Sep 17 00:00:00 2001 From: yasinshaw Date: Sun, 8 Mar 2026 21:49:35 +0800 Subject: [PATCH] vault backup: 2026-03-08 21:49:35 --- .obsidian/workspace.json | 2 +- questions/07-系统设计/限购库存系统设计.md | 943 ++++++++++++++++++++++ 2 files changed, 944 insertions(+), 1 deletion(-) create mode 100644 questions/07-系统设计/限购库存系统设计.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index caeead3..c659d7e 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -198,6 +198,7 @@ }, "active": "fcbc762a80282002", "lastOpenFiles": [ + "questions/07-系统设计/限购库存系统设计.md", "questions/07-系统设计/交易撮合引擎突发流量处理.md", "16-LeetCode Hot 100/三数之和-改进版示例.md", "算法解题思路改进方案.md", @@ -228,7 +229,6 @@ "16-LeetCode Hot 100/翻转二叉树.md", "16-LeetCode Hot 100/二叉树的最大深度.md", "16-LeetCode Hot 100/二叉树的中序遍历.md", - "16-LeetCode Hot 100/最小栈.md", "16-LeetCode Hot 100", "00-项目概述", "questions/15-简历面试", diff --git a/questions/07-系统设计/限购库存系统设计.md b/questions/07-系统设计/限购库存系统设计.md new file mode 100644 index 0000000..05f3ad7 --- /dev/null +++ b/questions/07-系统设计/限购库存系统设计.md @@ -0,0 +1,943 @@ +# 限购库存系统设计 + +## 题目描述 + +设计一个限购库存系统,用于处理商品秒杀、限购等场景。 + +**业务场景**: +- 某商品库存有限(如1000件) +- 用户抢购热情高涨,短时间内大量请求 +- 每个用户有购买数量限制(如每人最多买3件) +- 需要防止超卖、刷单、恶意攻击 + +**核心需求**: +1. 保证库存准确性,不能超卖 +2. 支持高并发,QPS 达到 10万+ +3. 防止用户刷单、恶意抢购 +4. 保证系统可用性和数据一致性 +5. 提供良好的用户体验 + +**问题**: +1. 如何设计库存扣减方案? +2. 如何防止超卖? +3. 如何处理高并发? +4. 如何防止刷单? +5. 如何保证数据一致性? + +--- + +## 思路推导 + +### 问题分析 + +**核心挑战**: +1. **超卖问题**:库存1000件,卖了1200件 +2. **性能问题**:10万QPS下系统不崩溃 +3. **公平性问题**:防止机器刷单 +4. **一致性问题**:分布式环境下的数据一致性 + +**为什么难?** +- 高并发 + 库存扣减 = 严重的锁竞争 +- 分布式环境 + 数据一致性 = CAP权衡 +- 用户体验 + 安全防护 = 难以平衡 + +### 为什么这样思考? + +**核心原则**:**性能 > 一致性 > 用户体验** + +就像演唱会抢票一样: +- 先到先得(性能优先) +- 不允许卖出比座位多的票(准确性) +- 防止黄牛刷票(安全性) + +--- + +## 解题思路 + +### 核心思想 + +**多级缓存 + 异步扣减 + 分布式锁 + 限流防护** + +``` +┌─────────────────────────────────────────────────┐ +│ 用户请求 │ +└──────────────────┬──────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ 1. 限流防护层 │ ← 防止刷单、恶意攻击 + │ - 用户限流 │ + │ - IP限流 │ + │ - 验证码 │ + └────────┬─────────┘ + │ + ▼ + ┌─────────────────┐ + │ 2. 预热层 │ ← 减轻数据库压力 + │ - Redis库存 │ + │ - 本地缓存 │ + └────────┬─────────┘ + │ + ▼ + ┌─────────────────┐ + │ 3. 防刷层 │ ← 防止机器刷单 + │ - 用户行为分析 │ + │ - 风控规则 │ + └────────┬─────────┘ + │ + ▼ + ┌─────────────────┐ + │ 4. 库存扣减层 │ ← 核心逻辑 + │ - Redis原子操作│ + │ - 分布式锁 │ + │ - 数据库乐观锁 │ + └────────┬─────────┘ + │ + ▼ + ┌─────────────────┐ + │ 5. 订单处理层 │ ← 异步处理 + │ - 消息队列 │ + │ - 订单落地 │ + └─────────────────┘ +``` + +--- + +## 详细方案 + +### 方案一:Redis + Lua 脚本(推荐) + +**核心思路**:利用 Redis 的原子性操作,保证库存扣减的准确性 + +#### 1.1 数据结构设计 + +```go +// 库存信息 +type StockInfo struct { + ProductID string // 商品ID + TotalStock int64 // 总库存 + AvailableStock int64 // 可用库存 + SoldStock int64 // 已售库存 + Price float64 // 价格 + LimitPerUser int // 每人限购 +} + +// 用户购买记录 +type UserPurchase struct { + UserID string + ProductID string + Quantity int + OrderTime time.Time +} +``` + +#### 1.2 Redis 数据结构 + +```redis +# 商品库存 +product:{productID}:stock = 1000 + +# 用户购买数量 +user:{userID}:product:{productID}:qty = 2 + +# 购买记录(用于防刷) +product:{productID}:purchase_set = {userID1, userID2, ...} + +# 分布式锁 +lock:product:{productID} +``` + +#### 1.3 Lua 脚本实现 + +```lua +-- 扣减库存的 Lua 脚本 +local function deduct_stock(product_id, user_id, quantity) + -- 1. 获取商品库存 + local stock_key = "product:" .. product_id .. ":stock" + local stock = tonumber(redis.call('GET', stock_key)) + + -- 2. 检查库存是否充足 + if stock < quantity then + return {err = "insufficient_stock", remaining = stock} + end + + -- 3. 获取用户已购买数量 + local user_qty_key = "user:" .. user_id .. ":product:" .. product_id .. ":qty" + local user_qty = tonumber(redis.call('GET', user_qty_key)) or 0 + + -- 4. 检查是否超过限购 + local limit_key = "product:" .. product_id .. ":limit" + local limit_per_user = tonumber(redis.call('GET', limit_key)) or 1 + + if user_qty + quantity > limit_per_user then + return {err = "exceed_limit", user_qty = user_qty, limit = limit_per_user} + end + + -- 5. 扣减库存(原子操作) + redis.call('DECRBY', stock_key, quantity) + + -- 6. 更新用户购买数量 + redis.call('INCRBY', user_qty_key, quantity) + redis.call('EXPIRE', user_qty_key, 3600) -- 1小时过期 + + -- 7. 记录购买用户 + local purchase_set = "product:" .. product_id .. ":purchase_set" + redis.call('SADD', purchase_set, user_id) + redis.call('EXPIRE', purchase_set, 86400) -- 24小时过期 + + return {ok = true, remaining = stock - quantity} +end + +-- 执行脚本 +return deduct_stock(KEYS[1], ARGV[1], ARGV[2]) +``` + +#### 1.4 Go 实现 + +```go +type StockService struct { + redis *redis.Client + script *redis.Script +} + +func NewStockService(redis *redis.Client) *StockService { + script := redis.NewScript(` + local product_id = KEYS[1] + local user_id = ARGV[1] + local quantity = tonumber(ARGV[2]) + + local stock_key = "product:" .. product_id .. ":stock" + local stock = tonumber(redis.call("GET", stock_key)) + + if stock == nil then + return {err = "product_not_found"} + end + + if stock < quantity then + return {err = "insufficient_stock", remaining = stock} + end + + local user_qty_key = "user:" .. user_id .. ":product:" .. product_id .. ":qty" + local user_qty = tonumber(redis.call("GET", user_qty_key)) or 0 + + local limit_key = "product:" .. product_id .. ":limit" + local limit_per_user = tonumber(redis.call("GET", limit_key)) or 1 + + if user_qty + quantity > limit_per_user then + return {err = "exceed_limit", user_qty = user_qty, limit = limit_per_user} + end + + redis.call("DECRBY", stock_key, quantity) + redis.call("INCRBY", user_qty_key, quantity) + redis.call("EXPIRE", user_qty_key, 3600) + + local purchase_set = "product:" .. product_id .. ":purchase_set" + redis.call("SADD", purchase_set, user_id) + redis.call("EXPIRE", purchase_set, 86400) + + return {ok = true, remaining = stock - quantity} + `) + + return &StockService{ + redis: redis, + script: script, + } +} + +func (s *StockService) DeductStock(ctx context.Context, productID, userID string, quantity int) (*StockResult, error) { + keys := []string{productID} + values := []interface{}{userID, quantity} + + result, err := s.script.Run(ctx, s.redis, keys, values).Result() + if err != nil { + return nil, err + } + + // 解析结果 + resultMap, ok := result.(map[string]interface{}) + if !ok { + return nil, errors.New("invalid result type") + } + + stockResult := &StockResult{} + if errMsg, exists := resultMap["err"]; exists { + stockResult.Error = errMsg.(string) + if errMsg == "insufficient_stock" { + stockResult.Remaining = int(resultMap["remaining"].(int64)) + } + } else if _, exists := resultMap["ok"]; exists { + stockResult.Success = true + stockResult.Remaining = int(resultMap["remaining"].(int64)) + } + + return stockResult, nil +} + +type StockResult struct { + Success bool + Error string + Remaining int +} +``` + +--- + +### 方案二:数据库乐观锁 + +**核心思路**:利用数据库的原子性和行锁,保证数据一致性 + +#### 2.1 表设计 + +```sql +-- 商品库存表 +CREATE TABLE products ( + product_id BIGINT PRIMARY KEY, + total_stock INT NOT NULL, + available_stock INT NOT NULL, + version INT NOT NULL DEFAULT 0, -- 乐观锁版本号 + limit_per_user INT NOT NULL DEFAULT 1, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_available_stock (available_stock) +) ENGINE=InnoDB; + +-- 用户购买记录表 +CREATE TABLE user_purchases ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + quantity INT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_product (user_id, product_id), -- 防止重复购买 + INDEX idx_product (product_id) +) ENGINE=InnoDB; + +-- 订单表 +CREATE TABLE orders ( + order_id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + quantity INT NOT NULL, + status TINYINT NOT NULL DEFAULT 0, -- 0:待支付, 1:已支付, 2:已取消 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_product (user_id, product_id), + INDEX idx_status (status) +) ENGINE=InnoDB; +``` + +#### 2.2 乐观锁实现 + +```go +func (s *StockService) DeductStockWithOptimisticLock(ctx context.Context, productID, userID int64, quantity int) error { + return s.db.Transaction(ctx, func(tx *gorm.DB) error { + // 1. 查询商品信息(带锁) + var product Product + err := tx.Set("gorm:query_option", "FOR UPDATE"). // 行锁 + Where("product_id = ?", productID). + First(&product).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("product not found") + } + return err + } + + // 2. 检查库存 + if product.AvailableStock < quantity { + return errors.New("insufficient stock") + } + + // 3. 检查用户购买数量 + var totalPurchase int64 + err = tx.Model(&UserPurchase{}). + Where("user_id = ? AND product_id = ?", userID, productID). + Select("COALESCE(SUM(quantity), 0)"). + Scan(&totalPurchase).Error + + if err != nil { + return err + } + + if int(totalPurchase)+quantity > product.LimitPerUser { + return errors.New("exceed purchase limit") + } + + // 4. 扣减库存(乐观锁) + result := tx.Model(&Product{}). + Where("product_id = ? AND available_stock >= ? AND version = ?", + productID, quantity, product.Version). + Updates(map[string]interface{}{ + "available_stock": gorm.Expr("available_stock - ?", quantity), + "version": gorm.Expr("version + 1"), + }) + + if result.RowsAffected == 0 { + return errors.New("stock update failed, please retry") + } + + // 5. 记录购买信息 + purchase := &UserPurchase{ + UserID: userID, + ProductID: productID, + Quantity: quantity, + } + + if err := tx.Create(purchase).Error; err != nil { + return err + } + + return nil + }) +} +``` + +**问题**:数据库压力大,不适合超高并发场景 + +--- + +### 方案三:分布式锁 + Redis + +**核心思路**:使用分布式锁保证并发安全 + +#### 3.1 Redis 分布式锁 + +```go +type DistributedLock struct { + redis *redis.Client + key string + value string + expire time.Duration +} + +func (d *DistributedLock) Lock(ctx context.Context) error { + // 使用 SETNX 实现 + ok, err := d.redis.SetNX(ctx, d.key, d.value, d.expire).Result() + if err != nil { + return err + } + + if !ok { + return errors.New("lock already held") + } + + return nil +} + +func (d *DistributedLock) Unlock(ctx context.Context) error { + // 使用 Lua 脚本保证原子性 + script := ` + if redis.call("get", KEYS[1]) == ARGV[1] then + return redis.call("del", KEYS[1]) + end + return 0 + ` + + _, err := d.redis.Eval(ctx, script, []string{d.key}, []string{d.value}).Result() + return err +} + +// 使用分布式锁扣减库存 +func (s *StockService) DeductStockWithLock(ctx context.Context, productID, userID string, quantity int) error { + lockKey := fmt.Sprintf("lock:product:%s", productID) + lock := &DistributedLock{ + redis: s.redis, + key: lockKey, + value: userID, + expire: 5 * time.Second, + } + + // 获取锁 + if err := lock.Lock(ctx); err != nil { + return err + } + defer lock.Unlock(ctx) + + // 扣减库存逻辑 + stockKey := fmt.Sprintf("product:%s:stock", productID) + result, err := s.redis.DecrBy(ctx, stockKey, quantity).Result() + + if err != nil { + return err + } + + if result < 0 { + return errors.New("insufficient stock") + } + + return nil +} +``` + +--- + +### 方案四:限流防护 + +**核心思路**:在扣减库存前进行多层限流 + +#### 4.1 用户级限流 + +```go +type RateLimiter struct { + redis *redis.Client +} + +// 滑动窗口限流 +func (r *RateLimiter) AllowUser(ctx context.Context, userID string, maxRequests int, window time.Duration) bool { + key := fmt.Sprintf("ratelimit:user:%s", userID) + + now := time.Now().UnixNano() + windowStart := now - window.Nanoseconds() + + // 使用 Redis Sorted Set 实现滑动窗口 + pipe := r.redis.Pipeline() + + // 移除窗口外的记录 + pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10)) + + // 添加当前请求 + pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: userID}) + + // 设置过期时间 + pipe.Expire(ctx, key, window) + + // 统计窗口内请求数 + pipe.ZCard(ctx, key) + + results, err := pipe.Exec(ctx) + if err != nil { + return false + } + + count := results[3].(*redis.IntCmd).Val() + + return count <= int64(maxRequests) +} +``` + +#### 4.2 验证码 + +```go +func (s *StockService) VerifyCaptcha(ctx context.Context, userID, ticket, captcha string) bool { + key := fmt.Sprintf("captcha:%s:%s", userID, ticket) + + stored, err := s.redis.Get(ctx, key).Result() + if err != nil { + return false + } + + // 验证码一次性使用 + s.redis.Del(ctx, key) + + return stored == captcha +} +``` + +--- + +### 方案五:防刷策略 + +**核心思路**:多维度识别和防止机器刷单 + +#### 5.1 用户行为分析 + +```go +type AntiSpamService struct { + redis *redis.Client +} + +// 检查用户行为风险 +func (a *AntiSpamService) CheckUserRisk(ctx context.Context, userID string) (risk float64, reason string) { + risk := 0.0 + + // 1. 检查购买频率 + purchaseCount, _ := a.redis.Get(ctx, fmt.Sprintf("user:%s:purchase_count:1h", userID)).Int() + if purchaseCount > 100 { + risk += 0.5 + reason = "高频购买" + } + + // 2. 检查注册时间 + registerTime, _ := a.redis.Get(ctx, fmt.Sprintf("user:%s:register_time", userID)).Int64() + if time.Now().Unix()-registerTime < 86400 { // 注册不到1天 + risk += 0.3 + if reason == "" { + reason = "新用户" + } + } + + // 3. 检查设备指纹 + deviceCount, _ := a.redis.Get(ctx, fmt.Sprintf("user:%s:device_count", userID)).Int() + if deviceCount > 5 { + risk += 0.4 + if reason == "" { + reason = "多设备" + } + } + + // 4. 检查IP关联账号数 + ipKey := fmt.Sprintf("ip:%s:user_count", getUserIP(ctx)) + ipUserCount, _ := a.redis.Get(ctx, ipKey).Int() + if ipUserCount > 10 { + risk += 0.3 + if reason == "" { + reason = "同IP多账号" + } + } + + return risk, reason +} + +// 是否允许购买 +func (a *AntiSpamService) AllowPurchase(ctx context.Context, userID string) (bool, string) { + risk, reason := a.CheckUserRisk(ctx, userID) + + if risk > 0.7 { + // 高风险:直接拒绝 + return false, "高风险用户:" + reason + } + + if risk > 0.4 { + // 中风险:要求验证码 + return false, "需要验证码" + } + + return true, "" +} +``` + +#### 5.2 黑名单机制 + +```go +type BlacklistService struct { + redis *redis.Client +} + +// 检查是否在黑名单 +func (b *BlacklistService) IsBlacklisted(ctx context.Context, userID string) (bool, string) { + // 1. 用户黑名单 + if exists, _ := b.redis.SIsMember(ctx, "blacklist:user", userID).Result(); exists { + return true, "用户黑名单" + } + + // 2. IP黑名单 + ip := getUserIP(ctx) + if exists, _ := b.redis.SIsMember(ctx, "blacklist:ip", ip).Result(); exists { + return true, "IP黑名单" + } + + // 3. 设备黑名单 + deviceID := getDeviceID(ctx) + if exists, _ := b.redis.SIsMember(ctx, "blacklist:device", deviceID).Result(); exists { + return true, "设备黑名单" + } + + return false, "" +} +``` + +--- + +### 方案六:库存预加载与预热 + +**核心思路**:提前将库存加载到 Redis,减轻数据库压力 + +#### 6.1 预热库存 + +```go +func (s *StockService) PreloadStock(ctx context.Context, productID string) error { + // 1. 从数据库加载库存 + var product Product + err := s.db.Where("product_id = ?", productID).First(&product).Error + if err != nil { + return err + } + + // 2. 加载到 Redis + stockKey := fmt.Sprintf("product:%s:stock", productID) + limitKey := fmt.Sprintf("product:%s:limit", productID) + + pipe := s.redis.Pipeline() + pipe.Set(ctx, stockKey, product.AvailableStock, 24*time.Hour) + pipe.Set(ctx, limitKey, product.LimitPerUser, 24*time.Hour) + + // 3. 加载用户购买记录 + var purchases []UserPurchase + s.db.Where("product_id = ? AND created_at > ?", productID, time.Now().Add(-24*time.Hour)). + Find(&purchases) + + for _, purchase := range purchases { + userQtyKey := fmt.Sprintf("user:%d:product:%s:qty", purchase.UserID, productID) + pipe.Set(ctx, userQtyKey, purchase.Quantity, time.Hour) + } + + _, err = pipe.Exec(ctx) + return err +} + +// 批量预热多个商品 +func (s *StockService) PreloadStocks(ctx context.Context, productIDs []string) error { + for _, productID := range productIDs { + if err := s.PreloadStock(ctx, productID); err != nil { + log.Errorf("Failed to preload stock for product %s: %v", productID, err) + } + } + return nil +} +``` + +#### 6.2 定时同步 + +```go +// 定时将 Redis 中的数据同步回数据库 +func (s *StockService) SyncToDatabase(ctx context.Context) error { + // 1. 获取所有商品库存 + keys, _, err := s.redis.Scan(ctx, "product:*:stock").Result() + if err != nil { + return err + } + + for _, key := range keys { + // 2. 获取 Redis 中的库存 + stock, _ := s.redis.Get(ctx, key).Int() + + // 3. 提取 productID + productID := strings.TrimPrefix(key, "product:") + productID = strings.TrimSuffix(productID, ":stock") + + // 4. 更新数据库 + s.db.Model(&Product{}). + Where("product_id = ?", productID). + Update("available_stock", stock) + } + + return nil +} +``` + +--- + +### 方案七:订单异步处理 + +**核心思路**:库存扣减后,异步创建订单 + +#### 7.1 消息队列 + +```go +type OrderMessage struct { + OrderID string `json:"order_id"` + UserID string `json:"user_id"` + ProductID string `json:"product_id"` + Quantity int `json:"quantity"` + Timestamp time.Time `json:"timestamp"` +} + +func (s *StockService) DeductStockAsync(ctx context.Context, productID, userID string, quantity int) (*OrderResult, error) { + // 1. 扣减库存(同步) + result, err := s.DeductStock(ctx, productID, userID, quantity) + if err != nil { + return nil, err + } + + // 2. 发送订单创建消息(异步) + orderID := generateOrderID() + orderMsg := &OrderMessage{ + OrderID: orderID, + UserID: userID, + ProductID: productID, + Quantity: quantity, + Timestamp: time.Now(), + } + + msgBytes, _ := json.Marshal(orderMsg) + if err := s.kafka.SendMessage("orders", msgBytes); err != nil { + // 消息发送失败,回滚库存 + s.rollbackStock(ctx, productID, userID, quantity) + return nil, err + } + + return &OrderResult{ + OrderID: orderID, + Success: true, + }, nil +} + +func (s *StockService) rollbackStock(ctx context.Context, productID, userID string, quantity int) { + stockKey := fmt.Sprintf("product:%s:stock", productID) + userQtyKey := fmt.Sprintf("user:%s:product:%s:qty", userID, productID) + + pipe := s.redis.Pipeline() + pipe.IncrBy(ctx, stockKey, quantity) + pipe.DecrBy(ctx, userQtyKey, quantity) + pipe.Exec(ctx) +} +``` + +--- + +## 实战案例 + +### 案例1:秒杀活动 + +**场景**: +- 商品:iPhone 15 Pro,库存1000台 +- 限购:每人1台 +- 预估QPS:10万 + +**处理流程**: + +``` +1. 活动前预热(T-10分钟) + ├─ 预热库存到 Redis + ├─ 预热用户购买记录 + └─ 启动监控告警 + +2. 活动开始(T=0) + ├─ 10万QPS 请求涌入 + ├─ 第一层:用户限流(每秒1次) + │ └─ 拦截 90% 请求 → 剩余 1万QPS + ├─ 第二层:验证码验证 + │ └─ 拦截 50% 请求 → 剩余 5千QPS + ├─ 第三层:Redis Lua 扣减库存 + │ └─ 原子操作,保证准确 + └─ 第四层:异步创建订单 + └─ Kafka 消息队列 + +3. 活动进行中(T+1分钟) + ├─ 库存:1000 → 0 + ├─ Redis 压力:正常 + ├─ 数据库压力:低(异步写入) + └─ 订单队列:5000 个待处理 + +4. 活动结束后 + ├─ 停止接收新订单 + ├─ 处理剩余订单 + ├─ 同步数据到数据库 + └─ 生成销售报表 +``` + +### 案例2:黄牛刷单检测 + +**场景**: +- 某用户使用脚本在1秒内发起100次请求 +- IP地址相同 +- 设备指纹相同 + +**处理流程**: + +``` +1. 监控检测异常 + ├─ 用户购买频率:100次/秒 + ├─ IP地址:单一来源 + └─ 触发告警 + +2. 风控规则匹配 + ├─ 规则1:1分钟内购买>10次 → 高风险 + ├─ 规则2:同IP多账号>5个 → 高风险 + └─ 规则3:新用户大额购买 → 中风险 + +3. 自动处理 + ├─ 加入黑名单(24小时) + ├─ 冻结订单 + ├─ 释放库存 + └─ 通知风控团队 + +4. 人工审核 + ├─ 检查用户历史行为 + ├─ 确认是否为恶意刷单 + └─ 决定是否永久封禁 +``` + +--- + +## P7 加分项 + +### 深度理解 + +1. **为什么选择 Redis 而不是数据库?** + - 性能:Redis QPS 可达 10万+,MySQL 只有几千 + - 原子性:Redis Lua 脚本保证原子操作 + - 轻量级:内存操作,比磁盘IO快得多 + +2. **如何保证 Redis 和数据库一致性?** + - 最终一致性:允许短暂的不一致 + - 定时同步:定时将 Redis 数据同步到数据库 + - 对账系统:定时比对 Redis 和数据库数据 + - 补偿机制:发现不一致时自动修复 + +3. **如何处理网络分区?** + - CAP权衡:选择 AP(可用性 + 分区容错) + - Redis Cluster:多主复制,保证高可用 + - 降级策略:Redis 不可用时降级为数据库 + +### 实战扩展 + +**相关技术**: +- Redis:缓存、分布式锁 +- Kafka:消息队列、异步处理 +- Hystrix:熔断器 +- Sentinel:限流熔断 +- Prometheus:监控告警 + +**最佳实践**: +1. 预热:提前加载库存到 Redis +2. 限流:多层限流保护 +3. 异步:订单异步处理 +4. 监控:实时监控告警 +5. 降级:异常情况降级处理 + +### 变体问题 + +1. **如何支持多种商品同时秒杀?** + - 为每个商品单独设置库存 key + - 使用 Redis Cluster 分片 + - 不同商品使用不同的 key 前缀 + +2. **如何处理库存回滚?** + - 订单超时未支付自动回滚 + - 使用延迟消息队列 + - 设置订单 TTL + +3. **如何支持预约抢购?** + - 预售阶段:只允许预约,不扣减库存 + - 抢购阶段:按预约顺序扣减库存 + - 使用 Sorted Set 实现排队 + +4. **如何防止黄牛倒卖?** + - 实名制购买 + - 限制转卖时间(如7天内不可转卖) + - 价格波动策略(价格逐步上涨) + +--- + +## 总结 + +**核心要点**: + +1. **Redis + Lua**:原子操作,保证库存准确性 +2. **多层限流**:防止系统过载 +3. **异步处理**:订单异步创建,提高性能 +4. **防刷策略**:多维度识别刷单行为 +5. **预热机制**:提前加载库存,减轻压力 + +**技术栈**: + +- 缓存:Redis(库存、用户记录) +- 消息队列:Kafka(异步订单) +- 限流:Redis + 滑动窗口 +- 分布式锁:Redis SETNX +- 监控:Prometheus + Grafana + +**关键指标**: + +- QPS:10万+ +- 库存准确性:100% +- 响应时间:P99 < 100ms +- 系统可用性:99.99% + +**易错点**: + +- 忘记使用 Lua 脚本导致并发问题 +- 过度依赖数据库导致性能瓶颈 +- 忽视防刷策略导致黄牛刷单 +- 没有降级预案导致系统雪崩 +- 忘记异步处理导致响应慢