news 2026/4/16 9:00:09

京东二面:用户付了钱,订单却被取消?这道“并发题”挂了无数 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/3/24 8:06:58

鲸奇智慧:2025年低空经济发展趋势报告

本报告聚焦 2025 年中国低空经济发展状况,系统梳理了产业全貌、核心赛道、发展阶段及挑战建议,核心内容如下:一、产业概况与定位低空经济是以低空空域(1000 米及以下为主,部分可扩至 3000 米)为依托&#x…

作者头像 李华
网站建设 2026/4/16 11:03:34

自然资源部:地下空间开发利用典型案例 2026

这份文档聚焦全国地下空间开发利用创新实践,精选 22 个典型案例形成可复制推广经验,核心是推动地下空间从 “附属配套” 向 “主动赋能” 转变。案例分类与核心特征地下综合开发类(6 个):以深圳岗厦北枢纽、苏州东站等…

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

AngularJS 依赖注入

AngularJS 依赖注入 引言 AngularJS 是一款非常流行的前端JavaScript框架,它使得开发者能够更轻松地构建单页面应用(SPA)。依赖注入(Dependency Injection,简称DI)是AngularJS中的一个核心概念,它通过将依赖关系从组件中分离出来,提高了应用的模块化和可维护性。本文…

作者头像 李华
网站建设 2026/4/16 1:14:58

WSDL 语法详解

WSDL 语法详解 引言 WSDL(Web Services Description Language)是一种用于描述网络服务的XML格式语言。它详细地描述了服务的接口,包括服务可以执行的操作、操作的输入输出参数等。WSDL对于构建和部署网络服务至关重要,以下是WSDL语法的详细介绍。 WSDL基本结构 WSDL文档…

作者头像 李华
网站建设 2026/4/4 13:01:49

【开题答辩全过程】以 基于JAVAweb的影视创作论坛系统为例,包含答辩的问题和答案

个人简介 一名14年经验的资深毕设内行人,语言擅长Java、php、微信小程序、Python、Golang、安卓Android等 开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。 感谢大家…

作者头像 李华
网站建设 2026/4/6 7:47:04

有铜半孔不踩坑|5个实操疑问,解决设计量产所有难题

做有铜半孔PCB设计和量产,很多工程师都有这样的困扰——基材选错导致可靠性不足、孔径铜厚匹配不当引发故障、阻焊出问题影响焊接,还要兼顾成本与特殊场景适配,新手易慌、老手也易踩漏。依托猎板在有铜半孔工艺全场景量产的技术积累和实操经验…

作者头像 李华