怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「量化怎么写」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际虚拟商品交易平台,卖游戏点卡、会员充值、礼品卡这类即时交付的虚拟商品。支持多币种与多支付渠道,商品库存是上游供应商提供的卡密池
为什么选这个背景 它天然包含资金链路、并发库存、多方对账四类高含金量问题,是应届生能讲出深度、面试官也愿意深挖的题材,而且和「电商」这个最通用的业务标签对得上。

模块一:订单与支付链路

  1. 订单与支付链路(三域解耦 + 五层资金一致性保障)★★★
    简历这样写 订单与支付链路(Spring Boot + MyBatis-Plus + RocketMQ + Redis + Stripe / PayPal SDK):按「订单域 / 支付域 / 账务域」三层解耦,支付单独立于订单以支持多渠道重试;用「幂等号 + 状态机 + 可靠消息 + 定时补偿 + 日终对账」五层保障资金一致性;将渠道回调的接收与业务处理解耦后,回调接口 P99 由约 1.2s 降至 180ms,压测 50 QPS 下未再出现因响应超时导致的渠道重复通知。
    展开完整拆解
    为什么要这么设计

    最初的版本是把支付信息直接挂在订单表上——订单表里存 pay_channelpay_statustrade_no。两个问题很快暴露出来。

    一是重试没法做。用户信用卡被拒后想换 PayPal 再付一次,这时候订单还是那一笔,但支付信息要换一套。挂在订单上就只能覆盖掉旧的,失败的那次支付记录就丢了,事后查不清用户到底试了几次、每次为什么失败。

    二是职责混在一起。订单要管商品、价格、收货信息、状态流转;支付要管渠道、验签、回调、退款。混在一张表和一个服务里,改支付逻辑要动订单代码,风控和对账也无从下手。

    所以拆成三域:订单域管交易意图(买什么、多少钱、给谁),支付域管收款过程(用哪个渠道、支付单的状态),账务域管资金记录(复式记账、可审计)。订单和支付是一对多,这是整个设计的核心判断。

    整体链路
    下单(价格在这一步就固定下来) │ ├─ 用户端 ─▶ 订单域:创建订单(商品 ID + 数量 + 幂等号) │ 订单域:定价 / 锁汇率 / 生成订单号 / 预占卡密库存 │ ◀─ 返回订单号 + 锁定价格(此后价格不再变,避免付款金额对不上) │ ├─ 用户端 ─▶ 支付域:创建支付单(带订单号) │ 支付域:按地区与金额选渠道 → 生成渠道支付单 │ ◀─ 返回渠道支付参数 │ └─ 用户端 ─▶ 渠道:唤起支付(跳转或 SDK) 支付结果(只认渠道回调与主动反查,不认客户端的说法) │ ├─ 渠道 ─▶ 支付域:异步回调 │ 验签 → 校验金额与币种 → 幂等落库 → 发支付成功消息 │ 回调可能重复、乱序、迟到几十分钟,三种都要能扛 │ ├─ 支付域 ─▶ 订单域:支付成功消息 │ 订单域:扣减卡密库存 → 发卡密 → 订单状态改为已完成 │ ├─ 支付域 ─▶ 账务域:记账(应收 / 手续费 / 待结算) │ └─ 用户端:轮询订单状态(超时不判失败,转「处理中」) 兜底两条,缺一不可 ├─ 定时补偿:扫中间态订单 → 反查渠道真实状态 → 推进或关单 └─ T+1 日终对账:渠道账单 / 支付单 / 账务流水 三方核对

    要点是回调只做最小的事(验签、落库、发消息)然后立刻返回,后面的扣库存、发货、记账全部异步。因为渠道对回调响应时间有要求,同步处理业务会导致渠道判定超时而不断重发。

    分步拆解
    1. 创建订单:前端不传金额。只传商品 ID、数量、币种、以及一个前端生成的幂等号。价格由服务端按当前汇率和地区税率计算,并把汇率快照和有效期一起存进订单。这样两件事同时被解决:金额不可篡改,以及国际业务的汇率波动纠纷(用户看到 19.99 就必须扣 19.99)。
    2. 预占库存而不是支付后再扣。下单时就把卡密标记为 LOCKED 并绑定订单号,支付成功只是把状态从锁定转成已发放。如果等支付成功才去分配,会出现「付了钱但卡密被别人抢走」——这是最难处理的客诉,因为钱已经收了。
    3. 创建支付单是独立一步。一个订单可以有多个支付单,每次支付尝试生成一个新的。支付单记录渠道、渠道流水号、金额币种、状态。发给渠道的商户单号用支付单号而不是订单号,否则渠道那边会因为单号重复而拒绝第二次尝试。
    4. 回调处理的六层防护。验签(防伪造,这是最严重的漏洞点)→ 校验金额和币种与支付单完全一致(不一致立刻告警)→ 用渠道流水号做唯一索引保证幂等 → 状态机只允许合法流转且终态不可逆(已成功的单收到失败通知直接忽略并告警)→ 原始报文落库 → 发 MQ 后立刻返回成功。
    5. 下游消费保证幂等。订单域消费「支付成功」消息后扣库存发货,用订单号加消息类型做去重键。账务域独立消费同一条消息记账,两者互不阻塞。
    6. 补偿与对账兜底。定时任务扫描「支付中超过 15 分钟」的支付单,主动调渠道查询接口确认真实状态;扫描「已支付但未发货超过 5 分钟」的订单重新触发发货。每日 T+1 拉渠道账单做三方对账(渠道账单 / 支付单 / 账务流水)。
    关键决策与取舍

    为什么不用分布式事务。链路里有外部渠道这个参与者,你没法让它加入你的事务;而且链路长(订单、支付、库存、账务、通知),2PC 会让资源长时间锁定,跨境场景下的网络延迟让这个代价更不可接受。所以选最终一致:强一致只保证在单库事务内能做到的部分(扣库存和分配卡密必须强一致,不能超发),跨服务的部分用可靠消息加补偿加对账。

    为什么回调要接收与处理解耦。这是那个耗时数字的来源。改造前回调里同步做完发货和记账才返回,P99 到了三秒多,而渠道的超时阈值通常是几秒——网络稍差就被判定为通知失败然后重发,重发又进一步加剧拥塞,形成正反馈。改成落库加发 MQ 后立刻返回,响应时间降到几十毫秒,重发问题自然消失。

    踩过的坑:关单和支付撞车。订单超时关闭的那一瞬间用户完成了支付,结果订单已关闭但钱收了。解法是三层:关单前主动查一次渠道状态;关单和支付成功都用 update ... where status = 前置状态 靠数据库行锁互斥;最关键的是兜底——已关闭的订单收到支付成功通知时,走「异常支付」流程自动发起退款并告警,绝不能默默忽略。

    数字是怎么测的

    P99 由约 1.2s 降至 180ms。改造前后各跑一轮压测对比即可。改造前那一秒多主要花在同步的扣库存、发货、记账上;改造后只剩验签、落库、发 MQ,剩下的一百多毫秒是网络往返加偶发的 GC 抖动——所以它不会低到几十毫秒。

    为什么用 P99 而不是平均值:渠道判定超时看的是尾部延迟,平均值会把问题掩盖掉。这个点面试官会追问,要能答上来。

    压测 50 QPS 下未再出现重复通知。验证方式是统计回调接收表里同一个渠道流水号出现的次数——原始报文都落了库,同一流水号出现多次就是重发。注意结论要限定在压测条件下,不要说成线上永远不会发生。

    面试追问
    Q:为什么一个订单要允许多个支付单?直接让用户重新下单不行吗? A:可以,但体验和数据都更差。重新下单意味着重新锁库存和重新报价,汇率变了金额就变了,用户会觉得被套路;而且原订单要作废,转化漏斗的数据会失真。更重要的是虚拟商品的库存是卡密,反复锁定释放会加剧并发竞争。允许一单多次支付尝试,订单和库存都不动,只是换一个渠道再试,这是成本最低的做法。
    Q:回调里发 MQ,如果 MQ 发送失败了怎么办?消息不就丢了? A:所以顺序是先落库再发 MQ,而不是只发 MQ。原始报文和一个处理状态字段先写进数据库,这一步成功就返回给渠道成功;MQ 发送失败或者下游消费失败,定时任务会扫描「已接收但未处理完成」的记录重新投递。数据库是最可靠的兜底,MQ 只负责让正常路径快起来。
    Q:状态机你具体怎么实现的?用了什么框架? A:没上框架,就是 update pay_order set status = ? where id = ? and status = ?,判断影响行数。前置状态写在 where 里,靠数据库的行锁保证互斥和幂等。加框架(比如 Spring StateMachine)在这个规模下是过度设计——状态只有五六个,流转规则写在一个枚举里加校验方法就够了,而且这样出问题时排查路径极短。如果状态机复杂到有并行状态和嵌套子状态,才值得上框架。
    Q:你说预占库存,那用户下单不付款,库存不就被占死了? A:靠超时关单释放,用延迟消息触发,另外有定时任务扫「锁定超过 30 分钟」的卡密兜底释放——不能只依赖延迟消息,消息丢了库存就永久漏了。释放操作本身也要幂等,重复执行不能把已经发放出去的卡密错误地放回池子。
    Q:如果我是面试官,我会问你这个项目里最难的点是什么。 A:不是技术选型,是「不确定态」的处理。支付链路里到处都是「我不知道对方成功没有」的状态:调渠道超时、回调没收到、MQ 不确定投递成功。最初的版本把超时当失败处理,导致过重复扣款。想清楚「超时只等于结果未知,必须靠查询确认,而且任何前端交互都不能基于这个未知状态做不可逆决定」之后,整条链路的设计才顺过来。

模块二:卡密库存与自动发货

  1. 卡密库存与自动发货(原子分配 + 加密存储 + 供应商自动补货)★★★
    简历这样写 卡密库存与自动发货(MySQL 行锁 + AES/KMS + RocketMQ + XXL-Job):用单条 SQL 原子分配替代「先查后改」,压测 50 并发下由每轮可复现数笔重复分配变为未再复现;卡密全程 AES 加密存储、密钥托管 KMS、内部查看留审计与授权;建立「阈值预警 → 自动向上游拉取 → 入库校验」的补货闭环,把缺货导致的「已支付无法发货」从每周数次降到月均一次以内
    展开完整拆解
    为什么要这么设计

    虚拟商品的库存和普通电商不是一回事。普通商品库存是一个可加减的数字,卡密是有具体身份的实体——每一张卡密都是唯一的、有价值的、发错了收不回来。所以它同时具备三个难点:并发分配的正确性、敏感数据的安全性、以及缺货即事故的供应链压力。

    最早的实现是「select 一张未使用的卡密 → update 标记为已用」,测试环境完全正常,上线后促销时出现同一张卡密发给两个用户。因为这两条语句之间有并发窗口,两个请求会查到同一张。

    整体链路
    下单 支付成功 发货 │ │ │ ├─ 原子分配一张卡密 ──────────────────────────────┐ │ update card │ │ set status='LOCKED', order_id=? │ │ where sku_id=? and status='UNUSED' limit 1 │ │ ↓ 影响行数=0 → 无库存,下单失败 │ │ ↓ 影响行数=1 → 拿到卡密 │ │ │ │ │ ├─ status: LOCKED→SENT │ │ │ 解密后展示给用户 ├─ 记审计日志 │ │ │ └─ 超时未支付 ──▶ 释放回 UNUSED(延迟消息 + 定时兜底) 库存水位监控 ──▶ 低于阈值 ──▶ 向上游拉取 ──▶ 入库校验 ──▶ 可售
    分步拆解
    1. 分配用一条 SQL 完成。update ... where status = 'UNUSED' limit 1 然后判断影响行数,靠数据库行锁保证同一张卡密不可能被两个请求拿到。这里不需要分布式锁——很多人第一反应是加 Redis 锁,但那是在数据库已经能保证原子性的情况下多引入一个失效点(锁超时、主从切换丢锁),是典型的过度设计。
    2. 状态机四态:UNUSED → LOCKED → SENT,另有 VOID 作废。每次流转都写变更流水(谁、什么时候、因为哪个订单、从什么状态变成什么状态)。这张流水表是后来排查「卡密发重了」类问题的唯一依据,没有它根本查不清。
    3. 加密存储。卡密密文入库,密钥放 KMS 不进代码仓库,应用只在展示给用户的那一刻解密。内部管理后台查看卡密要单独授权并强制留痕——这一条是防内部泄露的,实际比防外部攻击更重要,因为卡密的变现路径太短。
    4. 分配即绑定,重试复用同一张。发货流程失败重试时,先查这个订单是否已绑定卡密,有就返回同一张而不是重新分配。这样发货天然幂等,也避免了「卡密已分配但订单以为没发」产生的孤儿卡密。
    5. 释放要双保险。超时关单用延迟消息释放,另有定时任务扫「LOCKED 超过 30 分钟」的兜底。释放要判断当前状态,已经 SENT 的绝不能放回池子。
    6. 补货闭环。按 SKU 监控可用库存水位,低于阈值触发向上游供应商 API 拉取;拉回来的卡密要入库校验(格式、重复、有效性抽检)才置为可售。同时监控「缺货导致的下单失败次数」,这是补货是否及时的直接指标。
    关键决策与取舍

    为什么没上 Redis 预加载卡密队列。高并发场景下把卡密 ID 预热进 Redis List 用 lpop 取,性能确实更好。但它引入了「Redis 取出成功但落库失败」这个新的一致性问题,而且 Redis 挂了要能回退到数据库模式,代码复杂度翻倍。我们的峰值 QPS 只有几百,数据库方案完全撑得住,所以选了简单可靠的那个。这个取舍要主动说——面试官想看的是你会不会为了炫技过度设计。

    可售数量为什么不单独维护计数。试过用一个 stock_count 字段做快速拦截,但它和实际卡密数会不一致(并发、补偿失败),最后变成两个数据源互相打架。改成以实际卡密表为准,前端展示用模糊表述(「库存紧张 / 充足」)而不是精确数字——反正精确库存对用户没意义,还暴露经营数据。

    数字是怎么测的

    压测 50 并发下重复分配未再复现。做法是写脚本对同一个 SKU 并发发起 50 次下单,然后查有没有两条订单绑定了同一个卡密 ID。旧写法(先查后改)每轮压测都能查出几笔,改成单条 SQL 后连续跑多轮都没再出现。并发 bug 只能这么验证,这也是这个模块最该写的量化。

    缺货导致的「已支付无法发货」从每周数次降到月均一次以内。数据来源是异常订单队列——这类订单会被单独打标并告警,条数可查。口径用周和月而不是日均,因为小体量项目按日均算出来是零点几单,反而不自然。

    被问到时要能补一句统计范围(比如「上线后观察了两个月」),限定范围的表述比绝对化的表述站得住。

    面试追问
    Q:update ... limit 1 在并发下真的安全吗?会不会两个事务都更新成功? A:不会。这条语句在执行时会对扫描到的行加排他锁,第二个事务要么等锁释放后发现 status 已经不是 UNUSED 而更新 0 行,要么锁到另一行。关键在于判断条件和更新动作在同一条语句里,中间没有窗口。反过来「先 select 再 update」之所以出错,就是因为两条语句之间的间隙不受保护。
    Q:如果 SKU 是热门商品,这条 SQL 会不会成为热点? A:会,所有请求都在争同一个 SKU 的行。缓解手段有几个:给 (sku_id, status) 建联合索引减少扫描范围;把同一 SKU 的卡密按分片键打散,请求随机落到一个分片上分散竞争;再往上才是 Redis 预加载。我们当时的量级用索引优化就够了,所以没继续往下做——如果面试官继续追问,我会说清后面两级方案的成本和适用条件。
    Q:卡密加密之后,怎么支持「按卡密查订单」这类运营需求? A:加密后没法直接等值查询,所以额外存一个卡密的哈希值并建索引,查询时对输入值算同样的哈希去匹配。哈希要加盐防彩虹表。这是加密字段做精确查询的通用做法,模糊查询就没办法了,只能靠其他字段定位。
    Q:上游供应商给的卡密里有重复或者失效的怎么办? A:入库时做校验:本地查重(卡密哈希唯一索引直接挡住重复)、格式校验、以及抽样调上游接口验证有效性(不能全量验,成本太高)。入库后先进「待验证」状态,抽检通过才转可售。真发出去之后用户反馈无效的,走售后补发并记录到供应商质量评分——这个评分会影响后续的采购决策,这属于业务闭环的一部分。

模块三:资金一致性治理(对账与补偿)

  1. 资金一致性治理(三方对账 + 差异自动核销 + 主动补偿)★★★
    简历这样写 资金一致性治理(XXL-Job + MySQL + RocketMQ + 渠道对账 API):建立「渠道账单 / 支付单 / 账务流水」三方对账体系,按长款、短款、金额差异、状态不一致四类差异分级处置,手续费与汇率造成的合理差异按规则自动核销;上线后一次性清理出 37 笔历史差异单,并把差异报表的日均条目从十余条(多为手续费导致的误报)降到 1 至 2 条;配套准实时补偿任务扫描中间态订单,把资金异常的发现方式从依赖用户反馈改为系统主动识别
    展开完整拆解
    为什么要这么设计

    前面所有的幂等、重试、状态机都是降低出错概率,但概率不为零。跨境支付链路涉及渠道、银行、汇率、多个内部服务,任何一环的漏网都会变成资金差异。没有对账的支付系统一定有隐藏资损,只是还没被发现。

    更现实的驱动是:出现资金问题时,如果只能靠用户投诉才知道,说明系统对自己的正确性一无所知。这在面试里是个很好的表达点——它体现的是从「被动响应」到「主动发现」的意识转变

    整体链路
    T+1 凌晨 │ ├─ 拉取渠道账单(API / 文件下载,失败可重试与补拉) │ ↓ ├─ 解析入库(原始账单单独存表,保留原文便于举证) │ ↓ ├─ 三方比对(按渠道流水号匹配) │ ┌───────────────┬──────────────┬─────────────────┐ │ ↓ ↓ ↓ ↓ │ 长款 短款 金额差异 状态不一致 │ 渠道有 我们有 多为手续费 一方成功 │ 我们无 渠道无 / 汇率 一方退款 │ ↓ ↓ ↓ ↓ │ 自动补单 冲正+追根因 容差内自动核销 按渠道为准修正 │ (触发发货) 超容差→人工 + 告警 │ ↓ └─ 差异未归零 → 进人工处理台 → 处理留痕 → 归零 准实时补偿(每 5 分钟) ├─ 支付中超 15 分钟 → 主动查渠道 ├─ 已支付未发货超 5 分钟 → 重新触发发货 └─ 卡密锁定超 30 分钟 → 释放
    分步拆解
    1. 为什么是三方而不是两方。多数实现只对「渠道 vs 支付单」,漏掉了账务。结果是支付对上了但财务账不平——因为记账那一步可能失败或重复。三方两两比对才能定位差异出在哪一环。
    2. 差异分四类,处置逻辑完全不同。长款(渠道有扣款我们没记录)最紧急,用户已经付钱了,要自动补单并触发发货;短款(我们记了成功渠道没有)说明有严重 bug 或状态误判,要冲正并追根因;金额差异绝大多数是正常的(手续费、汇率、分期费用),所以要按规则计算「预期差异」并在容差内自动核销;状态不一致以渠道为准修正。
    3. 容差自动核销是让对账真正可用的关键。第一版对金额做严格相等校验,结果每天报几百条差异,全是手续费导致的,运营根本不看了——对账系统一旦产生大量误报就等于没有。改成按渠道费率规则算出预期金额,只有超出容差才算真差异,日均差异单立刻降到个位数。
    4. 补偿任务和对账是互补的。对账是 T+1 的、覆盖全面;补偿是准实时的、只处理已知的几种中间态。用户体验靠补偿(5 分钟内自愈),账目正确靠对账(次日兜底)。两者都要有。
    5. 人工处理台不能省。自动化处理不了的差异要有界面展示差异详情、原始账单报文、关联订单和支付单,并提供一键处理动作和处理留痕。差异必须有归零流程——只列出来没人跟进的报表等于没做。
    关键决策与取舍

    为什么补偿任务的扫描间隔是 5 分钟而不是 1 分钟。更短的间隔能更快自愈,但会频繁扫描订单表并调用渠道查询接口——渠道的查询接口是有频率限制的,打太频繁会被限流甚至封禁。5 分钟是在用户感知(大部分用户不会在 5 分钟内投诉)和渠道压力之间取的平衡。

    为什么对账要单独存渠道原始账单。不只是为了排查,更是为了纠纷举证。国际支付的拒付(chargeback)争议中,需要向渠道提交完整的交易凭证,原始账单是最有力的材料。这一点是被一次真实的拒付争议教育的。

    规模上的诚实说明。当前是单机跑批,数据量在几十万笔量级完全够用。如果量级上到千万,就要上分片批处理(比如 XXL-Job 的分片广播)。面试时主动说清方案的适用边界,比假装它能扛任何量级更可信。

    数字是怎么测的

    一次性清理出 37 笔历史差异单。来源是对账任务第一次全量跑完后的输出条数,符合「上线一个新能力先把存量问题捞一遍」的真实过程。这是个绝对数,被问到时只需要说清「这 37 笔里长款几笔、短款几笔、金额差异几笔,各自怎么处理的」——所以真要写它,得能报出这个分布。

    差异报表日均条目从十余条降到 1 至 2 条。数据来自对账系统输出的待处理差异条数,取容差核销上线前后各两周对比。关键是要说清降下来的那部分是误报(手续费和汇率造成的合理差异),不是真实差异变少了——这个区分如果不主动讲,被追问出来会很尴尬。

    「把发现方式从依赖用户反馈改为系统主动识别」是纯定性表述,但它是这个模块最有价值的部分。面试官真正关心的是你有没有「系统要能自己知道自己出错」的意识,这个用一句话就能传达,不需要百分比。

    面试追问
    Q:对账发现长款,你自动补单,那万一是渠道账单错了呢? A:自动补单有前置条件,不是无脑补:订单必须存在、金额币种必须匹配、订单状态必须是待支付或支付中、并且这笔渠道流水号在我们这里确实没有成功记录。四条都满足才自动处理,任何一条不满足就转人工。而且补单动作本身也记流水,事后可回溯可撤销。
    Q:容差的阈值怎么定?定错了不就漏掉真实差异了? A:容差不是拍一个固定金额,而是按规则算出预期差异再比对——渠道费率是合同里写明的,手续费可以精确算出来;汇率差异用当日汇率区间算。真正的容差只留给舍入误差这类极小的量(比如一分钱)。所以它不是「放宽标准」,而是「把已知的合理差异从差异里剔除」。这个区分很重要,否则确实会漏。
    Q:补偿任务和用户的重试会不会冲突?比如任务正在补单,用户同时又付了一次。 A:会,所以补单和支付成功的处理都走同一套状态机加条件更新,谁先成功另一个就更新 0 行然后放弃。如果真出现了两笔成功支付,会被「一个订单对应多笔成功支付」的检测规则捞出来,自动对多余那笔发起退款并告警。这也是为什么前面说这类防护必须分层——单靠任何一层都不够。
    Q:这个模块你觉得做得不够好的地方是什么? A:两点。一是对账只做了 T+1,没做准实时对账,如果渠道当天出现批量异常,要等到次日才发现;更好的做法是加一层小时级的抽样对账。二是差异的根因分析还是人工的——系统能发现差异并分类,但「为什么会产生这个差异」需要人去查日志,如果能把差异自动关联到具体的失败环节会更有价值。这两点是我如果继续做会优先补的。

没有匹配的内容,换个关键词试试。

项目拆解 · 国际虚拟商品交易平台(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据