☰
基于Java的游戏陪玩与护航服务平台:订单状态机、抢单与实时推送实践
2026/10/22 8:28:54 网站建设 项目流程

前阵子朋友找上我,说他们的游戏陪玩社群还停留在微信群派单、人工记账的阶段,想升级成一个真正能扛住周末晚高峰的服务平台。项目前后做了两个月,后端从零到一,完全基于 Java 技术栈。核心业务就是游戏陪玩和护航:陪玩是按小时卖陪伴,护航是高段位玩家带低段位玩家组排上分、按局结算。这篇文章既是项目源码的思路复盘,也是给打算做同类服务平台的人留一份可以直接参考的工程笔记。

这个项目最适合两类人阅读:一类是想了解带交易、带实时通信、带抢单场景的服务平台怎么设计的人;另一类是自己准备动手写一个类似项目,不想在并发和资金安全上翻车的人。文章里我不会只贴代码,更多会讲我为什么这样设计、哪些地方踩过坑、压测数据是什么样。整篇文章围绕订单生命周期、匹配分配、实时推送、风控结算这几条主线展开,看完你基本能复现出一套可跑的骨架。

1. 先理清业务逻辑:陪玩、护航和普通游戏订单有什么不一样

很多第一次接触这类项目的人,上来就建表写接口,结果做到一半发现业务规则根本撑不住。原因很简单——陪玩和护航看起来都是"买游戏服务",实际是两条完全不同的业务链路,混在一起设计必乱。

1.1 两条订单链路:按小时卖陪伴与按局卖结果

先说陪玩。陪玩的本质是服务时长,用户下单买的是"有人陪我打游戏、聊天"这段时间。计价单位是小时或者局数,质量取决于陪玩师的互动态度、说话方式、游戏体验,不以胜负为核心指标。这类订单的特点是没有明确"成功"标准,只要时间到了就履约完成,纠纷集中在"陪玩师中途消失""态度不好"这些软性问题上。

护航就不一样了。护航是高段位玩家带队低段位玩家组排,目标明确,比如"保证赢X局""帮我上到某个段位"。计价方式是按局算或按段位阶梯打包,金额比普通陪玩大,服务时间可能横跨整个晚上。这类订单是结果导向,必须定义清晰的履约标准,比如"赢的局数达到多少""结束时段位是否提升",否则纠纷会吵到平台怀疑人生。

这两条链路的差异直接决定了设计:

  • 陪玩订单关注的是时长,状态机里要记录开始时间、结束时间、超时处理;
  • 护航订单关注的是结果,状态机里要有"完成局数""目标达成情况"这类业务字段;
  • 陪玩履行不依赖胜负,护航履行要绑定游戏对局数据,哪怕只是让用户上传结算截图做人工确认,也比完全没有强。

所以在表结构上,我用一张订单主表承载公共字段,再用service_type区分两类服务,护航相关的局数、目标段位、分区等字段单独放到扩展表里,避免把主表撑成一棵圣诞树。

1.2 订单状态机:让每一次状态跳转都有据可依

订单系统最忌讳的就是在业务代码里随手写order.setStatus(4)。状态一旦多了,你根本不知道从哪个状态能跳到哪个状态,线上出了问题也只能对着数据库发呆。

我落地了一套显式状态机,状态枚举如下:

public enum OrderStatus { CREATED(0, "已创建待支付"), PAID(1, "已支付待匹配"), MATCHING(2, "匹配中"), DELIVERING(3, "护航/陪玩进行中"), COMPLETED(4, "已完单"), CONFIRMED(5, "用户已确认"), CANCELED(6, "已取消"), REFUNDING(7, "退款中"), REFUNDED(8, "已退款"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } }

状态跳转规则我是这么定义的:

CREATED -> PAID由支付回调触发;PAID -> MATCHING是支付成功后自动进入匹配池;MATCHING -> DELIVERING表示有护航师/陪玩接单成功;DELIVERING -> COMPLETED是服务结束;COMPLETED -> CONFIRMED是用户确认;COMPLETED -> REFUNDING是用户发起售后;MATCHING -> CANCELED是匹配超时或用户主动取消。

我建议把可跳转关系写成一个Map<OrderStatus, Set<OrderStatus>>放在枚举旁边,每次状态变更前先做合法性校验,非法跳转直接抛异常。这么做最大的好处是,线上排查问题时你只需要对着一张固定的状态转移图说话,而不是靠猜。

1.3 状态切换的并发安全:为什么禁止在业务代码里直接改状态

有了状态机只是第一步,真正的坑在并发。我举个例子:一个热门护航师的订单同时被两个人抢,两个请求都查到订单状态是MATCHING,都认为可以接单,然后都执行了update status = DELIVERING——数据库最终只有一个请求更新成功,但业务代码里可能出现一个已经失去资格的护航师以为自己接单成功,后面一堆脏逻辑被触发。

所以状态更新绝对不能走"先查后改",要走条件更新,也就是乐观锁的思路。后面数据库设计那一节我会给出具体的 SQL,这里先说明结论:所有状态跳转必须带上期望状态条件,影响行数为 0 就代表已经被别人抢先,立刻回滚后续动作。

2. 技术选型的取舍:Java 全家桶怎么组合才不臃肿

技术选型这块我见过太多人一上来就搞 Spring Cloud 全家桶,服务拆七八个,最后连部署都费劲。这个项目我整了两个原则:能用单体解决的不拆微服务,能用成熟组件的不自己造轮子。

2.1 单体优先:不是所有平台都需要微服务

项目初期的业务规模,用一个 Spring Boot 单体应用完全能扛住。我跟朋友算过一笔账:注册用户几十万,日活峰值几千到一万,核心接口的 QPS 也就几百到一千的量级。这个量级下单体能顶住,而且运维成本极低,一台 4 核 8G 的服务器起步就够了。

微服务的好处是独立扩展、独立部署,但代价是分布式事务、服务治理、链路追踪这些复杂度成倍增加。对一个两三个后端开发的项目来说,这些复杂度完全没必要提前承担。我在代码里做的是模块化单体——同一个工程,但包结构按领域切得清清楚楚,将来真要拆服务,顺着模块边界拆就是了。

2.2 组件分工表:MySQL、Redis、RabbitMQ、WebSocket 各管哪一块

组件选型我列一张表,每个组件管什么、为什么选它,一目了然:

组件版本承担的职责补充说明
MySQL8.0订单、用户、钱包流水、评价等强一致数据事务的基石,资金类数据必须在这里落地
Redis6.x抢单锁、热数据缓存、在线状态、频率计数抢单场景的原子性靠 Lua 脚本实现
RabbitMQ3.x派单广播、延迟消息、异步通知用延迟队列处理超时未支付、超时未接单
WebSocketSpring 原生订单状态实时推送、在线聊天量级上来后可替换为 Netty 实现

这套组合没什么花哨的东西,但每一件都踩在业务刚需上。MySQL 管钱管状态,Redis 扛并发抢单,RabbitMQ 解耦异步任务,WebSocket 做实时同步。我特别想强调一点:别一上来就引一堆中间件,先把哪些是刚需想清楚。比如如果初期不想引入 RabbitMQ,完全可以用定时任务扫表来代替延迟消息,效果差点但逻辑更直白,后面再平滑迁移。

2.3 工程结构:Maven 多模块怎么切

模块划分直接影响后续维护体验。我采用的是 Maven 多模块结构,每个模块职责单一:

platform-parent ├── platform-common // 工具类、统一响应、状态枚举、常量 ├── platform-core // 用户、订单、结算、评价核心业务 ├── platform-chat // WebSocket、消息推送、聊天记录 ├── platform-job // 定时任务:超时扫描、对账、待结算 └── platform-web // Controller、过滤器、启动类

platform-common里放的是所有模块共享的东西,比如那个订单状态枚举就放这里;platform-core是业务逻辑最重的模块,订单状态流转、抢单、结算都在这里实现;platform-chat单独拆出来是因为 WebSocket 的会话管理跟普通 HTTP 接口的生命周期差别很大,混在一起容易出问题;platform-job兜底各种定时扫描,别小看它,线上很多脏数据都是靠定时任务修复的。

3. 数据库建模与订单主流程:把钱和状态管清楚

数据库设计直接决定这个平台能不能平稳跑起来。资金、订单这类核心数据,我坚持用 MySQL 而不是引入 MongoDB 这类文档数据库,因为事务和强一致在这个场景里没有妥协空间。

3.1 订单表设计:索引、版本号、金额字段一次说清

订单主表我这样设计,你可以直接抄:

CREATE TABLE `t_order` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `buyer_id` bigint(20) NOT NULL COMMENT '下单玩家', `seller_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '接单护航师/陪玩', `service_type` tinyint(4) NOT NULL COMMENT '1=陪玩 2=护航', `game_id` int(11) NOT NULL COMMENT '游戏编号', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '订单状态', `amount` decimal(10,2) NOT NULL COMMENT '订单金额', `protect_round` int(11) NOT NULL DEFAULT 0 COMMENT '护航局数', `matched_at` datetime DEFAULT NULL COMMENT '匹配成功时间', `started_at` datetime DEFAULT NULL COMMENT '服务开始时间', `finished_at` datetime DEFAULT NULL COMMENT '服务结束时间', `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_buyer_status` (`buyer_id`, `status`), KEY `idx_seller_status` (`seller_id`, `status`), KEY `idx_status_updated` (`status`, `updated_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='护航/陪玩订单表';

几个关键点我拆开说。

第一,order_no是业务订单号,必须唯一,不能用数据库自增 id 直接展示给用户,否则容易被人枚举订单量。生成规则我用的是"日期 + 随机数 + 业务后缀",比如20250115210000123401,下单时生成,唯一索引兜底防重。

第二,索引设计围绕两个高频查询:买家查自己的订单列表、护航师查自己的接单列表。idx_buyer_status和idx_seller_status就是为此服务的。另外idx_status_updated是为定时任务准备的——按状态和更新时间扫描超时订单,这个索引能把扫描开销降一个量级。

第三,金额字段一律用decimal(10,2),Java 侧用BigDecimal,绝对不用double或float存钱。钱这个东西,精度差一分钱都是事故,后面结算章节我还会展开。

3.2 接单写入:CAS 更新状态的核心 SQL

有了表结构,最重要的就是那条条件更新 SQL。我用 MyBatis 注解方式实现,核心就一句话:

@Update("UPDATE t_order SET status = #{targetStatus}, seller_id = #{sellerId}, " + "matched_at = NOW(), version = version + 1 " + "WHERE id = #{orderId} AND status = #{expectedStatus} AND version = #{version}") int compareAndSetStatus(@Param("orderId") Long orderId, @Param("expectedStatus") int expectedStatus, @Param("targetStatus") int targetStatus, @Param("sellerId") Long sellerId, @Param("version") int version);

调用方先查出订单拿到当前状态和 version,然后执行这条更新。数据库层面会先按id + status + version定位行,找到才更新,否则影响行数为 0。

这里的关键是:MySQL 的行锁会串行化同一行上的并发更新。两个抢单请求同时到达,第一个更新成功,第二个因为 status 已经不是MATCHING了,影响行数为 0,立刻返回"已被抢"。这种方案比用应用层分布式锁简单得多,性能也更好,因为锁粒度被压到单行数据上。

3.3 结算与退款:BigDecimal 和资金流水

资金设计是整个平台最需要敬畏的部分。我落地了三个原则:

  • 所有金额计算用BigDecimal,构造时用字符串构造器,禁止用new BigDecimal(0.1)这种浮点构造;
  • 每一笔资金变动必须落在t_wallet_ledger流水表里,一条流水对应一个原子操作,不允许在业务代码里直接改钱包余额;
  • 退款和结算都要走独立的状态路径,不能随手把订单状态改成已退款就算了。

流水表设计示例:

CREATE TABLE `t_wallet_ledger` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `order_no` varchar(32) NOT NULL, `change_type` tinyint(4) NOT NULL COMMENT '1=支付 2=平台抽成 3=结算 4=退款', `change_amount` decimal(10,2) NOT NULL, `balance_after` decimal(10,2) NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`, `created_at`), KEY `idx_order` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

每次变动都记录变动后的余额快照,这样对账时可以直接从流水重放余额,而不需要信任当前余额字段。上线这几个月,所有资金纠纷我们都是靠流水表翻出来的,一次都没吵到说不清的地步。

4. 抢单与派单:护航订单最吃性能的一环

护航平台的流量高峰集中在晚上和周末,热门护航师的需求远远大于供给。这时候抢单接口就是全系统的性能命门。我做的是"手动抢单 + 系统派单"双通道,玩家可以指定护航师直接下单,也可以发一个公开护航单让一批护航师抢。

4.1 手动抢单的 Redis Lua 方案:原子性靠脚本

手动抢单的场景是:一个乘客把护航单发布出来后,可能有几十个护航师同时在抢。这时候必须保证"一个单子只能被一个人抢到",而且不能出现两个人同时成功的竞态。

单纯用 Java 代码判断加 Redis 锁很容易出错——因为检查锁和设置锁是两个步骤,中间被其他线程插一脚就出问题。正确做法是用 Lua 脚本把判断和设置合并成一个原子操作:

-- KEYS[1] = 抢单锁key,形如 grab:order:{orderId} -- ARGV[1] = 护航师id -- ARGV[2] = 锁过期秒数 if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 end return 0

用 Spring Data Redis 调用这段脚本:

String lua = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 end return 0"; DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class); Long success = stringRedisTemplate.execute(script, Collections.singletonList("grab:order:" + orderId), companionId, "30");

脚本返回 1 代表这个护航师拿到了抢单资格,然后立即执行数据库 CAS 更新订单状态,更新成功再释放锁;如果数据库 CAS 失败(比如订单已经被系统派单流程抢走),则删除锁并提示用户。

这套方案的好处是:Redis 的SET NX EX自带原子性和过期时间,不用单独写解锁逻辑,也不会出现拿到了锁却忘记释放导致死锁的情况。压测下来,单机 Redis 每秒能处理上万次抢单请求,瓶颈完全转移到数据库那一条 CAS 更新上,而数据库层面一行行锁的竞争其实很小。

4.2 系统派单的匹配逻辑:段位、时段、评分加权

有些单子用户不想等,希望平台自动分配合适的护航师。这时候需要一套匹配权重,不能光看评分高就派,还得考虑护航师在不在线、最近接单响应速度、擅长游戏和段位是否匹配。

我给每个候选护航师计算一个综合分:

finalScore = 历史评分 * 0.5 + 近7天接单率 * 0.3 + 响应速度分 * 0.2

然后按finalScore从高到低取前 5 名,通过 RabbitMQ 向这 5 个人推送"新订单派给你了"的消息,谁先确认谁接单。这个设计照顾了两类护航师:头部护航师靠评分拿单,腰部护航师靠高响应速度也能分到单,生态不至于僵死。

这里有一个容易被忽略的细节:派单消息发出后,必须设置确认超时时间,比如 30 秒内未确认就顺延给下一位候选者。否则消息发出去了,5 个人都没看手机,订单就卡在"匹配中"。

4.3 无人接单的兜底:延迟队列和定时扫描

订单进入匹配池后,如果 5 分钟都没有护航师接单,用户体验就很差了。兜底逻辑我同时做了两层。

第一层用 RabbitMQ 延迟消息:下单进入匹配池时,发一条延迟 5 分钟的消息,消息到期触发检查,如果订单还在匹配中,就自动通知玩家"当前时段护航师较少,建议提高悬赏金额或稍后再试"。

第二层是定时扫描,作为延迟消息的补充:platform-job里每隔一分钟扫描一次t_order表,找出状态为MATCHING且updated_at超过 10 分钟的订单,执行兜底处理。定时扫描的好处是不依赖消息队列的可靠性——哪怕 RabbitMQ 挂了、消息丢了,扫描任务照样能把卡住的订单捞出来。延迟队列负责及时性,定时扫描负责兜底,两者互补,这也是我坚持两个都要的根本原因。

5. 实时状态同步:玩家端怎么看到"正在护航"

护航服务一旦开始,玩家最关心的就是"现在什么进度了"。这里实时性不是锦上添花,而是核心体验。我做的方案是 WebSocket 推送为主、HTTP 拉取兜底。

5.1 WebSocket 接入设计:握手鉴权与频道划分

我用的是 Spring 的原生 WebSocket 方案,端点路径设计为/ws/order/{orderId},握手时通过拦截器校验 token 并绑定用户身份:

public class AuthHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { String token = ((ServletServerHttpRequest) request).getServletRequest().getParameter("token"); Long userId = JwtUtil.parseUserId(token); if (userId == null) { return false; } attributes.put("userId", userId); return true; } }

频道划分的原则是按订单隔离——每个订单一个订阅频道,订单内的状态消息和聊天消息只推给关联的买家和护航师,避免跨单串消息的严重事故。

5.2 消息可靠性:先落库再推送 + 断线拉取

WebSocket 最大的问题是消息不可靠:用户手机断网、App 被系统杀掉、网络切换,消息就丢了。我遇到过最尴尬的情况是——订单状态已经变成"护航开始",玩家端却还停在"匹配中",玩家疯狂投诉。

我从一开始就设计了一条规则:任何业务状态变更,先把消息写入消息表,再走 WebSocket 推送。消息表是t_push_message,每次推送都会记录消息 id、接收人、消息类型和内容。客户端每次建立连接后,带上自己最近收到的一条消息 id,服务端把后续遗漏的消息一次性补推过去。

这条"先写库再推送,断线后按 id 补拉"的机制,把消息可靠性从"尽力而为"拉到了"基本不丢"。代价是多写一张表、多一次查询,但对订单这类关键业务完全值得。

5.3 心跳与在线状态:连接治理的细节

WebSocket 连接如果不管理,最后会变成一场内存泄漏事故。客户端侧我约定每 30 秒发送一次 ping,服务端用一个ConcurrentHashMap记录每个会话的最近心跳时间,定时任务每 2 分钟扫描一次,超过 90 秒没有心跳的会话直接关闭。

别小看这个细节。线上出现过一次事故:大量玩家开着 App 不关,服务端 session 越积越多,最终把 JVM 堆打满。加了这个心跳清理机制后再没有复现过。另外我建议给 Spring 的WebSocketHandler加ConcurrentWebSocketSessionDecorator,限制单个会话的发送缓冲和发送速率,防止某个慢客户端拖垮整个 WebSocket 线程。

6. 风控与信任:护航平台最容易翻车的两张牌

很多做这类平台的人把精力全花在功能和并发上,结果上线第一个星期就被刷单、诈骗、恶意退款搞崩了心态。风控不是可选项,是生死线。

6.1 刷单、恶意取消、护航师刷量怎么识别

我落地了几条低成本但有效的风控规则。

  • 下单频率限制:同一个用户 ID 或同一设备指纹,一小时内最多下 3 个订单,超过就触发人工审核;
  • 取消率监控:用户当天取消订单超过 2 次,次日下单进入风控队列,人工确认后才允许下单;
  • 自买自卖识别:护航师和买家如果来自同一 IP、同一设备,订单完成后的评价直接标记为高风险,不计入评分;
  • 护航师刷量检测:同一护航师与同一买家短时间内多次互相成交,且金额接近、时长很短,系统自动冻结结算,进入人工核查。

这些规则的实现都不复杂,就是 Redis 计数器加定时任务,但组合在一起能把大部分的简单刷单行为拦在门外。风控的思维是"宁可错杀不可放过"——对高风险行为先冻结、后核查,比事后追回要省力得多。

6.2 纠纷仲裁与状态回退:从申诉到退款的状态路径

护航订单的纠纷主要集中在"局数没达到目标"。比如用户买了"保证赢四局"的护航,最后只赢了两局,用户要求退款。

我的处理流程是:用户发起售后 → 订单状态从COMPLETED变为REFUNDING→ 进入仲裁列表 → 客服在系统里录入协商结果 → 如果是部分退款,走部分退款逻辑;如果全额退款,走REFUNDING -> REFUNDED状态路径。

这里有一个我特别想强调的点:仲裁结果不能直接改订单状态,必须通过仲裁单据驱动。我单独建了t_dispute表,记录用户的申诉理由、护航师的举证、客服的裁决意见。所有状态变更都从仲裁单触发,而不是客服在某条订单上直接点一个"改为退款"。这样处理的好处是每一个退款都有完整审计链路,财务对账时能查得清清楚楚。

6.3 资金安全:延迟结算与平台服务费

资金结算我采用了"延迟到账"策略:订单完成后,金额不会立刻结算给护航师,而是进入一个"待结算"观察期。观察期默认 24 小时,期间用户可发起售后;观察期结束后才触发结算,把"订单总金额 - 平台服务费"打入护航师的钱包余额。

平台服务费的比例在护航和陪玩之间不一样,护航按成交金额的 10%,陪玩按 15%。计算平台抽成时全部用BigDecimal,并且把费率作为一个系统配置项放在配置中心,不要写死在代码里——运营随时可能调整费率,写死一次就够你改一次版本。

延迟结算的核心目的就是给用户售后留出时间窗口。虽然会牺牲护航师的资金体验,但通过明确的规则公示("完单后次日到账"),用户的信任和护航师的接受度是可以平衡的。对平台来说,资金安全永远比几个小时的到账速度重要。

7. 压测报告与上线复盘:两个月的实测记录

写这么多设计,最后还是靠压测数据说话。上线前我做了一轮比较完整的压力测试,又经历了两个月线上运行,踩了不少坑,这块的收获可能比前面所有设计都值钱。

7.1 压测结果:瓶颈全在抢单和状态更新

压测场景我模拟的是周末晚高峰:200 个并发用户同时抢单,每个护航单持续 30 秒在线等待,混合接口包括下单、支付回调、抢单、状态查询、WebSocket 连接。

首轮压测数据很打脸:

接口平均响应时间QPS错误率
创建订单182ms8120.1%
抢单接口655ms2452.3%
订单状态查询95ms14500%
WebSocket 推送780ms1206.5%

结论非常清楚:抢单接口和 WebSocket 推送是两大瓶颈。抢单接口慢是因为 Redis 和 MySQL 之间串行交互,每抢一单要打一次 Redis Lua 再加一次数据库 CAS,网络来回多次;WebSocket 推送慢是因为同一时刻大量消息在 Tomcat 线程池里排队,部分慢客户端阻塞了推送线程。

7.2 三个线上问题的完整排查链路

我把上线后遇到的三个最典型的问题完整复盘一遍,这类问题你在自己项目里大概率也会遇到。

问题一:抢单成功后数据库状态却更新失败。

表现是用户看到"抢单成功",但订单状态还是匹配中。排查链路:先查日志,发现某个护航师在 Redis 拿到了锁,但数据库 CAS 更新影响行数为 0。为什么会锁定成功但更新失败?因为在他抢单之前,系统派单流程已经把一个PAID -> MATCHING的订单直接派给了另一个护航师,订单状态从MATCHING变成了DELIVERING。这个护航师手里的锁是基于旧状态的,等到他要更新时,订单已经不在匹配中。

修复方式:抢到 Redis 锁之后,业务侧重新查一次订单当前状态,确认真实状态还是MATCHING才允许 CAS 更新;如果状态已经不是匹配中,直接释放锁并返回"订单已被接走"。

问题二:每周五晚高峰,接口整体变慢。

表现是订单状态查询也开始超时。排查链路:看监控,MySQL 的活跃连接数打满,慢查询日志里全是SELECT * FROM t_order WHERE buyer_id = ? ORDER BY id DESC LIMIT 0,10这种语句。原因是有个骑墙查询没有走索引——我在查询列表时把service_type也作为过滤条件,导致走了idx_buyer_status后还要回表二次过滤,数据量大时慢查询爆发。

修复方式:拆成两个列表接口,一个查陪玩订单一个查护航订单,或者给(buyer_id, service_type, status)建复合索引。同时把列表查询默认只查最近 30 天,历史订单走专门的归档查询。

问题三:WebSocket 连接数正常,但消息延迟达到秒级。

排查链路:本以为推送线程有问题,结果发现是客户端大量使用代理网络,握手握手协议栈慢,大量会话停留在半开连接状态。心跳清理逻辑只处理了完全没有心跳的会话,没有处理"有心跳但接收极慢"的会话。加上ConcurrentWebSocketSessionDecorator限流后,慢会话被主动降级,整体推送延迟降回毫秒级。

7.3 优化清单与最终效果

压测和线上问题处理完之后,我做了几项关键优化:

  • 抢单流程合并 Redis 和 DB 为"锁 + 二次校验 + CAS"三步,减少无效做法,QPS 从 245 提升到 1500+;
  • WebSocket 推送全链路加ConcurrentWebSocketSessionDecorator和消息表兜底,错误率从 6.5% 降到 0.2%;
  • 列表查询全部走复合索引,慢查询数量从每天几百条降到个位数;
  • DB 连接池、Tomcat 线程池按高峰流量重新配置,避免资源耗尽。

优化后的压测数据是我个人觉得可以接受的:抢单接口 QPS 到 1500 左右,平均响应时间降到 120ms,订单状态查询 QPS 到 2500,错误率整体低于 0.1%。这个量级支撑现在的用户规模绰绰有余,还能留出不少余量给未来的增长。

最后分享一点我的真实感受:做这类服务平台,技术难度其实不在某个单点有多高级,而在于把订单状态、并发抢单、消息可靠性、资金安全这些事老老实实做好。两个月做下来,我最庆幸的是当初坚持把状态机和资金流水表设计得足够严谨——正是这些不性感的基础,撑住了后来所有花里胡哨的功能。如果你也打算做类似的平台,我建议你先花一半时间把订单和资金模型设计清楚,再动手写代码,这个顺序真的能帮你少熬很多个夜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询