风险驱动设计-RDD
一、什么是风险驱动设计
1. 定义
风险驱动设计:架构设计时,优先识别系统风险点,以化解、缓解风险作为架构决策第一驱动力,而不是上来就堆砌功能、套流行框架。
核心理念:
架构的首要目标不是实现功能,而是控制风险。 功能决定系统 “能不能做出来”;风险决定系统 “能不能稳定、安全、可维护地长期活下去”。
传统做法(功能驱动): 需求→功能列表→选技术→开发,风险往往后置,上线才暴露出性能、安全、扩展问题,改造成本极高。
RDD 做法: 需求 → 识别风险 → 评估风险等级 → 架构方案优先解决高风险 → 迭代验证风险是否被解决 → 再实现业务功能
2.RDD 中风险是什么?
风险 = 发生概率 × 影响损失 软件常见 5 大类风险
| 风险类型 | 说明 | 例子 |
|---|---|---|
| 性能风险 | 系统扛不住并发、响应慢 | 秒杀活动,1w QPS 数据库直接卡死 |
| 可用性风险 | 系统容易宕机、单点故障 | 单台应用服务器挂掉整个服务不可用 |
| 安全风险 | 数据泄露、权限越权、注入攻击 | 用户密码明文存储、接口没有鉴权 |
| 可变更 / 可维护风险 | 后续改需求代价巨大 | 模块严重耦合,改一个功能十几个地方要动 |
| 业务合规风险 | 不符合政策、行业规范 | 金融系统没有日志审计、支付缺少风控 |
风险分等级:高、中、低,架构资源优先投入高风险项。
二、RDD 完整标准实施步骤
一共 5 步循环迭代
步骤 1:识别风险 Identify
通读业务需求、非功能需求 (NFR),列出所有潜在风险清单
不光看功能,重点看非功能需求:并发量、数据量、SLA、安全、未来 2 年业务扩张。
举个电商订单系统例子 业务需求:用户可以下单、支付、查订单 识别风险清单: R1:大促峰值下单并发高,数据库压力过大(性能风险‑高) R2:订单服务单点故障,下单直接失败(可用性风险‑高) R3:订单支付接口被恶意重复调用,重复扣款(业务 + 安全风险‑高) R4:后期业务扩张,订单需要对接会员、物流、售后,模块耦合(可扩展‑中) R5:订单日志丢失,出现问题无法排查(可运维‑低)
步骤 2:风险评估 Assess
两个维度打分 1)发生可能性 (高 / 中 / 低) 2)一旦发生造成的损失 (高 / 中 / 低)
得到风险优先级矩阵,优先处理可能性高 + 损失大 上面案例评估
- R1 大促数据库压力:可能性高、损失高 →最高优先级
- R2 单点故障:可能性中、损失高 →高优先级
- R3 重复支付:可能性中、损失高 →高优先级
- R4 模块耦合:可能性高,损失中等 →中等优先级
- R5 日志丢失:可能性低,损失低 →低优先级,可以后期再处理
步骤 3:架构方案 Mitigate 缓解风险
针对每一条高风险,给出对应的架构对策
一条风险可以有多种缓解策略:规避、降低、转移、接受
- 规避:直接从架构上消除风险源头,比如不用单体,拆分微服务
- 降低:减少风险发生概率 / 降低故障影响,例如加缓存、限流、重试
- 转移:风险交给第三方组件,比如用 MQ 削峰、云厂商负载均衡
- 接受:低风险,暂时不做特殊架构,后续迭代再优化
针对电商订单风险给出方案 R1:大促数据库压力大 缓解方案:读写分离 + Redis 热点订单缓存 + MQ 异步削峰 + 分库分表 R2:单点故障 缓解方案:服务集群部署、Nginx 负载均衡、服务健康检查、熔断降级 R3:重复支付 缓解方案:生成全局唯一订单幂等号,数据库唯一索引、分布式锁、幂等校验 R4:模块耦合 缓解方案:DDD 领域拆分,订单独立微服务,通过消息事件和物流会员解耦 R5:日志丢失 缓解方案:异步日志组件,暂时先用现成框架,后面接入 ELK(低风险先接受)
重点:架构决策必须能对应解决某个具体风险,不能为了微服务而微服务。如果单体架构已经可以解决全部高风险,那就不要强行微服务。
步骤 4:架构原型 / 验证风险 Validate
不能纸上谈兵,做最小原型验证风险方案是否真的生效 比如风险 R1 数据库压力 写压测脚本,模拟 1 万并发下单,测试加 MQ + 缓存之后,数据库 QPS 是否降到安全阈值。 如果压测仍然超时,说明当前架构方案不足以化解风险,需要重新调整方案,例如增加分库。
步骤 5:迭代,循环执行
风险不是一次性全部解决。随着需求变化、用户规模增长,不断产生新风险。RDD 是一个循环过程。 上线之后流量上来了,又出现新风险:例如 Redis 缓存雪崩 →再次走识别‑评估‑设计‑验证流程
三、RDD 和 其他架构方法对比
| 设计方法 | 核心驱动力 | 适合场景 |
|---|---|---|
| RDD 风险驱动 | 优先解决高风险 | 需求不稳定、高并发、高可用、大型复杂系统 |
| FDD 功能驱动 | 优先实现业务功能 | 简单内部小系统、低并发、业务简单 |
| DDD 领域驱动 | 以业务领域模型驱动 | 业务逻辑极其复杂,领域边界模糊的系统 |
| ATAM 架构权衡分析法 | 多方案风险评估 | 已经有多个候选架构方案,需要做取舍评估 |
示例:
业务功能需求
- 运营可以上架秒杀商品,设置库存、秒杀开始 / 结束时间
- 用户可以查看正在秒杀的商品
- 用户参与秒杀,成功后生成秒杀订单
- 用户支付订单,15 分钟未支付自动释放库存
硬性非功能约束
- 秒杀开启瞬间预估 8000 QPS 的瞬时峰值请求
- 业务数据库 MySQL,单表单库最多承受 800 写 QPS
- 绝对不能超卖:实际卖出数量不能大于商品库存,一旦超卖直接造成公司经济损失
- 同一个用户不能重复抢到同一件秒杀商品
- 秒杀的大流量不能拖垮普通商城(购物车、普通下单)功能
RDD 风险驱动完整实施全过程
RDD 核心思想:先把全部风险找出来,优先解决高风险,再开发业务功能
步骤 1:风险识别
通读业务 + 非功能需求,逐条挖掘潜在风险
| 编号 | 风险名称 | 风险详细描述 | 风险分类 |
|---|---|---|---|
| R1 | 瞬时流量打垮数据库风险 | 秒杀瞬间 8000 并发写请求,MySQL 最多扛 800 写 QPS,数据库直接卡死,整个服务不可用 | 性能风险 |
| R2 | 库存超卖风险 | 高并发场景下,多线程同时读取库存 > 0,同时扣减库存,最终实际销量大于商品库存,造成资金损失 | 业务资损风险 |
| R3 | 用户重复抢购风险 | 同一用户短时间多次提交秒杀请求,多次抢到同一个商品 | 业务风险 |
| R4 | 流量雪崩风险 | 秒杀超大流量占用全部服务器资源,商城普通下单、购物车等正常业务被拖垮 | 可用性风险 |
| R5 | 恶意爬虫刷接口风险 | 脚本批量访问秒杀接口,大量无效请求消耗服务器资源 | 安全风险 |
| R6 | 超时未支付库存不释放风险 | 用户秒杀成功 15 分钟没有付款,库存没有回收,商品库存被永久占用 | 业务逻辑风险 |
| R7 | 故障排查风险 | 缺少完整操作日志,一旦出现超卖、报错无法定位问题根因 | 可运维风险 |
步骤 2:风险评估
两个维度打分:发生概率 + 损失严重程度,计算风险优先级
风险等级规则 高风险:概率高 + 损失高,必须在架构层面提前解决 中风险:解决成本可控,可以二期优化 低风险:暂时接受风险,后期迭代补充
| 风险编号 | 发生概率 | 损失严重度 | 最终风险等级 |
|---|---|---|---|
| R1 | 很高 | 极高 | 🔴最高优先级 |
| R2 | 很高 | 极高 | 🔴最高优先级 |
| R3 | 中等 | 高 | 🟠高优先级 |
| R4 | 中等 | 高 | 🟠高优先级 |
| R5 | 很高 | 中等 | 🟡中等优先级 |
| R6 | 中等 | 中等 | 🟡中等优先级 |
| R7 | 低 | 低 | 🟢低优先级,暂时接受 |
架构资源优先投入 R1 R2 R3 R4,先搞定最高风险,再做剩下的业务功能
步骤 3:风险缓解方案
风险应对 4 种策略
- 降低:减少风险发生概率 / 降低故障损失
- 转移:交给中间件、第三方组件承担风险
- 规避:从架构层面直接消除风险源头
- 接受:风险很小,暂时不做专门处理
R1:瞬时高并发压垮 MySQL
风险:8000QPS 直接打数据库,MySQL 扛不住 可选方案对比 方案 1:直接升级高配数据库(成本高,治标不治本) 方案 2:Redis 预加载库存 + MQ 异步削峰(选中方案) 缓解策略:转移 + 降低 架构对策:
- 秒杀开始前,提前把秒杀商品库存全部加载进 Redis 缓存
- 用户秒杀请求,第一步先走 Redis 做库存判断,不直接访问数据库
- Redis 校验库存充足之后,发送消息到 RabbitMQ 消息队列,立刻给前端返回「排队中」
- 后台消费者线程慢慢消费 MQ 消息,再操作数据库扣减库存
把瞬时 8000 的峰值流量削平,数据库实际写入压力被降到安全范围
代码
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Resource
private StringRedisTemplate redisTemplate;
@Resource
private RabbitTemplate rabbitTemplate;
@PostMapping("/do")
public String seckill(Long goodsId,Long userId){
String redisKey = "seckill:stock:"+goodsId;
//1 先查Redis库存,不访问数据库
String stockStr = redisTemplate.opsForValue().get(redisKey);
if(stockStr == null || Integer.parseInt(stockStr) <= 0){
return "商品已经抢空";
}
//2 Redis预扣库存
Long remain = redisTemplate.opsForValue().decrement(redisKey);
if(remain < 0){
//扣减后小于0,回滚库存
redisTemplate.opsForValue().increment(redisKey);
return "商品已经抢空";
}
//3 请求发送到MQ,异步处理数据库操作
SeckillMessage msg = new SeckillMessage();
msg.setGoodsId(goodsId);
msg.setUserId(userId);
rabbitTemplate.convertAndSend("seckill.direct","seckill.order",msg);
return "排队成功,请稍后查看订单";
}
}
这段代码作用:把绝大多数流量拦截在 Redis 层,数据库不再直面瞬时大流量
R2:库存超卖风险
风险:多线程同时读取库存,同时扣减,造成超卖 Redis 的 decrement 是原子操作,可以防止 Redis 层面超卖。 但是 MQ 异步落库的时候,仍然存在数据库超卖风险。 缓解策略:降低风险 对策:数据库库存字段上加乐观锁 扣减库存时带上版本号,只有库存 >=1 并且版本号匹配,才允许扣减 数据库库存表增加 version 版本字段 Java MQ 消费者代码(解决 R2 超卖风险)
@Component
public class SeckillConsumer {
@Resource
private StockMapper stockMapper;
@Resource
private OrderMapper orderMapper;
@RabbitListener(queues = "seckill.queue")
public void consume(SeckillMessage message){
Long goodsId = message.getGoodsId();
Long userId = message.getUserId();
//乐观锁扣库存
int rows = stockMapper.deductStockOptimistic(goodsId);
//rows等于0:扣减失败,已经没有库存
if(rows <= 0){
//库存不足,回滚Redis库存
redisTemplate.opsForValue().increment("seckill:stock:"+goodsId);
return;
}
//扣库存成功,生成秒杀订单
createSeckillOrder(goodsId,userId);
}
}
Mapper 层 SQL 乐观锁
UPDATE stock
SET num = num - 1 , version = version +1
WHERE goods_id=#{goodsId} AND num > 0 AND version = #{version}
即使 Redis 出现多扣,数据库乐观锁可以兜底,彻底杜绝超卖
R3:同一个用户重复抢购同一件商品
风险:用户疯狂点击,多次秒杀成功,抢到多份商品 缓解策略:规避风险 方案:Redis 保存已经成功抢到商品的用户 Id,秒杀前先校验用户是否已经抢购过 改造 Controller,增加用户限购校验
//用户限购key
String userBuyKey = "seckill:user:buy:"+goodsId;
//判断该用户是否已经抢到过
Boolean isMember = redisTemplate.opsForSet().isMember(userBuyKey,userId.toString());
if(Boolean.TRUE.equals(isMember)){
return "您已经抢购过该商品,不可重复购买";
}
//抢购成功之后把用户id存入集合
redisTemplate.opsForSet().add(userBuyKey,userId.toString());
在 Redis 层直接拦截重复用户,不需要访问数据库
R4:秒杀流量拖垮普通商城业务
缓解策略:风险隔离 架构对策
- 秒杀服务单独部署独立服务器集群,不和普通商城服务共用实例
- Nginx 网关层配置限流:秒杀接口单独限流,和普通接口流量分开
- 秒杀服务单独使用独立线程池,线程资源互不抢占
R5:爬虫恶意刷接口
缓解策略:降低风险 方案:网关层增加验证码、频率限流,同一个 IP 一秒钟最多访问 5 次秒杀接口 可以放在二期迭代开发,优先保证 R1‑R4
R6:15 分钟未支付库存释放
方案:Redis 设置订单过期时间 + 定时任务,超时没有支付,库存回补到 Redis 可以放在二期实现
R7:日志缺失
暂时接受风险,优先保障秒杀核心稳定性,后续接入日志框架
步骤 4:风险验证
架构方案做完之后,必须验证风险是否真正被解决,每个风险对应验证方式
| 风险编号 | 验证方案 | 验收标准 |
|---|---|---|
| R1 | 压测工具 JMeter,模拟 8000 并发请求 | MySQL 数据库写 QPS 稳定小于 700,服务不宕机 |
| R2 | 100 库存,并发 2000 请求 | 最终生成订单数量≤100,零超卖 |
| R3 | 同一个账号并发提交 10 次秒杀请求 | 只能下单成功一次 |
| R4 | 秒杀压测同时,调用普通购物接口 | 普通接口响应速度不受明显影响 |
更多推荐




所有评论(0)