Week 4 · Chapter 1 · 系统设计场景题
复习难度:⭐⭐⭐⭐⭐ | 预计时长:6-8小时 | 重点程度:高
解题套路
1. 听清需求:不要急着画图,先问清楚
- QPS多少?用户量?峰值多少?
- 需要强一致还是最终一致?
- 功能范围边界是什么?
2. 算容量:
- 读多写少? → 缓存优先
- 写多读少? → 消息队列削峰
- 存储量 = 单条大小 × 条数 × 副本系数
3. 画出核心架构:
- 接入层 → 服务层 → 存储层
- 标注清楚关键决策点
4. 讲清 trade-off:
- 选了什么,放弃了什么
- 可能的瓶颈在哪里
场景1:设计审批流引擎(你简历里有)
需求拆解
- 多业务系统接入(20+)
- 4种审批模式:顺签/抢签/多人会签/组内抢签+组间会签
- 5000+员工,累计57万+审批单
- 需要规则引擎 + 时效管理 + 催办机制
核心数据模型
// 审批流程定义
type Workflow struct {
ID string
Name string
Nodes []Node // 审批节点列表
Edges []Edge // 流程连线
RuleID string // 关联规则引擎
}
// 审批节点
type Node struct {
ID string
Type string // START/APPROVER/COPY/END
ApproveType string // SINGLE/SIGN/RUSH/SIGN_RUSH
Assignees []string // 用户ID列表或用户组ID
TimeoutMs int64 // 超时时间(毫秒)
}
// 审批单
type ApprovalOrder struct {
ID string
WorkflowID string
Status string // PENDING/APPROVED/REJECTED/CANCELLED
CurrentNodeID string
CreatedAt time.Time
ExpiredAt time.Time // 时效截止时间
}
规则引擎设计
设计思路:
规则 = 条件(Condition)+ 动作(Action)
条件表达:
- 申请金额 > 10000 → 需要部门总监审批
- 申请人属于XX部门 → 路由到XX审批节点
- 时效超时72h → 自动催办
实现方案:
- 简单版:决策表(Decision Table)
- 中等:决策树(Decision Tree)
- 复杂:Drools(Java规则引擎) / 自己实现表达式引擎
表达式引擎示例(Go):
evaler := NewEvaluator("amount > 10000 && dept == 'IT'")
result, _ := evaler.Eval(map[string]any{"amount": 50000, "dept": "IT"})
高并发抢签(Redis分布式锁)
抢签 = 同一时间只一个人能签
实现:
1. SELECT FOR UPDATE 加行锁(DB层)
2. Redis SETNX + Lua 脚本原子判断
Redis 抢签:
key = "approval:rush:{orderId}"
Lua:
if redis.call('GET', key) then return 0 end
redis.call('SET', key, uid, 'EX', 300)
return 1
场景2:设计权限系统(RBAC)
双轨 RBAC 设计(你简历里有)
功能型角色:定义"能做什么"(如:审批管理员、查看员)
数据型角色:定义"能对谁做"(如:只看本部门数据)
绑定关系:
用户 → 功能角色 → 权限
用户 → 数据角色 → 数据范围
例:
用户A(属于IT部门)→ 功能角色=审批人 + 数据角色=IT部门可见
→ 只能审批IT部门提交的单据
权限快速鉴权
// 传统方式:用户→角色→权限,N次查询
func CheckPermission(userID, resource, action string) bool {
roles := db.GetRolesByUser(userID) // 查N次
perms := db.GetPermsByRoles(roles) // 再查N次
return perms.Contains(resource, action)
}
// 优化:权限数据加载到 Redis,毫秒级鉴权
func CheckPermissionCache(userID, resource, action string) bool {
key := fmt.Sprintf("perm:%s", userID)
perms := redis.Smembers(key)
permKey := fmt.Sprintf("%s:%s", resource, action)
return perms[permKey]
}
// 优化2:布隆过滤器快速过滤(不存在一定不存在,存在再做精确判断)
场景3:设计高并发抢单
需求:限时抢单,1000 QPS,库存有限
核心问题:
1. 库存超卖
2. 分布式会话
3. 下游服务压力
防超卖
// DB层:乐观锁
UPDATE inventory SET stock = stock - 1
WHERE id = ? AND stock > 0
// Redis层:Lua原子扣减
local stock = redis.call('GET', 'inventory:'..itemId)
if not stock or tonumber(stock) <= 0 then return 0 end
redis.call('DECR', 'inventory:'..itemId)
return 1
整体架构
限流网关 → 抢单服务 → Redis库存预扣 → 消息队里 → 异步下单
↓
库存不足 → 直接返回抢单失败
场景4:百万级零差错(你的简历亮点)
背景:游戏活动奖励发放,百万级用户参与
核心要求:
1. 奖励不能多发(超发)
2. 奖励不能少发(漏发)
3. 数据一致性
解法
1. 幂等发放:每个用户每个活动只有一条领取记录(UNIQUE KEY)
2. 事务保证:活动参与 + 记录创建在同一个事务里
3. 异步补偿:MQ消费失败 → 定时任务扫描未完成 → 重试
4. 对账机制:日终对账,统计应发/实发/差异
// 幂等发放核心逻辑
func AwardUser(tx *gorm.DB, userID, activityID string) error {
// 1. 幂等检查:是否已发放
var record ActivityAwardRecord
err := tx.Where("user_id=? AND activity_id=?", userID, activityID).First(&record).Error
if err == nil {
return nil // 已发放,直接返回
}
// 2. 事务保证:扣库存 + 写发放记录
err = tx.Transaction(func(tx *gorm.DB) error {
// 扣库存(乐观锁)
result := tx.Exec(`UPDATE activity_stock SET stock=stock-1
WHERE activity_id=? AND stock>0`, activityID)
if result.RowsAffected == 0 {
return errors.New("库存不足")
}
// 写发放记录
return tx.Create(&ActivityAwardRecord{
UserID: userID,
ActivityID: activityID,
Status: "AWARDED",
}).Error
})
return err
}
高频追问
Q:如何保证接口幂等性?
① 唯一键(DB唯一索引 / Redis SETNX) ② 每次操作带全局唯一请求ID,服务端去重 ③ Token 机制:先获取 Token,提交时校验 Token 并删除
Q:设计一个延迟任务队列?
① RabbitMQ 延时插件(最大2小时) ② RocketMQ 延时消息(定时投递) ③ Redis ZSet(score=执行时间,定时扫描) ④ 死信队列(TTL过期)