Java Notes

Home首页 / Question 2: payment callback第 2 题:支付回调

Question 2 · must know · payments第 2 题 · 必背 · 支付链路

WeChat fires the callback three times: ship once, deduct once.微信回调同时来三条:只发一次货、库存只扣一次。

One payment, three callbacks. Two of them must do nothing — and must still answer OK.一次支付,三条回调。其中两条必须什么都不做,而且还得回“成功”。

Short answer先给答案Three layers, in this order: a unique-index idempotent table catches the duplicate, a status CAS makes the order move forward exactly once, and a distributed lock only covers the small critical section that touches stock.三层,顺序不能反:唯一索引的幂等表拦住重复;状态 CAS 让订单只能往前走一次;分布式锁只包住动库存的那一小段。

Must know必背payments支付speak 60s60 秒口播4 follow-ups追问 4 个

01Why three callbacks at all为什么会来三条

The payment platform is not rude — it is built to be unreliable. If it does not hear an OK, it keeps knocking.支付平台不是没礼貌,它是按不可靠来设计的:没收到成功应答就一直敲。

So “it will not come twice” is never true. The only useful question is: what happens the second time?所以“不会来两次”永远不成立。唯一有用的问题是:第二次来的时候,会发生什么?

02Layer one: the idempotent table第一层:幂等表(拿唯一索引当锁)

Before doing any work, try to insert a row keyed by the platform trade number. The database rejects the second insert — and that rejection is the lock.干活之前,先拿平台流水号插一行记录。第二次插会被数据库拒掉——这个“拒掉”就是锁。

create table pay_idempotent (
  id           bigint auto_increment primary key,
  out_trade_no varchar(64) not null,   -- 微信/支付宝流水号
  source       varchar(16) not null,   -- wechat / alipay
  created_at   datetime    not null,
  unique key uk_trade (out_trade_no, source)
);

In code it is just insert, and on DuplicateKeyException return success. The second callback does no work and still tells the platform “done”.代码里就是 insert,撞到 DuplicateKeyException 就直接回成功:第二条回调不干活,却仍然告诉平台“已处理”。

Why not select then insert: two threads can both see “not there”. The unique index is the only thing the database itself polices.为什么不用 select 再 insert:两个线程能同时查出“不存在”。唯一索引是数据库自己把门的东西。

03Layer two: status CAS第二层:状态 CAS(比较再交换)

The idempotent row stops duplicates, but it cannot stop a crash halfway. The order state machine is the second guard:幂等表拦得住重复,拦不住“做到一半崩了”。订单状态机是第二道闸:

update orders
   set status = 'PAID', pay_time = now()
 where order_no = ? and status = 'PENDING';   -- 只有待支付才能改成已支付
Rows updated影响行数What it means说明
1I won — deduct stock, then ship我抢到了:扣库存、发货
0Someone already did it — just return OK别人处理过:直接回成功
0, and status is CLOSED0,且状态是已关闭Money arrived but the order closed — refund钱到了但单子关了:走退款

Check and change live in one statement, so there is no gap for another thread. That is also why retrying after a crash is safe.判断和修改在同一条语句里,中间没有缝给别人插进来。这也是“崩了以后重试是安全的”的原因。

04Layer three: the lock, only where it is needed第三层:锁,只加在该加的地方

A Redisson lock is not the first line of defence. It protects the critical section: read stock, check, deduct, write back.Redisson 锁不是第一道防线,它护的是临界区:读库存、判断、扣减、写回。

05Four follow-ups, one line each四个追问,一句话一个

Follow-up追问One-line answer一句话答
Where is the lock, and what is the key?锁加在哪,key 是什么?Around the stock deduction only; the key is the sku id.只包扣库存那段;key 是 sku id。
What if the holder dies?持锁的进程挂了怎么办?The lease expires; watchdog stops renewing. Never rely on a bare expire.租约自动过期,watchdog 不再续期。别用裸的 expire。
Master fails over and the lock is lost?主从切换丢锁怎么办?Fall back on the database: unique key and CAS still hold.靠数据库兜底:唯一键和 CAS 仍然守得住。
Could you do it without a lock?能不能不上锁?Yes: update stock set qty = qty - 1 where qty > 0, then check the rows.能:update ... where qty > 0,看影响行数。

06The 60-second answer60 秒照念稿

Say it in this order照着念,再换成自己的话

First: duplicates are normal. The platform retries until it hears success, and two instances can get it at once.先说重复是常态:平台没收到成功就一直重试,两台机器也可能同时收到。

Layer one: insert the trade number into a table with a unique index; a duplicate key just returns success.第一层:拿流水号往唯一索引的幂等表里插,撞键就直接回成功。

Layer two: a status CAS — only PENDING may become PAID; zero rows means someone else already did it.第二层:状态 CAS,只有待支付能改成已支付;影响 0 行说明别人做过了。

Layer three: a Redisson lock keyed by sku id around the deduct and write-back; if it is lost on failover, the first two layers still hold.第三层:Redisson 按 sku 加锁护住扣减写回;就算主从切换丢锁,前两层还兜着。

Stock is deducted once because the CAS decides which callback owns the order, and the idempotent row turns the rest into cheap no-ops.库存只扣一次,是因为 CAS 决定这条回调归谁,幂等表把其余的变成廉价空转。

07How to say it as mine怎么挂到我自己的项目上

Only use what is really on the resume — this hangs on the order system, WeChat Native pay, and Redisson:只挑简历上真有的。这题挂在订单系统、微信 Native 支付、Redisson 上:

08Three things not to say三句别说

When you do not know a piece, say how you would check: “I would confirm whether the retry body is re-signed…” Honest with a direction beats guessing.真不会的时候,说你会怎么确认:“我会先确认重试报文是否重新签名……”诚实加方向,比硬编强。

→New words新词

idempotent /ˌaɪdemˈpoʊtnt/ Running it twice is the same as running it once.幂等:执行两次和执行一次一样 ▶ hear it
callback /ˈkɔːlbæk/ The platform calls us back to say it is paid.回调:平台回头通知“已付款” ▶ hear it
duplicate /ˈduːplɪkət/ The same thing arriving again.重复:同一件事又来了 ▶ hear it
unique index /juˈniːk ˈɪndeks/ The database refuses a second copy.唯一索引:数据库拒绝第二份 ▶ hear it
compare-and-swap /kəmˈper ən swɑːp/ Change it only if it still looks like this.CAS:还是这个样子才允许改 ▶ hear it
oversell /ˌoʊvərˈsel/ Selling more than you have.超卖:卖出去的比库存还多 ▶ hear it

Click ▶ hear it for real people on YouGlish; 🔊 for the computer voice.点 ▶ hear it 听真人发音(YouGlish);点 🔊 听电脑朗读。

✓Check yourself自测

  1. Why must the second callback still answer success?第二条回调为什么必须回成功?
  2. What does “0 rows updated” mean in the CAS?CAS 影响 0 行是什么意思?
  3. Name two things that make a retry after a crash safe.崩了以后重试为什么安全?说出两个兜底。

Answer out loud first. Only then open the answers below.先自己出声答,再看下面的答案。

Answers参考答案

09New questions this one opened这道题又长出了哪些新问题

A good answer opens the next question. That is the whole method.一个答案会生出下一个问题——方法就是这个。

Question新问题One line一句话
为什么用唯一索引当锁,而不是先查再插?两个线程能同时查出“不存在”;唯一索引是数据库自己把门的。待写
幂等记录插进去了,但业务崩了怎么办?靠状态 CAS 兜底:只允许 PENDING → PAID,重试时影响 0 行就按已处理返回。待写
钱到了但订单已经关闭,怎么处理?CAS 影响 0 行且状态是 CLOSED → 走退款,不能悄悄吞掉。待写
Redisson 的 watchdog 到底在续什么?续的是锁的租约;线程活着才续,进程挂掉租约自动过期。待写
主从切换把锁丢了怎么办?靠数据库兜底:唯一索引 + CAS 仍然守得住。待写
MQ 消费重复,和这个是一回事吗?机制一样:消费端幂等(业务唯一键 / 状态机),别指望队列只投一次。待写
那库存怎么保证只扣一次、还不超卖?预扣 + 条件更新(qty >= 1)+ 延时关单回滚。待写
缓存和数据库不一致怎么办?Cache Aside:写库后删缓存,加延迟补偿与过期兜底。待写

← Question 1: HashMap← 第 1 题:HashMap