944 lines
25 KiB
Markdown
944 lines
25 KiB
Markdown
# 限购库存系统设计
|
||
|
||
## 题目描述
|
||
|
||
设计一个限购库存系统,用于处理商品秒杀、限购等场景。
|
||
|
||
**业务场景**:
|
||
- 某商品库存有限(如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 脚本导致并发问题
|
||
- 过度依赖数据库导致性能瓶颈
|
||
- 忽视防刷策略导致黄牛刷单
|
||
- 没有降级预案导致系统雪崩
|
||
- 忘记异步处理导致响应慢
|