功能设计面试
功能设计面试
【中等】如何设计一个排行榜功能?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 缓存
💎 关键结论
首选 Redis zset(有序集合):以用户为 member、排行指标为 score,天然有序。ZINCRBY 原子加分,ZREVRANGE 取 Top N,ZREVRANK 查个人名次,O(logN) 复杂度,实时且简单。
⚡记忆卡片
- 口诀:zset 装榜单,score 是分数,ZREVRANGE 取 Top N。
- 关键词:zset / ZINCRBY / ZREVRANK
- 链路:事件触发加分 → 自动排序 → 范围查询取名次
📖 核心知识
数据模型
ZADD rank:activity:{id} score memberId:member = 用户 ID,score = 排行指标(销售额、积分、点赞数)。- 实时更新:业务事件发生时
ZINCRBY rank:activity:{id} increment memberId原子累加。
核心查询
| 查询需求 | 命令 | 复杂度 |
|---|---|---|
| Top N | ZREVRANGE key 0 N-1 WITHSCORES | O(logN + M) |
| 个人名次 | ZREVRANK key memberId | O(logN) |
| 个人分数 | ZSCORE key memberId | O(1) |
| 周边排名 | ZREVRANGE key rank-5 rank+5 | O(logN + M) |
设计要点
- 同分处理:zset 同分时按 member 字典序排列;如需时间优先,可把时间因子编入 score(如 score = 分值 × 10^10 - 时间戳)。
- 冷热分离:zset 只存 ID 与分数,用户详情在查出排名后按 ID 回查 DB/缓存,避免大 value。
- 持久化兜底:开启 Redis AOF,或定时把榜单快照落库,防止故障后榜单丢失。
- 容量参考:百万级成员的单个 zset,Top N 查询仍为毫秒级。
🔬 扩展知识
详情
- 【L3】多维度排行榜(分地区、分类目、分活动):按维度拆分独立 zset
rank:{维度}:{维度值},查询时聚合,避免单一大 key。 - 【L3】海量榜单(亿级成员):按分数区间分桶建两级索引,或改为离线定时批量计算快照 + 实时增量修正。
- 【L4】吞吐权衡:超高并发可改为 MQ 异步加分 + 定时刷入,用秒级实时性换吞吐。
📚 延伸阅读:Redis Sorted Set 官方文档
🔀 发散问题
- Q:score 编大数会有精度问题吗? → score 是 double 类型,超过 2^53 的整数会丢精度,复合分数要控制量级或降精度处理。
- Q:如何防刷榜? → 加分入口做限流与风控校验,并记录增量流水供对账;流量统计可参考本文档「如何实现接口每分钟调用统计功能?」。
- Q:Redis 宕机榜单丢了怎么办? → 用 DB 快照恢复 + 回放增量消息重建,切换思路见本文档「如何实现数据的不停服迁移?」。
【中等】如何设计一个点赞功能?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:功能设计 / 缓存
💎 关键结论
点赞本质是一个按时间排序的去重集合:用 Redis 抗流量(String 计数 + Set 记录点赞者),MQ 异步批量落库,可支撑十万级 QPS 的高并发,响应在 10ms 以内。
⚡记忆卡片
- 口诀:Redis 记点赞,MQ 削峰,批量入库。
- 关键词:INCR 计数 / Set 去重 / Write Behind
- 链路:点赞写 Redis → MQ 异步通知 → 批量聚合入库
📖 核心知识
点赞功能的核心操作是:
- 点赞
- 取消点赞
- 查看点赞列表
本质是一个按时间排序的去重集合。
实现思路
- 缓存抗流量:点赞信息先存储在 Redis。点赞数存储在 String 类型,采用 INCR 原子增减;点赞者维护在 Set 类型。响应时间在 10ms 以内。
- 异步削峰:点赞事件通过 MQ 异步通知,吞吐量可轻松抗住每秒 10 万级消息。
- 批量入库:消费者批量聚合一段时间窗口的点赞信息,如每 5 秒或每 1000 条点赞消息,触发批量入库。——Write Behind 缓存同步更新策略。
数据模型参考
| 数据 | 存储 | 操作 |
|---|---|---|
| 点赞数 | Redis String like:count:{目标ID} | INCR/DECR 原子增减 |
| 点赞者 | Redis Set like:users:{目标ID} | SADD/SREM/SISMEMBER 判断是否点过赞 |
| 点赞明细 | MySQL(用户、目标、时间)唯一索引 | 异步批量落库 |
🔬 扩展知识
详情
- 【L3】点赞列表分页:Set 不支持按时间排序分页,列表展示需落 DB 或改用 zset(score = 时间戳)。
- 【L3】Redis 与 DB 一致性:Write Behind 窗口内宕机可能丢数据,以 MQ 持久化消息为落库凭据,消费失败重试保证最终一致。
- 【L4】亿级目标热点:计数 key 分桶聚合,热点 Set 按目标 ID 哈希拆分。
📚 延伸阅读:Redis INCR 命令
🔀 发散问题
- Q:用户快速连点又取消怎么办? → 以 Redis Set 的 SISMEMBER 判断当前态做切换,MQ 消息携带目标状态按序聚合,最终以 DB 为准。
- Q:点赞数如何对账? → 定时比对 Redis 计数与 DB 计数,不一致以 DB 为准修正 Redis;对账兜底思路可参考本文档「如何解决订单重复支付情况?」。
【中等】如何实现一个订单超时取消功能?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:功能设计 / 延迟任务
💎 关键结论
本质是延迟任务调度:下单发 30 分钟延迟消息为主,到期查状态,仍待支付则取消并释放资源;定时扫表兜底漏网单,两道防线都要求幂等。
⚡记忆卡片
- 口诀:延迟消息为主,扫表兜底,取消先查状态。
- 关键词:延迟消息 / 扫表兜底 / 幂等取消
- 链路:下单发延迟消息 → 到期查状态 → 取消 + 释放库存
📖 核心知识
本质是一个延迟任务调度问题:下单时记录“创建时间 + 超时时长”,到期后检查订单状态,若仍为待支付则取消并释放资源。
方案对比
| 方案 | 原理 | 精度 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 定时任务扫表 | 定时扫描“创建时间 + 30min < 当前时间”的订单 | 分钟级 | 高(DB 为准) | 量小、精度要求低 |
| JDK DelayQueue | 内存延迟队列(最小堆) | 高 | 差(宕机丢任务) | 单机小工具,不适合生产 |
| Redis ZSet | score = 到期时间戳,定时任务轮询取到期元素 | 秒级 | 中(需持久化保障) | 中小量级自研延迟任务 |
| 延迟消息队列 | RocketMQ 延迟级别 / RabbitMQ 死信+TTL | 级别固定 | 高(消息持久化+重试) | 主流生产方案 |
| 时间轮 | 环形数组 + 链表,O(1) 添加/取消 | 高 | 依赖宿主 | 组件内部实现(Netty/ Kafka) |
推荐组合:延迟消息为主 + 定时扫表兜底
- 下单成功发一条 30 分钟延迟消息;到期消费时先查订单状态,已支付则忽略,未支付则取消。
- 定时任务(如每 5 分钟)扫描漏网的超时订单,防止消息丢失;两道防线都要求幂等。
时间轮原理(高频追问)
- 环形数组每个槽挂链表,指针每 tick(如 100ms)走一格执行当前槽任务;多层时间轮(秒/分/时)解决长延迟精度与内存的矛盾。
- Netty
HashedWheelTimer是经典实现;Kafka 也用时间轮管理生产者/消费者超时。
取消动作的完整性:改状态(带状态机条件)→ 释放预占库存 → 退优惠券/积分 → 记录日志;取消操作需幂等(延迟消息与扫表可能重复触发)。
量化参考:日下单 100 万、超时率 30%,则每日 30 万条延迟消息,峰值每秒仅几十条,RocketMQ 轻松承载;扫表兜底按 create_time + status 联合索引扫描,单次百毫秒级。
🔬 扩展知识
详情
- 【L3】超时时长可配置(大促改为 15 分钟):超时时长随下单时刻的活动规则快照进订单(而非读取时动态计算),延迟消息按下单时时长设置;配置变更只影响新订单,存量订单不受影响,避免规则漂移。
- 【L3】纯 Redis ZSet 自研延迟任务:score = 到期时间戳,轮询取到期元素,但 Redis 故障时任务丢失,只能做非关键场景。
- 【L4】JDK DelayQueue 是内存最小堆实现,宕机即丢任务,只适合单机小工具;生产必须选择有持久化的任务存储。
📚 延伸阅读:Netty HashedWheelTimer / RocketMQ 延迟消息
🏭 实战场景
详情
电商大促:峰值 5000 TPS 下单,订单超时取消时长 30 分钟,预计超时率 40%,要求取消精度分钟级、库存释放零遗漏。
分析要点:峰值 5000 TPS 下单对应 30 分钟后峰值约 2000/s 的取消任务,RocketMQ 延迟消息承载无压力;消费取消时批量处理(每次拉一批,按订单 ID 批量查状态 + 批量改状态),减少 DB 交互;扫表兜底每 5 分钟扫描 status=待支付 AND create_time < now-30min 的漏网单;取消与支付并发竞态用带状态条件的 UPDATE 解决,已支付单触发自动退款;全链路幂等键(订单 ID),消息重试与扫表重复触发均安全。
⚠️ 常见误区
详情
常见误区:
- ❌ “JDK DelayQueue 可以直接用于生产” → 内存队列宕机即丢任务,生产必须用延迟消息队列等持久化方案。
- ❌ “延迟消息和扫表有一个就够了” → 延迟消息精度高但依赖 MQ 可靠性,扫表简单但精度差且大表扫描有 DB 压力,双防线组合才是成本与可靠性的最佳平衡。
- ❌ “取消和支付的竞态靠时序保证” → 靠“操作前查状态 + 带条件更新”兜底:取消消费时以 DB 为准,已支付则忽略;支付回调发现已取消则触发自动退款。
- ❌ “延迟级别配了就等于实际触发时间” → 曾有线上事故:RocketMQ 延迟级别配错(30min 配成 1h),叠加扫表兜底间隔 10 分钟,导致超时订单最晚 70 分钟才取消,库存被无效占用引发超卖投诉——延迟任务上线必须验证实际触发时间。
🔀 发散问题
- Q:用户在第 29 分 59 秒支付成功,第 30 分钟取消任务触发怎么办? → 取消消费时先查订单状态(以 DB 为准),已支付则忽略;支付回调发现已取消则自动退款,并发处理见本文档「如何设计一个取消订单功能?」。
- Q:延迟消息丢失或 MQ 故障期间下的单怎么办? → 扫表兜底按 create_time 扫描全部超时未支付单;两道防线都幂等,重复触发无副作用。
- Q:取消为什么要联动库存? → 取消必须同步释放预占库存,库存与订单的原子性见本文档「如何避免用户重复下单(多次下单为支持,占用库存)?」。
【中等】如何解决订单重复支付情况?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 支付
💎 关键结论
支付渠道(微信、支付宝)是第三方系统、数据不互通,无法阻止用户付款,只能做回调幂等处理:每次回调先查状态,判定重复支付则记录流水并原路退款,唯一索引防重,每日对账兜底。
⚡记忆卡片
- 口诀:重复支付即退款,幂等记录防重,对账兜底保安全。
- 关键词:回调幂等 / 原路退款 / 对账兜底
- 链路:回调查状态 → 判重记流水 → 自动退款 → 对账兜底
📖 核心知识
背景:比如用户用微信、支付宝支付,由于支付渠道是第三方系统,数据不互通,因此无法阻止用户付款。
解决核心思路:支付回调幂等性处理。
第一步、支付回调处理:
- 每次回调先检查订单当前状态。
- 若订单已支付(或已全额支付),则判定为重复支付。
第二步、重复支付处理:
- 记录重复支付流水。
- 立即发起退款(自动调用支付网关退款接口),将多付金额原路退回。
- 退款若失败,发起重试,重试超过一定次数,记录下来,通知运营转为人工处理。
第三步、幂等性:支付流水表建立唯一索引(如订单号+支付渠道+交易号),防止重复记录。
第四步、对账兜底:每日对账系统检查支付流水与订单状态,发现多付但未退款的,自动触发退款。
🔬 扩展知识
详情
- 【L3】对账实现:每日下载第三方账单,与本地流水逐笔比对(金额、状态、交易号),差异进入差错处理队列人工/自动跟进。
- 【L3】回调可靠性:第三方回调失败会按策略重试,我方回调接口必须幂等且快速返回,避免被对方判定超时而重复轰炸。
- 【L4】组合支付(余额 + 第三方)、部分退款场景需按支付流水拆分退款,对账粒度细化到流水级。
📚 延伸阅读:MySQL 唯一索引
🔀 发散问题
- Q:退款接口一直失败怎么办? → 重试 + 超限转人工,重试与降级策略参考本文档「调用第三方接口应注意哪些问题?」。
- Q:如何防止伪造回调? → 验签 + 比对金额,只处理签名合法且金额与订单一致的回调。
- Q:和超时取消的竞态怎么处理? → 订单已被超时取消但用户又付款,支付回调应触发自动退款,见本文档「如何实现一个订单超时取消功能?」。
【中等】如何实现一个分布式单例对象?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:功能设计 / 分布式锁
💎 关键结论
两层保证:进程内单例(每台机器只初始化一次)+ 进程间互斥(集群中只有一个实例真正工作,其余待命)。用分布式锁控制创建,并把对象状态存入外部存储供所有进程访问。
⚡记忆卡片
- 口诀:本地只初始化,全局抢锁,抢不到就待命。
- 关键词:分布式锁 / 外部存储 / 领导者选举
- 链路:本地单例 → 分布式锁控制创建 → 状态注册到外部存储
📖 核心知识
要让一个对象在分布式环境下全局唯一,需要满足两个条件:
- 进程内单例:在每台机器上,这个对象只初始化一次(本地单例)。
- 进程间互斥:在整个集群中,只允许一台机器的这个对象真正工作,其他机器的对象处于**“待命”或“禁用”**状态。
实现思路分为两步:
- 用分布式锁控制创建过程,保证同一时刻只有一个进程能创建。
- 把对象存到外部存储,让所有进程都能访问到。
落地要点
- 本地层:懒加载 + 双重检查(DCL)或静态内部类,保证每个进程只初始化一次。
- 集群层:用 Redis/ZooKeeper 分布式锁竞争“工作权”,未持锁者降级为待命,并周期性重新竞争。
- 共享状态:单例对象的可变状态(配置、计数、任务进度)放 Redis/DB,任何进程都能读取与接续。
🔬 扩展知识
详情
- 【L3】更标准的做法是领导者选举:ZooKeeper 临时节点 / etcd 租约竞选 leader,只有 leader 对外工作,会话失效自动重新选举,适合调度器、唯一消费者这类长期任职场景。
- 【L4】租约续期与脑裂:持锁者必须在租约到期前续期,续期失败要主动让位;写操作携带 fencing token,防止“僵尸节点”恢复后脏写。
🔀 发散问题
- Q:分布式锁和领导者选举有什么区别? → 锁是短时操作互斥,用完即释放;选举是长期任职 + 租约续期,直到节点故障才重新选主。
- Q:锁失效导致“双活”怎么办? → 靠外部存储的唯一性约束或 fencing token 兜底,两层防护思路见本文档「如何避免用户重复下单(多次下单为支持,占用库存)?」。
【中等】如何实现接口每分钟调用统计功能?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 统计
💎 关键结论
“记、存、看”三步:拦截器异步记录“接口 + 分钟桶”,用 Redis Hash 一分钟一个 Key 做 HINCRBY 原子计数,HGETALL 读取展示,写多读少性能极高。
⚡记忆卡片
- 口诀:Redis Hash 来计数,每分钟一个 Key,Field 是接口,Value 是次数。
- 关键词:HINCRBY / 分钟桶 / Grafana
- 链路:拦截记录 → MQ 异步 → Hash 计数 → 查询展示
📖 核心知识
整体思路分三步:记、存、看。
一、记(数据埋点)
拦截接口调用(拦截器、过滤器或 AOP),记录每次请求。
- 关键信息:
- 接口名:
/api/user - 时间桶:当前时间的分钟级窗口,例如
2026-02-26 14:00
- 接口名:
- 操作:每一次请求,就给对应的“接口+分钟”计数器加 1。为了不影响正常请求业务,可以丢入一个 MQ 统一异步处理。
二、存(数据存储)
统计的核心是:写入极其频繁(每次请求都要写),读取相对低频(每分钟/每小时看一次)。
推荐方案:Redis Hash:
- 数据结构:用 Redis 的 Hash。
- Key:统计日期+分钟,如
stats:20260226:1400 - Field:接口名,如
/api/user - Value:调用次数(整数)
- Key:统计日期+分钟,如
- 操作:每次请求执行
HINCRBY stats:20260226:1400 /api/user 1 - 优点:
- 极高性能:内存操作,原子递增。
- 结构清晰:一个 Key 存一分钟的所有接口数据。
- 自动过期:可以给 Key 设置过期时间(比如保留 7 天),自动清理旧数据。
三、看(数据展示)
从存储中读取数据并展示出来。
- 查询实时分钟数据:直接
HGETALL stats:20260226:1400,拿到这一分钟所有接口的计数。 - 查询历史趋势:遍历多个分钟 Key,聚合出接口的调用趋势。
- 可视化:可以对接 Grafana,或自己写一个简单的接口返回 JSON 数据供前端图表展示。
🔬 扩展知识
详情
- 【L3】多粒度聚合:分钟级数据定时上卷为小时/天级,长期存储只保留低粒度,控制容量与 Key 数量。
- 【L3】大规模与告警:QPS 极高时先本地聚合再批量 HINCRBY;统计值写入后做阈值判断,流量突增/突降触发告警。
- 【L4】组件替换:需要长期保留与复杂查询时,可换时序数据库(Prometheus/InfluxDB)或 OLAP 引擎。
📚 延伸阅读:Redis Hash 官方文档
🔀 发散问题
- Q:QPS 太高单个 Redis 顶不住怎么办? → 客户端/接入层本地聚合后批量递增,或把统计 Key 分片到多个实例。
- Q:想统计热点接口 Top N 怎么做? → 每分钟桶再维护一个 zset(score = 次数)做排名,参考本文档「如何设计一个排行榜功能?」。
- Q:给第三方接口做监控怎么扩展? → 按依赖维度埋点并设 SLA 告警,参考本文档「调用第三方接口应注意哪些问题?」。
【中等】如何设计一个购物车功能?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 电商
💎 关键结论
购物车设计 = Redis 存储 + 实时库存价格校验 + 合并结算 + 多端同步,核心是数据一致性和用户体验;购物车只存 skuId 与数量,价格库存查询时实时校验。
⚡记忆卡片
- 口诀:Hash 存购物车,价格库存实时校,登录合并,结算原子扣。
- 关键词:Hash 存储 / 实时校验 / 合并结算
- 链路:加购 → 校验与选中 → 合并结算 → 扣库存下单
📖 核心知识
数据存储
| 维度 | 关注点 | 解决方案 |
|---|---|---|
| 存储选型 | 高性能+持久化平衡 | Redis(主)+ MySQL(备/历史) |
| 数据结构 | 灵活查询 | Hash 结构:cart:user:{id} → field=skuId, value=商品详情 |
| 过期策略 | 僵尸数据清理 | 7 天过期 + 定期清理未登录购物车 |
核心功能
| 功能 | 难点 | 解决 |
|---|---|---|
| 添加商品 | 重复 SKU 合并 | 存在则累加数量,不超过限购 |
| 数量更新 | 库存边界 | 实时校验库存上限、下限 1 |
| 实时价格 | 价格变动 | 查询时从商品服务拉取最新价 |
| 选中结算 | 批量操作 | 维护selected状态位,全选/反选 |
库存处理
- 预占库存:结算时 Redis 原子扣减(Lua 脚本)
- 释放库存:超时未支付/取消订单 → 回补
- 实时校验:添加/更新时查真实库存
多端同步
- 未登录 → LocalStorage
- 登录时 → 合并 LocalStorage 到 Redis
- 多端 → 同一 Redis,实时同步
异常场景
| 场景 | 问题 | 处理 |
|---|---|---|
| 库存不足 | 下单失败 | 标记失效,提示用户 |
| 价格变动 | 金额不符 | 重新计算,弹窗确认 |
| 商品下架 | 无法购买 | 自动移除,提示原因 |
🔬 扩展知识
详情
- 【L3】容量与淘汰:常见做法限制购物车条目上限(如约 100 件),超限按最久未使用淘汰;游客购物车与失效商品定期清理。
- 【L3】合并冲突策略:登录合并时同 SKU 数量累加并受库存与限购约束,价格与上下架状态以实时查询为准。
- 【L4】边界划分:购物车只负责“选购意向”,价格计算交给促销服务、库存扣减交给下单链路,避免购物车成为大杂烩服务。
📚 延伸阅读:Redis Hash 官方文档
🔀 发散问题
- Q:价格要不要存进购物车? → 不存,查询时实时拉取最新价,避免旧价格引发金额纠纷。
- Q:结算后超时未支付怎么办? → 释放预占库存并回补,参考本文档「如何实现一个订单超时取消功能?」。
- Q:结算提交如何防重复下单? → 幂等键 + 唯一索引兜底,见本文档「如何避免用户重复下单(多次下单为支持,占用库存)?」。
【中等】如何设计一个抢红包功能?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 电商
💎 关键结论
核心是“预生成金额 + 原子抢占”:用二倍均值法/线段分割法生成金额列表(以“分”为整数计算),存入 Redis 队列,LPOP 原子弹出保证不重不超,落库记账,过期剩余退回。
⚡记忆卡片
- 口诀:每次随机上限是剩余均值两倍,保证公平不超总;预切线段存队列,顺序取出无争议。
- 关键词:二倍均值法 / 线段分割法 / LPOP
- 链路:发红包预生成金额 → 原子弹出抢占 → 落库记账 → 过期退回
📖 核心知识
两种红包
| 类型 | 算法 | 要点 |
|---|---|---|
| 普通红包 | 总金额 / 个数 | 每人固定金额,简单。 |
| 随机红包 | 二倍均值法(实时) 或 线段分割法(预生成) | 总额固定,每人 ≥ 0.01 元,随机公平。 |
随机红包核心算法
- 二倍均值法(实时计算)
- 公式:当前金额 = 随机 [0.01, 剩余金额/剩余人数 × 2 - 0.01]
- 特点:实时计算,期望公平,但最后一人金额波动大。
- 线段分割法(预生成)
- 原理:把总金额(分)看作线段,随机切 N-1 刀,按切点分段作为金额。
- 特点:提前生成金额列表存入队列,抢时顺序取,每人概率完全相同。
技术关键点
- 金额单位:用 “分”(整数),避免浮点误差。
- 并发控制:Redis
LPOP原子弹出预先生成的金额列表,或用 Lua 脚本保证一致性。 - 持久化:红包状态(已抢列表、剩余金额)需持久化到 DB/Redis,重启后恢复。
- 过期退回:未抢完的红包超时后,根据已抢记录计算剩余金额,原路退回。
🔬 扩展知识
详情
- 【L3】方差对比:二倍均值法最后一人拿剩余、方差大;线段分割法预生成全部金额,各份期望与方差更均匀,公平性敏感场景优先预生成。
- 【L3】高并发红包雨:用 Lua 脚本原子完成“判断剩余份数 + 弹出金额”,抢的结果异步落库,单 Redis 实例可支撑数万 QPS。
- 【L4】扩展玩法:群红包“手气王”统计可用 zset 实时聚合;企业红包需叠加审批与财务对账流程。
📚 延伸阅读:Redis LPOP 命令
🔀 发散问题
- Q:最后一人金额波动大怎么办? → 二倍均值法最后一人拿剩余,方差较大;改用线段分割法预生成可消除波动。
- Q:0.1 元发 10 人可行吗? → 不够每人 1 分,发红包时前置校验:总金额 ≥ 人数 × 最小单位(1 分)。
- Q:系统重启怎么办? → 持久化存储红包状态,重启后从存储恢复继续服务;预生成方案天然支持断点续抢。过期退回的延迟任务可参考本文档「如何实现一个订单超时取消功能?」。
【中等】如何设计一个取消订单功能?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:功能设计 / 电商
💎 关键结论
五步口诀“校验、改状态、释放库存、退券、退款”;取消与支付并发冲突用数据库行锁/乐观锁 + 状态机条件更新,两方只有一方能成功,对账补偿兜底防资损。
⚡记忆卡片
- 口诀:校验、改状态、释放库存、退券、退款。
- 关键词:状态机 / 条件更新 / 对账补偿
- 链路:校验状态 → 带条件改状态 → 释放库存退券 → (已支付)退款
📖 核心知识
基本流程
| 操作 | 说明 |
|---|---|
| 校验状态 | 只有待支付/已超时订单才可取消,已支付/已发货等不能取消 |
| 更新状态 | 将订单状态改为“已取消” |
| 释放库存 | 恢复商品库存(若锁定过库存) |
| 退还优惠券/积分 | 若有使用的优惠券需退回 |
| 触发退款 | 若已支付(仅当允许取消已支付订单时),走退款流程 |
并发冲突场景:取消与支付同时发生
- 用户点击“取消”的同时,支付回调也到达。
- 若不加控制,可能出现:
- 订单被取消后却支付成功(资金损失)
- 订单支付成功却被取消(体验问题)
核心目标:保证最终一致性,避免资损。
方案一、基于数据库行锁 + 状态机(推荐)
- 原理:在更新订单状态时,使用数据库行锁或乐观锁,保证状态变更的原子性。
- 实现:
UPDATE orders SET status = 'CANCELED' WHERE id = ? AND status = 'PENDING'UPDATE orders SET status = 'PAID' WHERE id = ? AND status = 'PENDING'- 两条更新语句同时执行时,只有一条能成功(因为
status = 'PENDING'条件)。
- 优点:简单可靠,利用数据库 ACID。
- 缺点:依赖数据库,但订单系统通常足够。
方案二、分布式锁
- 原理:对订单 ID 加锁(如 Redis 锁),取消和支付先争抢锁,获得锁的一方执行,另一方等待或重试。
- 适用:跨多个服务或数据库,需要强一致性。
方案三、最终一致性 + 对账补偿
- 原理:允许短暂不一致,通过后续对账或消息补偿修复。
- 流程:
- 取消和支付都正常执行,但记录流水。
- 后台定时任务检查“已取消但已支付”的异常订单,自动退款。
- 优点:高并发下性能好。
- 缺点:需补偿逻辑,可能存在资损窗口。
最佳实践(推荐方案)
业务优化
在页面上可以限时订单取消计时为 10 分钟,但实际后端是延迟 11 分钟取消订单。这样就可以避免用户在取消订单限时最后一刻下定决心付款的情况。
数据库行锁/乐观锁 + 状态机 + 幂等 + 补偿
- 取消和支付都先检查订单状态(SELECT ... FOR UPDATE 或使用乐观锁)。
- 更新时带上原状态条件(
status = 'PENDING')。 - 若更新失败(影响行数为 0),说明状态已变更,根据新状态做不同处理:
- 若已被支付,取消操作失败,提示用户“订单已支付,不能取消”。
- 若已被取消,支付回调需拒绝或触发退款。
- 使用幂等设计:支付回调需幂等,避免重复处理。
- 兜底:对账系统定期扫描异常订单,自动修复(如已取消却支付成功则退款)。
🔀 发散问题
- Q:用户已支付后才想取消怎么办? → 转退款流程,退款重试与人工兜底见本文档「如何解决订单重复支付情况?」。
- Q:系统自动超时取消怎么实现? → 延迟任务触发取消,见本文档「如何实现一个订单超时取消功能?」。
- Q:取消为什么要先查状态再更新? → 防止与支付回调竞态造成资损,条件更新的幂等思想与本文档「如何避免用户重复下单(多次下单为支持,占用库存)?」一致。
【中等】如何避免用户重复下单(多次下单为支持,占用库存)?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:功能设计 / 幂等
💎 关键结论
幂等性设计:同一请求无论提交多少次,只创建一笔订单、只扣一次库存。客户端幂等键 + Redis 快速判断 + 分布式锁 + DB 唯一索引多层防护,重试链路必须透传幂等键。
⚡记忆卡片
- 口诀:幂等键先行,Redis 快判断,唯一索引兜底。
- 关键词:幂等键 / 分布式锁 / 唯一索引
- 链路:生成幂等键 → Redis 判断加锁 → 建单扣库存 → 唯一索引兜底
📖 核心知识
核心思想:同一请求无论提交多少次,只创建一笔订单,只扣一次库存(幂等性设计)。
三层防护
- 前端防重:按钮置灰 + Loading,防止用户双击或连续点击。
- 后端幂等:
- 幂等键:客户端生成唯一标识(如 UUID),在创建订单时传入。
- 处理流程:后端先查 Redis 该幂等键是否存在 → 存在则直接返回已创建订单,不再扣库存;不存在则加分布式锁,执行业务(创建订单、扣库存),完成后存入 Redis(带过期时间)。
- 数据库兜底:唯一约束——订单表为幂等键字段建立唯一索引,保证重复插入失败。
库存保护要点
- 库存扣减必须和订单创建在同一个事务中,保证原子性。
- 若幂等键已存在,直接返回已有订单,不再扣减库存。
🔬 扩展知识
详情
- 【L3】Redis 与 DB 状态不一致:Redis 写入成功但建单失败 → 幂等键要带短 TTL 或建单失败时主动清理,避免用户重试被误判为重复。
- 【L3】唯一索引冲突的语义:并发下后到的请求插入失败,应捕获异常后查询已存在订单返回(而不是报错),保证用户体验一致。
- 【L3】表单 Token 与幂等键的区别:表单 Token 是服务端预发一次性令牌,提交时校验并删除,防“同一页面重复提交”;幂等键是客户端生成、允许重复提交但返回同一结果,对网络超时重试更友好。现代 API 设计首选幂等键。
- 【L4】幂等键的传递:客户端重试(网络超时)必须携带同一幂等键;网关重试同理,防止重试风暴下创建多单。
- 【L4】失效场景:幂等键带短 TTL 时,Redis 过期后用户真重试可能重复建单,靠唯一索引兜底;幂等键若由服务端生成(如每次请求返回新 token),前端刷新/重进页面会重新获取,防重失效。
📚 延伸阅读:Stripe Idempotent Requests
🏭 实战场景
详情
弱网地区电商平台:移动端下单接口超时率 8%,用户习惯超时后立即重试,曾日均产生 3000 笔重复订单。
分析要点:客户端生成幂等键(进入下单页时生成一次,重试不变),服务端三层防护:Redis 幂等键快速判断(存在则直接返回已建订单)→ 分布式锁防并发穿透 → DB 幂等键唯一索引最后防线;网关重试必须透传幂等键,非幂等接口禁止自动重试;捕获唯一索引冲突后查已有订单返回(而非报错);上线后监控重复订单数降为 0。
⚠️ 常见误区
详情
常见误区:
- ❌ “前端按钮置灰就能防重复下单” → 网络超时重试、网关自动重试都会绕过前端,必须靠后端幂等。
- ❌ “Redis 幂等键就够了” → Redis 提供快速判断但非绝对可靠(故障/过期后失效),DB 唯一索引才是最后防线,两层缺一不可。
- ❌ “网关自动重试能提升成功率,默认开启” → 曾有线上事故:网关对超时请求自动重试但未透传幂等键,弱网用户每单重试产生 2~3 笔重复订单,库存被无效占用——重试链路必须透传幂等键,网关重试仅限幂等接口。
🔀 发散问题
- Q:幂等键存 Redis 多久合适? → 覆盖用户重试窗口 + 业务时效:如 10~30 分钟;同时依赖 DB 唯一索引永久兜底(幂等键入订单表)。Redis 只加速判断,不承担唯一性责任。
- Q:用户真的想下两单相同商品怎么办? → 幂等键区分“重试”与“新意图”:同一幂等键重复提交返回同一订单,新下单生成新幂等键;还可叠加业务限频(同用户同 SKU 1 秒内只允许 1 单)防误操作。
- Q:支付环节的重复问题怎么处理? → 支付回调幂等 + 重复支付退款,见本文档「如何解决订单重复支付情况?」。
【困难】如何设计数据同步方案(同步到数仓)?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:功能设计 / 数据同步
💎 关键结论
Canal 伪装从库解析 Binlog(业务零侵入),MQ 按业务 ID 分区保序,Flink 清洗写入数仓,断点续传 + 幂等去重 + 每日对账,做到不丢不重、顺序一致、延迟可控。
⚡记忆卡片
- 口诀:Canal 采 Binlog,分区保序,Flink 清洗,对账兜底。
- 关键词:Binlog / 分区保序 / 断点续传
- 链路:MySQL Binlog → Canal → MQ → Flink → 数仓
📖 核心知识
数据同步 = Canal 采集 Binlog + MQ 保序 + Flink 清洗 + 每日对账,核心是数据不丢不重、顺序一致、延迟可控。
总体架构
订单库(MySQL) → 变更捕获 → 消息队列 → 数据清洗 → 数仓(Hive/ClickHouse)
↑ ↑
业务无侵入 可重试、保序核心方案选型
| 方案 | 原理 | 准确率 | 性能 | 复杂度 | 推荐 |
|---|---|---|---|---|---|
| Canal(Binlog) | 模拟 MySQL 从库解析 binlog | 最高 | 高 | 中 | ⭐️ 首选 |
| MQ 双写 | 业务代码同步发 MQ | 中 | 低 | 低 | 不推荐 |
| DataX 离线抽取 | 定时全量/增量抽取 | 高 | 低(T+1) | 低 | 离线场景 |
| Flink CDC | 实时流计算 | 高 | 高 | 高 | 实时数仓 |
方案权衡:Binlog 方案对业务零侵入但强依赖源库 binlog 格式(ROW 模式)与主从拓扑,分库分表后每库都要部署采集实例;业务双写 MQ 方案延迟低但侵入业务代码,且事务内发 MQ 有一致性问题。
关键机制
- 数据一致性保障
- 断点续传:记录 binlog 位点(position)
- 去重:MQ 消息幂等消费(订单 ID+版本号)
- 补偿:每日 T+1 对账,不一致重新同步
- 事务边界:binlog 事务原子性(同一事务一起发送)
- 顺序性保障:同一个订单的变更必须顺序处理
- 方案 1:MQ 分区键 = order_id(同订单进同一分区)
- 方案 2:Flink keyBy(order_id) 单线程处理
- 性能优化
- 批量发送:Canal 每 500ms/1000 条发送一次
- 压缩传输:启用 MQ 消息压缩(Gzip/Snappy)
- 目标优化:ClickHouse 批量写入(每批 1 万条)
- 分流处理:订单主表+明细表分开同步
binlog 同步流程
订单库 → Canal → Kafka/RocketMQ → Flink/Spark → 数仓
↑ ↑ ↑ ↑
业务写 伪装从库 持久化消息 清洗、维表关联Canal 配置要点
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.filter.regex=order_db.order_table # 监听表
canal.mq.topic=order-binlog
canal.mq.partitionsHash=order_id # 保序数据质量保障
实时监控:
- 延迟监控:当前时间 - 最后一条同步时间 < 5 分钟
- 数量监控:源库增量 ≈ 数仓增量(允许误差)
- 异常监控:解析失败率 < 0.1%
对账机制:每日凌晨执行,不一致则触发离线重新同步:
SELECT COUNT(*) FROM order_db.order_table
MINUS
SELECT COUNT(*) FROM dwd_order_table WHERE dt = 昨日脏数据处理:
- 格式错误 → 发到死信队列(人工介入)
- 维表不存在 → 先存待补表,维表到位后更新
容灾
- 主链路:Canal → Kafka(主集群)
- 备链路:DataX 每日全量(兜底)
- Kafka 故障:消息积压,Canal 本地文件缓存(需配置)
- 数仓故障:消息持久化,恢复后重放
量化参考:日增 1000 万变更的库,binlog 增量约几 GB,Canal 解析 + MQ 传输延迟正常在秒级;Flink keyBy 单键串行处理,热点 key(大卖家)可能成为瓶颈,需盐值打散或拆分处理。
🔬 扩展知识
详情
- 【L3】分库分表后采集 binlog:每分片库部署 Canal 实例(或一个 Canal 集群监听多实例),消息带上分片标识;分区键仍用业务 ID(order_id)保证同订单变更进同一 Kafka 分区保序;全量初始化用 DataX 并行抽取各分片。
- 【L3】源库 DDL 变更(加字段):数仓侧先加字段(向后兼容)再上线源库 DDL;Canal 解析到新结构自动适配,但新旧消息混合期内消费端需容忍字段缺失;严禁先改源库后改数仓,会丢数据。
- 【L4】同步正确性验证三层:实时延迟监控(位点 lag)、抽样比对(每小时随机抽 1000 条源库记录比对数仓)、日终全量对账(COUNT + 关键字段 checksum);发现差异自动触发位点回溯重放。
- 【L4】技术演进:Flink CDC 可一体化完成快照 + 增量与 Schema Evolution,减少 Canal + MQ 环节,新建实时数仓可优先考虑。
📚 延伸阅读:Canal(alibaba) / Flink CDC 文档
🏭 实战场景
详情
订单库分 16 库 64 表,日增 2000 万订单、变更 1 亿次,要求同步到数仓延迟 < 1 分钟、同订单变更保序。
分析要点:每分片库部署 Canal 实例(16 个)解析 ROW 格式 binlog,批量发送(500ms/1000 条);Kafka 分区键 = order_id hash,保证同订单变更进同一分区保序,分区数按吞吐估算(1 亿变更/日 ≈ 峰值数千 TPS,64 分区足够);Flink keyBy(order_id) 清洗 + 维表关联后写 ClickHouse(批量 1 万条/批,避免频繁小写);监控位点 lag 分钟级告警,日终对账 COUNT + checksum,差异自动回溯重放。
积压事故教训:曾有线上事故:Canal 消费积压 6 小时未告警,数仓报表与在线库差异巨大误导运营决策——同步延迟必须有分钟级告警,不能只靠日终对账发现。
⚠️ 常见误区
详情
常见误区:
- ❌ “业务双写 MQ 比 Binlog 更可控” → 双写侵入业务代码,事务内发 MQ 有一致性问题,且漏发无感知;Binlog 零侵入、天然按事务有序,是首选。
- ❌ “有日终对账就够了” → 积压、位点回退等问题当天无法发现,6 小时积压事故就是教训,必须配分钟级延迟告警。
- ❌ “保序就要全局单分区” → 牺牲全部吞吐;只需同一主键有序,按业务 ID(order_id)分区即可并行又保序。
🔀 发散问题
- Q:同步链路上线前的存量数据怎么办? → DataX 并行全量抽取 + 记录位点后接增量,整体切换思路见本文档「如何实现数据的不停服迁移?」。
- Q:同步延迟突增如何排查? → 按链路分段定位(采集位点、MQ 积压、消费 TPS),监控埋点思路参考本文档「如何实现接口每分钟调用统计功能?」。
【中等】如何实现数据的不停服迁移?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / 数据迁移
💎 关键结论
四阶段口诀“双写 → 迁移 → 灰度切读 → 降级切换”:新旧库双写,历史数据补录迁移,读流量 1%→100% 灰度放量,稳定后只写新库,全程业务无感知、可回滚。
⚡记忆卡片
- 口诀:读流量灰度切,逐步放量稳观察;双写降级只写新,观察备份再下线。
- 关键词:双写 / 补录迁移 / 灰度切换
- 链路:双写 → 历史迁移 + 增量补录 → 灰度切读 → 降级下线
📖 核心知识
核心思想
双写 + 数据同步 + 灰度切换
让新旧两套存储同时提供服务,逐步把流量切到新库,全程业务无感知。
第一阶段、准备与双写
- 新库上线,应用修改代码:所有写操作同时写入旧库和新库(双写)。
- 双写需保证最终一致性,可异步或同步(视业务容忍度)。
- 读操作仍从旧库读取,确保对业务无影响。
第二阶段、历史数据迁移
- 将旧库中的全量历史数据批量迁移到新库。
- 迁移过程中,增量数据仍在双写,需保证迁移与增量不冲突。
- 常见做法:记录迁移开始的时间戳或位点,迁移完成后,再补录期间产生的增量。
- 使用工具(如 DataX、Kettle)或自研脚本,分批迁移,避免影响线上。
第三阶段、灰度读切换
- 待历史数据迁移完成且双写稳定后,开始灰度切读流量。
- 逐步将读请求从旧库切到新库,比如 1%、10%、50%、100%。
- 每个灰度步骤观察业务指标(响应时间、错误率、数据一致性)。
第四阶段、双写降级与最终切换
- 当读流量全部切到新库且稳定运行后,可考虑将双写降级为只写新库。
- 此时旧库可作为备份,观察一段时间无异常后,正式下线旧库。
- 保留旧库一段时间(如一周),以备紧急回滚。
🔬 扩展知识
详情
- 【L3】补录冲突处理:迁移期间双写增量与历史数据冲突时,按“写入时间比较,新值覆盖旧值”,或以迁移起始位点为准迁移后重扫增量补录。
- 【L3】回滚预案:双写期间旧库数据完整,读切换异常可随时切回旧库;降级为单写是不可逆操作,必须经过足够观察期再执行。
- 【L4】异构迁移(MySQL→ES/MongoDB 等):需额外处理字段映射、类型转换与唯一性校验,切换前做双读比对(影子比对)验证一致性。
📚 延伸阅读:DataX(alibaba)
🔀 发散问题
- Q:迁移过程中如何保证增量不丢? → 记录迁移开始时间戳/位点,迁移完成后补录增量,以位点为准而非人工把控。
- Q:增量捕获不想侵入业务代码怎么办? → 用 Binlog 捕获,参考本文档「如何设计数据同步方案(同步到数仓)?」。
【中等】如何快速找到附近距离用户最近的 N 家商户?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:功能设计 / LBS
💎 关键结论
通法都是“空间索引粗筛 + 精确距离重排取 Top N”:小规模高并发用 Redis GEO 一条命令直取;海量持久化用 GeoHash 前缀匹配 + 邻域扩展,或 ES geo_distance,按数据量与查询形态选型。
⚡记忆卡片
- 口诀:GeoHash 编经纬,前缀匹配找附近,邻域扩展防边界,精确计算再排序。
- 关键词:GeoHash / Redis GEO / geo_distance
- 链路:经纬度编码 → 区域粗筛 → 精确距离排序 → 取 Top N
📖 核心知识
方案一、GeoHash + MySQL(通用、易实现)
- 原理:将二维经纬度编码为一维字符串,前缀相同的区域相邻。
- 步骤:
- 商户入库时,根据经纬度计算 GeoHash 值(如 6-8 位),存入数据库并建索引。
- 查询时,计算用户位置的 GeoHash,取前缀匹配(如前 6 位),查询该区域及周围 8 个邻域的商户。
- 对查询结果计算精确距离,排序取前 N 个。
- 优点:实现简单,支持分库分表,精度可控。
- 缺点:边界问题需扩展邻域,精确距离计算需回表。
方案二、Redis GEO(高性能、简单)
- 原理:Redis 3.2+ 内置 GEO 类型,基于有序集合(ZSet)存储经纬度,提供地理距离计算。
- 步骤:
- 用
GEOADD将商户 ID 和经纬度加入 Redis。 - 用
GEORADIUS或GEORADIUSBYMEMBER查询用户附近 N 个商户,直接返回距离排序结果。
- 用
- 优点:性能极高(内存操作),API 简单,支持距离排序和返回距离。
- 缺点:数据需全量驻留内存,容量受限于内存;适合商户数量可容纳的场景。
方案三、Elasticsearch Geo Distance(搜索型、可扩展)
- 原理:ES 内置地理点类型和地理距离查询,利用倒排索引和空间索引。
- 步骤:
- 定义字段类型为
geo_point,索引商户经纬度。 - 使用
geo_distance查询,按距离排序,取前 N 个。
- 定义字段类型为
- 优点:支持海量数据,可与全文搜索结合,分布式天然扩展。
- 缺点:需部署 ES 集群,运维成本较高。
方案四、空间索引(PostGIS / MySQL Spatial)(数据库原生)
- 原理:关系数据库的空间扩展,使用 R-Tree 索引加速几何计算。
- 步骤:
- 创建
GEOMETRY列存储点坐标,建立空间索引。 - 使用
ST_Distance和ST_Within等函数查询最近点。
- 创建
- 优点:数据库原生支持,无需额外组件,数据一致性好。
- 缺点:某些数据库实现(如 MySQL)的空间索引性能可能不如 GeoHash 或 Redis;PostGIS 性能优异但需 PostgreSQL。
方案选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 商户数百万以内,并发高 | Redis GEO | 简单、快、内存可控 |
| 商户数千万以上,需持久化 | GeoHash + MySQL / MongoDB | 成本低,可水平扩展 |
| 需要复杂查询(文本+地理) | Elasticsearch | 功能全面,扩展性强 |
| 已有 PostgreSQL | PostGIS | 原生支持,性能优异 |
优化技巧
- 多级索引:先用行政区划(城市、区县)粗筛,再用 GeoHash 细查。
- 缓存热点:热门区域的查询结果可缓存,减少重复计算。
- 动态网格:根据商户密度动态调整网格大小,平衡精度和性能。
- 异步更新:商户位置变化不频繁,可异步更新索引。
🔬 扩展知识
详情
- 【L3】GeoHash 边界问题:地图上相邻的两点可能 GeoHash 前缀完全不同,查询必须扩展周围 8 个邻域;位数越少网格越粗、候选越多,精度与性能需权衡。
- 【L3】Redis GEO 容量:GEO 本质是 zset,百万级商户占用百 MB 级内存,但需全量驻留内存,容量超限必须转 GeoHash + DB/ES。
- 【L4】组合过滤:实际业务常叠加“距离 + 评分/销量/配送范围”复合排序,ES 比 Redis GEO 更适合。
📚 延伸阅读:GeoHash - Wikipedia / ES Geo queries
🔀 发散问题
- Q:商户位置动态变化(如骑手)怎么办? → 异步更新索引(MQ 消费后更新 GEO/GeoHash),位置变更低频可接受最终一致。
- Q:全国商户量太大如何分片? → 按城市或 GeoHash 前缀分片,查询先定位分片再检索。
- Q:热门区域查询结果怎么缓存? → 热点区域结果短 TTL 缓存 + 定时刷新,缓存抗流量思路参考本文档「如何设计一个点赞功能?」。
【中等】调用第三方接口应注意哪些问题?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:功能设计 / 外部集成
💎 关键结论
五类注意点:预防性设计(超时/重试/熔断/限流)、安全性、一致性(幂等/补偿)、资源隔离、可观测性;核心原则是不信任任何第三方,故障时必须有降级兜底。
⚡记忆卡片
- 口诀:超时限流再熔断,重试幂等带唯一键,线程隔离防雪崩。
- 关键词:超时重试 / 熔断降级 / 资源隔离
- 链路:超时控制 → 重试熔断 → 隔离降级 → 日志监控
📖 核心知识
要点
- 预防性设计:超时、重试、熔断、限流。
- 安全性:加密、认证、凭证管理、脱敏。
- 一致性:幂等、补偿、版本兼容。
- 资源隔离:线程池、连接池、信号量。
- 可观测性:日志、监控、链路追踪。
网络与超时控制
- 连接超时:设置合理的建立连接超时(如 3-5 秒),避免长时间等待。
- 读取超时:设置接口响应数据读取超时(如 10 秒),防止对方服务慢导致线程挂起。
- DNS 解析超时:确保 DNS 查询不成为瓶颈,可考虑使用备用 DNS 或 IP 直连。
异常处理与重试
- 区分异常类型:网络抖动(可重试) vs 业务错误(不可重试,需人工介入)。
- 重试策略:指数退避 + 最大重试次数(如 3 次),避免加重对方负载。
- 幂等性保证:重试时需确保接口支持幂等,否则需业务层去重。
接口限流与熔断
- 限流:根据对方接口配额或自身系统能力,进行本地限流(如 Guava RateLimiter)或分布式限流。
- 熔断:引入熔断机制(如 Sentinel、Hystrix),当错误率达到阈值时快速失败,防止雪崩。
- 降级:定义降级逻辑(如返回缓存数据或默认值),保障核心业务。
数据安全与认证
- 传输加密:使用 HTTPS 确保数据传输安全,防止中间人攻击。
- 身份认证:妥善管理 API Key、Token 等凭证,避免硬编码(使用配置中心或密钥管理服务)。
- 敏感信息:请求/响应中若包含敏感数据,需进行脱敏或加密处理。
幂等性与数据一致性
- 请求幂等:对于可能重复提交的场景(如支付通知),需通过业务唯一键去重。
- 最终一致性:若第三方接口响应延迟或失败,需设计补偿机制(如定时对账、状态同步)。
接口版本管理与兼容性
- 版本控制:明确接口版本(如 URL 路径包含 v1),避免无通知升级导致兼容问题。
- 变更通知:关注第三方接口变更公告,提前适配。
- 灰度验证:新版本接口上线前,先在小流量验证。
资源隔离与线程池管理
- 线程池隔离:为第三方调用分配独立的线程池,避免占用核心业务线程资源。
- 信号量隔离:对于非阻塞调用,可使用信号量控制并发数。
- 连接池管理:合理配置 HTTP 连接池大小、空闲连接回收策略,避免资源泄漏。
量化参考:连接超时建议 1~3s(内网调用 1s 即可),读超时按对方 SLA 的 P99 × 2 设置;重试最多 1~2 次且带退避,重试风暴可使对方压力翻倍加剧故障。
🔬 扩展知识
详情
- 【L3】方案权衡:重试是把双刃剑——对非幂等接口自动重试会引发重复扣款/建单,重试策略必须区分幂等性;熔断阈值设太敏感会在抖动期误断,设太钝会在故障时拖死线程池——需按第三方历史 RT/错误率基线校准。
- 【L4】事故教训:对接物流接口未设读超时,对方故障时我方线程全部阻塞在等待,线程池耗尽拖垮自身下单链路——第三方调用的超时必须小于自身接口的超时,否则故障会传染。
- 【L4】场景实战:电商对接 3 家快递公司查件接口,各家 SLA 不同(RT P99 从 200ms 到 2s),高峰期我方查件 QPS 5000,要求对方故障时不影响下单主链路——查件调用独立线程池(舱壁模式),与下单链路线程资源完全隔离;超时按各家 SLA 分别配置(P99×2);单家错误率超 30% 熔断,降级返回缓存的最后已知物流状态;物流状态改定时拉取 + 缓存,用户查看走缓存而非实时调对方;按对方配额限流,超额排队。核心原则:把第三方依赖从主链路移除,能异步就异步。
📚 延伸阅读:Sentinel(alibaba) / Resilience4j
🔀 发散问题
- Q:第三方接口无幂等支持,但我们必须重试怎么办? → 业务层造幂等:每次请求携带我方唯一请求号,我方记录请求号与结果,重试前先查本地记录;资金类操作重试前必须查单确认状态。
- Q:如何监控第三方接口的健康度? → 按第三方维度埋点:成功率、RT P99、超时率,与其 SLA 对比告警;建立“依赖健康看板”,故障时快速判断是自己还是对方的问题;重要依赖定期做故障演练。
- Q:对方接口升级不兼容怎么应对? → 防腐层隔离对方模型(升级只改适配层),双版本双跑灰度切换;合同层要求变更提前通知;响应解析对未知字段宽容(反序列化不报错)。