跳至主要內容

技术方案怎么写:一个后端视角的评审清单

郑天祺大约 16 分钟产品与协作项目管理技术方案评审清单

技术方案怎么写:一个后端视角的评审清单

前言

你有没有经历过这样的技术评审?

PM:"我们这个需求很简单,就是加一个按钮,用户点了就能导出报表。"
你(后端):"好的。"

一周后,你发现这个"简单按钮"需要跨 3 个微服务查数据、实时聚合 500 万行记录、支持 10 万用户并发导出。

技术方案,就是这个"简单按钮"与你实际要写的 3000 行代码之间的桥梁。本文将从一个后端开发的视角,告诉你如何写出一个不会被评审会"怼穿"的技术方案。


一、什么时候需要写技术方案

1.1 判断矩阵:要不要写方案?

并不是所有需求都需要一份完整的技术方案。写文档是有成本的,关键在于评估需求的复杂度和风险。

需求评估矩阵:

┌────────────┬─────────────────┬──────────────────────┐
│ 影响范围    │ 低复杂度        │ 高复杂度              │
├────────────┼─────────────────┼──────────────────────┤
│ 单模块/服务 │ 不需要正式方案   │ 轻量方案(一页纸)     │
│            │ 代码Review即可   │                      │
├────────────┼─────────────────┼──────────────────────┤
│ 跨模块/服务 │ 轻量方案         │ 完整技术方案           │
│            │ 团队内对齐       │ 正式评审               │
├────────────┼─────────────────┼──────────────────────┤
│ 跨团队/部门 │ 完整技术方案      │ 完整方案 + 架构评审    │
│            │ 跨团队对齐       │                      │
└────────────┴─────────────────┴──────────────────────┘

1.2 必须写方案的场景

场景为什么需要方案
新系统/新模块架构决策需要文档化,方便后续维护
数据库变更(加表/改表)DBA 需要评审,避免上线后性能问题
接口对外暴露接口是契约,变更需要多方确认
涉及资金/安全高风险需求必须有文档备案
参与人数 > 2需要统一认知,减少沟通成本
有技术难点方案是提前暴露风险的机制
需求周期 > 3 天时间越长,越需要方案对齐预期

1.3 不需要写方案的场景

  • 修一个 Bug(除非涉及架构调整)
  • 加一个非关键字段(除非涉及数据库变更)
  • 小范围重构(不改变对外行为)
  • 文案/配置修改

一句话准则:如果这个需求因为"没想到"而出问题,会导致线上事故——那就该写方案。


二、技术方案的标准结构

2.1 方案骨架:七大章节

一份合格的技术方案通常包含以下 7 个部分。这不是教条——如果某个章节对你这个需求确实不适用,可以省略,但要明确写出"不适用"的理由。

技术方案标准结构:

1. 背景与目标(Why)
   ├── 业务背景:为什么要做这个需求
   ├── 要解决的问题
   └── 预期目标(可量化的)

2. 核心设计(What + How 概览)
   ├── 架构图:系统/模块/组件关系
   ├── 核心流程图:关键业务流程
   └── 技术选型与决策理由

3. 数据模型设计
   ├── 新增/变更的数据库表
   ├── 索引设计
   ├── 缓存设计
   └── 数据迁移方案

4. 接口设计
   ├── API 列表及签名
   ├── 请求/响应示例
   ├── 错误码定义
   └── 接口幂等性设计

5. 关键流程设计
   ├── 核心流程时序图
   ├── 异常处理流程
   ├── 状态机设计
   └── 并发控制方案

6. 风险与对策
   ├── 技术风险
   ├── 业务风险
   └── 降级/熔断/限流策略

7. 上线计划与回滚方案
   ├── 上线步骤与检查点
   ├── 灰度策略
   ├── 监控与告警
   └── 回滚方案与数据回滚

三、第一章:背景与目标——先把 Why 说清楚

3.1 写什么

背景与目标是整个方案的"电梯演讲"。一个好的背景描述应该让不了解这个需求的人,在 3 分钟内理解"为什么要做"。

3.2 示例对比

❌ 老张的版本

接到需求单 PRD-2024-0887,需要在订单系统中增加批量导出功能。

✅ 老李的版本

业务背景:运营团队每周需要手动拼接 5 个系统的数据来生成客户对账单,平均耗时 4 小时/周。随着客户数从 500 增长到 3000,这项工作已无法手工完成。

核心问题

  1. 数据散落在订单、支付、退款、CRM 四个系统中,无统一出口
  2. 手动拼接容易出错,已出现 3 次对账差异导致的客户投诉
  3. 数据量大(月均 50 万+ 订单),Excel 无法承载

预期目标

  • 运营导出对账单耗时从 4 小时降至 5 分钟以内
  • 数据准确率达到 99.9%(由系统保证口径统一)
  • 支撑 3000 个客户、月均 100 万订单的数据量

3.3 目标要 SMART

SMART 目标示例:

❌ "提升系统性能"        → 太模糊
✅ "订单查询接口 P99 响应时间从 2s 降至 500ms"

❌ "支持大数据量"        → 不可衡量
✅ "支持百万级订单数据的实时聚合查询"

❌ "提高数据准确性"      → 不可验证
✅ "对账差异率从 0.5% 降至 0.01%"

❌ "做一个好用的导出功能" → 太主观
✅ "用户从点击导出到下载完成不超过 10 秒"

四、第二章:核心设计——让人一眼看懂你的方案

4.1 架构图:画到什么粒度

架构图的核心原则:让不同角色的人都能找到自己关心的信息。

不同角色看架构图中的关注点:

角色         │ 关注什么
─────────────┼─────────────────
Tech Lead    │ 技术选型是否合理、是否引入新依赖
架构师       │ 与现有系统架构是否一致、扩展性
同级开发     │ 模块划分是否清晰、改动范围
测试         │ 涉及哪些模块、测试范围
运维         │ 新增什么服务/中间件、资源需求

4.2 技术选型:要有决策理由

技术选型要回答两个问题:
1. 为什么选 A 而不是 B?
2. A 的局限性是什么?什么时候需要换成 B?

示例:导出功能的技术选型

┌─────────┬──────────┬──────────┬──────────┐
│   方案    │ 同步导出  │ 异步导出  │ 实时查询  │
├─────────┼──────────┼──────────┼──────────┤
│ 实现复杂度│ ⭐       │ ⭐⭐⭐   │ ⭐⭐⭐⭐  │
│ 用户体验  │ ⭐⭐     │ ⭐⭐⭐⭐  │ ⭐⭐⭐⭐⭐│
│ 资源消耗  │ ⭐⭐⭐⭐⭐│ ⭐⭐     │ ⭐       │
│ 数据实时性│ ⭐⭐⭐⭐⭐│ ⭐⭐⭐   │ ⭐⭐⭐⭐⭐│
│ 大数据量  │ ❌       │ ✅       │ ✅       │
└─────────┴──────────┴──────────┴──────────┘

选择:异步导出
理由:数据量大(单次导出可达 10 万行),同步导出会导致网关超时
局限:数据非实时(T+0 而非实时),需要通知用户下载

五、第三章:数据模型设计——方案的心脏

5.1 表设计检查清单

数据模型设计自查清单:

□ 每个字段的 COMMENT 都写了吗?
□ 主键用的是自增 ID 还是业务主键?为什么?
□ 哪些字段需要加索引?(按查询场景列出)
□ 唯一索引覆盖了业务唯一性约束吗?
□ 是否需要分表/分区?(按数据量预估)
□ 是否需要软删除?
□ 敏感字段是否加密/脱敏?
□ 是否有数据归档策略?
□ 表变更的 DDL 脚本写好了吗?
□ 老数据的迁移方案是什么样的?

5.2 示例:导出任务表设计

CREATE TABLE export_task (
    id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '任务ID',
    task_no         VARCHAR(32)  NOT NULL COMMENT '任务编号,幂等键',
    user_id         BIGINT       NOT NULL COMMENT '创建用户ID',
    export_type     VARCHAR(32)  NOT NULL COMMENT '导出类型: ORDER/PAYMENT/CUSTOMER',
    query_params    JSON         NOT NULL COMMENT '查询参数,JSON 格式',
    file_name       VARCHAR(255) COMMENT '导出文件名',
    file_url        VARCHAR(512) COMMENT 'OSS 文件地址',
    file_size       BIGINT       DEFAULT 0 COMMENT '文件大小(字节)',
    total_count     INT          DEFAULT 0 COMMENT '总记录数',
    status          VARCHAR(16)  NOT NULL DEFAULT 'PENDING'
        COMMENT '任务状态: PENDING/PROCESSING/SUCCESS/FAILED/CANCELLED',
    error_msg       TEXT         COMMENT '失败原因',
    retry_count     INT          DEFAULT 0 COMMENT '重试次数',
    created_time    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    UNIQUE KEY uk_task_no (task_no),
    INDEX idx_user_type_status (user_id, export_type, status),
    INDEX idx_status_created (status, created_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导出任务表';

六、第四章:接口设计——你对上下游的承诺

6.1 API 设计原则

1. 一致性
   - URL 路径风格统一(/api/v1/{resource}/{id})
   - 响应格式统一(code/message/data 三段式)
   - 分页参数统一(pageNum/pageSize)

2. 自描述性
   - 不读文档也能大致猜出接口用途
   - 参数命名见名知义

3. 幂等性
   - GET 天然幂等
   - POST/PUT 需要安全机制保证幂等
   - DELETE 第二次调用返回成功(已删除)

4. 向后兼容
   - 加字段不破坏兼容性
   - 改字段类型/删字段 = 新版本

6.2 接口文档示例

# 创建导出任务
POST /api/v1/export/tasks
Content-Type: application/json

Request:
{
    "exportType": "ORDER",          // 导出类型,必填
    "queryParams": {                // 查询参数,必填
        "startDate": "2026-06-01",
        "endDate": "2026-06-10",
        "orderStatus": "PAID",
        "pageSize": 10000           // 单批次导出数量,默认 10000
    },
    "requestId": "req_20260611_001" // 幂等键,必填
}

Response (200):
{
    "code": 0,
    "message": "success",
    "data": {
        "taskNo": "EXP20260611001",
        "status": "PENDING",
        "createdTime": "2026-06-11T10:30:00Z"
    }
}

Error Codes:
- 1001: exportType 不支持
- 1002: 日期范围不能超过 90 天
- 1003: 导出频率超限(每小时最多 10 次)
- 409: 重复请求(requestId 已存在)

七、第五章:关键流程设计——把"暗坑"照亮

7.1 时序图要点

画时序图的三个层次:

层次 1:正常流程(Happy Path)
  → 明确各系统/模块的交互顺序
  → 标注关键的同步/异步调用

层次 2:异常流程(Error Path)
  → 每个外部调用失败后如何处理
  → 如何保证最终一致性

层次 3:边界情况(Edge Cases)
  → 并发请求如何处理
  → 数据量极端情况如何处理
  → 系统重启/崩溃后面临什么状态

7.2 导出功能时序图

用户        BFF        导出服务      订单服务     OSS           MQ
 │           │            │            │           │            │
 │ POST /export/tasks     │            │           │            │
 ├──────────►│            │            │           │            │
 │           │ createTask │            │           │            │
 │           ├───────────►│            │           │            │
 │           │            │ INSERT task│           │            │
 │           │            │──┐         │           │            │
 │           │            │◄─┘         │           │            │
 │           │            │ publish    │           │            │
 │           │            ├────────────┼───────────┼───────────►│
 │           │  taskNo    │            │           │            │
 │           │◄───────────┤            │           │            │
 │ 200 OK   │            │            │           │            │
 │◄──────────┤            │            │           │            │
 │           │            │            │           │            │
 │           │            │◄── consumer │           │            │
 │           │            │            │           │            │
 │  轮询状态 │            │            │           │            │
 │ GET /export/tasks/{taskNo}         │           │            │
 ├──────────►│            │            │           │            │
 │           │ queryTask  │            │           │            │
 │           ├───────────►│            │           │            │
 │           │ status: PROCESSING      │           │            │
 │           │◄───────────┤            │           │            │
 │           │            │            │           │            │
 │  ...      │            │ 查询订单   │           │            │
 │           │            ├───────────►│           │            │
 │           │            │◄───────────┤           │            │
 │           │            │ 上传文件   │           │            │
 │           │            ├───────────►│           │            │
 │           │            │◄────URL────┤           │            │
 │           │            │ UPDATE status=SUCCESS  │            │
 │           │            │──┐         │           │            │
 │           │            │◄─┘         │           │            │
 │           │            │            │           │            │
 │ 再次轮询  │            │            │           │            │
 ├──────────►│            │            │           │            │
 │           │◄───────fileUrl──────────┤           │            │
 │ download  │            │            │           │            │
 │◄──────────┤            │            │           │            │

7.3 并发控制方案

/**
 * 导出任务创建 - 并发控制
 */
@Service
public class ExportTaskService {

    // 使用 Redis 实现频率限制
    public CreateTaskResponse createTask(CreateTaskRequest request) {
        String userId = request.getUserId();

        // 1. 频率限制:每小时最多 10 个导出任务
        String rateLimitKey = "export:rate_limit:" + userId + ":" +
                              LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
        Long currentCount = redisTemplate.opsForValue().increment(rateLimitKey);
        if (currentCount == 1) {
            redisTemplate.expire(rateLimitKey, 1, TimeUnit.HOURS);
        }
        if (currentCount > 10) {
            throw new RateLimitException("导出频率超限,每小时最多 10 次");
        }

        // 2. 幂等性控制:基于 requestId
        String idempotentKey = "export:idempotent:" + request.getRequestId();
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(idempotentKey, "1", 24, TimeUnit.HOURS);
        if (!success) {
            // 幂等键已存在,返回已有任务
            ExportTask existing = taskRepository.findByRequestId(request.getRequestId());
            if (existing != null) {
                return CreateTaskResponse.from(existing);
            }
        }

        // 3. 并发数限制:同一用户最多 3 个处理中的任务
        int processingCount = taskRepository.countByUserIdAndStatus(
            userId, TaskStatus.PROCESSING);
        if (processingCount >= 3) {
            throw new ConcurrentLimitException("处理中的导出任务过多,请稍后再试");
        }

        // 4. 创建任务
        ExportTask task = createExportTask(request);
        taskRepository.save(task);

        // 5. 发送 MQ 异步处理
        mqProducer.send(new ExportTaskMessage(task.getTaskNo()));

        return CreateTaskResponse.from(task);
    }
}

八、第六章:风险与对策——提前给自己"找茬"

8.1 风险识别清单

常见技术风险:

□ 数据库性能风险
  - 新表的数据量预估是多少?半年后呢?
  - 查询是否会扫描全表?
  - JOIN 操作是否涉及大表?

□ 外部依赖风险
  - 依赖的服务 SLA 是多少?
  - 依赖不可用时如何降级?
  - 是否有超时和重试机制?

□ 并发风险
  - 接口 QPS 预估是多少?峰值是多少?
  - 是否有并发控制(加锁、队列)?
  - 是否会引发缓存击穿/雪崩?

□ 数据一致性风险
  - 跨服务的数据是否要求强一致?
  - 最终一致性是否可接受?
  - 对账/补偿机制是什么?

□ 安全风险
  - 是否涉及敏感数据?
  - 是否有权限控制?
  - SQL 注入/XSS/CSRF 防护?

8.2 风险应对策略

风险等级与应对策略:

高风险(影响大 + 概率高)
  → 必须有完整的应对方案
  → 必须在上线前验证
  → 例:数据库全表扫描 → 加索引 + 分页查询

中风险(影响大 + 概率低 或 影响小 + 概率高)
  → 有降级/兜底方案
  → 上线后重点监控
  → 例:外部服务不可用 → 使用缓存数据兜底

低风险(影响小 + 概率低)
  → 记录在案,可以接受
  → 例:导出文件格式中的某个非关键字段为 null

九、第七章:上线计划与回滚方案——给自己留后路

9.1 上线计划

上线计划模板:

1. 上线前检查
   □ 代码 Review 通过
   □ 单元测试覆盖率 ≥ 80%
   □ 集成测试通过
   □ SQL 脚本已在预发环境执行
   □ 配置项已在配置中心添加
   □ 监控大盘/告警规则已配置
   □ 灰度开关已配置

2. 上线步骤(写出每一步要做什么、谁来做、怎么做)
   Step 1: DDL 变更(DBA 执行,预计 10 分钟)
     检查点:表创建成功,索引创建成功,无锁表
   Step 2: 配置变更(配置中心,预计 5 分钟)
     检查点:新配置项已生效,日志中打印配置值
   Step 3: 灰度发布(10% 流量,观察 30 分钟)
     检查点:错误率无上升,响应时间无恶化
   Step 4: 全量发布
     检查点:所有节点部署成功
   Step 5: 数据校验
     检查点:导出功能正确,文件内容完整

3. 灰度策略
   - 按用户 ID 哈希:10% → 50% → 100%
   - 每阶段间隔 ≥ 30 分钟
   - 每个阶段自动/人工确认后继续

9.2 回滚方案

回滚方案三要素:

1. 代码回滚
   - 方式:K8s 回滚到上一个版本
   - 时间:5 分钟
   - 影响:导出功能暂时不可用

2. 数据回滚
   - 新表:DROP TABLE export_task (确认无依赖后)
   - 已有表新增字段:不删除(DBA 不建议线上做 DROP COLUMN)
   - 已有表数据变更:执行反向 SQL

3. 配置回滚
   - 配置中心一键回滚
   - 时间:1 分钟

❗ 什么情况立即回滚?
   - 错误率超过 1%
   - P99 响应时间超过基线的 3 倍
   - 影响核心业务流程(下单、支付)
   - 出现资金/数据安全问题

十、技术方案评审 Checklist

10.1 评审前自检(作者自查)

提交评审前,请逐项确认:

□ 我在方案中明确回答了"为什么做"吗?
□ 核心流程图清晰吗?新人能不能看懂?
□ 数据库表设计有 COMMENT 吗?索引合理吗?
□ 接口定义完整吗?有错误码吗?
□ 异常流程都考虑了吗?
□ 并发场景处理了吗?
□ 风险识别了吗?有降级方案吗?
□ 监控指标和告警规则定义了吗?
□ 上线步骤清晰吗?回滚方案可行吗?
□ 预估工期合理吗?

10.2 评审中常见被问倒的问题

评审中最容易被问倒的 10 个问题:

1. "这个查询走索引了吗?Explain 的结果是什么样的?"
   → 准备:贴出 EXPLAIN 结果,说明扫描行数和索引选择

2. "并发量达到 X 的时候,这个方案还成立吗?"
   → 准备:预估峰值 QPS,压力测试结果

3. "数据库单表能撑多久?什么时候需要分库分表?"
   → 准备:数据量预估(日增 × 保留天数),分表阈值

4. "外部服务挂了怎么办?"
   → 准备:每个外部依赖的降级策略

5. "如果 MQ 消息丢失了怎么办?"
   → 准备:消息可靠性保证机制 + 补偿方案

6. "这个接口被恶意调用怎么办?"
   → 准备:鉴权、限流、风控策略

7. "缓存和数据库不一致怎么办?"
   → 准备:缓存更新策略(旁路缓存 / 写穿 / 写回)

8. "为什么不用 XXX 技术?"
   → 准备:技术选型的对比分析和决策理由

9. "数据兜底逻辑在哪里?如果算错了,怎么修复?"
   → 准备:数据修复方案和工具

10. "你预估这个需求要多久?为什么是这些天?"
    → 准备:WBS(工作分解结构),每个子任务的工时

10.3 评审者的关注点

不同角色在评审时关心什么:

Tech Lead:
  - 技术选型是否合理?
  - 是否引入了不必要的复杂度?
  - 与团队技术规划是否一致?

架构师:
  - 是否符合整体架构原则?
  - 扩展性如何?未来半年内是否需要调整?
  - 是否引入了新的技术债务?

同级开发:
  - 改动范围大吗?会影响我负责的模块吗?
  - 有没有可以复用的部分?
  - 逻辑有没有明显漏洞?

DBA:
  - SQL 是否高效?是否使用了正确的索引?
  - 表设计是否合理?字段类型是否合适?
  - DDL 是否会对线上造成影响(锁表、主从延迟)?

测试:
  - 测试范围是什么?
  - 有哪些边界情况需要重点覆盖?
  - 需要构造哪些测试数据?

运维/SRE:
  - 需要新增什么资源(服务器、中间件)?
  - 对现有集群的影响?
  - 监控和告警是否充分?

十一、方案反模式

11.1 反模式一:过度设计

症状:
  - 需求是"用户想看最近 7 天的订单",方案里设计了实时流计算 + OLAP
  - 日活 100 的系统,方案里用了微服务 + 消息队列 + 缓存三层架构
  - 5 张表的简单 CRUD,设计了 CQRS + Event Sourcing

对策:
  - 问自己:如果去掉这个组件,系统还能满足需求吗?
  - 原则:先把简单方案做到极致,出问题再演进,而不是一开始就过度设计
  - 名言:"Premature optimization is the root of all evil." —— Donald Knuth

11.2 反模式二:文档过于简略

症状:
  - "这个大家都知道的,不用写了吧"
  - 核心流程只有一句"调用 XX 服务获取数据"
  - 异常处理写的是"记录日志并返回错误"
  - 上线计划写的是"正常发布"

对策:
  - 评审的目的不是"你知道",而是"别人也知道并且认同"
  - 每个外部调用,写出:调用方式(同步/异步)、超时时间、失败重试次数
  - 异常处理:写出具体的降级逻辑,不是口号

11.3 反模式三:没有异常路径

症状:
  - 方案只画了正向流程图
  - 所有外部调用都假设成功
  - 没有考虑超时、重试、降级

对策:
  - 每个外部调用问一句:"如果它挂了怎么办?"
  - 异常路径要用不同颜色的箭头在流程图中标出
  - 关键节点要标注"如果卡在这里超过 X 分钟,系统会怎样"

11.4 反模式四:直接跳入实现细节

症状:
  - 方案开头就是"我们选用 Redis 7.0 作为缓存"
  - 没讲清楚问题是什么,就开始讨论怎么实现
  - 评审者一脸懵:"等等,你先说说为什么要做这个缓存?"

对策:
  - 严格遵循 Why → What → How 的顺序
  - 如果没有讲清 Why,不要开始讲 What
  - 如果没有讲清 What,不要开始讲 How

十二、总结

12.1 好方案的标准

维度标准
清晰性新人看完能理解"做什么"和"为什么"
完整性正向流程、异常流程、边界情况都覆盖了
可行性在给定的时间、资源和技术约束下可实现
可维护性未来的人能通过方案理解你当时的设计决策
可验证性上线后能验证方案是否达到预期目标

12.2 一句话总结

技术方案不是为了"过评审",而是为了让你在上线前把坑都踩一遍。方案评审时被问到哑口无言,比上线后出事故好得多。

上次编辑于:
贡献者: zhengtianqi