从单体到微服务:一个考研失败应届生的血泪实战
去年三月,我坐在自习室里,盯着屏幕上查分页面加载的转圈图标,手心全是汗。结果不出所料——数学崩了,总分连国家线都没过。那一刻,我感觉自己像被整个世界抛弃了。但生活还得继续,投简历、刷LeetCode、背八股文,成了接下来几个月的日常。
现在我在北京一家中型电商公司做后端开发,每天通勤一小时(感谢帝都地铁的“人肉压缩”功能),用着Vim敲代码(别问,问就是信仰),偶尔被产品经理凌晨三点的消息惊醒。上周五晚上,我又一次加班到十一点,就因为要把老系统从单体架构迁移到微服务——这事儿说来话长。
老系统快撑不住了
我们公司的核心业务系统最初是个典型的Spring Boot单体应用,三年前上线时只有几个模块:用户、商品、订单。那时候代码量不大,部署简单,一个JAR包丢到服务器上,java -jar 就完事了。但随着业务爆炸式增长,这个“巨石”越来越臃肿。
- 编译时间:本地
./mvnw clean package要等5分钟 - 启动时间:开发环境启动要2分半
- 团队协作:10个后端挤在一个Git仓库,PR冲突天天见
- 故障影响:上个月商品模块OOM,直接拖垮整个系统,双11预热期间差点被老板当场开除
最离谱的是,有一次测试同学提了个Bug:“用户注册后积分没加”。我定位半天发现是订单服务里有个积分回调逻辑写错了——对,你没看错,注册和积分居然在同一个服务里!这代码耦合度,简直比我和我妈的关系还紧密。
领导说:拆!
某次周会上,CTO拍桌子:“必须微服务化!下个季度OKR就看这个!” 我心里一咯噔——又要背锅了。但转念一想,这不正好能往简历上添点“高并发”、“分布式架构”的关键词吗?毕竟投了37份简历石沉大海的经历告诉我:没微服务经验,连面试机会都难拿。
第一步:怎么拆?
很多人以为微服务就是把代码按功能切成几块,其实远不止如此。我们参考了领域驱动设计(DDD)的思想,先画了业务上下文边界:
| 原单体模块 | 拆分后服务 | 职责 |
|---|---|---|
| user, profile, address | user-service | 用户注册、登录、个人信息 |
| product, category, sku | product-service | 商品管理、库存查询 |
| order, cart, payment | order-service | 下单、购物车、支付回调 |
| point, coupon, activity | promotion-service | 积分、优惠券、营销活动 |
注意:不要过度拆分!我见过有人把“用户头像上传”单独做成一个服务,结果维护成本爆炸。我们的原则是:业务内聚,技术解耦。
第二步:服务通信选型
拆完怎么让服务之间说话?常见方案有:
- RESTful HTTP:简单直接,但性能一般
- gRPC:高性能,但调试麻烦
- 消息队列:异步解耦,适合最终一致性
考虑到团队技术栈和运维能力,我们选择了 Spring Cloud + REST 的组合。虽然性能不是最优,但胜在熟悉、监控工具多、新人上手快。而且——说实话,老板根本不在乎那几毫秒的延迟,他只关心能不能按时上线。
关键依赖配置(pom.xml):
<!-- 服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 熔断降级 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
服务调用示例(user-service 调用 order-service):
// 使用 FeignClient 声明式调用
@FeignClient(name = "order-service", fallback = OrderServiceFallback.class)
public interface OrderServiceClient {
@GetMapping("/orders/user/{userId}")
List<OrderDTO> getOrdersByUserId(@PathVariable Long userId);
}
// 熔断降级实现
@Component
public class OrderServiceFallback implements OrderServiceClient {
@Override
public List<OrderDTO> getOrdersByUserId(Long userId) {
// 返回空列表或缓存数据,避免级联失败
log.warn("Order service is down, returning empty list for user: {}", userId);
return Collections.emptyList();
}
}
当时第一次测试熔断时,我把order-service停掉,user-service居然没挂!那一刻我激动得差点把键盘砸了——终于不用再背全站故障的锅了!
数据库怎么拆?这才是真痛点
服务拆了,数据库呢?很多人直接“一个服务一个库”,听起来很美,但现实很骨感。
我们遇到的第一个坑:分布式事务。
比如用户下单时,需要同时操作 order 表和 point 表(扣减库存+增加积分)。以前在单体里,一个 @Transactional 就搞定。现在跨服务了,咋办?
方案对比:
| 方案 | 优点 | 缺点 | 我们的选择 |
|---|---|---|---|
| 2PC (XA) | 强一致性 | 性能差,锁表时间长 | ❌ |
| TCC | 灵活可控 | 开发复杂,需补偿逻辑 | ⚠️ 太重 |
| 本地消息表 | 实现简单 | 侵入业务代码 | ✅ |
| RocketMQ事务消息 | 解耦好 | 依赖特定MQ | ❌ |
最后我们采用了 本地消息表 + 定时补偿 的方式。核心思路:
- 下单时,在 order-service 的本地事务中同时插入订单记录和消息记录(状态=待发送)
- 后台任务扫描未发送消息,调用 promotion-service 扣积分
- 如果调用成功,更新消息状态为已发送;失败则重试
-- order_db.message_outbox
CREATE TABLE message_outbox (
id BIGINT PRIMARY KEY,
business_type VARCHAR(50), -- 如 'ORDER_CREATED'
payload JSON,
status TINYINT DEFAULT 0, -- 0: pending, 1: sent
create_time DATETIME,
update_time DATETIME
);
虽然不是100%实时,但保证了最终一致性,而且对业务代码侵入小。产品经理居然也没挑刺——可能他根本没看懂?
接口设计:别让前端骂你
微服务拆分后,前端要调N个接口才能渲染一个页面?那不得被前端同事追着打?
我们的解决方案:BFF(Backend For Frontend)层。
- Web端用一个
web-bff服务聚合数据 - App端用
mobile-bff提供精简字段 - BFF内部调用各个微服务,组装成前端需要的格式
比如用户个人中心页:
// web-bff 中的 controller
@GetMapping("/profile")
public UserProfileVO getProfile(@RequestParam Long userId) {
UserDTO user = userServiceClient.getUser(userId);
List<OrderDTO> orders = orderServiceClient.getRecentOrders(userId);
PointDTO points = promotionServiceClient.getPoints(userId);
return UserProfileVO.builder()
.nickname(user.getNickname())
.avatarUrl(user.getAvatar())
.recentOrders(orders)
.totalPoints(points.getAmount())
.build();
}
这样前端只需要一次请求,体验丝滑。而且BFF可以根据渠道定制字段——再也不用听前端抱怨“为什么App返回了50个字段我只用3个”了。
运维之痛:日志、监控、链路追踪
微服务最大的噩梦不是开发,是排查问题。以前单体时代,一个 grep "ERROR" app.log 就能找到Bug。现在10个服务,每个都有自己的日志,怎么关联?
我们的三件套:
- ELK 日志收集:Filebeat 收集各服务日志 → Logstash 过滤 → Elasticsearch 存储 → Kibana 查询
- Prometheus + Grafana:监控每个服务的QPS、延迟、JVM内存
- SkyWalking 链路追踪:通过 traceId 串联跨服务调用
关键配置(logback-spring.xml):
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5000</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<pattern>
<pattern>
{
"service": "user-service",
"traceId": "%X{traceId:-}",
"spanId": "%X{spanId:-}",
"level": "%level",
"message": "%msg"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
上周线上出现慢查询,我打开SkyWalking,一眼就看到是 promotion-service 调用 order-service 超时,再点进去发现是数据库索引缺失——10分钟定位问题,要是放以前得熬通宵。
给应届生的真心话
作为考研失败后仓促就业的应届生,我深知简历上“参与微服务改造”这几个字有多重要。但别为了写简历而盲目上微服务!我们团队就吃过亏:早期强行拆分,结果服务太多,CI/CD流水线跑一次要20分钟,上线效率反而更低。
微服务不是银弹,而是权衡。如果你的系统QPS不到100,团队不到5人,老老实实用单体吧!等哪天你的 application.properties 文件超过500行,Git提交记录里全是“修复xx模块影响yy模块”的注释时,再考虑拆也不迟。
最后说点扎心的:我现在每天写YAML配置的时间比写Java还多。但每次打开简历,看到“主导微服务架构迁移,支撑日均百万级请求”这句话,就觉得那些通宵和报错堆栈都值了。
毕竟在北京,房租可不会因为你考研失败就打折啊。
附:避坑清单(血泪总结)
- ❌ 不要共享数据库表!每个服务必须有自己的DB
- ❌ 不要用同步调用做非核心流程(比如发短信),改用MQ
- ✅ 所有服务必须有健康检查接口
/actuator/health - ✅ 接口版本控制从第一天就要做(
/v1/users) - ✅ 配置中心(Nacos/Apollo)早早上,别硬编码IP
- 💡 小技巧:用
docker-compose本地模拟多服务环境,比改hosts香多了
共勉。

评论 0