功能设计面试
功能设计面试
【中等】如何设计一个排行榜功能?⭐⭐⭐
可以使用 Redis 的 zset 数据类型来实现。
【中等】如何设计一个点赞功能?⭐⭐
点赞功能的核心操作是:
- 点赞
- 取消点赞
- 查看点赞列表
本质是一个按时间排序的去重集合。
实现思路
- 缓存抗流量:点赞信息先存储在 Redis。点赞数存储在 String 类型,采用 INCR 原子增减;点赞者维护在 Set 类型。响应时间在 10ms 以内。
- 异步削峰:点赞事件通过 MQ 异步通知,吞吐量可轻松抗住每秒 10 万级消息。
- 批量入库:消费者批量聚合一段时间窗口的点赞信息,如每 5 秒或每 1000 条点赞消息,触发批量入库。——Write Behind 缓存同步更新策略。
【中等】如何实现一个订单超时取消功能?⭐⭐⭐⭐
本质是一个延迟任务调度问题:下单时记录“创建时间 + 超时时长”,到期后检查订单状态,若仍为待支付则取消并释放资源。
方案对比
| 方案 | 原理 | 精度 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 定时任务扫表 | 定时扫描“创建时间 + 30min < 当前时间”的订单 | 分钟级 | 高(DB 为准) | 量小、精度要求低 |
| JDK DelayQueue | 内存延迟队列(最小堆) | 高 | 差(宕机丢任务) | 单机小工具,不适合生产 |
| Redis ZSet | score = 到期时间戳,定时任务轮询取到期元素 | 秒级 | 中(需持久化保障) | 中小量级自研延迟任务 |
| 延迟消息队列 | RocketMQ 延迟级别 / RabbitMQ 死信+TTL | 级别固定 | 高(消息持久化+重试) | 主流生产方案 |
| 时间轮 | 环形数组 + 链表,O(1) 添加/取消 | 高 | 依赖宿主 | 组件内部实现(Netty/ Kafka) |
推荐组合:延迟消息为主 + 定时扫表兜底
- 下单成功发一条 30 分钟延迟消息;到期消费时先查订单状态,已支付则忽略,未支付则取消。
- 定时任务(如每 5 分钟)扫描漏网的超时订单,防止消息丢失;两道防线都要求幂等。
时间轮原理(高频追问)
- 环形数组每个槽挂链表,指针每 tick(如 100ms)走一格执行当前槽任务;多层时间轮(秒/分/时)解决长延迟精度与内存的矛盾。
- Netty
HashedWheelTimer是经典实现;Kafka 也用时间轮管理生产者/消费者超时。
取消动作的完整性:改状态(带状态机条件)→ 释放预占库存 → 退优惠券/积分 → 记录日志;取消操作需幂等(延迟消息与扫表可能重复触发)。
方案权衡与踩坑:延迟消息精度高但依赖 MQ 可靠性,扫表简单但精度差且大表扫描有 DB 压力,两者组合是成本与可靠性的最佳平衡;纯 Redis ZSet 自研方案在 Redis 故障时任务丢失,只能做非关键场景。曾有线上事故:RocketMQ 延迟消息级别配错(30min 配成 1h),叠加扫表兜底间隔 10 分钟,导致超时订单最晚 70 分钟才取消,库存被无效占用引发超卖投诉——延迟任务上线必须验证实际触发时间。
量化参考:日下单 100 万、超时率 30%,则每日 30 万条延迟消息,峰值每秒仅几十条,RocketMQ 轻松承载;扫表兜底按 create_time + status 联合索引扫描,单次百毫秒级。
拓展追问
- 追问 1:用户在第 29 分 59 秒支付成功,第 30 分钟取消任务触发怎么办?
取消消费时先查订单状态(以 DB 为准),已支付则忽略取消;反之支付回调发现订单已取消则触发自动退款。两个方向的竞态都靠“操作前查状态 + 带条件更新”兑底,不靠时序保证。 - 追问 2:延迟消息丢失或 MQ 故障期间下的单怎么办?
扫表兜底就是为此存在:无论消息是否到达,扫表按 create_time 扫描全部超时未支付单;两道防线都幂等,重复触发无副作用。 - 追问 3:若要求超时时长可配置(大促改为 15 分钟)怎么设计?
超时时长随下单时刻的活动规则快照进订单(而非读取时动态计算),延迟消息按下单时时长设置;配置变更只影响新订单,存量订单不受影响,避免规则漂移。
场景题
电商大促:峰值 5000 TPS 下单,订单超时取消时长 30 分钟,预计超时率 40%,要求取消精度分钟级、库存释放零遗漏。请设计。
分析要点:峰值 5000 TPS 下单对应 30 分钟后峰值约 2000/s 的取消任务,RocketMQ 延迟消息承载无压力;消费取消时批量处理(每次拉一批,按订单 ID 批量查状态 + 批量改状态),减少 DB 交互;扫表兜底每 5 分钟扫描 status=待支付 AND create_time < now-30min 的漏网单;取消与支付并发竞态用带状态条件的 UPDATE 解决,已支付单触发自动退款;全链路幂等键(订单 ID),消息重试与扫表重复触发均安全。
【中等】如何解决订单重复支付情况?⭐⭐⭐
比如用户用微信、支付宝支付,由于支付渠道是第三方系统,数据不互通,因此无法阻止用户付款。
解决核心思路:支付回调幂等性处理。
记忆点:重复支付即退款,幂等记录防重,对账兜底保安全。
第一步、支付回调处理:
- 每次回调先检查订单当前状态。
- 若订单已支付(或已全额支付),则判定为重复支付。
第二步、重复支付处理:
- 记录重复支付流水。
- 立即发起退款(自动调用支付网关退款接口),将多付金额原路退回。
- 退款若失败,发起重试,重试超过一定次数,记录下来,通知运营转为人工处理。
第三步、幂等性:支付流水表建立唯一索引(如订单号+支付渠道+交易号),防止重复记录。
第四步、对账兜底:每日对账系统检查支付流水与订单状态,发现多付但未退款的,自动触发退款。
【中等】如何实现一个分布式单例对象?⭐⭐
要让一个对象在分布式环境下全局唯一,需要满足两个条件:
- 进程内单例:在每台机器上,这个对象只初始化一次(本地单例)。
- 进程间互斥:在整个集群中,只允许一台机器的这个对象真正工作,其他机器的对象处于**“待命”或“禁用”**状态。
实现思路分为两步:
- 用分布式锁控制创建过程,保证同一时刻只有一个进程能创建。
- 把对象存到外部存储,让所有进程都能访问到。
【中等】如何实现接口每分钟调用统计功能?⭐⭐
要点:记、存、看
记(数据埋点)
拦截接口调用(拦截器、过滤器或 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 数据供前端图表展示。
要点:“Redis Hash 来计数,每分钟一个 Key,Field 是接口,Value 是次数。”
【中等】如何设计一个购物车功能?⭐⭐
要点:购物车设计 = Redis 存储 + 实时库存价格校验 + 合并结算 + 多端同步,核心是数据一致性和用户体验。
数据存储
| 维度 | 关注点 | 解决方案 |
|---|---|---|
| 存储选型 | 高性能+持久化平衡 | Redis(主)+ MySQL(备/历史) |
| 数据结构 | 灵活查询 | Hash 结构:cart:user:{id} → field=skuId, value=商品详情 |
| 过期策略 | 僵尸数据清理 | 7 天过期 + 定期清理未登录购物车 |
核心功能
| 功能 | 难点 | 解决 |
|---|---|---|
| 添加商品 | 重复 SKU 合并 | 存在则累加数量,不超过限购 |
| 数量更新 | 库存边界 | 实时校验库存上限、下限 1 |
| 实时价格 | 价格变动 | 查询时从商品服务拉取最新价 |
| 选中结算 | 批量操作 | 维护selected状态位,全选/反选 |
库存处理
- 预占库存:结算时 Redis 原子扣减(Lua 脚本)
- 释放库存:超时未支付/取消订单 → 回补
- 实时校验:添加/更新时查真实库存
多端同步
- 未登录 → LocalStorage
- 登录时 → 合并 LocalStorage 到 Redis
- 多端 → 同一 Redis,实时同步
异常场景
| 场景 | 问题 | 处理 |
|---|---|---|
| 库存不足 | 下单失败 | 标记失效,提示用户 |
| 价格变动 | 金额不符 | 重新计算,弹窗确认 |
| 商品下架 | 无法购买 | 自动移除,提示原因 |
【中等】如何设计一个抢红包功能?⭐⭐
两种红包
| 类型 | 算法 | 要点 |
|---|---|---|
| 普通红包 | 总金额 / 个数 | 每人固定金额,简单。 |
| 随机红包 | 二倍均值法(实时) 或 线段分割法(预生成) | 总额固定,每人 ≥ 0.01 元,随机公平。 |
随机红包核心算法
- 二倍均值法(实时计算)
- 公式:当前金额 = 随机 [0.01, 剩余金额/剩余人数 × 2 - 0.01]
- 特点:实时计算,期望公平,但最后一人金额波动大。
- 记忆点:每次随机上限是剩余均值两倍,保证公平不超总。
- 线段分割法(预生成)
- 原理:把总金额(分)看作线段,随机切 N-1 刀,按切点分段作为金额。
- 特点:提前生成金额列表存入队列,抢时顺序取,每人概率完全相同。
- 记忆点:预切线段存队列,顺序取出无争议。
技术关键点
- 金额单位:用 “分”(整数),避免浮点误差。
- 并发控制:Redis
LPOP原子弹出预先生成的金额列表,或用 Lua 脚本保证一致性。 - 持久化:红包状态(已抢列表、剩余金额)需持久化到 DB/Redis,重启后恢复。
- 过期退回:未抢完的红包超时后,根据已抢记录计算剩余金额,原路退回。
扩展问题
- 最后一人金额波动大?
- 是的,二倍均值法最后一人拿剩余,方差较大;线段分割法可消除波动。
- 0.1 元发 10 人?
- 不够每人 1 分,发红包时前置校验:总金额 ≥ 人数 × 最小单位(1 分)。
- 系统重启怎么办?
- 持久化存储红包状态,重启后从存储恢复继续服务;预生成方案天然支持断点续抢。
【中等】如何设计一个取消订单功能?⭐
取消订单基本流程
记忆点:“校验、改状态、释放库存、退券、退款。”
| 操作 | 说明 |
|---|---|
| 校验状态 | 只有待支付/已超时订单才可取消,已支付/已发货等不能取消 |
| 更新状态 | 将订单状态改为“已取消” |
| 释放库存 | 恢复商品库存(若锁定过库存) |
| 退还优惠券/积分 | 若有使用的优惠券需退回 |
| 触发退款 | 若已支付(仅当允许取消已支付订单时),走退款流程 |
并发冲突场景:取消与支付同时发生
- 用户点击“取消”的同时,支付回调也到达。
- 若不加控制,可能出现:
- 订单被取消后却支付成功(资金损失)
- 订单支付成功却被取消(体验问题)
核心目标:保证最终一致性,避免资损。
解决方案
方案一、基于数据库行锁 + 状态机(推荐)
- 原理:在更新订单状态时,使用数据库行锁或乐观锁,保证状态变更的原子性。
- 实现:
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),说明状态已变更,根据新状态做不同处理:
- 若已被支付,取消操作失败,提示用户“订单已支付,不能取消”。
- 若已被取消,支付回调需拒绝或触发退款。
- 使用幂等设计:支付回调需幂等,避免重复处理。
- 兜底:对账系统定期扫描异常订单,自动修复(如已取消却支付成功则退款)。
【中等】如何避免用户重复下单(多次下单为支持,占用库存)?⭐⭐⭐⭐
核心思想:同一请求无论提交多少次,只创建一笔订单,只扣一次库存(幂等性设计)。
前端防重
按钮置灰 + Loading:防止用户双击或连续点击。
后端幂等
- 幂等键:客户端生成唯一标识(如 UUID),在创建订单时传入。
- 处理流程:
- 后端先查 Redis 该幂等键是否存在 → 存在则直接返回已创建订单,不再扣库存。
- 不存在则加分布式锁,执行业务(创建订单、扣库存),完成后存入 Redis(带过期时间)。
数据库兜底
- 唯一约束:订单表为幂等键字段建立唯一索引,保证重复插入失败。
库存保护要点
- 库存扣减必须和订单创建在同一个事务中,保证原子性。
- 若幂等键已存在,直接返回已有订单,不再扣减库存。
边界问题(L3/L4 追问)
- Redis 与 DB 状态不一致:Redis 写入成功但建单失败 → 幂等键要带短 TTL 或建单失败时主动清理,避免用户重试被误判为重复。
- 唯一索引冲突的语义:并发下后到的请求插入失败,应捕获异常后查询已存在订单返回(而不是报错),保证用户体验一致。
- 幂等键的传递:客户端重试(网络超时)必须携带同一幂等键;网关重试同理,防止重试风暴下创建多单。
方案权衡与踩坑:Redis 幂等键提供快速判断但非绝对可靠(Redis 故障/过期后失效),DB 唯一索引才是最后防线——两层缺一不可。曾有线上事故:网关对超时请求自动重试但未透传幂等键,弱网用户每单重试产生 2~3 笔重复订单,库存被无效占用——重试链路必须透传幂等键,网关重试仅限幂等接口。
失效场景:幂等键带短 TTL 时,Redis 过期后用户真重试可能重复建单,靠唯一索引兜底;幂等键若由服务端生成(如每次请求返回新 token),前端刷新/重进页面会重新获取,防重失效。
拓展追问
- 追问 1:幂等键存 Redis 多久合适?
覆盖用户重试窗口 + 业务时效:如 10~30 分钟;同时依赖 DB 唯一索引永久兜底(幂等键入订单表)。Redis 只加速判断,不承担唯一性责任。 - 追问 2:表单 Token 方案与幂等键方案有何区别?
表单 Token 是服务端预发一次性令牌,提交时校验并删除,防“同一页面重复提交”;幂等键是客户端生成、允许重复提交但返回同一结果,对网络超时重试更友好。现代 API 设计首选幂等键。 - 追问 3:用户真的想下两单相同商品怎么办?
幂等键区分“重试”与“新意图”:同一幂等键重复提交返回同一订单,新下单生成新幂等键;还可叠加业务限频(同用户同 SKU 1 秒内只允许 1 单)防误操作。
场景题
弱网地区电商平台:移动端下单接口超时率 8%,用户习惯超时后立即重试,曾日均产生 3000 笔重复订单。请根治。
分析要点:客户端生成幂等键(进入下单页时生成一次,重试不变),服务端三层防护:Redis 幂等键快速判断(存在则直接返回已建订单)→ 分布式锁防并发穿透 → DB 幂等键唯一索引最后防线;网关重试必须透传幂等键,非幂等接口禁止自动重试;捕获唯一索引冲突后查已有订单返回(而非报错);上线后监控重复订单数降为 0。
【困难】如何设计数据同步方案(同步到数仓)?⭐⭐⭐⭐
数据同步 = Canal 采集 Binlog + MQ 保序 + Flink 清洗 + 每日对账,核心是数据不丢不重、顺序一致、延迟可控。
总体架构
订单库(MySQL) → 变更捕获 → 消息队列 → 数据清洗 → 数仓(Hive/ClickHouse)
↑ ↑
业务无侵入 可重试、保序核心方案选型
| 方案 | 原理 | 准确率 | 性能 | 复杂度 | 推荐 |
|---|---|---|---|---|---|
| Canal(Binlog) | 模拟 MySQL 从库解析 binlog | 最高 | 高 | 中 | ⭐️ 首选 |
| MQ 双写 | 业务代码同步发 MQ | 中 | 低 | 低 | 不推荐 |
| DataX 离线抽取 | 定时全量/增量抽取 | 高 | 低(T+1) | 低 | 离线场景 |
| Flink CDC | 实时流计算 | 高 | 高 | 高 | 实时数仓 |
关键机制
- 数据一致性保障
- 断点续传:记录 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 本地文件缓存(需配置)
- 数仓故障:消息持久化,恢复后重放
方案权衡与踩坑:Binlog 方案对业务零侵入但强依赖源库 binlog 格式(ROW 模式)与主从拓扑,分库分表后每库都要部署采集实例;业务双写 MQ 方案延迟低但侵入业务代码,且事务内发 MQ 有一致性问题——曾有线上事故:Canal 消费积压 6 小时未告警,数仓报表与在线库差异巨大误导运营决策——同步延迟必须有分钟级告警,不能只靠日终对账发现。
量化参考:日增 1000 万变更的库,binlog 增量约几 GB,Canal 解析 + MQ 传输延迟正常在秒级;Flink keyBy 单键串行处理,热点 key(大卖家)可能成为瓶颈,需盐值打散或拆分处理。
拓展追问
- 追问 1:分库分表后如何采集 binlog?
每分片库部署 Canal 实例(或一个 Canal 集群监听多实例),消息带上分片标识;分区键仍用业务 ID(order_id)保证同订单变更进同一 Kafka 分区保序;全量初始化用 DataX 并行抽取各分片。 - 追问 2:源库 DDL 变更(加字段)同步链路怎么处理?
数仓侧先加字段(向后兼容)再上线源库 DDL;Canal 解析到新结构自动适配,但新旧消息混合期内消费端需容忍字段缺失;严禁先改源库后改数仓,会丢数据。 - 追问 3:如何验证同步数据正确?
三层:实时延迟监控(位点 lag)、抽样比对(每小时随机抽 1000 条源库记录比对数仓)、日终全量对账(COUNT + 关键字段 checksum);发现差异自动触发位点回溯重放。
场景题
订单库分 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,差异自动回溯重放。
【中等】如何实现数据的不停服迁移?⭐⭐⭐
核心思想
双写 + 数据同步 + 灰度切换
让新旧两套存储同时提供服务,逐步把流量切到新库,全程业务无感知。
第一阶段、准备与双写
- 新库上线,应用修改代码:所有写操作同时写入旧库和新库(双写)。
- 双写需保证最终一致性,可异步或同步(视业务容忍度)。
- 读操作仍从旧库读取,确保对业务无影响。
第二阶段、历史数据迁移
- 将旧库中的全量历史数据批量迁移到新库。
- 迁移过程中,增量数据仍在双写,需保证迁移与增量不冲突。
- 常见做法:记录迁移开始的时间戳或位点,迁移完成后,再补录期间产生的增量。
- 使用工具(如 DataX、Kettle)或自研脚本,分批迁移,避免影响线上。
第三阶段、灰度读切换
- 待历史数据迁移完成且双写稳定后,开始灰度切读流量。
- 逐步将读请求从旧库切到新库,比如 1%、10%、50%、100%。
- 每个灰度步骤观察业务指标(响应时间、错误率、数据一致性)。
- 记忆点:读流量灰度切,逐步放量稳观察。
第四阶段、双写降级与最终切换
- 当读流量全部切到新库且稳定运行后,可考虑将双写降级为只写新库。
- 此时旧库可作为备份,观察一段时间无异常后,正式下线旧库。
- 保留旧库一段时间(如一周),以备紧急回滚。
- 记忆点:双写降级只写新,观察备份再下线。
【中等】如何快速找到附近距离用户最近的 N 家商户?⭐⭐⭐
方案一、GeoHash + MySQL(通用、易实现)
- 原理:将二维经纬度编码为一维字符串,前缀相同的区域相邻。
- 步骤:
- 商户入库时,根据经纬度计算 GeoHash 值(如 6-8 位),存入数据库并建索引。
- 查询时,计算用户位置的 GeoHash,取前缀匹配(如前 6 位),查询该区域及周围 8 个邻域的商户。
- 对查询结果计算精确距离,排序取前 N 个。
- 优点:实现简单,支持分库分表,精度可控。
- 缺点:边界问题需扩展邻域,精确距离计算需回表。
- 记忆点:GeoHash 编经纬,前缀匹配找附近,邻域扩展防边界,精确计算再排序。
方案二、Redis GEO(高性能、简单)
- 原理:Redis 3.2+ 内置 GEO 类型,基于有序集合(ZSet)存储经纬度,提供地理距离计算。
- 步骤:
- 用
GEOADD将商户 ID 和经纬度加入 Redis。 - 用
GEORADIUS或GEORADIUSBYMEMBER查询用户附近 N 个商户,直接返回距离排序结果。
- 用
- 优点:性能极高(内存操作),API 简单,支持距离排序和返回距离。
- 缺点:数据需全量驻留内存,容量受限于内存;适合商户数量可容纳的场景。
- 记忆点:Redis GEO 内存跑,GEORADIUS 命令好,性能极简成本高。
方案三、Elasticsearch Geo Distance(搜索型、可扩展)
- 原理:ES 内置地理点类型和地理距离查询,利用倒排索引和空间索引。
- 步骤:
- 定义字段类型为
geo_point,索引商户经纬度。 - 使用
geo_distance查询,按距离排序,取前 N 个。
- 定义字段类型为
- 优点:支持海量数据,可与全文搜索结合,分布式天然扩展。
- 缺点:需部署 ES 集群,运维成本较高。
- 记忆点:ES 地理点,geo_distance 搜周边,海量数据可扩展。
方案四、空间索引(PostGIS / MySQL Spatial)(数据库原生)
- 原理:关系数据库的空间扩展,使用 R-Tree 索引加速几何计算。
- 步骤:
- 创建
GEOMETRY列存储点坐标,建立空间索引。 - 使用
ST_Distance和ST_Within等函数查询最近点。
- 创建
- 优点:数据库原生支持,无需额外组件,数据一致性好。
- 缺点:某些数据库实现(如 MySQL)的空间索引性能可能不如 GeoHash 或 Redis;PostGIS 性能优异但需 PostgreSQL。
- 记忆点:空间索引 R-Tree,ST 函数算距离,PostGIS 更专业。
方案选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 商户数百万以内,并发高 | Redis GEO | 简单、快、内存可控 |
| 商户数千万以上,需持久化 | GeoHash + MySQL / MongoDB | 成本低,可水平扩展 |
| 需要复杂查询(文本+地理) | Elasticsearch | 功能全面,扩展性强 |
| 已有 PostgreSQL | PostGIS | 原生支持,性能优异 |
优化技巧
- 多级索引:先用行政区划(城市、区县)粗筛,再用 GeoHash 细查。
- 缓存热点:热门区域的查询结果可缓存,减少重复计算。
- 动态网格:根据商户密度动态调整网格大小,平衡精度和性能。
- 异步更新:商户位置变化不频繁,可异步更新索引。
【中等】调用第三方接口应注意哪些问题?⭐⭐⭐⭐
要点
- 预防性设计:超时、重试、熔断、限流。
- 安全性:加密、认证、凭证管理、脱敏。
- 一致性:幂等、补偿、版本兼容。
- 资源隔离:线程池、连接池、信号量。
- 可观测性:日志、监控、链路追踪。
网络与超时控制
- 连接超时:设置合理的建立连接超时(如 3-5 秒),避免长时间等待。
- 读取超时:设置接口响应数据读取超时(如 10 秒),防止对方服务慢导致线程挂起。
- DNS 解析超时:确保 DNS 查询不成为瓶颈,可考虑使用备用 DNS 或 IP 直连。
异常处理与重试
- 区分异常类型:网络抖动(可重试) vs 业务错误(不可重试,需人工介入)。
- 重试策略:指数退避 + 最大重试次数(如 3 次),避免加重对方负载。
- 幂等性保证:重试时需确保接口支持幂等,否则需业务层去重。
接口限流与熔断
- 限流:根据对方接口配额或自身系统能力,进行本地限流(如 Guava RateLimiter)或分布式限流。
- 熔断:引入熔断机制(如 Sentinel、Hystrix),当错误率达到阈值时快速失败,防止雪崩。
- 降级:定义降级逻辑(如返回缓存数据或默认值),保障核心业务。
数据安全与认证
- 传输加密:使用 HTTPS 确保数据传输安全,防止中间人攻击。
- 身份认证:妥善管理 API Key、Token 等凭证,避免硬编码(使用配置中心或密钥管理服务)。
- 敏感信息:请求/响应中若包含敏感数据,需进行脱敏或加密处理。
幂等性与数据一致性
- 请求幂等:对于可能重复提交的场景(如支付通知),需通过业务唯一键去重。
- 最终一致性:若第三方接口响应延迟或失败,需设计补偿机制(如定时对账、状态同步)。
接口版本管理与兼容性
- 版本控制:明确接口版本(如 URL 路径包含 v1),避免无通知升级导致兼容问题。
- 变更通知:关注第三方接口变更公告,提前适配。
- 灰度验证:新版本接口上线前,先在小流量验证。
资源隔离与线程池管理
- 线程池隔离:为第三方调用分配独立的线程池,避免占用核心业务线程资源。
- 信号量隔离:对于非阻塞调用,可使用信号量控制并发数。
- 连接池管理:合理配置 HTTP 连接池大小、空闲连接回收策略,避免资源泄漏。
方案权衡与踩坑:重试是把双刃剑——对非幂等接口自动重试会引发重复扣款/建单,重试策略必须区分幂等性;熔断阈值设太敏感会在抖动期误断,设太钝会在故障时拖死线程池——需按第三方历史 RT/错误率基线校准。曾有线上事故:对接物流接口未设读超时,对方故障时我方线程全部阻塞在等待,线程池耗尽拖垮自身下单链路——第三方调用的超时必须小于自身接口的超时,否则故障会传染。
量化参考:连接超时建议 1~3s(内网调用 1s 即可),读超时按对方 SLA 的 P99 × 2 设置;重试最多 1~2 次且带退避,重试风暴可使对方压力翻倍加剧故障。
拓展追问
- 追问 1:第三方接口无幂等支持,但我们必须重试怎么办?
业务层造幂等:每次请求携带我方唯一请求号,即使对方不承诺幂等,也在我方记录请求号与结果,重试前先查本地记录;资金类操作重试前必须查单确认状态。 - 追问 2:如何监控第三方接口的健康度?
按第三方维度埋点:成功率、RT P99、超时率,与其 SLA 对比告警;建立“依赖健康看板”,故障时能快速判断是自己的问题还是对方的问题;重要依赖定期做故障演练(Mock 对方故障验证降级链路)。 - 追问 3:对方接口升级不兼容怎么应对?
防腐层隔离对方模型(升级只改适配层),双版本双跑灰度切换;合同/协议层要求变更提前通知;响应解析对未知字段宽容(反序列化不报错),对方加字段不至于直接崩。
场景题
电商对接 3 家快递公司查件接口,各家 SLA 不同(RT P99 从 200ms 到 2s),高峰期我方查件 QPS 5000,要求对方故障时不影响下单主链路。请设计。
分析要点:隔离:查件调用独立线程池(舱壁模式),与下单链路线程资源完全隔离;超时按各家 SLA 分别配置(P99×2);熔断:单家错误率超 30% 熔断,降级返回缓存的最后已知物流状态;异步化:物流状态用定时拉取 + 缓存,用户查看走缓存而非实时调对方,把同步依赖变异步依赖;限流:按对方配额限制我方调用频率,超额排队。核心原则:把第三方依赖从主链路移除,能异步就异步。