这篇文章是我今天初步学习微服务有感而发,想谈谈我的理解,有时候我们做项目时线上还动不动因为某个模块的锅整站瘫痪。这种场面,你要是也经历过,那这篇内容就是给你写的。
文章比较长,我先把路线图放这儿,方便你跳着看:
单体架构到底坑在哪?微服务怎么解决
什么项目需要微服务(别瞎拆)
从单体拆到两个服务
拆完不会互相调用了怎么办
Nacos 是怎么让几百个服务不乱套的
拆完还有坑
我自己收集了几道高频面试题
一、你的项目有没有这些症状
不用急着往下读,先花十秒钟回答三个问题
- 你们改一个功能,是不是要整个系统重新打包、一起发布?发布窗口还得专门排期?
- 团队是不是已经大到"这行代码到底归谁管"都说不清了?
- 是不是经常出现某个热点功能把服务器 CPU 吃满,其它模块跟着一起卡死?
三条里你中了任意两条,说明你的项目已经进入了"单体的痛苦期"。至于什么是单体,咱们接着看。
二、单体架构
单体架构一句话概括:所有功能模块塞进同一个工程,部署的时候打包成一个包、跑在一个进程里,底下还共用一套数据库。
这么干的好处很实在——项目小的时候,上手快、部署简单、运维省心,一两个人就能搞定。早期的小项目几乎都是这么过来的。
坏处嘛,藏在后面。业务一旦长大,团队一旦变多,三个问题就藏不住了
1.协作成本高:几十个人改同一份代码,模块之间物理边界越来越模糊,改个公共类都可能引发"战争"。
2.发布效率低:任何模块变更都要全量发布,一处出错,整个发布失败,大家干瞪眼。
3.可用性差:模块之间互相影响,热点功能能把系统资源榨干,其它服务跟着遭殃。
说白了,单体的痛不是"不能用了",而是"人一多、量一大就喘不过气"。
三、把一个大家伙拆成一伙小团队
微服务的思路很朴素:既然一个工程扛不住,那就把功能模块拆出来,每个模块独立部署成一个服务。
拆完之后的组织方式有三个关键词
单一职责:一个服务只负责一块业务,核心数据不依赖别的模块。
团队自治:每个服务有自己的开发、测试、发布、运维人员,队伍规模控制在 10 人以内。
服务自治:服务独立部署、独立数据库,做好隔离,不拖累邻居。
拿一个常见的电商项目举例:商品、用户、购物车、订单、支付,这些模块完全可以拆成几个独立服务,分给不同的小团队各自维护、各自上线。
回到上一节那三个问题,逐个对照:
单体的痛 | 拆完微服务之后 |
几十人改同一份代码 | 每个服务代码量骤减,后台就一两个人维护 |
改一处要全量发布 | 哪个服务变了,单独打包部署那一个就行 |
热点功能拖垮全系统 | 服务独立部署、独立资源,隔离到位互不干扰 |
我自己画了一张图
四、不是所有项目都该上微服务
很多人一听到"大厂都在用微服务"就热血沸腾,马上就好奇心使然把自己的项目拆成微服务。拆完发现:服务是拆了,运维成本翻了好几倍,线上问题反而更多了。
拆之前还得先判断你的项目值不值得拆,而不是盲目跟风
团队规模:人就三五个,单体完全够用,别硬拆。
业务复杂度:模块之间天然独立(比如商品和支付),拆起来才划算;高度耦合的业务硬拆,只会增加沟通成本。
发布频率:业务迭代快、要频繁上线,独立部署的价值才体现得出来。
量级预期:真有高并发、大流量的预期,才需要独立的扩展能力。
能拆得动的才叫微服务,拆不动的那叫灾难。小项目老老实实用单体,等团队和业务长到单体现在那个痛法,再拆也不迟。
五、我们先把单体项目跑起来
道理讲完了,来点真格的。为了把"拆分"和"治理"讲透,咱们拿一个典型的电商单体项目练手——它有用户、商品、购物车、订单、支付五个模块,一开始全在一个工程里,我自己是把mysql,nacos,mq等这些部署docker里面的。
一般简单的电商单体项目都长这样
模块 | 干什么的 |
用户 | 登录、余额变更 |
商品 | 商品检索 |
购物车 | 增删改查购物车 |
订单 | 创建订单、查订单、改状态 |
支付 | 生成支付单、支付、查支付单 |
六、服务怎么拆
拆服务不是拍脑袋切一刀。目标层面就八个字:高内聚、低耦合。
高内聚:一个服务里的业务要相互关联、能自圆其说,职责尽量单一。
低耦合:服务之间依赖要少,实在要依赖,接口也要稳定。
方式层面分两种拆法
纵向拆分:按功能模块拆。电商项目顺着业务线,拆成、用户、商品、购物车、订单、支付,天然清晰。
横向拆分:把模块之间共用的能力抽出来做成通用服务。比如用户、订单、支付都要发消息,那就单独抽一个消息服务,谁用谁调。
工程组织上还有两种形态
独立工程:每个服务一个仓库。解耦最彻底,但仓库多,管理起来繁琐。
聚合工程:所有服务作为模块聚在一个大工程里。代码集中、管理方便,代价是耦合度高、编译时间长。
初学者建议从聚合工程入手,先专注把拆分逻辑跑通,别一上来就折腾多仓库。
那到底哪些因素会影响拆分效果?我画了张鱼骨图,把六大类关键因素一次性摆出来,建议收藏:
看图你会发现,拆得好不好,远不止"切几刀"那么简单——粒度、数据、调用、团队、运维、选型,这些都需要我们关注。这也正是后面几节要逐个解决的。
七、跨服务远程调用
我今天在拆的时候就遇到第一个坑。
我先把商品、购物车两个模块拆成独立服务。拆完一测,购物车列表接口返回的商品信息全是空的——为什么?因为查询购物车的时候,代码里本来要查商品详情,可商品相关的逻辑已经整个搬到商品服务那边去了,本地根本查不到。
这时候就得把"本地方法调用"改成"跨服务远程调用"。改造之前,先认识两个家伙:
服务提供者:被别的服务调用的那个,比如商品服务。
服务消费者:发起调用的一方,比如购物车服务。
要注意,这两个角色不是固定的,是相对的——购物车调商品的时候它是消费者,但它也可能被别的服务调用。
最简单的远程调用方式,是用 Spring 自带的 RestTemplate 发一个 HTTP 请求。做法三步:
第一步,在购物车服务里注册一个 RestTemplate 的 Bean:
@Configuration public class RemoteCallConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }第二步,在购物车服务的实现类里,用 RestTemplate 把商品 ID 发给商品服务的接口,拿回商品信息:
// 1. 取出购物车里所有商品id Set<Long> itemIds = vos.stream() .map(CartVO::getItemId) .collect(Collectors.toSet()); // 2. 远程调用商品服务,批量查询商品 ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( "http://localhost:8081/items?ids={ids}", // 商品服务的地址和接口 HttpMethod.GET, // GET 请求 null, // 无请求体 new ParameterizedTypeReference<List<ItemDTO>>() {}, // 返回类型 Map.of("ids", String.join(",", itemIds)) // 请求参数 ); if (!response.getStatusCode().is2xxSuccessful()) { return; } List<ItemDTO> items = response.getBody(); // 3. 把商品信息按id组装成map,回填到购物车VO里 Map<Long, ItemDTO> itemMap = items.stream() .collect(Collectors.toMap(ItemDTO::getId, Function.identity())); for (CartVO v : vos) { ItemDTO item = itemMap.get(v.getItemId()); if (item == null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); }第三步,重启购物车服务,再测一次,商品信息就能正常查到了。
但是调通了是好事,我当时细想一下,这段代码里藏着大问题:商品服务的 IP 和端口被硬编码在代码里了。这不就是"写死"吗?地址一换就要改代码重新发布,商品服务要是起了多个实例,也没法做负载均衡。这个烂摊子,下一小节我专门来收拾。
八、服务治理
上一节结尾的问题,一句话概括就是:服务多了,调用关系靠人记是记不住的。这时候就需要一个"中间人"——注册中心。
注册中心的工作模型有四个关键动作
服务注册:服务启动时,主动把自己的 IP、端口上报给注册中心,相当于"入职报到"。
服务发现:消费者启动时,从注册中心拉取它要调用的服务的地址列表,相当于"查通讯录"。
服务续约:服务定期给注册中心发心跳,报告"我还活着",不然会被当失联。
服务剔除:注册中心发现某实例长时间没心跳,就把它的地址从列表里清掉,防止请求打到死机器上。
目前国内用得最多的注册中心是阿里开源的Nacos(文档全、功能强,还能当配置中心用)。Eureka 是 Netflix 家的老前辈,现在新项目基本都不碰了。
搭建 Nacos 也就三步
- 建一个库,用来存 Nacos 自己的元数据。
- 改一下环境配置文件里的 MySQL 地址,把整个目录拷到服务器上。
- 用 Docker 起容器(端口 8848/9848/9849,加 --restart=always 保证意外重启能自己拉起来)。这是我自己用的docker命令
docker run -d \ --name nacos \ --env-file /root/nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --network app-net \ --restart=always \ nacos/nacos-server:v2.1.0-slim
起来之后访问 http://你的服务器IP:8848/nacos/,登录账号密码默认都是 nacos
把服务接进来,每个服务要做两件事
先在 pom.xml 里加两个依赖,一个管注册发现,一个管负载均衡:
<!-- nacos 服务注册发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 负载均衡 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>再在 application.yml 里告诉它 Nacos 在哪
spring: cloud: nacos: server-addr: 你的服务器IP:8848 # nacos 地址重启服务,打开 Nacos 控制台,看到两个服务都注册上来了,这一步就齐活了。
服务发现怎么落地?之前购物车调商品,用的是 http://localhost:8081/... 这种写死的地址。现在改成通过服务名调用,做法很简单:给 RestTemplate 的 Bean 加一个 @LoadBalanced 注解,然后把请求地址里的 IP 换成服务名:
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } // 服务名 item-service 会自动被解析成真实地址 restTemplate.exchange( "http://item-service/items?ids={ids}", // 换成服务名,不再写IP HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() {}, Map.of("ids", String.join(",", itemIds)) );负载均衡顺手就来了。给商品服务再起一个实例(换个端口),启动后去 Nacos 控制台能看到两个实例都在线。再访问购物车接口,观察两个实例的访问日志——你会发现请求被轮询着分发,这个实例一轮、那个实例一轮,雨露均沾。这就是默认的轮询策略。
到这里,"服务怎么找到服务"这个核心问题就闭环了。下面这张思维导图把微服务技术栈的全貌画了出来,如果我画的有什么缺失的话,请各位大佬们及时提醒我一下
顺便说一句:注册中心只是治理的第一步。上面的思维导图里还有网关、熔断、配置中心、链路追踪——每一样都是专门解决一个痛点的,后面可以沿着这张图逐个击破。
九、拆完还有一些坑
实战跑通了,你以为就完事了?天真。下面五个坑,几乎每个拆微服务的团队都会至少踩一个:
坑一:为拆而拆,服务数量爆炸"每个功能都拆一个服务"听着很酷,结果几十个服务上线,运维累到吐血,调用链绕成蜘蛛网。
坑二:数据库还在共享表面上是微服务,底层大家还在连同一张表。服务 A 改了表结构,服务 B 直接查崩。铁律只有一条:每个服务拥有自己的数据库,跨服务的数据只能通过接口或消息拿。
坑三:分布式事务没想清楚下单要同时扣库存、建订单、清购物车,原来一个本地事务搞定,拆完变成三个服务的操作,本地事务管不了了。资金类的强一致场景,拆之前必须想清楚方案(Saga、本地消息表、消息队列+重试)
坑四:没有监控就敢上线服务一多,一个请求要穿三四个服务,出问题根本不知道卡在哪个环节。全链路追踪、日志采集、告警这三件套,拆完第一天就该铺上,别等线上事故来提醒你。
坑五:链路越长,性能越差一次请求多跳一次网络调用,延迟就多一分。拆得太碎,光服务间通信就能把性能拖垮。这也是为什么说"能拆得动的才叫微服务"。
十、我收集到几个和这个知识点相关的面试题
大家可以看一下或者有别的更好的也欢迎交流
单体架构和微服务架构的区别是什么?
微服务一定比单体好吗?什么时候该选单体,什么时候该拆?
注册中心的"注册、发现、续约、剔除"分别解决什么问题?
服务提供者和消费者是固定的角色吗?为什么?
负载均衡有哪些常见策略?@LoadBalanced 注解到底做了什么?
拆完微服务,跨服务的数据一致性怎么保证?
每道题都试着用"先答概念、再说场景、最后说取舍"的结构回答一遍,面试基本就稳了。
结尾:聊聊你踩过的坑
写这篇的初衷很简单:网上讲微服务的文章很多,但要么太抽象,要么一上来就是大厂架构图,新手根本接不住。所以我把整个"拆分 + 治理"的过程掰开揉碎,用一套能跑通的例子串起来。
结尾留个互动:你所在的项目现在是什么架构?拆微服务的过程中踩过最痛的坑是什么?欢迎在评论区聊聊,我每条都会看。
如果这篇对你有帮助,点个赞、收藏一下,后面我会沿着思维导图继续写网关、熔断、配置中心、链路追踪这几个主题——关注不迷路,咱们下篇见。
文中涉及的 IP、端口、账号密码均为本地开发示例,请按你自己的实际环境替换。实战类文章建议边读边敲,命令和环境差异以你本机的报错为准。