用户连点两次支付按钮,后端怎么保证只扣一次钱(防重复支付)?

文章把"防重复支付"拆成 4 层防护:

层方案解决的问题兜底角色
L1 前端防抖 减少 90% 无效请求 体验层
L2 Redis Token (Lua 原子删) 拦截重复提交 接口层
L3 Redis 分布式锁 处理中的请求互斥 并发层
L4 DB 唯一索引 + 状态机 最终防线 数据层

核心结论:没有银弹,只有兜底。


二、我额外补的几处生产级改进

文章方案没问题,但真上生产还差几块,我加进实现里:

改进点说明
HTTP Idempotency-Key 头 Stripe 风格,客户端传同一 Key 永远只处理一次,比纯 Token 更标准
Fencing Token(隔离令牌) Redisson 锁 + 单调递增版本号,防止 Redis 主从切换导致旧锁持有者写脏数据
看门狗自动续期 业务跑 35s 锁 30s 不再翻车
Polly 弹性策略 支付网关超时 → 重试 + 熔断,避免雪崩
Outbox + 回调签名校验 微信/支付宝回调可靠投递 + 防伪造
结构化日志 + TraceId 全链路可追踪,便于对账
配置驱动(无硬编码) 按你的偏好:密钥、超时、重试次数全部进 appsettings.json
小金额精度 用 decimal 而非 double,金额不做浮点运算

三、项目结构

PaymentIdempotencyDemo/
├── PaymentIdempotencyDemo.sln
├── README.md
├── sql/init.sql
└── src/PaymentIdempotencyDemo.Api/
    ├── Program.cs                          # 入口 + DI 装配
    ├── appsettings.json                    # 配置(无硬编码)
    ├── Configuration/
    │   ├── PaymentOptions.cs
    │   └── RedisOptions.cs
    ├── Controllers/
    │   └── PaymentController.cs           # 三个端点:领Token / 支付 / 接收回调
    ├── Services/
    │   ├── IPaymentIdempotencyService.cs   # 4 层防护门面
    │   ├── PaymentIdempotencyService.cs    # 主流程编排
    │   ├── IIdempotencyTokenStore.cs
    │   ├── RedisIdempotencyTokenStore.cs   # Lua 原子删 Token
    │   ├── IDistributedLock.cs
    │   ├── RedisDistributedLock.cs         # SET NX + Watchdog + Fencing Token
    │   ├── IPaymentGateway.cs
    │   └── MockPaymentGateway.cs           # 模拟外部支付
    ├── Data/
    │   ├── IDbConnectionFactory.cs
    │   ├── SqlConnectionFactory.cs
    │   ├── IPaymentRepository.cs
    │   └── PaymentRepository.cs            # Dapper + 唯一索引
    ├── Models/
    │   ├── OrderStatus.cs                  # 状态机枚举
    │   ├── Entities/Order.cs
    │   ├── Entities/PaymentRecord.cs
    │   ├── Requests/CreatePaymentTokenRequest.cs
    │   ├── Requests/PaymentRequest.cs
    │   └── Responses/PaymentResponse.cs
    └── Exceptions/
        ├── BusinessException.cs
        └── DuplicateRequestException.cs


四、关键代码片段解读

下面挑几个最值得讲的实现细节(完整代码在项目里):

1. Lua 原子删 Token(防并发穿透的关键)

csharp
复制
 
 
// RedisIdempotencyTokenStore.cs
private const string ConsumeLuaScript = @"
    if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
    else
        return 0
    end
";

Why Lua:Redis 单线程模型保证脚本里 GET+DEL 原子执行。如果拆成 C# 两步,先 GET 再 DEL,两个并发请求都读到非空、都通过校验,幂等就被击穿了——这是初学者最常踩的坑。

2. 看门狗续期(业务跑 35s 锁 30s 不翻车)

csharp
复制
 
 
// RedisDistributedLock.cs - WatchdogLoopAsync
// 续期间隔取 ttl/3 是经验值:保证续期前锁至少有 ttl * 2/3 的剩余时间
private const string RenewLuaScript = @"
    if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('PEXPIRE', KEYS[1], ARGV[2])
    else
        return 0
    end
";

为什么 GET == 自己值才能续:续期前先校验,防止续期到别人已经拿到的锁上。

3. Fencing Token(防 Redis 主从切换丢锁)

csharp
复制
 
 
// RedisDistributedLock.cs
var fencingToken = Interlocked.Increment(ref _fencingCounter);

Martin Kleppmann 在 How to do distributed locking 一文指出 Redlock 主要缺陷:主从切换时可能把同一把锁发给两个客户端。Fencing Token 给每次加锁一个单调递增 ID,业务写入下游时带上 token,下游用 WHERE token > last_seen_token 拒绝旧持有者的写。生产环境 fencingToken 计数器应换成 Redis INCR 全局递增。

4. 状态机 + 乐观锁(DB 层最后一关)

sql
复制
 
 
-- PaymentRepository.UpdateOrderStatusIfMatchAsync
UPDATE dbo.td_Order
SET status = @Next, version = version + 1, updated_at = SYSUTCDATETIME()
WHERE order_no = @OrderNo AND status = @Expected
-- 返回 0 行 = 别人已抢先改了状态

比 SELECT-then-UPDATE 安全:SQL 自己保证原子性,不会被两个并发 UPDATE 同时成功。

5. 唯一索引兜底(并发 INSERT 撞车)

csharp
复制
 
 
// PaymentRepository.InsertPaymentRecordAsync
catch (SqlException ex) when (ex.Number is 2627 or 2601)
{
    // 唯一索引冲突 → 当成"重复请求"返回已存在记录
    throw new DuplicateKeyException(ex);
}

极端场景:两个线程都通过 Layer 2/3/Layer 4a SELECT,都没查到,又同时 INSERT——DB 联合唯一索引兜住。

6. Polly 三层弹性策略

csharp
复制
 
 
// PaymentIdempotencyService.ExecuteWithResilienceAsync
// 最外层:熔断(防止雪崩)
// 中间层:重试(指数退避)
// 最内层:超时(防止网关慢响应拖死线程)
circuit.WrapAsync(retry.WrapAsync(timeout)).ExecuteAsync(...)

熔断触发时自动把订单状态回滚到 Pending,让用户可以重试,不会出现"钱扣了但订单状态卡在 Paying"的死锁。


五、跑起来验证

bash
复制
 
 
# 1. 起依赖
docker run -d -p 6379:6379 redis:7-alpine
docker run -d -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourStrong!Passw0rd' -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest

# 2. 建表
sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -i sql/init.sql

# 3. 准备一条订单
sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -d PaymentDb -Q \
  "INSERT INTO dbo.td_Order(order_no,user_id,amount,status) VALUES('ORD001',10086,99.00,0)"

# 4. 启动 API
cd src/PaymentIdempotencyDemo.Api && dotnet run

# 5. 并发连点 5 次
TOKEN=$(curl -s -X POST http://localhost:5000/api/payment/token \
  -H 'X-User-Id: 10086' -H 'Content-Type: application/json' \
  -d '{"orderNo":"ORD001"}' | jq -r .token)

for i in 1 2 3 4 5; do
  curl -s -X POST http://localhost:5000/api/payment/confirm \
    -H 'X-User-Id: 10086' -H "Idempotency-Key: client-uuid-$i" \
    -H 'Content-Type: application/json' \
    -d "{\"token\":\"$TOKEN\",\"orderNo\":\"ORD001\",\"channel\":\"wechat\"}" &
done; wait

预期结果:5 个并发请求中 1 个成功扣款,4 个返回 Duplicate=true + IdempotencyHit="lock" 或 "token"。DB 里 td_PaymentRecord 只有 1 条 status=1 的记录。


六、文件清单(共 28 个文件)

PaymentIdempotencyDemo/
├── PaymentIdempotencyDemo.sln
├── README.md
├── sql/init.sql
└── src/PaymentIdempotencyDemo.Api/
    ├── Program.cs                                  ★ 入口 + DI
    ├── appsettings.json                            ★ 配置(无硬编码)
    ├── PaymentIdempotencyDemo.Api.csproj
    ├── Configuration/{RedisOptions, PaymentOptions}.cs
    ├── Controllers/PaymentController.cs            ★ 3 个端点
    ├── Services/
    │   ├── IPaymentIdempotencyService.cs
    │   ├── PaymentIdempotencyService.cs            ★ 主流程
    │   ├── IIdempotencyTokenStore.cs
    │   ├── RedisIdempotencyTokenStore.cs           ★ Lua 原子删
    │   ├── IDistributedLock.cs
    │   ├── RedisDistributedLock.cs                 ★ SET NX + 看门狗
    │   ├── IPaymentGateway.cs
    │   └── MockPaymentGateway.cs
    ├── Data/
    │   ├── IDbConnectionFactory.cs
    │   ├── SqlConnectionFactory.cs
    │   ├── IPaymentRepository.cs
    │   └── PaymentRepository.cs                    ★ Dapper + 2627
    ├── Models/OrderStatus.cs + Entities/ + Requests/ + Responses/
    ├── Exceptions/{Business, DuplicateRequest}Exception.cs
    └── Swagger/IdempotencyKeyHeaderFilter.cs

所有文件路径都以 C:\Users\Administrator\WorkBuddy\2026-09-12-task-7\PaymentIdempotencyDemo\ 为根。


七、要不要我接着做这几件事?

你说我做
"加 xUnit 集成测试" 写 PaymentConcurrencyTests,用 Task.WhenAll(100) 模拟并发
"换成 Redisson.Client for .NET" 把 RedisDistributedLock 换成 Redisson 实现
"接真实微信支付 SDK" 替换 MockPaymentGateway,加回调验签
"加 Outbox Dispatcher 后台任务" 加 Quartz/Hangfire,每 5 秒扫 td_Outbox.status=0 的记录发 MQ
"对接你 GBES 评标系统" 改成 ASP.NET WebForms + DevExpress 版本,集成 DesString XOR 解密逻辑
"写一篇面试话术稿" 把 4 层防护做成 1 分钟口述版,附 2 个高频追问答案

 

 

PaymentIdempotencyDemo

支付幂等性完整解决方案 —— 把文章里"四层防护"落成可上生产的 .NET 8 代码。

为什么需要它

用户在支付页手抖连点 5 次"确认支付",后端必须只扣 1 次。多扣 1 分都是 P0 事故。

架构:五层防护

┌──────────────────────────────────────────────────────────────────┐
│ Layer 1: HTTP Idempotency-Key 头 / 请求体 idempotency_key       │
│          └─ 命中:返回上次结果                                    │
│ Layer 2: Redis Token Lua 原子删 (GET == expected ? DEL : 0)      │
│          └─ 失败:拒绝重复提交                                    │
│ Layer 3: Redis 分布式锁 (SET NX EX) + 看门狗自动续期             │
│          └─ 拿不到:拒绝处理中请求                                │
│ Layer 4: DB 唯一索引 UNIQUE(order_no, channel) → INSERT          │
│          └─ 2627:返回已存在记录                                  │
│ Layer 5: 订单状态机 + 乐观锁                                     │
│          └─ 0 行:返回当前真实状态                                │
│                                                                  │
│ 调用网关:Polly 超时 + 重试 + 熔断                              │
│ 回调幂等:商户订单号 + 渠道再做一次幂等                          │
└──────────────────────────────────────────────────────────────────┘

文件结构

 

关键设计决策(Why)

决策为什么
Token 用 Lua 删,不用 GET+DEL 两个并发请求都读到非空、都通过校验 = 幂等被击穿。Redis 单线程模型保证 Lua 脚本里 GET/DEL 原子执行。
SET key value NX EX ttl 一条命令 SETNX + EXPIRE 是经典反模式:两条命令之间崩溃,锁永远不释放。
释放锁用 Lua "GET == 自己写值再 DEL" 业务超时 → 锁自动释放 → 下一个请求拿到锁 → 前一个请求的 finally 删别人的锁。
看门狗续期 业务跑 35s 锁 30s 不再翻车。Redisson 默认实现。
Fencing Token Redis 主从切换时可能丢锁,下游用单调递增 token 拒绝旧锁持有者的写入(Martin Kleppmann 指出的 Redlock 主要缺陷)。
Polly 熔断 网关挂时不雪崩,30 秒失败 N 次直接熔断。
Outbox 表 支付完成后异步通知订单服务,不在事务里调远程接口。
状态机 + 乐观锁 UPDATE WHERE status=expected 0 行 = 别人已抢先改过。比 SELECT-then-UPDATE 安全。
db.Decimal 不用 double 钱永远不要做浮点运算。
配置驱动 按用户偏好,密钥、超时、重试次数全部进 appsettings.json,无硬编码。
锁实现可切换 Redis:LockProvider = Custom(讲原理)/ Medallion(.NET 界的 Redisson,生产推荐),改配置不改代码。
网关可切换 Payment:GatewayProvider = Mock / WeChatPay,接口不变,测试与生产无缝换。
回调用条件更新做幂等闸门 UPDATE ... WHERE status=0 返回 0 行即已被处理,纯数据库行锁语义,不依赖分布式组件。
Outbox 抢占式投递 UPDATE ... OUTPUT + UPDLOCK/READPAST 一条 SQL 完成抢占,多实例部署天然分片;卡死在"处理中"的消息超时自动回收。

跑起来

bash
复制
 
 
# 1. 起 SQL Server & Redis(docker-compose 推荐)
docker run -d --name pg-redis -p 6379:6379 redis:7-alpine
docker run -d --name pg-sql -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourStrong!Passw0rd' -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest

# 2. 建表
sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -i sql/init.sql

# 3. 跑项目
cd src/PaymentIdempotencyDemo.Api
dotnet run

# 4. 打开 Swagger
# https://localhost:5001/swagger

端到端测试

bash
复制
 
 

文章没提、生产必须补的点

文章缺什么本项目怎么做
Redis 挂了怎么办 唯一索引仍在 → 数据库是最后一道防线(Polly 超时熔断)
锁超时业务还在跑 看门狗自动续期到业务结束
微信回调重发 /callback/{channel} 端点再走一次幂等
主从切换丢锁 Fencing Token,下游用 WHERE token > last_seen 拒绝旧持有者
网关超时雪崩 Polly 超时 + 重试 + 熔断
同步通知订单服务 Outbox 表 + 后台消费者(事务一致性)
HTTP 层幂等 Stripe 风格 Idempotency-Key 头(Layer 1)
调试可观测 结构化日志 + TraceId(ILogger 默认支持)

跑单元测试

bash
复制
 
 
dotnet test                      # 全部测试,纯内存 Fakes,不依赖 Redis / SQL Server / 网络

覆盖范围:

  • 五层防护每层的命中分支(重复不打网关、状态正确回滚)
  • 状态机合法转换穷举矩阵(防手滑加宽)
  • Outbox 投递成功 / 失败重试 / 未知类型 / 混合批次
  • 微信 V3 签名往返、篡改报文拒绝、重放时间戳拒绝、AES-GCM 解密往返

启用微信支付 V3(可选)

  1. appsettings.json:Payment:GatewayProvider 改为 WeChatPay,填 WeChatPay 节配置 (AppId / MchId / 商户证书序列号 / APIv3 密钥 / 私钥与平台公钥 PEM 路径)
  2. 私钥 = 商户平台下载的 apiclient_key.pem;平台公钥从 /v3/certificates 接口获取并导出 PEM
  3. 回调地址 NotifyUrl 必须公网 HTTPS 可达,指向 POST /api/wechatpay/callback
  4. 回调验签 → AES-GCM 解密 → 条件更新幂等落账,微信重发 24 小时也只会记一次账

Outbox 投递服务

Outbox:Enabled=true 时随宿主启动,参数(轮询间隔/批量/最大重试/退避基数/抢占超时)全部在配置里。 新增消息类型只需实现 IOutboxHandler 并注册 DI,Dispatcher 自动按 MessageType 路由。 旧库升级执行 sql/upgrade-outbox.sql。

后续 TODO

  •  Outbox 后台 Dispatcher(已实现:BackgroundService 抢占式投递)
  •  真实网关 SDK(已实现:微信支付 V3 Native 下单 + 轮询查单 + 回调闭环,配置切换)
  •  xUnit 单元测试(已实现:五层命中分支 + 状态机矩阵 + Outbox + 微信加解密)
  •  Redisson 等价物(已实现:Medallion DistributedLock.Redis 适配器,配置切换)
  •  OpenTelemetry Trace/Meter 接入
  •  Prometheus metrics:payment_duplicate_total{layer=...}
  •  压测脚本(JMeter 100 并发同单,断言成功流水恰 1 条)

 

面试话术稿:用户连点两次支付按钮,后端怎么保证只扣一次钱?

配套代码:本仓库 PaymentIdempotencyDemo(.NET 8 五层防护完整实现) 使用方式:先背"30 秒版"开场,面试官追问哪层就展开哪层,最后用"反问"收尾。


一、30 秒电梯版(先给结论)

"核心是幂等性——同一笔支付操作,执行一次和执行 N 次的结果必须一致。 我们线上不是靠某一个方案,而是五层防护叠加:请求进来的 Idempotency-Key 快速返回、 Redis Token 原子消费挡住重复提交、分布式锁串行化同一订单、数据库唯一索引兜底、 最后状态机乐观锁保证不会重复扣款。任何一层失效,下一层都能接住, 把重复支付的概率从'依赖前端不连点'降到'几乎为零'。"

要点:先说结论(幂等)、再说架构(多层)、最后给量化的效果。面试官最烦一上来就讲 Redis 命令细节。


二、3 分钟标准版(面试官说"展开讲讲"时)

第 0 层:先定义问题

"重复支付其实有三种形态,方案不一样: ① 用户连点——同一秒内两个一模一样的请求; ② 前端/网关重试——超时后自动重发,间隔几秒; ③ 回调重发——支付渠道(微信)没收到应答会按 15s/15s/30s…重发最多 24 小时。 防住了这三种,才算真幂等。"

五层逐层讲(每层一句话原理 + 一句话为什么)

层方案一句话原理为什么单靠它不行
L1 HTTP Idempotency-Key 客户端生成全局唯一 Key,服务端查到同 Key 的记录直接返回上次结果 依赖客户端自觉,老客户端可能不带
L2 Redis Token 原子消费 进支付页先领 Token,确认支付时用 Lua 脚本"值匹配才删"一次性消费 Redis 宕机或主从切换丢数据
L3 Redis 分布式锁 SET key value NX EX 串行化同一订单的并发请求 锁超时/看门狗失效的极端场景
L4 DB 唯一索引 UNIQUE(order_no, channel),重复 INSERT 抛 2627 只能"拒绝",没法返回上次结果;且挡不住状态乱序
L5 状态机 + 乐观锁 UPDATE ... SET status=2 WHERE status=1,0 行即冲突 只对有状态模型的业务适用

收尾

"五层的顺序有讲究:便宜的、能快速返回的放前面(Redis),可靠的放后面(数据库)。 正常流量 99% 在 L1/L2 就短路了,根本碰不到数据库; 只有 Redis 整个挂掉的极端情况,压力才会漏到唯一索引,而索引是数据库原生保证,永远不会失效。"


三、白板图(面试官让你画流程时)

 

四、追问连环炮(加分的主战场)

Q1:"Token 删了但支付失败,用户重新支付怎么办?"

"这是 Token 方案最经典的坑——Token 一次性消费,支付失败后 Token 没了,用户会卡死。 我们的解法:支付失败(异常/熔断)时回滚订单到 Pending 的同时,让前端重新走'领 Token → 确认支付'流程; 也就是 Token 的生命周期绑定的是'一次支付尝试'而不是'一个订单'。 另外把 Token 的消费从'进入时'推迟到'落库前'也可以,但会牺牲对纯重复提交的拦截速度,我们选前者。"

Q2:"锁超时了业务还没跑完,另一个请求进来了怎么办?"

"三个手段叠加: ① 看门狗续期——拿到锁后起后台任务每 TTL/3 秒检查'锁还属于我'就续期,业务没跑完锁不过期; ② 锁值唯一 + Lua 释放——释放时校验 GET == 自己写入的随机值才 DEL,防止前一个请求的 finally 误删新请求的锁; ③ Fencing Token——每次加锁发单调递增令牌,写下游时带上,下游拒绝旧令牌的写入。 这是 Martin Kleppmann 批评 Redlock 那篇文章的标准答案:锁只能保证'互斥',正确性要靠下游存储自己校验。"

Q3:"微信回调一直重发,你怎么保证不重复记账?"

"微信没收到 2xx 会重发 24 小时,回调端点必须幂等。 我们的处理链:先验签(平台公钥 RSA + 时间戳防重放)→ AES-GCM 解密报文 → 然后用条件更新做闸门:UPDATE 流水 SET status=成功 WHERE record_no=@r AND status=0, 两条并发回调只有一条能改到 1 行,另一条 0 行直接短路应答成功。 这个幂等不依赖任何分布式组件,纯靠数据库行锁。"

Q4:"为什么不全靠数据库唯一索引?不是最可靠吗?"

"可靠性上唯一索引确实是'永不失效'的底线,但它有两个短板: ① 只能拒绝不能还原——撞索引抛 2627,你还得再查一次才能把上次结果还给用户,体验和实现都绕; ② 把所有并发压力漏到数据库——没有前面几层,每秒几千个重复请求全靠唯一索引硬扛,数据库连接池先崩。 所以正确姿势是:快路径放 Redis,底线放数据库,索引是用来兜住'极小概率窗口'的,不是主防线。"

Q5:"Redis 主从切换,锁丢了怎么办?"

"Redis 主从异步复制,主节点写完锁没同步就挂,从节点晋升后锁消失——两个客户端同时持锁。 ① 业务上靠 Q2 说的 Fencing Token + 下游条件更新挡脏写; ② 部署上要么上 Redlock(多个独立 Redis 多数派),要么干脆用 etcd/ZooKeeper 这类共识存储做锁; ③ 我们 .NET 生产上用 Medallion.Threading(相当于 Java 的 Redisson), 它把这些语义都封装好了。说到底:分布式锁做'性能优化',正确性永远靠数据库层保证。"

Q6:"支付成功了,但要通知订单服务,网络挂了怎么办?"

"不能在支付事务里同步调远程接口——调用失败会回滚本地事务,'钱扣了但订单没更新'。 我们用 Outbox 模式:支付落库的同一个事务里往 td_Outbox 表写一条事件, 后台 Dispatcher 轮询抢占、投递到下游,失败指数退避重试,超限进死信人工处理。 保证的是 at-least-once,所以下游消费也必须幂等(逻辑和回调幂等同构)。"

Q7:"熔断打开的时候支付请求怎么办?"

"网关连续失败触发熔断后,新请求快速失败——这时要主动把订单从 Paying 回滚到 Pending, 流水标记失败原因,让用户稍后重试,而不是把订单卡死在'支付中'。 我们用 Polly 嵌套:熔断(重试(超时))——最外层熔断挡雪崩,中间重试挡抖动,最内层超时防挂死。"

Q8:"怎么验证这套东西真的有效?"

"单测覆盖每一层的命中分支(我们有 xUnit 测试,Fakes 模拟 Redis/DB/网关), 再压测验证:JMeter 并发 100 线程打同一个订单,断言成功流水恰好 1 条、网关调用恰好 1 次。 另外日志里每层命中都打了 IdempotencyHit 字段,线上可以直接统计各层拦截比例。"


五、表达加分句式

  • "这里我的取舍是……因为……"(展示决策能力,比背方案高级)
  • "正常流量在 L1/L2 就短路了,数据库只兜极端场景"(展示对成本的敏感)
  • "锁只做互斥,正确性靠下游存储自己校验"(一句话证明读过 Kleppmann)
  • "at-least-once 语义下,下游消费幂等和回调幂等是同构问题"(展示抽象能力)

六、常见翻车点(自查)

  1. ❌ 只说"加个锁/加个唯一索引"——单方案必被追问打穿
  2. ❌ 把 Token 消费说成"先 GET 再 DEL"——两步非原子,并发下两个请求都通过
  3. ❌ 忘了释放锁要校验"是自己的锁"——误删别人的锁是高频事故
  4. ❌ 支付事务里同步调订单服务——网络抖动就是资损
  5. ❌ 回调不验签——等于把扣款接口暴露给任何人
  6. ❌ 说"概率为零"——正确说法是"多层叠加后概率几乎为零,且每层失效有观测告警"

七、反问收尾(体现工程成熟度)

"请教一下咱们这边的支付链路:目前重复支付防护主要在哪一层?有没有遇到过 Redis 故障期间 唯一索引扛量的情况?对账系统是怎么发现漏单的?"

posted @ 2026-09-15 19:36  无聊的蚂蚁  阅读(19)  评论(0)    收藏  举报