从单体到微服务:我在深圳裸辞半年后重新上岗的实战复盘

♂邓文
2026-01-14 01:37
阅读 459

去年十月,我一怒之下把 Mac 合盖扔进了背包,正式裸辞。原因嘛——连续三个季度在“大厂 KPI 压力测试”中被拉爆,加上产品天天改需求、测试凌晨三点发 bug 截图、运维说“你这个服务内存泄漏快把节点干崩了”,我终于受不了了。Gap 半年期间,除了打游戏、爬山、偶尔读几本技术书(比如《微服务设计》和《Go语言实战》),我也开始反思:自己到底适不适合继续卷大厂?

今年三月回深圳找工作时,发现腾讯系公司对 Go + 微服务经验的需求暴涨。面试官一上来就问:“你们之前系统拆过微服务吗?怎么拆的?遇到雪崩没?”——我差点当场背诵《微服务架构设计模式》目录。

所以上周五晚上,刚入职新公司第三周,我就接到了一个“史诗级任务”:把老掉牙的 Java 单体系统,用 Go 重构为微服务架构,支持下个季度百万级 DAU 的营销活动。这不是送人头,这是送命。

今天这篇,就是我边踩坑边写下的开发心得,不讲理论,全是血泪实战。


老系统什么样?——一个“祖传”单体应用的日常崩溃

我们接手的旧系统,是典型的 Spring Boot 单体:用户、订单、库存、优惠券全塞在一个 Git 仓库里,数据库也只有一张 MySQL 实例,主从都懒得配。代码结构像意大利面条,UserService 里调 OrderDAOOrderService 又反向依赖 CouponUtil,循环依赖靠 @Lazy 强行续命。

最离谱的是,去年双11前夜,因为一个促销接口没做限流,直接把整个 DB 连接池耗尽,导致登录都失败。运维在群里吼:“谁写的 /api/v1/coupon/apply?这玩意儿 QPS 飙到 8000 还查三次表?!” ——是我写的。那一刻我真的想砸电脑。

产品经理还补刀:“这个功能下周就要上线新版本,加个分布式锁就行了吧?”

行你个头啊!单体架构下,加锁只是缓兵之计,根本问题是耦合度太高、扩展性为零、故障爆炸半径无限大


为什么选 Go?——性能、简洁、还有 Mac 上跑得飞快

新团队要求用 Go 重写核心服务。一开始我是抗拒的——毕竟写了五年 Java,Spring Cloud 玩得贼溜。但架不住现实打脸:

  • Go 编译成二进制,部署简单,Docker 镜像才 20MB;
  • Goroutine 处理高并发比线程池轻量太多;
  • 在我的 M1 Max 上,go run 秒启动,而 IDEA 启动 Spring Boot 要喝完一杯咖啡;
  • 最重要的是:新老板说“Java 太重,Go 更适合微服务” ——嗯,领导都这么说了,还能咋办?

于是,我翻出那本翻烂了的《Go Web 编程》,又啃了一遍《微服务架构:原理与设计》,开始动手拆。


拆分策略:不是所有模块都值得独立

很多人以为“微服务 = 把每个功能拆成一个服务”,这是大错特错。我见过有团队把“用户头像上传”都搞成独立服务,结果调用链路长到 APM 图都能绕地球一圈。

我们的拆分原则很务实:

  1. 高频变更的模块优先拆(比如优惠券、营销活动);
  2. 资源消耗差异大的分开(比如订单写多读少,用户信息读多写少);
  3. 业务边界清晰的独立(比如支付、风控);
  4. 别为了微服务而微服务——有些低频功能,暂时留在单体里更稳。

最终我们拆出了四个核心服务:

服务名 功能 语言 数据库
user-svc 用户注册/登录/资料 Go PostgreSQL
order-svc 下单/支付/状态流转 Go MySQL (独立实例)
coupon-svc 优惠券发放/核销 Go Redis + MySQL
inventory-svc 库存扣减/回滚 Go Redis (高性能场景)

注意:coupon 和 inventory 都重度依赖 Redis,因为要扛住秒杀场景。这里就涉及一个关键算法问题。


性能优化实战:秒杀场景下的库存扣减算法

产品经理又来了:“我们要搞 9.9 元秒杀 iPhone,10000 台,5 分钟抢完。”
我:“……你确定不是想让我们半夜被 PagerDuty 叫醒?”

在单体时代,我们用数据库行锁 + 事务控制库存,结果一到高峰就超卖。现在用微服务 + Redis,必须设计更可靠的扣减算法。

我们最终采用 “Redis Lua 脚本 + 异步持久化” 方案:

-- check_and_decr.lua
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
    return -1
end
redis.call('DECR', KEYS[1])
return stock - 1

Go 服务调用:

func DeductInventory(ctx context.Context, skuID string, qty int) error {
    script := redis.NewScript(`
        local stock = tonumber(redis.call('GET', KEYS[1]))
        if stock <= 0 then return -1 end
        redis.call('DECR', KEYS[1])
        return stock - 1
    `)
    res, err := script.Run(ctx, rdb, []string{"stock:" + skuID}).Result()
    if err != nil || res.(int64) == -1 {
        return errors.New("库存不足")
    }
    // 异步写入 MySQL 日志,用于对账
    go logToDB(skuID, qty)
    return nil
}

为什么用 Lua? 因为 Redis 执行 Lua 脚本是原子的,避免了 GET -> CHECK -> DECR 三步操作中的竞态条件。

上线前压测:单 Redis 实例轻松扛住 5w QPS,CPU 利用率不到 40%。对比旧系统,简直是降维打击。


服务通信:gRPC vs REST?我们选了 gRPC + Protobuf

一开始团队争论用 REST 还是 gRPC。REST 简单,但性能差、无类型安全;gRPC 学习曲线陡,但高效、强契约。

考虑到内部服务调用频繁(order-svc 要调 user-svc 验身份,调 inventory-svc 扣库存),我们果断上 gRPC。

定义 .proto 文件:

syntax = "proto3";
package svc;

service InventoryService {
  rpc DeductStock(DeductRequest) returns (DeductResponse);
}

message DeductRequest {
  string sku_id = 1;
  int32 quantity = 2;
}

message DeductResponse {
  bool success = 1;
  string message = 2;
}

生成 Go 代码后,服务间调用就像本地函数,还自带超时、重试、熔断(配合 Sentinel)。

不过有个坑:gRPC 默认用 HTTP/2,某些老版 Nginx 不支持。运维第一次部署时直接 502,查了半小时才发现要升级 Nginx 或改用 Envoy。


熔断与限流:别让一个服务拖垮全家

微服务最大的风险就是级联失败。比如 coupon-svc 挂了,order-svc 如果不处理,就会线程阻塞,最后整个集群雪崩。

我们用了 Sentinel Go 版本(阿里开源),配置如下:

# sentinel规则
- resource: "coupon_client.ApplyCoupon"
  limitApp: default
  grade: 1  # QPS 模式
  count: 1000  # 最大 1000 QPS
  strategy: 0
  controlBehavior: 0  # 快速失败

同时,在 order-svc 调用 coupon-svc 时加了 fallback:

resp, err := couponClient.Apply(ctx, req)
if err != nil {
    log.Warn("Coupon service unavailable, skip discount")
    // 跳过优惠,但允许下单
    resp = &ApplyResponse{Discount: 0}
}

宁可降级,不可宕机——这是我从上次双11事故中学到的血泪教训。


数据一致性:分布式事务怎么搞?

微服务拆开后,最头疼的就是跨服务数据一致性。比如下单成功,但库存没扣,或者优惠券用了却没记录。

我们没上 Seata(太重),而是采用 “最终一致性 + 补偿机制”

  1. order-svc 创建订单,状态为 pending
  2. 发送消息到 Kafka:{"event": "order_created", "order_id": "123"}
  3. inventory-svc 消费消息,扣库存;
  4. coupon-svc 消费消息,锁定优惠券;
  5. 所有步骤成功,order-svc 收到回调,更新状态为 confirmed
  6. 任一环节失败,启动补偿:比如库存扣失败,则发送 cancel_order 事件,回滚已操作。

为了防止消息丢失,我们做了:

  • Kafka 开启 ACK=all;
  • 消费者手动提交 offset;
  • 关键消息落库,定时对账 Job 扫描异常订单。

虽然不如强一致那么“干净”,但在高并发场景下,可用性 > 强一致性,业务也能接受几秒延迟。


运维与监控:没有可观测性,等于盲人摸象

新系统上线第一天,测试同学说“下单慢”。我一看 APM(用的 SkyWalking),发现 coupon-svc 的 P99 延迟高达 2s。

排查发现:Go 的 GC 在高峰期触发 STW(Stop-The-World),虽然只有几十毫秒,但叠加网络抖动就炸了。

解决方案:

  • 调整 GOGC=20(默认 100),减少堆增长速度;
  • 对象池复用(sync.Pool);
  • 关键路径避免分配新内存。

另外,我们强制要求:

  • 所有服务暴露 /metrics(Prometheus 格式);
  • 日志带 trace_id,方便链路追踪;
  • 告警分级:P0(服务不可用)自动 call 值班人,P1(延迟高)发企业微信。

裸辞半年后的感悟:技术不是炫技,是解决问题

回看这段从单体到微服务的迁移,最大的收获不是学会了 Go 或 gRPC,而是明白了:架构演进的核心驱动力是业务压力,不是技术潮流

如果你的系统日活就 1000,硬拆微服务,纯属自虐。但如果你要扛住百万流量、快速迭代、团队并行开发,微服务就是必选项。

至于那些书——《微服务设计》教我拆分原则,《Go语言实战》帮我避坑,但真正让我成长的,是凌晨三点修复线上 Bug 的经历,是和运维吵架后一起优化部署流程的协作,是产品经理说“这次真的不改需求了”(然后下午又改了)的无奈。

在深圳这座“卷都”,技术人的价值不在于会多少框架,而在于能不能让系统稳如老狗,让用户丝滑下单

所以,别怕重构,别怕裸辞,别怕从零开始。只要你还在敲代码,就还有机会写出更优雅的系统。

对了,我现在 MacBook 上跑着四个 Go 服务,终端里 docker-compose up 一敲,整套环境秒起。Windows?哦,还在角落吃灰,偶尔拿来测 IE 兼容性——开玩笑的,谁还用 IE 啊!

(完)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝