互联网应用形态的变化,是一条清晰而复杂的技术演进线索。早期网站通常只是一组静态页面和简单表单,像开放的公共广场,主要承担信息展示;今天的互联网服务则往往是用户、内容、交易、推荐、履约等多个子系统协同工作的平台化体系。这个演进过程中,单体架构、前后端分离、微服务、容器化、服务治理等技术方案相继出现,它们不是为了炫技,而是为了解决规模、协作和稳定性的实际问题。
下面从一个一线后端开发的视角展开。我会用一个最小可运行的文章服务作为起点,逐步拆解单体应用如何演进为带 API 网关和注册中心的微服务系统,并给出关键代码、配置、启动顺序和排错路径,也解释每一步背后的取舍。这个主题适合正在学习 Spring Boot、微服务,或者准备在旧系统中做服务拆分的开发者。读完可以按同样的思路在自己的项目中跑通从单体到平台化服务的最小闭环。
1. 理解互联网应用演进的三个关键阶段
要理解“为什么会走到平台化架构”,不能只盯着微服务的概念。互联网应用从公共网页到平台化系统,经历过三个特征明显的阶段,每个阶段都留下了现在仍在使用的技术选择。
1.1 Web 1.0:公共网页时代的信息发布模型
早期的互联网产品更像是“电子公告板”。网站由静态 HTML 页面组成,内容由站长或编辑手工维护,用户能做的主要是浏览和提交简单表单。这里的技术核心是页面文件本身,部署方式也简单:把文件上传到服务器目录即可。
这个阶段最大的问题是无法形成真正的系统。
- 页面之间没有统一的用户体系,登录态极难管理。
- 内容更新依赖人工,数据无法结构化沉淀。
- 用户行为无法追踪,做不了个性化推荐。
- 一旦流量上升,静态文件和数据库脚本混在一起,维护成本会快速膨胀。
所以公共网页很快被动态应用取代。但它的“公共”属性仍然有价值:信息开放、入口平等、返回内容直接。今天谈到内容型产品时,这种“公共性”依然被看作产品体验的一部分,只是底层技术已经完全变了。
1.2 Web 2.0 与动态应用:用户不再是旁观者
动态应用的核心变化是引入了后端编程语言、数据库和前端交互。用户注册、登录、发布文章、发表评论成为标配,网站从“内容展示载体”变成“业务系统”。
这个阶段最常见的架构就是单体应用:
浏览器 / 客户端 ↓ Controller(接收请求 + 参数校验) ↓ Service(业务逻辑 + 事务) ↓ Repository(数据访问) ↓ 数据库单体应用的好处非常多:事务可以覆盖整个业务链路,调试方便,部署只需要一个包,团队早期不需要复杂的服务治理。它的坏处则要到业务复杂到一定程度才明显:构建时间变长、模块边界容易模糊、多人提交代码频繁冲突、任何一处内存泄漏都可能拖垮整个进程。
这里需要解释清楚一个观点:单体不是错的,它只是有适用边界。很多团队在业务不够复杂时强行微服务,结果引入了分布式事务、链路追踪、服务发现等一系列难题,反而拖慢了迭代速度。
1.3 平台化阶段:多端、多服务、多团队并行
当用户规模、业务线数量和团队人数同步增长后,单体应用开始出现明显的瓶颈。前端已经从单一网页扩展为 App、H5、小程序、开放 API 等多种入口;后端则开始按照用户、内容、订单、支付等业务域拆分。
平台化阶段的技术特征通常包括:
- 前端与后端通过 API 协作,前后端独立发布。
- 后端拆分出多个微服务,每个服务有独立数据库或至少独立数据域。
- 引入注册中心解决服务地址动态变化的问题。
- 引入 API 网关统一处理路由、鉴权、限流和跨域。
- 引入消息队列处理跨服务异步消息。
- 引入配置中心、日志平台和链路追踪来提高运维效率。
表格可以更清楚地对比三个阶段:
| 阶段 | 典型产品形态 | 核心交互 | 主要技术栈 | 最大挑战 |
|---|---|---|---|---|
| 公共网页 | 门户、个人主页 | 浏览、表单提交 | HTML、CSS、PHP/ASP | 内容更新、访问量 |
| 动态应用 | 博客、电商、社区 | 注册、登录、发布 | Spring Boot、MySQL、前端框架 | 安全性、数据一致性 |
| 平台化服务 | 开放平台、多端应用 | 多端访问、开放 API | 微服务、网关、MQ、容器 | 协作效率、稳定性 |
理解这个演进脉络之后,再看后续章节的代码和配置,就不会觉得是一堆组件堆砌,而是一个系统在规模和协作压力下的自然选择。
2. 起点:用单体应用搭建最小可运行的文章服务
这一节会搭建一个全流程可验证的 Spring Boot 单体文章服务。它虽然简单,但包含了实体、数据访问、接口、配置、启动和验证,是后面做服务拆分的基础。
2.1 为什么先写单体
在讲解微服务之前先把单体跑通,不是因为微服务不重要,而是因为微服务的难点不在组件数量,而在于把单体里的业务边界看清楚。
一个单体项目里有几个模块:文章发布、作者信息、评论列表、数据统计。如果一开始连这些边界都没有定义清楚,直接拆服务只会把单体里的耦合变成服务间的网络调用。
单体阶段的另一个好处是便于做接口验证。后面引入网关、注册中心、Feign 之后,出现问题时可以快速回到“调用单个服务接口是否正常”这一步来隔离故障。
注意:学习微服务时,建议始终保留一套可运行的单体基线代码。遇到服务间调用异常时,用它确认数据库、业务代码和环境本身没有问题。
2.2 项目结构和 Maven 依赖
以 Spring Boot 2.7.x 为例。如果使用 Spring Boot 3.x,要把 JDK 调整为 17 或更高,并注意javax包名到jakarta包名的变化。
项目结构可以先保持最简单的三层:
article-service/ ├── pom.xml └── src/main/ ├── java/com/example/article/ │ ├── ArticleApplication.java │ ├── Article.java │ ├── ArticleRepository.java │ └── ArticleController.java └── resources/ └── application.ymlpom.xml核心依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>这里把 H2 作为内存数据库使用,目的只有一个:让入门案例不需要额外安装 MySQL,也不需要在电脑上预留数据库端口。
2.3 实体、仓库和控制器
创建一个文章实体:
package com.example.article; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "article") public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @Column(length = 2000) private String content; private Long authorId; private LocalDateTime createdAt; public Article() { } public Article(String title, String content, Long authorId) { this.title = title; this.content = content; this.authorId = authorId; this.createdAt = LocalDateTime.now(); } public Long getId() { return id; } public String getTitle() { return title; } public String getContent() { return content; } public Long getAuthorId() { return authorId; } public LocalDateTime getCreatedAt() { return createdAt; } }仓库接口:
package com.example.article; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository<Article, Long> { }控制器:
package com.example.article; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/articles") public class ArticleController { private final ArticleRepository articleRepository; public ArticleController(ArticleRepository articleRepository) { this.articleRepository = articleRepository; } @GetMapping public List<Article> list() { return articleRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<Article> detail(@PathVariable Long id) { return articleRepository.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public Article create(@RequestBody Article article) { articleRepository.save(article); return article; } }注意几个工程细节:
- 直接返回实体只适合快速演示。现实项目中建议用 DTO 对输出字段做裁剪,避免把内部字段直接暴露给前端。
createdAt在构造函数里初始化,避免接口调用方随意传时间。- 控制器只做参数接收和路由,真正的业务逻辑后续要下沉到 Service 层。
2.4 配置和启动验证
application.yml:
server: port: 8081 spring: datasource: url: jdbc:h2:mem:article_db driver-class-name: org.h2.Driver username: sa password: "" jpa: hibernate: ddl-auto: update show-sql: true h2: console: enabled: true启动服务:
mvn spring-boot:run确认启动日志里出现Tomcat started on port(s): 8081后,用命令验证接口:
curl http://localhost:8081/api/articles预期输出是一个空数组:
[]创建一个文章:
curl -X POST http://localhost:8081/api/articles \ -H "Content-Type: application/json" \ -d '{"title":"单体应用","content":"单体不是贬义词","authorId":1}'再次查询:
curl http://localhost:8081/api/articles这时能看到一条文章记录。这个过程验证了实体映射、仓库接口、控制器和数据库配置全部正常。
3. 拆分:从单体到前后端分离和微服务的改造路径
单体能跑通只是第一步。当出现多个团队、多条产品线、多种客户端之后,下一步通常是引入 API 网关、服务注册中心和服务间调用框架。
3.1 为什么平台化系统需要 API 网关
单体时代,前端可以直接访问各模块的地址。拆分服务之后,每个服务有独立的 IP 和端口,如果让前端分别维护几十个地址,是灾难。
API 网关的价值在于:
- 统一入口:客户端只访问一个域名。
- 路由转发:根据路径把请求转发给不同服务。
- 公共逻辑前置:鉴权、限流、跨域、日志可以放在网关层统一处理。
- 流量控制:在网关层对突发流量做拦截,避免后端被打满。
Spring Cloud Gateway 的最小配置:
spring: cloud: gateway: routes: - id: article-route uri: lb://article-service predicates: - Path=/api/articles/** - id: user-route uri: lb://user-service predicates: - Path=/api/users/**lb://表示通过负载均衡从注册中心获取服务实例地址。这里的article-service是服务在注册中心里的应用名。
3.2 注册中心解决什么问题
服务拆分后,实例数量会动态变化。上线、扩容、故障转移都可能导致 IP 变化,服务消费方不能继续使用固定 IP。注册中心的作用就是让服务启动时登记自己的地址,消费方通过服务名发现可用实例。
以 Nacos 为例,在服务提供方加入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>配置:
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848启动 Nacos 后,在控制台的“服务管理”页面能看到已经注册的服务名和实例列表。没有注册中心,网关的lb://路由就无法解析到具体地址。
3.3 服务间调用:用 OpenFeign 实现用户信息查询
微服务之间经常需要互相调用。以文章服务为例,列表页需要显示作者昵称,而作者信息在用户服务里。
在接入 Feign 之前,文章服务保存的是authorId,这只解决了数据关联,没有解决展示问题。服务拆分后,不能直接 join 数据库,必须通过 API 获取数据。
在文章服务中加入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>定义 Feign 客户端接口:
package com.example.article.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user-service") public interface UserClient { @GetMapping("/api/users/{id}") UserInfo getUser(@PathVariable("id") Long id); }其中UserInfo是用户服务返回的 DTO。在启动类上开启 Feign:
@SpringBootApplication @EnableFeignClients public class ArticleApplication { public static void main(String[] args) { SpringApplication.run(ArticleApplication.class, args); } }调用时要注意网络故障和超时。服务间调用不再像单体方法调用那样可以依赖数据库事务,必须考虑“对方服务不可用”的情况。可以在调用处加降级逻辑或超时配置:
feign: client: config: default: connect-timeout: 2000 read-timeout: 30003.4 改造后的目录组织
一个常见的微服务工程目录可以是:
platform/ ├── gateway/ ├── user-service/ ├── article-service/ ├── comment-service/ └── common/common模块不写业务代码,只放公共的返回结构、异常定义和工具类。这样既减少重复代码,又避免不同服务之间互相依赖业务包。
拆分后的团队协作方式也会变化:每个服务有独立的仓库或至少独立的版本管理,接口变更要提前沟通。这里依然要记住一个原则:服务拆分不是目的,让团队能独立发布和扩容才是目的。
4. 平台化之后的硬问题:数据一致性、幂等、限流与认证
拆分完成只是开始。平台化带来的真正难点,是单体时代被本地事务、单库查询和进程内 Session 掩盖的问题开始暴露。
4.1 分布式环境下的事务处理
单体里一个方法可以同时更新文章表和用户积分表,使用@Transactional即可保证原子性。拆成两个服务后,文章服务和用户服务各自有数据库,事务边界被切断了。
以“发布文章并给作者增加积分”为例,有几种思路:
- 分布式事务框架:Seata AT 模式可以降低开发成本,但会引入额外的协调器,性能和时间开销不能忽略。
- TCC:Try、Confirm、Cancel 三阶段,逻辑复杂,不适合所有场景。
- 本地消息表:在文章服务中写入文章和消息记录,同一个本地事务保证两张表一致,再通过异步任务发送消息。
- 事务消息:使用 RocketMQ 等支持事务消息的队列,先发 half 消息,再执行业务,最后提交或回滚。
本地消息表的简化流程:
1. 在 article 表插入文章。 2. 在 message 表中插入一条待发送消息,状态为 pending。 3. 同一个本地事务提交。 4. 定时任务或消息发送组件扫描 pending 消息,发送到 MQ。 5. 用户服务消费消息,增加积分,并记录消费 ID。这个方案的核心是:把跨服务的分布式事务,转换为“本地事务 + 最终一致性”。它没有消灭可能出现的中间不一致,但能保证数据最终收敛,且实现相对可控。
注意:不要以为引入了消息队列就有了分布式事务。消息可能重复、可能丢失、可能乱序,消费端必须做幂等和重试。
4.2 接口幂等:为什么支付、下单和回调都需要幂等
幂等指的是同一个请求执行多次,结果和只执行一次一致。服务拆分布署多实例后,客户端重试、消息重复消费、网关超时重发都可能导致同一请求被执行多次。
常见的幂等方案:
- 数据库唯一键:利用唯一约束保证重复插入只成功一次。
- Redis 分布式锁或原子操作:基于
SETNX或setIfAbsent拦截重复请求。 - 状态机校验:比如订单状态从“待支付”到“已支付”,重复支付回调无法跳过状态判断。
Redis 去重示例:
@Autowired private StringRedisTemplate redisTemplate; public boolean tryLock(String requestId) { Boolean success = redisTemplate.opsForValue() .setIfAbsent("request:" + requestId, "1", Duration.ofMinutes(10)); return Boolean.TRUE.equals(success); }使用要点是:requestId必须由调用方生成并作为参数传入,不能由服务端从内存中生成,否则重复请求的 ID 不同,无法去重。
4.3 限流和熔断:保护下游,而不是把故障放大
微服务链路中,一个服务故障可能被调用方重试放大,最终拖垮整条链路。限流解决的是“请求太多”,熔断解决的是“下游已经扛不住时不要再继续打”。
以 Spring Cloud Alibaba Sentinel 为例,可以在代码中定义流控规则:
@Configuration public class SentinelConfig { @PostConstruct public void init() { FlowRule rule = new FlowRule(); rule.setResource("/api/articles"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(20); FlowRuleManager.loadRules(Collections.singletonList(rule)); } }关键参数含义:
| 参数 | 含义 | 常见误区 |
|---|---|---|
| QPS | 每秒允许通过的请求数 | 设置过高等同于没限流 |
| 线程数 | 同时处理的调用数 | 需要结合实际耗时判断 |
| 熔断阈值 | 错误比例或慢调用比例 | 设置过低会误伤正常流量 |
| 超时时间 | 调用下游的最大等待时间 | 不设置会导致线程池耗尽 |
限流和熔断不是安全功能,而是稳定性功能。它们的目的是在流量异常时保护后端资源,让部分请求成功,而不是让所有请求都失败。
4.4 登录态从 Session 到 Token:无状态认证的取舍
单体时代,Session 可以直接存在进程内存里。拆成多实例后,用户可能第一次请求落在实例 A,第二次请求落在实例 B,如果实例 B 没有用户 Session,登录态就会丢失。
常见的解决方案有 Session 粘滞、Session 复制、Redis 集中存储 Session。更常见的平台化选择是使用 Token,例如 JWT。
JWT 的结构是Header.Payload.Signature:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEwMDB9.kZsPhhM6eZPfWQ服务端校验 JWT 时不需要查 Session,只需要验证签名和有效期。好处是天然适合多实例,坏处是签发后的 Token 无法主动失效,被泄露后更难处理。
所以在生产环境中,JWT 通常配合短有效期和刷新 Token 使用。不要把 Token 有效期设置得太长,也不要在 Token 里放敏感信息。
5. 小步迁移:不靠重写,靠灰度拆分
很多团队在决定微服务改造时,第一反应是重新写一套系统。这个选择风险极高。更稳妥的路径是保持现有系统可运行,逐步把边界清晰的能力抽离成独立服务。
5.1 什么时候才应该拆分
不是所有系统都需要微服务。判断信号可以从以下几个维度看:
| 信号 | 仍然适合单体 | 值得拆分的迹象 |
|---|---|---|
| 团队规模 | 1-2 个后端 | 多个团队交叉修改同一个仓库 |
| 发布频率 | 每周一次 | 每天多次,且互相阻塞 |
| 数据量 | 单库可以承受 | 单表数据量持续增长 |
| 模块边界 | 明确但耦合低 | 模块之间依赖混乱 |
| 故障影响范围 | 局部模块异常影响整体 | 需要独立扩容和隔离 |
如果一个系统只是量大了,但模块边界非常清晰,优先考虑的是数据库拆分、缓存优化和异步化,而不是直接引入微服务。
5.2 从旧系统拆分新服务的顺序
拆分的顺序应该是“先易后难,先读后写,先辅助后核心”。
推荐步骤:
- 抽离无状态服务,例如短信发送、文件存储、图片处理。
- 抽离读取压力大的数据,例如文章列表、商品详情。
- 抽离独立业务域,例如用户中心。
- 最后才拆分强事务依赖的核心交易链路。
- 数据库层面最后拆,先用共享库过渡,再逐步迁移到独立库。
每一步完成后都要运行一遍全链路回归测试,确认流量切换后没有明显错误率上升。
5.3 灰度发布与回滚设计
微服务的价值很大一部分在于可以独立扩容,但如果发布方式还是“一次全部替换”,风险并不会因为拆分而下降。
灰度发布的基本思路是:
- 新服务版本先接入少量用户或少量流量。
- 观察日志、错误率、耗时和关键业务指标。
- 确认稳定后逐步放大流量。
- 出现问题时用 Nacos 或网关配置快速切回旧版本。
网关层可以按请求头或用户 ID 做灰度规则。发布前至少要确认三个问题:是否知道新版在哪一层出问题、是否有足够日志定位、能否快速回滚。
5.4 迁移期常见工程坑
迁移过程常见的坑往往不是框架使用问题,而是对业务边界、数据归属和运维能力的预估不足。
| 坑点 | 现象 | 原因 | 应对 |
|---|---|---|---|
| 跨库 join 报错 | 原 SQL 查不到数据 | 表分到了不同数据库 | 在服务端聚合多次查询 |
| 分布式事务未设计 | 积分和数据对不上 | 本地事务失效 | 使用最终一致性方案 |
| 日志分散 | 排查问题要切多个系统 | 缺少 TraceId | 引入链路追踪和统一日志 |
| 配置漂移 | 同样代码不同行为 | 每台实例配置不同 | 配置中心统一管理 |
| 依赖顺序混乱 | 启动即报错 | 服务启动未按依赖关系等待 | 网关做重试,注册中心做健康检查 |
迁移不是一次事件,而是一段持续过程。每拆一个服务,都要把监控、日志、接口文档、异常处理同步补齐,否则系统会在看不见的地方累积技术债。
6. 运行验证和常见问题排查
当系统从单体变成多服务后,验证方式也随之变化。单个服务的接口能通,不代表整条链路能通;服务能启动,不代表能注册;路由能匹配,不代表下游服务可访问。
6.1 这套最小系统的启动顺序与验证命令
假设本地已经启动 Nacos,并且有三个应用:user-service、article-service、gateway。
启动顺序建议:
- 启动 Nacos。
- 启动 user-service。
- 启动 article-service。
- 启动 gateway。
验证注册中心:
curl "http://127.0.0.1:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=10"确认服务列表中包含user-service和article-service。
通过网关调用文章服务:
curl http://localhost:8080/api/articles这里的端口是网关端口。如果网关配置了路由,请求会被转发到article-service。
继续创建文章:
curl -X POST http://localhost:8080/api/articles \ -H "Content-Type: application/json" \ -d '{"title":"微服务网关","content":"通过网关访问","authorId":1}'通过网关再次查询,能看到新文章。这个流程证明:注册中心、服务发现、网关路由和文章服务全部正常串联。
6.2 常见问题排查表
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 服务启动后没有注册到 Nacos | 缺少 discovery 依赖 | 查看启动日志有无 Nacos 注册 | 添加依赖并检查 spring.cloud.nacos.discovery.server-addr |
| 网关访问 404 | 路由路径不匹配 | 查看网关 debug 日志 | 检查 Path 断言与 Controller 的 RequestMapping |
| Feign 调用报连接超时 | 服务实例未启动 | 在注册中心确认服务健康状态 | 启动对应服务,或调大 connect-timeout |
| 接口提示 401 | Token 缺失或过期 | 检查请求头 Authorization | 刷新 Token 或走登录接口 |
| 数据库启动失败 | H2 驱动缺失 | 查看堆栈中的 ClassNotFoundException | 检查 runtime scope 的依赖是否被排除 |
| 消息重复消费 | 消费者没有做幂等 | 检查消费日志中的业务 ID | 消费端增加唯一键或 Redis 去重 |
排查问题时,建议按照“从外到内”的顺序:
- 调用方是否真的把请求发到正确入口。
- 网关路由是否命中,IP 和端口是否正确。
- 目标服务是否注册到配置的注册中心。
- 目标服务本机接口是否可直接访问。
- 目标服务的数据库连接和依赖资源是否正常。
- 日志中是否有明确异常堆栈。
6.3 学习环境与生产环境的差异
学习环境可以用内存数据库和单机注册中心快速跑通,但生产环境不能照搬。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据库 | H2,随进程消失 | MySQL/PostgreSQL,独立数据卷和备份 |
| 配置 | application.yml 写死 | Nacos 配置中心,环境隔离,敏感信息加密 |
| 服务实例 | 各 1 个 | 至少 2 个以上,避免单点 |
| 网关 | 单机端口 | 负载均衡器 + 多实例 |
| 日志 | 控制台输出 | JSON 结构化日志 + 集中采集 |
| 监控 | 无 | JVM、接口耗时、错误率、链路追踪 |
| 安全 | 无鉴权 | 网关鉴权、最小权限、接口限流 |
生产环境最重要的是“可恢复性”:数据库要有备份策略,配置变更要有审计,服务发布要有回滚方案。这些能力要在上线前准备好,而不是出故障后再补。
7. 最佳实践:平台化系统的工程底线
架构演进到了一定程度,决定系统长期稳定性的往往不是某个框架,而是一组长期必须遵守的工程规范。
7.1 代码分层和依赖方向
无论单体还是微服务,分层依然是保证可维护性的基础。一个典型的分层边界如下:
- Controller:接收参数、校验参数、返回响应。
- Service:业务规则、事务边界、跨服务调用。
- Repository:数据访问。
- Entity/DTO/VO:实体、传输对象、视图对象分离。
依赖方向只能是上层依赖下层。Controller 不能直接操作 Repository,DTO 不能直接被 Repository 层复用,否则一旦实体结构变化,所有层都会受影响。
7.2 配置外置与敏感信息保护
平台化系统里,同一套代码会部署在开发、测试、生产多个环境。配置写在代码里会导致环境切换困难和敏感信息泄露。
推荐做法:
- 配置文件使用
application-{profile}.yml区分环境。 - 数据库密码、密钥、Token 由配置中心管理。
- 敏感配置使用加密工具处理,不保存明文。
学习环境下可以直接在 yml 里写password: "",生产环境必须避免这类操作。
7.3 日志与链路追踪
多个服务互相调用后,一个请求会在多个服务里产生多段日志。没有关联标识时,排查问题只能靠猜。
比较简单的做法是在网关或拦截器生成traceId,放入 MDC,然后在日志 pattern 中输出。例如 Logback 的 pattern:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>这样日志中每一行都有 traceId,跨服务时再把 traceId 通过 HTTP 头传递。完整方案可以接入 SkyWalking 或 Zipkin,但哪怕先手工加一个 traceId,也能显著提升排查效率。
7.4 安全与合规底线
平台化系统把更多业务能力开放给前端和第三方后,安全边界必须更清晰。
- 接口鉴权不能只在网关做,服务内部也要做二次校验。
- 权限模型遵循最小权限原则,不把所有功能开放给所有角色。
- 敏感数据返回前脱敏,例如手机号、身份证号只保留必要部分。
- 用户数据要有注销、导出和删除机制,这既是合规要求,也是用户信任的基础。
- 限流配置要区分普通用户、签约用户和管理员,避免单个用户拖垮系统。
这些内容不是写得越多越好,而是在设计每个接口时就要考虑清楚:这个接口能不能被恶意调用,会带来什么后果,如何通过代码和运维手段限制风险。
7.5 继续深入的方向
从单体到微服务只是平台化工程的第一步。接下来可以继续关注:
- 云原生部署:容器化、Kubernetes、弹性伸缩。
- 领域驱动设计:在拆分服务前找到真正的业务边界。
- 单元测试和契约测试:一个服务内部变化不破坏其他服务。
- Serverless 和事件驱动架构:更适合波动型流量的场景。
- 可观测性体系:指标、日志、链路三者结合。
回到最开始的判断:互联网服务从公共网页走向平台化,不是某一个团队拍脑袋决定的方向,而是规模、协作和用户体验共同推动的结果。理解这个演进过程,不仅要知道如何写代码、配网关,更要在每一个技术决策里想清楚“它解决了什么问题,又带来了什么新的代价”。这套思路,比记住任何一个框架的配置都更重要。