news 2026/4/14 21:35:52

京东二面:用户付了钱,订单却被取消?这道“并发题”挂了无数 3 年经验开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
京东二面:用户付了钱,订单却被取消?这道“并发题”挂了无数 3 年经验开发

昨天,一位粉丝在群里复盘他的京东零售(JD Retail)二面经历,心情非常郁闷。

他说前 40 分钟聊 Redis 架构、JVM 调优都对答如流,面试官频频点头。本以为稳了,结果临走前,面试官抛出了最后一道“场景设计题”

面试官:“我们的订单系统设置了 30 分钟未支付自动取消。现在有个极端情况: 用户在第29 分 59 秒支付成功了,支付宝回调刚好到了。 同时,第30 分 00 秒的‘超时取消定时任务’也触发了。这两个请求撞在一起(并发),你的代码怎么写才能保证——钱不能白扣,订单不能误删?

这位兄弟自信满满地写了一段伪代码:“先查一下订单状态,如果是‘未支付’,就更新状态。哪个先到就执行哪个呗。”

面试官看了一眼代码,摇了摇头说:“回去等通知吧。按你这个写法,支付宝重试回调时你会误退款,并发时你会掉单。京东一天得产生几万次资损事故。”

为什么看似简单的逻辑,在大厂面试官眼里却是“致命 Bug”? 今天 Fox 就带你拆解这个让无数候选人折戟的“并发竞态陷阱”,并给出资深架构师的标准解法。


一、 还原“车祸现场”:消失的那 0.01 秒

面试官担心的,到底是什么?

场景还原:假设订单 ID 为1001,状态为OrderStatus.UNPAID(未支付)。

  • 线程 A(支付回调):拿着支付宝的成功通知,准备把订单改成PAID

  • 线程 B(定时任务):发现订单已过 30 分钟,准备把订单改成CANCELED

这时候,两个线程在服务器里疯狂赛跑。

惨案发生:如果线程 B(取消)稍微快了那么 1 毫秒,先把数据库状态改成了CANCELED。 紧接着线程 A(支付)进来了,如果你的代码逻辑不够严谨(比如发生了覆盖写)。

结局:你的数据库里出现了一个“已支付” 但业务逻辑上 “已取消” 的幽灵订单,或者一个“已取消” 但用户 “已付钱”的冤案。

这在电商里叫“掉单”,在金融里叫“资损”,在面试里叫“挂了”。


二、 错误示范:90% 程序员都在写的“自杀式代码”

那位兄弟在面试时写的代码,大概长这样(伪代码):

// 线程A(支付回调)或 线程B(超时取消)都在执行这段逻辑 Order order = orderMapper.selectById(orderId); // 1. 先查 if (order.getStatus() == OrderStatus.UNPAID) { // 2. 内存判断 order.setStatus(newStatus); // 3. 改状态 orderMapper.updateById(order); // 4. 写回数据库 }

这就是典型的Check-Then-Act(先检查后执行)陷阱!

为什么会炸?因为步骤 1 和 步骤 4 之间,不是原子的!有一个极其微小的时间窗口。

  • T1:线程 B(取消任务)查到了UNPAID

  • T2:线程 A(支付回调)也查到了UNPAID

  • T3:线程 B 执行update,数据库变成了CANCELED

  • T4:线程 A 随后执行update,强行把数据库覆盖成PAID

结果:订单变成了“已支付”,但超时任务认为自己取消成功了,可能已经释放了库存。用户收不到货,客服电话被打爆。


三、 破局:资深架构师的「数据库乐观锁」

要解决这个问题,不需要引入 Redis 分布式锁(太重,引入外部依赖),也不需要数据库悲观锁for update(太慢,影响吞吐)。

最优雅的解法是:数据库乐观锁(基于行锁的条件更新,类 CAS 范式)。

核心心法:永远不要相信你在内存里查到的状态,要把前置条件带进 SQL 的 WHERE 子句里。

补充:这里的“类 CAS”是数据库乐观锁的实现范式,和 JVM 的 CAS(原子类、Unsafe)底层实现不同,但核心思想一致——「比较原值,符合才更新」。

正确写法(MyBatis):

1. 支付回调的 SQL

UPDATE orders SET status = #{paidStatus}, pay_time = now() WHERE id = #{orderId} AND status = #{unpaidStatus}; -- 关键!利用数据库行锁做原子校验

2. 超时取消的 SQL

UPDATE orders SET status = #{canceledStatus}, close_time = now() WHERE id = #{orderId} AND status = #{unpaidStatus}; -- 关键!

逻辑推演:无论线程 A 和 线程 B 怎么并发,数据库的行锁(Row Lock)会保证这两条 UPDATE串行执行谁先抢到锁,谁先执行;另一个后执行的,因为条件status = UNPAID不满足,返回的影响行数(rows)为 0

这就从根源上杜绝了“覆盖写”的竞态问题。


四、 致命细节:你考虑「幂等性」了吗?

面试时,很多同学写到上面那一步就觉得完美了。且慢!这里藏着一个会导致资损的 P0 级 Bug。

如果你在代码里这样写:

int rows = orderMapper.paySuccess(orderId); if (rows == 0) { // 认为更新失败就是被取消了,直接自动退款 refundService.autoRefund(orderId); // ❌ 致命错误!!! }

为什么错了?别忘了,支付宝/微信的回调是有重试机制的! 如果第一次回调由于网络波动超时了,但实际上数据库已经更新为PAID了。 几秒后,支付宝发起第二次重试回调

  1. SQL 执行UPDATE ... WHERE status = UNPAID

  2. 此时状态已是PAID,更新失败,rows返回 0。

  3. 你的代码进入else分支,给用户发起退款!

结果:用户没申请退款,订单也是成功的,你却把钱退给人家了?重大资损!

示例代码:
// 1. 原子更新:类CAS乐观锁更新支付状态 int rows = orderMapper.paySuccess(orderId, OrderStatus.UNPAID, OrderStatus.PAID); if (rows == 1) { // 抢锁成功,处理正常发货、扣减库存等业务逻辑 return success(); } // 2. 抢锁失败,必须二次查库判断原因(防重试、防并发) Order order = orderMapper.selectById(orderId); // 情况A:幂等处理(重复回调) if (order.getStatus() == OrderStatus.PAID) { log.info("订单已支付,忽略重复回调,orderId:{}", orderId); return success(); } // 情况B:真正的竞态(被超时任务抢先取消了) if (order.getStatus() == OrderStatus.CANCELED) { log.warn("订单超时后才支付成功,触发冲突处理流程,orderId:{}", orderId); // 进入后续业务决策(退款 或 订单捞回),禁止同步调用退款API processConflict(orderId); return success(); } // 其他异常状态(如关闭、退款中),直接返回成功,避免重复处理 log.warn("订单状态异常,忽略回调,orderId:{}, status:{}", orderId, order.getStatus()); return success();

这才是大厂要的逻辑:原子性更新 + 二次确认(Double Check) + 全场景幂等处理。


五、 工程化兜底

如果你把上面的方案答出来,面试官已经会给你打 S 级了。但如果你想冲击 SSP offer,可以再补充两点工程化思考

1. 拒绝同步退款(性能地雷)

在回调接口里直接调用微信/支付宝的退款 API 是大忌!

  • 风险:第三方接口万一卡顿(比如响应 3 秒),你的回调线程就会被阻塞,导致服务吞吐量雪崩,甚至引发级联故障。

  • 解法:异步解耦。发布一个RefundEvent消息到 MQ,或者将订单标记为PENDING_REFUND,由后台定时任务慢慢处理退款,不阻塞主线程。

2. 业务容错(温度 > 规则)

如果用户确实在第 29 分 59 秒付了钱,只是回调晚到了 2 秒。

  • 技术视角:“超时了就是超时了,退款!” —— 这样会被用户骂死,流失核心用户。

  • 业务视角:判断pay_time(支付时间)是否在 30 分钟有效期内。如果是,执行「订单捞回」逻辑(将CANCELED逆向流转回PAID,重新扣库存、触发发货)。 毕竟,把钱收进来,永远比推出去更重要。

3. 分布式定时任务兜底

微服务多实例部署时,超时取消任务会被多节点重复执行。需给定时任务加分布式锁(如 Redisson),防止多节点同时触发取消逻辑,减少无效竞态。


六、 核心总结

对于“订单超时”这种场景:

  • Redis ZSet / 时间轮 / xxl-job解决的是「怎么发现超时」

  • 数据库乐观锁(类 CAS)解决的是「怎么安全地关闭订单,避免竞态」

  • 二次查库 + 幂等处理解决的是「第三方回调重试,避免资损」

京东、阿里、字节的面试,考的永远不是 API 怎么调,而是你在极端场景下能不能守住系统的底线——不丢单、不资损、不崩服务。


写在最后

技术不仅是写代码,更是对业务资产的守护

觉得这篇真的能帮你避坑的,点个赞,收藏起来,转发给身边正在面试、做电商系统的兄弟。

https://mp.weixin.qq.com/s/tzEbqnZ4yMYibIWlX3H-mw

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 11:34:59

告别利润回吐:5个你必须知道的股票顶部信号

引言:为什么你总是卖飞或坐“过山车”?在股票投资中,在我接触的投资者中,有一个普遍现象:买点掌握得很好,卖点却一塌糊涂,导致利润大幅回吐,如同坐上了一趟“过山车”。你是否也遇到…

作者头像 李华
网站建设 2026/4/12 14:46:44

AI生成网站入门指南:从零基础到专业建站的路径

过去,搭建一个网站往往意味着「会代码 懂服务器 找设计师」。而现在,AI生成网站正在彻底改变这条路径。即使是零基础用户,也可以通过自然语言,把一个想法快速变成可用的网站或系统。这篇文章会从入门视角,拆解 AI生成…

作者头像 李华
网站建设 2026/4/10 10:01:11

基于 Spring AI Alibaba 构建企业级智能客服

基于 Spring AI Alibaba 构建企业级智能客服 ——Graph 工作流 Java 实战:电商售后工单的智能化落地 关键词:Spring AI Alibaba、Graph、RAG、Function Calling、企业级 AI 客服 适用场景:电商售后 / 技术支持 / 工单系统 一、为什么“电商售后 + 技术支持”是 AI 客服最…

作者头像 李华
网站建设 2026/4/10 8:35:11

电气监测数据如何成为碳核算与交易的黄金标准?

在"双碳"目标的宏大叙事下,每一吨二氧化碳的减排都意义非凡。然而,对绝大多数企业而言,碳管理仍停留在"宏观估算"和"年度报告"的层面。一个根本性难题在于:我们难以将最终的碳排放总量,…

作者头像 李华
网站建设 2026/4/10 17:33:53

程序员年薪百万的八大硬核技术方向:AI与大模型研发领跑高薪榜单

本文详细介绍了八大高薪技术就业方向,包括AI与大模型研发、芯片设计、大数据、网络安全、智能硬件、新能源、量化投资和生物医药。每个方向均分析核心岗位、薪资水平、能力要求和行业趋势,显示这些领域结合技术前沿与高薪潜力,为求职者提供年…

作者头像 李华