从单体到微服务:我在深圳裸辞半年后重新上岗的实战复盘
去年十月,我一怒之下把 Mac 合盖扔进了背包,正式裸辞。原因嘛——连续三个季度在“大厂 KPI 压力测试”中被拉爆,加上产品天天改需求、测试凌晨三点发 bug 截图、运维说“你这个服务内存泄漏快把节点干崩了”,我终于受不了了。Gap 半年期间,除了打游戏、爬山、偶尔读几本技术书(比如《微服务设计》和《Go语言实战》),我也开始反思:自己到底适不适合继续卷大厂?
今年三月回深圳找工作时,发现腾讯系公司对 Go + 微服务经验的需求暴涨。面试官一上来就问:“你们之前系统拆过微服务吗?怎么拆的?遇到雪崩没?”——我差点当场背诵《微服务架构设计模式》目录。
所以上周五晚上,刚入职新公司第三周,我就接到了一个“史诗级任务”:把老掉牙的 Java 单体系统,用 Go 重构为微服务架构,支持下个季度百万级 DAU 的营销活动。这不是送人头,这是送命。
今天这篇,就是我边踩坑边写下的开发心得,不讲理论,全是血泪实战。
老系统什么样?——一个“祖传”单体应用的日常崩溃
我们接手的旧系统,是典型的 Spring Boot 单体:用户、订单、库存、优惠券全塞在一个 Git 仓库里,数据库也只有一张 MySQL 实例,主从都懒得配。代码结构像意大利面条,UserService 里调 OrderDAO,OrderService 又反向依赖 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 图都能绕地球一圈。
我们的拆分原则很务实:
- 高频变更的模块优先拆(比如优惠券、营销活动);
- 资源消耗差异大的分开(比如订单写多读少,用户信息读多写少);
- 业务边界清晰的独立(比如支付、风控);
- 别为了微服务而微服务——有些低频功能,暂时留在单体里更稳。
最终我们拆出了四个核心服务:
| 服务名 | 功能 | 语言 | 数据库 |
|---|---|---|---|
| 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(太重),而是采用 “最终一致性 + 补偿机制”:
- order-svc 创建订单,状态为
pending; - 发送消息到 Kafka:
{"event": "order_created", "order_id": "123"}; - inventory-svc 消费消息,扣库存;
- coupon-svc 消费消息,锁定优惠券;
- 所有步骤成功,order-svc 收到回调,更新状态为
confirmed; - 任一环节失败,启动补偿:比如库存扣失败,则发送
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