Skip to content

第 18 章:SaaS 之二 — 配额、计费与 API 治理

示例工程:仍是 examples-middleware/todo_api_saas(Postgres + Redis)。 第 17 章解决「谁能进来、能看谁的数据」,本章解决「能用多少、怎么收钱、接口怎么对外」。

学习目标

  1. 会用 Redis 实现按租户限流与套餐配额,说清两者的区别
  2. 理解幂等键(Idempotency-Key)解决什么事故,能完整实现
  3. 掌握计费 webhook 的通用模式:HMAC 验签 → 幂等处理 → 快速响应
  4. 建立 API 治理的清单意识:版本化、分页、错误规范

18.1 限流与配额:一个防打挂,一个是产品

两者常被混为一谈,实际分工完全不同:

限流(rate limit)配额(quota)
目的保护系统不被打挂执行产品定价
单位请求数/时间窗业务资源量(条数、席位、存储)
超出后429,稍后重试即可403 + 引导升级套餐
示例实现30 请求/分钟/租户free 租户最多 5 条 todo

限流INCR rl:{tenant_id}:{当前分钟} + EXPIRE 60,计数过 30 返回 429。这是固定窗口算法——实现最简单,缺点是窗口边界有毛刺(两个窗口交界处最多放过 2 倍流量),滑动窗口/令牌桶是它的工程化升级(留作练习)。注意限流的 key 按租户分——SaaS 里一个失控客户不能影响其他客户,这也是隔离的一部分。

配额:free 租户创建第 6 条 todo 时收到 403 {"error":"todo quota exceeded (free plan), upgrade to pro"}。配额检查放在 services 层 create 里(COUNT 后判断)——配额是「产品定价的技术执行点」,它和第 18.3 节的 webhook 合起来就是商业闭环:付费 → webhook 升级 plan → 配额即刻放开。

18.2 幂等键:客户端重试不该变成重复下单

真实事故的标准剧本:客户端 POST 创建订单 → 网络超时 → 客户端重试 → 下了两单。超时不等于失败,这是分布式的基本事实(第 15.5 节的幂等在 API 层的形态)。

行业标准解法(Stripe 等支付 API 皆如此):客户端为每次「业务意图」生成一个 Idempotency-Key 头,服务端:

text
收到 POST + Idempotency-Key
  → SET idem:{tenant}:{key} <响应> NX EX 86400
  → NX 成功(第一次见):正常执行业务,把响应存进这个 key
  → NX 失败(重试来了):不执行业务,直接返回上次存的响应

客户端重试多少次,业务只发生一次,且每次拿到相同的响应。示例在 POST /todos 上实现了完整链路,集成测试断言「同 key 两次 POST 返回同一个 todo id,库里只有一条」。

18.3 计费 webhook:支付商回调的通用模式

SaaS 计费的主流方案是接支付平台(Stripe / Paddle / 国内的支付宝、微信支付企业能力),你的服务端要做的核心工作是正确处理它们的回调(webhook)todo_api_saasPOST /webhooks/billing 演示了完整模式:

text
支付商 → POST /webhooks/billing
         body: {"event":"subscription.updated","tenant_slug":"acme","plan":"pro"}
         头:   X-Signature: HMAC-SHA256(body, WEBHOOK_SECRET)

服务端 → 1. 验签(恒定时间比较);失败 401,什么都不做
         2. 处理事件:更新租户 plan
         3. 快速返回 200

三步各有讲究:

  • 验签是唯一防线:webhook 端点公网可达、没有 JWT——任何人都能 POST。没有验签,攻击者一条 curl 就能把自己升成 pro。比较签名要用恒定时间比较(普通 == 的提前返回会泄露时序信息)
  • 处理要幂等:支付商在收不到 200 时会重发同一事件(at-least-once,和第 14.6 消息队列同源)——示例的「设 plan」天然幂等,涉及加钱/发货的事件要配 18.2 的幂等手段
  • 快速 200:回调处理超时会被判失败并重发;重活(发邮件、开通资源)应扔进消息队列异步做(第 14.6),webhook 只做验签 + 落记录

订阅模型本身(套餐、席位、按量、试用期、退款)是产品问题,技术侧记住骨架:plan 存在租户上,配额读 plan 执行,webhook 是 plan 的唯一写入口

18.4 API 治理清单

对外 API 是 SaaS 的产品界面,规范一开始就要立好(改约定 = 破坏所有客户的集成):

  • 版本化:路径版本 /v1/todos 最直白;破坏性变更只能进 /v2/v1 按承诺的周期维护
  • 分页:一律强制分页(limit + 游标或 offset),无上限的列表接口会在客户数据量大后拖垮数据库(第 12 章扩展作业的分页在 SaaS 里不是可选项)
  • 错误规范:统一错误体(示例全程 {"error": "..."})、语义正确的状态码——401 未认证 / 403 无权限或超配额 / 404 不存在或不属于你 / 429 限流,客户端才能写出可靠的错误处理
  • 对外 webhook:当你的 SaaS 要回调客户的系统时,角色互换——你要给客户提供验签密钥、做重试与退避、提供事件日志查询
  • 审计日志:企业客户的刚需。最小形态:(时间, tenant_id, user_id, 动作, 资源) 追加写一张表,敏感操作(删除、导出、权限变更)必记

本章小结

  • 限流保护系统(429,按租户分 key),配额执行定价(403 + 升级引导),分工不同
  • 固定窗口限流最简单但有边界毛刺,滑动窗口/令牌桶是升级方向
  • 幂等键 = SET NX 存响应:重试安全的行业标准,超时不等于失败
  • webhook 三步:HMAC 恒定时间验签 → 幂等处理 → 快速 200,重活进队列
  • plan 存租户、配额读 plan、webhook 是 plan 唯一写入口——商业闭环的技术骨架
  • API 治理四件套:版本化、强制分页、统一错误体、审计日志

自测清单

  • [ ] 我能说出限流和配额在目的、单位、响应码上的区别
  • [ ] 我能解释固定窗口限流的边界毛刺问题
  • [ ] 我能画出幂等键从客户端生成到服务端判重的完整流程
  • [ ] 我能说出 webhook 验签缺失的具体攻击后果
  • [ ] 我能解释为什么 webhook 处理必须幂等、必须快
  • [ ] 我能列出 API 治理清单里至少四项

练习

  1. 把限流从固定窗口改成滑动窗口(Redis ZSET 存时间戳,ZREMRANGEBYSCORE 清窗口外),对比边界行为。
  2. 按套餐差异化限流:free 30 次/分、pro 300 次/分(plan 已在租户上,读出来选阈值即可)。
  3. 给 DELETE 和 webhook 处理加审计日志表,实现 18.4 的最小形态,并给 admin 加一个 GET /audit 查询接口。
  4. GET /todos 加游标分页(?after_id=&limit=),体会为什么 SaaS 列表接口必须分页。

动手验证

bash
cd examples-middleware
docker compose up -d postgres redis
cargo test -p todo_api_saas -- --ignored   # 配额/幂等/webhook 验签的完整断言

导航:上一章 | 返回目录 | 下一章