结构即约束:给输出结构、给量化验收、给取舍
上一讲我们了解了大模型的脾气:会恭维、会讨好。那怎么办?很多人的第一反应是”那我说得再严肃一点、再详细一点”。没用的。你说”帮我认真写个总结”,它还是会给你一大段——因为你只是给了个方向,还是没说你到底要什么。
今天这一讲就解决这个问题:把”我要什么”说具体。核心方法其实就一句话:结构即约束(structure as constraint)。
问题一:「解释」和「评审」,是一个任务吗?
先看一个我们天天都会遇到的场景:扔给 AI 一段代码,说”解释一下这段代码”。
我实测了一次。你猜大模型是怎么干的?它会逐段翻译:贴一段代码讲一段,”定义一个结算方法,接收订单对象……遍历订单中的所有商品……”看完等于把代码又读了一遍。然后它觉得不过瘾,又附赠了 7 条问题清单,最后还送了一份完整的 BigDecimal 改写版——整整 100 行,其中大半根本每人想看。
问题出在哪?“解释”这个词太模糊了。你眼里的”解释”是”帮我看懂”,它理解的”解释”是”把每个元素都说明一遍”——逐行翻译是成本最低的解释。
我换了个说法:”我马上要接手维护这段代码,只有 30 秒。请按这个结构讲:1. 一句话:这段代码是干嘛的;2. 关键路径:正常流程三步以内;3. 坑:藏着哪些会出生产事故的问题,每个坑指到行。不要逐行翻译代码——我识字。“
结果:15 字的一句话、三步路径、6 个坑全部指到具体表达式。同样的模型,同样的代码。你没有换模型,你只是把任务说清了。
“解释”和”评审”是两个任务。你不说清要哪种,它默认选最省力的那种。
展开看实测原文(朴素 vs 工程化,沈剑2026-09-17实测:deepseek-flash,可复现,均为独立session以排除上下文影响)
实验材料(同一份材料喂给两条提示词)
```java
public double settle(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
return total;
}
```
朴素提示词
解释一下这段代码。
```java
public double settle(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
return total;
}
```
朴素输出(逐段翻译+问题清单+没人要的改写,共 100 行)
这段代码实现了一个简单的**订单结算/支付流程**:根据订单商品计算总金额,扣减优惠券金额,然后调用支付服务扣款,最后返回总金额。
逐段解释:
```java
public double settle(Order order) {
```
定义一个结算方法,接收订单对象,返回 `double` 类型的结算金额。
```java
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
```
遍历订单中的所有商品,用 `单价 × 数量` 累加得到订单总金额。
```java
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
```
如果订单使用了优惠券,并且优惠券不为空,就从总金额中减去优惠券金额。
```java
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
```
调用支付服务,对当前用户扣款。
如果支付过程中发生异常,只记录一条 `info` 日志,然后继续执行,不向外抛出异常。
```java
return total;
}
```
最终返回计算出的总金额。
---
这段代码的主要问题是:
1. **金额不该用 `double`**
`double` 存在浮点精度问题,金额计算建议使用 `BigDecimal`。
2. **支付失败被吞掉了**
捕获异常后只打印 `"settle failed"`,没有记录异常堆栈,也没有抛出异常或返回失败状态。
调用方可能仍然认为结算成功了,但实际支付已经失败。
3. **没有校验优惠券金额**
如果优惠券金额大于订单总金额,`total` 可能变成负数。
4. **空指针风险**
`order`、`order.getItems()`、`item` 等都没有判空。
5. **异常捕获过于宽泛**
`catch (Exception e)` 会捕获所有异常,通常不推荐。应该捕获具体的业务异常,比如 `PaymentException`。
6. **日志信息不足**
只记录了 `"settle failed"`,没有订单 ID、用户 ID、金额、异常对象,不方便排查问题。
7. **返回值和业务语义不一致**
方法叫 `settle`,但即使支付失败也返回总金额。返回 `total` 无法表达支付是否成功。
改进方向大致如下:
```java
public BigDecimal settle(Order order) {
BigDecimal total = order.getItems().stream()
.map(item -> item.getPrice()
.multiply(BigDecimal.valueOf(item.getCount())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (order.getCoupon() != null) {
total = total.subtract(order.getCoupon().getAmount());
if (total.compareTo(BigDecimal.ZERO) < 0) {
total = BigDecimal.ZERO;
}
}
try {
paymentService.charge(order.getUserId(), total);
} catch (PaymentException e) {
log.error("settle failed, userId={}, orderId={}, total={}",
order.getUserId(), order.getId(), total, e);
throw e;
}
return total;
}
```
总结:这段代码能完成最基本的金额计算和支付调用,但作为真实支付/结算逻辑,异常处理、金额类型、参数校验和返回值设计都需要加强。
工程化提示词
我马上要接手维护这段代码,只有 30 秒。请按这个结构讲:
1. 一句话:这段代码是干嘛的(不超过 20 字);
2. 关键路径:正常流程三步以内说完;
3. 坑:这段代码里藏着哪些会出生产事故的问题?每个坑必须指到具体的行或表达式,并说明后果。
不要逐行翻译代码——我识字。
```java
public double settle(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
return total;
}
```
工程化输出(15 字一句话+三步路径+6 坑全指到行)
1. 一句话:结算订单金额并调用支付扣款。
2. 关键路径:
1. 遍历 `items`,累加 `price * count`;
2. 有券则 `total -= coupon.amount`;
3. 调 `paymentService.charge(userId, total)`,返回 `total`。
3. 坑:
- `catch (Exception e) { log.info("settle failed"); }` + `return total;`:支付异常被吞,方法仍返回成功金额。后果:上游以为扣款成功,可能已发货/记账但没收到钱,资损。
- `paymentService.charge(order.getUserId(), total)`:没有 `orderId`/幂等键,只传 userId+金额。后果:重试、并发或重复结算会重复扣款。
- `double total`、`total += item.getPrice() * item.getCount()`、`total = total - order.getCoupon().getAmount()`:金额用 `double`,浮点精度丢失。后果:支付金额与应收不一致、对账差异、多扣少扣。
- `total = total - order.getCoupon().getAmount();`:未校验券金额和 `total` 下限。后果:`total` 可为负,`charge` 负数可能触发退款、异常或绕过支付。
- `order.getItems()` / `item.getPrice() * item.getCount()`:计算段不在 try 内,空值或包装类型拆箱会 NPE。后果:结算直接中断,且无补偿。
- `log.info("settle failed")`:无异常对象 `e`、无 `orderId`,级别还是 info。后果:线上支付失败无法定位/告警。
问题二:给了结构,它会变成什么样?
有人担心:给结构会不会把大模型卡得太死、导致它失去灵气?我们再来看一组实测。
让大模型帮我写设计文档。朴素的问法:”帮我写一份「订单查询接口加缓存」的设计文档。”它写了 700 行,”随着订单量增长……”,验收标准一栏写着“明显下降”。
“明显下降”这四个字,是设计文档里最常见的废话。下降多少?从哪到哪?什么时候验收?全没有。不可量化的验收标准,等于没有验收标准。
我的工程化版本加了三条要求:
① 验收标准必须可量化,写成”从 X 到 Y”,禁止”明显提升””显著优化”;
② 关键决策必须写”为什么这么选”,给一个被否决的备选;
③ 信息不够先问我,不许编。
结果它干了一件有意思的事:它选择不写。”已知信息只有 p99 = 800ms……先问清楚,再写最终设计文档。”然后给了结构化的问题清单(当前与目标 QPS、读写比、基线命中率),预告了验收标准的写法(p99:从 800ms 到目标值),最后附一张填空表单:请补充与回复我这些信息,我才能帮你完成这项工作。
会反问的 AI,比会回答的 AI 值钱。没信息时不输出,比输出漂亮但编造的文档有用得多。
展开看实测原文(朴素 vs 工程化,沈剑2026-09-28实测:deepseek-flash,可复现,均为独立session以排除上下文影响)
已知信息(无额外材料:只有 p99 800ms)
订单查询 p99 延迟 800ms,业务方要求优化。
朴素提示词
帮我写一份「订单查询接口加缓存」的设计文档。
朴素输出(700 行通用初稿,验收写「明显下降」)
# 订单查询接口加缓存设计文档
> 版本:v0.1
> 状态:初稿
> 适用范围:按 `orderId` 查询订单详情接口
> 说明:以下为通用设计初稿,可按实际系统、QPS、订单量、一致性要求调整。
---
## 1. 背景
订单查询接口是交易链路中读流量最高的接口之一,典型调用方包括:
- 用户端:订单列表、订单详情、支付结果页、售后页
- 客服端:客服工作台查询订单
- 商家端:发货、对账、售后处理
- 内部系统:风控、结算、履约
随着订单量增长,订单查询直接查 MySQL 会带来:
- DB QPS 高,影响写链路稳定性
- 查询 P99 升高
- 大促期间热点订单查询集中
- 主从延迟导致读写不一致感知增强
因此计划对订单详情查询接口增加 Redis 缓存,降低 DB 压力,提升查询性能。
---
## 2. 目标
### 2.1 核心目标
1. 降低订单查询接口平均 RT 和 P99。
2. 降低订单 DB 回源 QPS,目标下降 70% 以上。
3. 缓存命中率目标 >= 90%。
4. 保证订单更新后,缓存最终一致,普通场景延迟 < 1s,极端场景由 TTL 兜底。
5. 支持灰度、降级、回滚,Redis 故障时接口可降级查 DB。
### 2.2 非目标
1. 本方案主要针对 **按 orderId 查询订单详情**。
2. 不覆盖按用户、状态、时间分页的订单列表缓存,列表缓存需单独设计。
3. 不追求强一致,订单查询允许秒级最终一致。
4. 不解决跨机房多活、异地多活缓存同步问题。
5. 不缓存支付流水、身份证、银行卡等强敏感信息。
---
## 3. 现状与假设
### 3.1 现状
- 订单详情接口:`GET /api/order/detail?orderId=xxx`
- 数据来源:MySQL 订单主表、订单明细表、地址表等
- 更新入口:支付、发货、取消、退款、修改地址、订单状态机流转等
- 当前无缓存或仅有本地缓存,DB 压力较大
### 3.2 假设
- Redis 集群可用,支持主从、哨兵或 Cluster。
- 已有或可接入 MQ,用于订单变更事件。
- 订单详情平均大小约 1KB~5KB。
- 订单查询允许最终一致,最大不一致窗口由 TTL 控制。
- 接口层已有鉴权能力,可校验用户是否有权查看订单。
---
## 4. 总体方案
采用 **Cache Aside + 删除缓存 + 延迟双删 + TTL 兜底** 方案。
核心原则:
1. 读请求优先查缓存,未命中查 DB 并回填。
2. 写请求更新 DB 后删除缓存,而不是更新缓存。
3. 删除缓存后发送延迟消息,进行二次删除,降低并发读写导致的脏缓存概率。
4. 所有缓存设置 TTL,TTL 作为最终一致性兜底。
5. Redis 异常时快速失败,降级查 DB,避免影响接口可用性。
6. 对空值、热点、雪崩、大 key 做专项治理。
---
## 5. 详细设计
### 5.1 接口定义
```http
GET /api/order/detail?orderId={orderId}
Authorization: Bearer {token}
```
响应示例:
```json
{
"code": 0,
"message": "success",
"data": {
"orderId": "2024052012345678",
"userId": 10001,
"status": "PAID",
"amount": 199.00,
"items": [
{
"skuId": "S001",
"skuName": "商品A",
"quantity": 1,
"price": 199.00
}
],
"receiver": {
"name": "张*",
"phone": "138****8888",
"address": "上海市浦东新区***"
},
"createTime": "2024-05-20T10:00:00+08:00",
"updateTime": "2024-05-20T10:01:00+08:00",
"version": 3
}
}
```
鉴权要求:
- 用户只能查询自己的订单。
- 客服、商家等角色按权限体系校验。
- 命中缓存后仍需校验订单归属,不能因为缓存命中跳过鉴权。
- 敏感字段建议脱敏后再缓存。
---
### 5.2 缓存 Key 设计
| 用途 | Key 格式 | 示例 |
|---|---|---|
| 订单详情 | `order:detail:v1:{orderId}` | `order:detail:v1:2024052012345678` |
| 防击穿锁 | `lock:order:detail:{orderId}` | `lock:order:detail:2024052012345678` |
| 本地缓存 | `local:order:detail:{orderId}` | `local:order:detail:2024052012345678` |
| 空值标记 | 同详情 Key,Value 中标记 empty | 见下文 |
Key 规范:
- 带业务前缀:`order`
- 带模块:`detail`
- 带版本:`v1`,后续结构变更可升级到 `v2`
- 使用 `orderId` 作为唯一标识
- 不建议 Key 中包含用户 ID,订单实体本身可复用;鉴权在接口层完成
---
### 5.3 缓存 Value 设计
建议使用 JSON 或 Protobuf 序列化。若 Redis 内存紧张,可使用 Protobuf + 压缩。
Value 包装结构:
```json
{
"empty": false,
"data": {
"orderId": "2024052012345678",
"userId": 10001,
"status": "PAID",
"amount": 199.00,
"items": [],
"receiver": {},
"createTime": "2024-05-20T10:00:00+08:00",
"updateTime": "2024-05-20T10:01:00+08:00",
"version": 3
},
"cacheTime": 1716170460000
}
```
空值结构:
```json
{
"empty": true,
"data": null,
"cacheTime": 1716170460000
}
```
说明:
- `empty=true` 表示订单不存在,用于防穿透。
- `version` 可用于辅助排查脏数据。
- 不建议缓存完整敏感信息,地址、手机号需脱敏。
- 若订单明细过大,可只缓存订单主信息,明细单独缓存或查 DB。
---
### 5.4 TTL 策略
TTL 采用 **动态 TTL + 随机抖动**。
| 订单状态 | 建议 TTL | 说明 |
|---|---:|---|
| 待支付 | 2 分钟 | 状态变化频繁 |
| 已支付 / 待发货 | 5 分钟 | 状态可能变化 |
| 已发货 / 配送中 | 10 分钟 | 状态相对稳定 |
| 已完成 / 已取消 / 已关闭 | 1 小时 | 终态,变化少 |
| 空值 | 30~60 秒 | 防穿透,短 TTL |
| 所有正常缓存 | TTL + random(0, 300s) | 防止雪崩 |
示例:
```text
待支付:120s + random(0, 300)s
已支付:300s + random(0, 300)s
已完成:3600s + random(0, 300)s
空值:60s
```
---
### 5.5 读流程
```mermaid
sequenceDiagram
participant C as Client
participant S as OrderService
participant R as Redis
participant DB as MySQL
C->>S: GET /order/detail?orderId
S->>S: 参数校验 + 鉴权
S->>R: GET order:detail:v1:{orderId}
alt 缓存命中
R-->>S: CacheValue
alt empty=true
S-->>C: 订单不存在
else 正常数据
S->>S: 校验归属 + 脱敏
S-->>C: 返回订单详情
end
else 缓存未命中
S->>R: SETNX lock:order:detail:{orderId}
alt 获取锁成功
S->>R: 再次 GET
alt 已命中
R-->>S: CacheValue
else 仍未命中
S->>DB: SELECT order
alt DB 存在
DB-->>S: Order
S->>R: SET key TTL+随机
S-->>C: 返回订单详情
else DB 不存在
S->>R: SET key empty=true TTL=60s
S-->>C: 订单不存在
end
end
S->>R: DEL lock
else 获取锁失败
S->>S: sleep 50ms 后重试缓存
alt 重试命中
S-->>C: 返回缓存
else 仍失败
S->>DB: 降级查 DB
S-->>C: 返回
end
end
end
```
读流程要点:
1. 先校验 `orderId` 格式,非法直接返回。
2. 缓存命中后必须做订单归属校验。
3. 缓存未命中时,使用 `SETNX` 或 Redisson 锁防击穿。
4. 锁粒度按 `orderId`,锁超时建议 500ms。
5. 获取锁失败时不无限等待,可短暂重试或直接查 DB,避免线程堆积。
6. DB 不存在时写空值缓存,防止恶意刷不存在订单。
7. 回填缓存时使用动态 TTL + 随机抖动。
---
### 5.6 写 / 更新流程
订单更新入口较多,建议统一发布订单变更事件,由缓存清理消费者删除缓存。
更新流程:
```mermaid
sequenceDiagram
participant W as WriteService
participant DB as MySQL
participant MQ as MQ
participant C as CacheConsumer
participant R as Redis
W->>DB: 更新订单
DB-->>W: commit
W->>R: DEL order:detail:v1:{orderId}
W->>MQ: 发送延迟删除消息
MQ->>C: OrderChangedEvent
C->>R: DEL order:detail:v1:{orderId}
```
建议策略:
1. 事务内更新 DB。
2. 事务提交后立即删除 Redis 缓存。
3. 发送延迟消息,例如 500ms~1s 后再次删除。
4. 缓存清理消费者根据 `OrderChangedEvent` 删除缓存。
5. 若删除失败,进行重试,进入重试队列,并告警。
6. TTL 作为最终兜底。
触发缓存删除的场景:
- 订单创建
- 支付成功
- 支付超时关闭
- 发货
- 确认收货
- 取消订单
- 退款/售后状态变更
- 修改收货地址
- 修改订单备注
- 订单状态机任意流转
- 影响订单详情展示的字段变更
建议事件结构:
```json
{
"eventId": "evt_123",
"orderId": "2024052012345678",
"changeType": "ORDER_STATUS_CHANGED",
"version": 4,
"updateTime": "2024-05-20T10:02:00+08:00"
}
```
---
### 5.7 一致性保障
一致性级别:**最终一致**。
保障手段:
1. **Cache Aside**:更新 DB 后删除缓存。
2. **延迟双删**:更新后立即删一次,延迟 500ms~1s 再删一次。
3. **事件驱动删除**:通过 MQ 或 binlog 订阅删除缓存,避免业务漏删。
4. **TTL 兜底**:即使删除失败,缓存也会过期。
5. **关键读走主库**:支付结果页、订单状态判断等强一致场景不走缓存或读主库。
6. **空值缓存**:不存在订单缓存 30~60s,防穿透。
7. **版本号辅助**:Value 中带 `version`,便于排查脏数据。
一致性问题及处理:
| 问题 | 处理 |
|---|---|
| 更新 DB 后删缓存失败 | 重试 + MQ + TTL 兜底 |
| 读线程回填旧值 | 延迟双删 + TTL + 关键读主库 |
| 主从延迟导致旧值回填 | 更新后延迟双删;必要时读主库 |
| 多入口更新漏删 | 统一订单变更事件 |
| 热点订单并发读写 | 单飞锁 + 延迟双删 |
---
### 5.8 异常与降级
| 异常 | 处理 |
|---|---|
| Redis 连接超时 | 超时 50~100ms,快速失败,降级查 DB |
| Redis 集群不可用 | 关闭缓存开关,直接查 DB,限流保护 |
| 缓存删除失败 | 重试 3 次,进入重试队列,告警 |
| 缓存写入失败 | 记录日志,忽略,返回 DB 结果 |
| 序列化失败 | 不缓存,记录异常 |
| DB 不存在 | 写空值缓存,TTL 60s |
| 权限校验失败 | 直接拒绝,不返回缓存数据 |
| 大 key 写入 | 超过阈值不缓存或拆分 |
降级原则:
- 缓存只是性能优化,不能成为强依赖。
- Redis 异常时,订单查询接口仍应可用。
- DB 降级时要配合限流,防止 Redis 故障后 DB 被打挂。
- 降级开关支持动态配置。
---
## 6. 缓存问题治理
### 6.1 缓存穿透
场景:大量请求查询不存在的 `orderId`,绕过缓存直查 DB。
方案:
1. 参数校验:`orderId` 格式、长度、前缀校验。
2. 空值缓存:不存在订单缓存 `empty=true`,TTL 30~60s。
3. 可选布隆过滤器:订单创建时写入,查询前判断。成本较高,按需使用。
4. 对异常高频查询做限流和风控。
---
### 6.2 缓存击穿
场景:热点订单缓存过期,大量请求同时查 DB。
方案:
1. 使用 `SETNX` 或 Redisson 锁,保证同一 `orderId` 只有一个线程回源。
2. 锁超时 500ms,避免死锁。
3. 获取锁失败线程短暂等待后重试缓存;仍失败则降级查 DB。
4. 超热订单可使用本地缓存 + 逻辑过期,但订单状态敏感,慎用逻辑过期。
---
### 6.3 缓存雪崩
场景:大量缓存同一时间过期,或 Redis 集群故障。
方案:
1. TTL 增加随机抖动,例如 `random(0, 300s)`。
2. Redis 集群部署,主从高可用。
3. 本地缓存作为二级缓存,TTL 1~5s。
4. 接口限流、降级、熔断。
5. DB 连接池保护,防止被打满。
6. 大促前预热热点订单。
---
### 6.4 热 Key
场景:某个订单被频繁查询,例如大促爆款订单、客服集中查询订单。
方案:
1. 本地缓存:Caffeine,TTL 1~5s。
2. Redis 热 key 探测,发现后告警。
3. 可选 Redis 多副本读,或热点 key 打散。
4. 更新时通过 MQ 广播删除本地缓存。
---
### 6.5 大 Key
场景:订单包含大量商品明细,Value 过大。
方案:
1. 限制缓存对象大小,建议单 Value <= 10KB。
2. 订单主信息和商品明细拆分缓存。
3. 大订单不缓存完整明细,只缓存主信息。
4. 必要时压缩 Value。
5. 监控大 key,超过阈值告警。
---
## 7. 监控与告警
### 7.1 核心指标
| 指标 | 说明 | 建议阈值 |
|---|---|---|
| 缓存命中率 | hit / (hit + miss) | >= 90% |
| Redis GET P99 | Redis 读延迟 | < 20ms |
| Redis 错误率 | 超时、连接失败 | < 0.1% |
| DB 回源 QPS | 缓存未命中查 DB | 明显下降 |
| 空值命中率 | 不存在订单查询 | 异常升高告警 |
| 删除失败次数 | 缓存删除失败 | > 0 告警 |
| 锁等待时间 | 防击穿锁等待 | 持续升高告警 |
| 热 Key | Top N 热 key | 发现即关注 |
| 大 Key | Top N 大 key | > 10KB 告警 |
| 接口 P99 | 订单查询接口延迟 | 按业务定 |
| 降级次数 | Redis 降级查 DB | > 0 关注 |
### 7.2 日志
- 缓存命中/未命中日志,采样记录。
- 缓存删除失败日志,必须记录。
- 降级日志,必须记录。
- 大 key、热 key 探测日志。
- 订单变更事件消费日志。
---
## 8. 容量与性能估算
假设:
- 日订单量:1000 万
- 订单详情平均大小:2KB
- 缓存 30 分钟内的订单:约 20 万~50 万
- 高峰查询 QPS:1 万
- 缓存命中率:95%
Redis 内存估算:
```text
活跃缓存订单数 * 单 Value 大小 * 冗余系数
= 50 万 * 2KB * 1.5
= 1.5GB
```
若缓存 24 小时订单:
```text
1000 万 * 2KB * 1.5 = 30GB
```
因此建议:
- 按实际业务只缓存活跃订单。
- 终态订单可缩短 TTL 或不缓存。
- Redis 集群预留 30% 内存。
- 使用 `allkeys-lru` 淘汰策略。
- Redis 超时设置 50~100ms。
- 连接池使用 Lettuce,注意连接数控制。
---
## 9. 配置项建议
```yaml
order:
detail:
cache:
enabled: true
key-prefix: "order:detail:v1:"
ttl:
unpaid: 120
paid: 300
shipped: 600
finished: 3600
null-value: 60
jitter: 300
lock:
timeout-ms: 500
wait-ms: 50
redis:
timeout-ms: 100
local:
enabled: true
ttl-ms: 3000
degrade:
enabled: true
```
---
## 10. 灰度、回滚与降级
### 10.1 灰度
1. 增加缓存总开关,默认关闭。
2. 先上线写路径缓存删除逻辑,确认事件正常。
3. 读路径按 `orderId` 尾号灰度:1% -> 10% -> 50% -> 100%。
4. 观察命中率、DB QPS、接口 P99、错误率、删除失败率。
5. 灰度期间保留直接查 DB 开关。
### 10.2 回滚
1. 关闭读缓存开关,接口直接查 DB。
2. 关闭写路径删除逻辑,避免无效 MQ 消费。
3. 缓存可保留,等待 TTL 自动过期。
4. 必要时执行批量删除脚本。
### 10.3 降级
1. Redis 超时或错误率升高,自动降级查 DB。
2. DB 降级时开启限流,保护 DB。
3. 本地缓存可短暂兜底。
4. 大促期间可主动关闭非核心订单缓存。
---
## 11. 测试方案
### 11.1 功能测试
1. 首次查询未命中,查 DB 并回填。
2. 第二次查询命中缓存。
3. 订单更新后,缓存被删除,下次查询新值。
4. 查询不存在订单,写空值缓存,60s 后恢复。
5. 越权用户查询订单,拒绝。
6. 敏感字段脱敏正确。
7. 订单明细过大时,按策略不缓存或拆分。
### 11.2 性能测试
1. 压测 1x、3x、5x 高峰 QPS。
2. 验证缓存命中率是否达到 90%+。
3. 验证 DB 回源 QPS 是否明显下降。
4. 验证 Redis 延迟、连接池、CPU、内存。
5. 验证热点订单并发查询。
### 11.3 一致性测试
1. 更新订单后立即查询,检查是否读到旧值。
2. 并发更新 + 查询,验证延迟双删效果。
3. 模拟删缓存失败,验证重试和 TTL 兜底。
4. 模拟主从延迟,验证是否回填旧值。
### 11.4 故障测试
1. Redis 宕机,接口降级查 DB。
2. Redis 主从切换,接口表现。
3. Redis 网络抖动,超时快速失败。
4. MQ 不可用,缓存删除是否重试。
5. DB 压力升高,限流是否生效。
---
## 12. 风险与待确认
### 12.1 风险
1. 缓存脏数据导致用户看到旧订单状态。
2. 删除缓存失败导致不一致窗口扩大。
3. Redis 内存增长过快。
4. 热点订单打爆 DB。
5. 敏感信息缓存导致泄露风险。
6. 多入口更新漏删缓存。
7. 大 key 导致 Redis 阻塞。
### 12.2 待确认
1. 订单查询是否允许最终一致?最大延迟是多少?
2. 支付结果页、订单状态判断是否必须强一致?
3. 是否已有统一订单变更事件或 MQ?
4. 订单详情平均大小、最大大小是多少?
5. Redis 集群容量、规格、是否 Cluster?
6. 是否需要本地缓存?本地缓存一致性要求?
7. 商家、客服、用户视角返回字段是否不同?
8. 订单列表接口是否也需要缓存?
9. 是否存在多机房、多活需求?
---
## 13. 附录:核心伪代码
### 13.1 读缓存伪代码
```java
public OrderDetail getOrderDetail(String orderId, Long userId) {
validateOrderId(orderId);
String key = "order:detail:v1:" + orderId;
CacheValue cv = redis.get(key, CacheValue.class);
if (cv != null) {
if (cv.isEmpty()) {
return null;
}
checkPermission(cv.getData().getUserId(), userId);
return cv.getData();
}
String lockKey = "lock:order:detail:" + orderId;
boolean locked = redis.setNx(lockKey, "1", 500, TimeUnit.MILLISECONDS);
if (!locked) {
sleep(50);
cv = redis.get(key, CacheValue.class);
if (cv != null) {
return cv.isEmpty() ? null : cv.getData();
}
return queryDb(orderId, userId);
}
try {
cv = redis.get(key, CacheValue.class);
if (cv != null) {
return cv.isEmpty() ? null : cv.getData();
}
OrderDetail db = orderMapper.selectById(orderId);
if (db == null) {
redis.set(key, CacheValue.empty(), 60, TimeUnit.SECONDS);
return null;
}
checkPermission(db.getUserId(), userId);
int ttl = calcTtl(db.getStatus()) + random(0, 300);
redis.set(key, CacheValue.of(db), ttl, TimeUnit.SECONDS);
return db;
} finally {
redis.del(lockKey);
}
}
```
### 13.2 更新缓存伪代码
```java
@Transactional
public void updateOrder(OrderUpdate cmd) {
orderMapper.update(cmd);
afterCommit(() -> {
cacheEvictService.evict(cmd.getOrderId());
mq.sendDelay(
new OrderChangedEvent(cmd.getOrderId(), System.currentTimeMillis()),
1,
TimeUnit.SECONDS
);
});
}
public void evict(String orderId) {
String key = "order:detail:v1:" + orderId;
try {
redis.del(key);
} catch (Exception e) {
retryQueue.add(orderId);
log.error("evict order cache failed, orderId={}", orderId, e);
}
}
```
---
## 14. 结论
订单详情查询接口加缓存整体收益明显,但必须接受最终一致。推荐采用:
- Redis 作为主缓存
- Cache Aside
- 更新 DB 后删除缓存
- MQ 延迟双删
- 动态 TTL + 随机抖动
- 空值缓存防穿透
- 单飞锁防击穿
- 本地缓存 + 限流降级防雪崩
- 全链路监控 + 灰度回滚
该方案可在保证接口可用性的前提下,显著降低 DB 压力和查询延迟。
工程化提示词
帮我写一份「订单查询接口加缓存」的设计文档。
要求:
1. 验收标准必须可量化(延迟、命中率、回源 QPS,写成"从 X 到 Y"),禁止"明显提升""显著优化"这类词;
2. 关键决策(用什么缓存、怎么失效)必须写"为什么这么选",且给一个被否决的备选;
3. 我提供的信息不够的,先问我,不许自己编——比如目标 QPS、订单数据量级、读写比,你不知道就别假设。
已知信息:订单查询 p99 延迟 800ms,业务方要求优化。
工程化输出(选择不写:先问清楚+产出形态预告+填空表单)
收到。已知信息只有「订单查询 p99 = 800ms」。按你第 3 条要求,我不能先假设目标 QPS、订单量级、读写比、一致性要求等,否则验收标准会变成编造,关键决策也没有依据。所以先问清楚,再写最终设计文档。
## 请先确认这些信息
### A. 不提供就无法写「从 X 到 Y」的验收标准
1. **当前与目标 QPS**
- 当前订单查询日均/峰值 QPS 是多少?
- 优化后要支撑的峰值 QPS 是多少?
- 主存储/DB 能承受的回源 QPS 上限是多少?
2. **读写比**
- 订单查询 QPS : 创建/支付/取消/退款等更新 QPS 是多少?
- 查询里,按订单号查 vs 按用户/商家/状态/时间范围列表查,占比多少?
3. **当前基线**
- 当前缓存命中率是多少?如果现在没有缓存,是否可视为无缓存?
- 当前回源 QPS 均值/峰值是多少?
- 当前 p50 / p95 / p99 分别是多少?
- p99 = 800ms 是在什么负载、什么环境、什么查询条件下测得的?
4. **目标值**
- 业务方要求的目标 p99 是多少?
- 目标缓存命中率是多少?
- 目标回源 QPS 上限是多少?
- 验收统计周期是 5 分钟、1 小时、1 天峰值,还是其他?
5. **验收口径**
- 命中率按请求级还是 key 级统计?
- 空结果是否计入命中率?
- 回源 QPS 按均值还是峰值?
- 只统计按订单号查询,还是所有订单查询?
### B. 不提供就无法写「为什么这么选」和「被否决备选」
6. **订单数据量级**
- 订单总量、日增、可缓存范围是多少?例如仅近 3 个月还是全量?
- 单条缓存对象大小多少?
- 热点集中度如何?头部订单/用户占比多少?
7. **查询模式**
- 查询条件有哪些?是否分页、排序、多条件组合?
- 是否要缓存列表结果?列表缓存失效很复杂,必须明确。
8. **一致性与失效**
- 订单更新后,允许读到旧数据多久?0ms、1s、5s、分钟级?
- 哪些状态必须强一致?支付状态、发货状态、退款状态是否可短期旧?
- 更新入口有哪些?是否有 binlog / CDC / MQ 事件?
- 能否接受写后删除、双写、短 TTL、版本号等策略?
9. **缓存基础设施**
- 已有 Redis 版本、集群规模、可用容量、预算?
- 是否可用本地缓存?是否多机房/多可用区?
- 是否允许引入新组件,如 Canal、MQ、配置中心?
10. **降级与安全**
- Redis 故障时,回源 DB 如何保护?限流、熔断、单飞、降级?
- 订单敏感字段是否要加密/脱敏后入缓存?
- 灰度、回滚、监控告警要求是什么?
## 你回复后,我会按这个格式写最终文档
验收标准会写实,不写「明显提升」「显著优化」,例如:
- p99:从 800ms 到 `<目标 p99>`ms,在峰值 QPS `<目标 QPS>` 下
- 缓存命中率:从 `<当前命中率>`% 到 `<目标命中率>`%
- 回源 QPS:峰值从 `<当前回源 QPS>` 到 `<目标回源 QPS>`
- 错误率:保持 `<目标错误率>`% 以下
- 缓存失效延迟:p99 从 `<当前>` 到 `<目标>`ms,如业务要求
关键决策会写成:
- 缓存选型:选 `<方案 A>`,因为 `<依据>`;被否决备选 `<方案 B>`,原因是 `<原因>`
- 失效策略:选 `<策略 C>`,因为 `<依据>`;被否决备选 `<策略 D>`,原因是 `<原因>`
文档结构会包括:背景与现状、目标与非目标、量化验收标准、Key/Value 与 TTL、缓存选型、失效与一致性、穿透/击穿/雪崩、降级、监控告警、灰度回滚、风险与未决问题。
请按下面模板回复,我就可以继续写最终设计文档:
```text
目标 QPS(均值/峰值):
当前 QPS(均值/峰值):
读写比:
订单总量/日增/可缓存范围:
单条缓存对象大小:
查询模式:
一致性要求:
更新链路/CDC/MQ:
当前缓存命中率/回源 QPS:
目标 p99/命中率/回源 QPS:
Redis/本地缓存现状:
多机房/预算/安全要求:
```
问题三:所以,”说具体”到底要说什么?
两组实测合起来,”说具体”就是三件套:
① 给输出结构:你要它按什么框架答(一句话/关键路径/坑),别只给题目。结构即约束。
② 给量化验收:什么叫”好”,写成”从 X 到 Y”,禁止”明显提升”。没有数字就没有验收。
③ 给取舍:关键决策要”为什么这么选”,加一个被否决的备选。可评审 = 可验收 + 有取舍。
注意这三件套有个共同点:它们都是你脑子里本来就有、但嘴上没说的东西。你知道你要 30 秒看完,你知道 p99 要降到多少,你知道为什么不选另一个方案——你只是没告诉它。提示词工程,一大半的工作,就是把你脑子里的约束,搬到纸上。
做技术方案评审的时候,我们不也这么要求的吗?结构、数字、取舍。对 AI,用同一套标准就行。
实操演练
必做部分 + 一步加餐。准备一个你日常在用的 AI 助手。
检验一下
①你的第一条结构化提示词:给你最高频的一个工作场景(读代码/写文档/写总结都行)写一条带三件套的提示词——输出结构 + 量化验收 + 至少一处取舍。实测一次,记录:它哪一点做到了,哪一点没做到。
这是资产库的第二格。和第 1 讲的实验记录放一起。
本讲实测来自沈剑的案例库 002、006(deepseek-flash,2026-09-17~09-28,独立会话,复现脚本和原始输出存档)。
官方参考:Anthropic · Prompt engineering overview(be clear and direct 一章与”结构即约束”同源)。
但还有一个没解决的问题:结构把输出管住了,可让它”选边站”的时候,它给你一张对比表,理由全是放之四海皆准的——跟你的业务有什么关系?——第 3 讲,我们讲约束即立场。
知其然,知其所以然。思路比结论更重要。