从外卖订单激增到Rust初探:一个美团Java工程师的实战复盘

马浩宇
2025-12-24 20:11
阅读 1677

去年双11那天晚上,我差点把键盘砸了。

不是因为代码写崩了——虽然那会儿确实离崩溃不远——而是产品经理在凌晨两点突然冲进钉钉群:“运营那边说,用户下单时的优惠券加载慢了300毫秒,转化率掉了0.5%,老板急了,能不能今晚搞定?”

我当时盯着屏幕上的GC日志和线程堆栈,心里一万只草泥马奔腾。这哪是优化性能,这是要命啊。

但没办法,干的就是这行。我在美团做外卖后端开发已经四年,经历过无数个大促、秒杀、节假日高峰,早就练就了一身“在火焰中写代码”的本领。今天这篇总结,既是对过去几年高并发实战经验的一次梳理,也掺杂了最近为跳槽刷题、顺带折腾R rust 的一点新思考。希望对还在一线“搬砖”的兄弟们有点帮助。


高并发不是玄学,是血泪堆出来的运营指标

很多人以为高并发就是堆机器、上缓存、搞异步,但实际在业务场景里,每一毫秒的延迟都可能直接对应真金白银的损失。比如我们外卖系统里的“优惠券可用性校验”服务,在高峰期每秒要处理8万+请求。这个服务看似简单,无非是查一下用户是否满足某张券的使用条件,但一旦变慢,整个下单流程就会卡住。

而“运营”这个词,在我们这儿从来不是虚的。运营团队会盯着各种漏斗数据:从首页曝光 → 商品点击 → 加购 → 下单 → 支付成功。任何一个环节掉链子,第二天晨会上你就要站出来解释。所以技术方案必须对齐业务目标,不能闭门造车。

举个例子:早些年我们用纯MySQL查券状态,结果大促一来,DB直接被打爆,连接池耗尽。后来上了Redis缓存,但又遇到缓存击穿问题——某个热门商户的券被大量用户同时查询,缓存失效瞬间所有请求打到DB。我们尝试过布隆过滤器、互斥锁、本地缓存,最后落地的方案是多级缓存 + 异步预热 + 熔断降级三件套。

// 伪代码示意:多级缓存结构
public CouponInfo getCoupon(String userId, String couponId) {
    // L1: 本地缓存(Caffeine)
    CouponInfo local = localCache.getIfPresent(couponId);
    if (local != null) return local;

    // L2: Redis 缓存
    String redisKey = "coupon:" + couponId;
    String json = redis.get(redisKey);
    if (json != null) {
        CouponInfo remote = JSON.parse(json);
        localCache.put(couponId, remote); // 回填本地缓存
        return remote;
    }

    // L3: DB 查询(带熔断)
    if (circuitBreaker.isClosed()) {
        try {
            CouponInfo db = couponDao.selectById(couponId);
            if (db != null) {
                redis.setex(redisKey, 300, JSON.toJson(db)); // 缓存5分钟
                localCache.put(couponId, db);
                return db;
            }
        } catch (Exception e) {
            circuitBreaker.recordFailure();
            // 降级:返回空或兜底券
            return fallbackCoupon();
        }
    }

    return fallbackCoupon();
}

这套方案上线后,P99延迟从420ms降到68ms,运营同学终于不再半夜@我了。

但代价是什么?代码复杂度飙升,测试用例翻倍,运维监控要覆盖三层缓存命中率、熔断状态、本地缓存驱逐率……高并发系统从来不是技术炫技,而是在性能、稳定性、可维护性之间反复横跳


踩过的坑:你以为的“最佳实践”,可能是别人的事故现场

有一次我们为了提升吞吐量,把Tomcat线程池从200调到1000。结果CPU飙到95%,响应时间反而翻倍。排查半天才发现,应用里有个隐藏的同步块:

synchronized (someSharedObject) {
    // 调用第三方风控接口(平均耗时200ms)
}

线程越多,阻塞越严重。这事儿告诉我们:不要盲目相信“调大线程池能提升并发”这种教条。真正的瓶颈可能藏在一个你根本想不到的地方。

还有一次更惨。我们在压测环境一切正常,结果上线后订单创建成功率暴跌。最后发现是JVM参数没对齐:压测用的是G1 GC,生产环境还是默认的Parallel GC。G1在大堆内存下表现更好,但我们生产堆只有4G,反而触发频繁Full GC。

这些教训让我养成了几个习惯:

  • 所有配置变更必须走灰度发布
  • 关键路径必须有全链路追踪(我们用的是CAT)
  • 每次大促前跑一遍“故障演练”:模拟DB慢、Redis挂、网络抖动

顺便吐槽一句:测试同学总说“功能测完了”,但高并发场景下的异常流,他们根本测不到。最后还得靠我们自己写混沌工程脚本。


技术选型:别被新框架忽悠瘸了

最近团队里有人提议把核心服务迁到Go或者Rust。理由很诱人:“内存安全”、“零成本抽象”、“超高性能”。

作为一个正在刷LeetCode准备跳槽、顺便研究Rust的Java老狗,我其实挺心动的。Rust的所有权模型确实能从根本上避免很多并发Bug,比如我们曾经因为HashMap在多线程下put导致的死循环(别问,问就是ConcurrentModificationException)。

但现实很骨感。

我们系统有上千个微服务,大部分是Spring Boot + MyBatis + Dubbo。如果重写,光是人力成本就够喝一壶。而且Rust生态在Web后端还不够成熟:没有像Spring那样成熟的DI框架,ORM工具也不如MyBatis灵活,连JSON序列化都要手动derive。

所以我的建议是:核心链路稳字当头,边缘服务可以试水新技术

比如我们最近用Rust写了一个轻量级的“地址解析服务”——接收用户输入的模糊地址,调用高德API标准化。这个服务QPS不高,但对内存和启动速度敏感(需要部署在边缘节点)。用Rust写完,镜像体积只有Java版的1/10,冷启动快了5倍。

// 简化的Rust地址解析逻辑
#[derive(Deserialize)]
struct AddressRequest {
    input: String,
}

#[derive(Serialize)]
struct AddressResponse {
    standardized: String,
    lat: f64,
    lng: f64,
}

async fn parse_address(req: Json<AddressRequest>) -> Result<Json<AddressResponse>, Error> {
    let client = reqwest::Client::new();
    let resp = client
        .get("https://restapi.amap.com/v3/geocode/geo")
        .query(&[("address", &req.input), ("key", API_KEY)])
        .send()
        .await?
        .json::<GeoResponse>()
        .await?;

    Ok(Json(AddressResponse {
        standardized: resp.geocodes[0].formatted_address.clone(),
        lat: resp.geocodes[0].location.parse_lat()?,
        lng: resp.geocodes[0].location.parse_lng()?,
    }))
}

虽然写起来比Java啰嗦(尤其错误处理),但运行时确实稳如老狗。不过要说替代主链路?再等三年吧。


实战经验总结:给后来者的几条忠告

经过这些年在美团外卖的摸爬滚打,加上最近准备跳槽时重新审视自己的技术栈,我总结了几点心得:

维度 错误做法 正确姿势
性能优化 盲目加缓存、调线程数 先压测定位瓶颈,再针对性优化
容灾设计 只考虑正常流程 必须设计降级、熔断、兜底策略
技术选型 追新不追稳 新技术先在非核心场景验证
监控告警 只监控CPU/内存 关注业务指标(如订单创建成功率)
协作沟通 只和技术团队聊 主动和运营、产品对齐目标

另外,别忽视“软技能”。我见过太多技术牛人因为不会表达,方案被否;也见过普通开发者靠清晰的文档和演示赢得信任。现在每次提技术方案,我都会附上一张“影响面分析图”:哪些服务受影响、运营指标如何变化、回滚方案是什么。


写在最后:技术人的长期主义

说实话,写这篇文章的时候,我已经投了几家公司的简历。但无论去哪,我相信这些实战经验都不会白费。

高并发系统没有银弹,只有不断试错、复盘、沉淀。每一次线上事故,都是免费的实战课;每一个深夜加班,都在为未来的技术判断力充值。

最近在Rust社区看到一句话特别戳我:“Ownership is not just a language feature, it’s a mindset.”(所有权不仅是语言特性,更是一种思维方式。)

其实做技术也一样。无论是Java还是Rust,无论是优化300毫秒还是重构整个架构,真正重要的是对系统的ownership——你要为它负责到底

共勉。

P.S. 如果你在准备跳槽,推荐刷《Java并发编程实战》+ LeetCode Hot 100。别信那些“三个月速成架构师”的鬼话,基本功才是硬通货。
P.P.S. 产品经理又在群里@我了,说春节档要推新玩法……溜了溜了,改代码去。

评论 0

最热最新
暂无评论
马浩宇Lv.1
0
影响力
0
文章
0
粉丝