新系统从 0 到 1 怎么搭:一套上线几年的微服务架构,逐层拆开
「新业务系统的设计与架构」这一档,我按自己交付过的那套来写。
它上线了,跑了几年,二十多个服务仓,三个研发团队、八十人左右一起往里写代码,业务覆盖商贸、仓储、生产、物流四条线。 技术栈是真名——Nacos、Spring Cloud Gateway、Seata、Flowable、RabbitMQ,这些没什么好遮的;客户名、行业、内部地址、人名一律不写,规模数字取约数。
这篇想给你的不是"我们用了什么",是每一条设计背后的判据。判据是可以直接搬走的:你换一个注册中心、换一套业务流程引擎,它们照样成立。
搭新系统真正难的地方从来不是选哪个中间件。八十个人同时往一个架构里写,三个月后同一件事会有五种写法——这套架构要解决的是这一件,下面每一节都在解它。
一、服务怎么切:判据只有一条
拆服务最容易的错法是按部门拆,最后得到一堆组织结构的服务化副本。我用的判据只有一条:这个东西是不是两个以上业务域都要用。是,就独立;只有一个域用,就留在那个域里。
按这条切出来的第一层是六个平台型服务,它们没有一条业务规则:
| 服务 | 只管这件事 |
|---|---|
| 网关 | 路由、鉴权入口、限流、请求清洗 |
| 鉴权 | 登录态、令牌签发与校验、权限数据 |
| 用户 | 账号、组织、人员,以及"谁能看哪些数据" |
| 字典 | 编码与枚举的集中维护 |
| 资源 | 文件、附件这类二进制 |
| 日志记录 | 业务审计日志的落库 |
第二层才是约十个业务域服务:客户、商品、交易、库存、财务、物流配送、基础资料、工作流,外加工流的对接方。工作流也是独立服务——它管的是"流程实例"这个概念,"这张单子该谁审"属于各业务域自己。这一刀切下去之后,四个业务域的审批改动不再互相排队发版。
「财务」这一格值得单独说,因为它最容易被放错层:几乎每个业务域都要往它那里写钱——开单要占额度、发货要挂应收、结算要开票,只按"两个以上业务域都要用"那半句判据,它简直该进平台层。它没有。平台层那六个服务的共同点是一条业务规则都没有,而财务整仓都是规则:账期从哪天起算、利息按什么规则滚、进项发票与出库单怎么核销、日结与银行流水对不上时以谁为准。六十多个控制器、近两千个类,按控制器数在业务域里排第三(物流、交易之后),却没有一个业务域能替它写这些规则。
所以那条判据其实是两问:要不要独立,看有几个业务域要用;进哪一层,看它自己有没有业务规则。 两问合起来能当场回答"这个要不要沉到平台层"——凡是"大家都要用、而且全是规则"的东西,答案是独立成域服务、让别人来调,而不是沉进平台层。规则一旦被共享,就没有人能为某一个域单独改它。
平台层的价值在第二个月就开始显出来:新接一条业务线,鉴权、字典、附件、审计日志这四件事一行都不用写,域服务里只出现自己的业务规则。"两个以上业务域都要用"这条判据的实际收益,是把重复实现的次数从十压到一。
有一条经验单独抄出来:日志记录这种东西一定要独立成服务,而不是一个公共包。 业务审计日志的写入量最后会压在业务库的连接池上,而它偏偏是最不该影响主流程的一类写。单拆出来、接收端挂消息队列、业务侧只发不写——主流程的响应时间和审计的写入量就此解耦,审计要加字段也不再动业务仓。
二、每个仓里的六层:把约定做成能复制的东西
二十多个仓、三个团队同时写,风险不是某个人写错,是同一件事出现五种写法,半年后没人能判断哪一种是标准。
所以每个服务固定切成六层,一个都不许省:
| 层 | 里面只有这些东西 |
|---|---|
api |
跨服务的接口声明,外加 fallback 目录里的降级实现 |
feign-client |
一个 *ApiClient extends *Api,调用方只认它 |
model |
传输对象:entity、dto、vo、param 四种 |
biz |
服务实现、Mapper 接口、Mapper XML |
webapi |
控制器,只做入参校验和转发 |
common |
常量、枚举、异常、工具 |
约定生效的方式不是文档,也不是评审,是骨架:新加一条业务线,把某个仓的 pom 和包结构整个复制过来改名字,而不是从同事的仓里挑一片抄。复制出来的东西天然合规,评审因此只需要看一件它擅长的事:有没有人把 SQL 写进了控制器。分层是不是对,不用看——它是复制来的。
这套办法还带来一个我认为最有价值的性质:偏离会变成一眼可见的事。 结构是复制来的,所以任何一处和骨架不一样的地方都会自己跳出来,评审第一眼就能判定那不是标准。当结构靠每个人自己搭,例外和标准长得一样;当结构是复制出来的,例外没有藏身处。
它也不是纸面标准。十七个仓数下来,十五个整整齐齐就是这六层,两处例外都在平台层:网关是一个独立应用,没有跨服务契约可言;日志记录只有 biz 与 common 两层,因为它只收不服务。六层是下限这件事,和"谁没到下限"是同一份清单给的。
这套切法是按多人协作设计的。三五个人的项目我不会给六层,从 api / biz / model 三层起步,等人多了再补——分层往上加便宜,往回拆贵,这是我对团队规模的适配,不是架构原则。
三、降级:统一还回一个「能被判出来的失败」
跨服务调用的声明有三百多处,对应的降级类两百二十多个,fallback 和 fallbackFactory 两种注册方式都在用。每个降级实现把接口整个实现一遍,方法不抛异常,一律返回统一响应体里的失败状态。
选它是因为网关之后还有别的调用方,抛异常会让一次远程故障沿着调用链变成一堆看不懂的堆栈。但光这么做不够,所以我配套定了三句规矩——这三句现在是我给新项目做代码检查时的固定项:
- 契约方法的返回类型一律包在统一响应里,禁止裸
boolean和裸List。裸boolean在降级时只能返回false,而false在那个语境里是"业务上不允许",不是"我调不通"——一个值承担两个意思,故障就会伪装成业务结论。 - 调用方拿到非成功状态时,要么明确处理,要么立刻抛出去,不允许"取不到就当空"。这条挡住的是最贵的那类事故:远程超时被读成"这个客户没有数据",于是它不进报警队列,而是被当成一次正常空结果写进报表。
- 降级类里的日志按方法名打点。故障排查时一眼能看出这条结果是降级的还是业务的。
有了这三句,"降级"从一个可能吞故障的黑洞变成了一次可定位、可报警的显式分支。这套架构里我最愿意直接卖给下一个项目的,就是这三句。
四、配置分三层,路由也放配置中心:接一个新服务不占发版窗口
服务多的系统,配置最先烂掉。我们的分法:
| 哪一层 | 放什么 | 谁读 |
|---|---|---|
| 共享层 | 数据源公共参数、日志级别、MQ 连接、序列化开关 | 所有服务,改一次全体生效 |
| 应用层 | 这个服务自己的参数,按环境分 | 单个服务 |
| 路由层 | 网关的路由与过滤器定义 | 只有网关 |
一共五套环境:本地、开发、测试、准生产,再加一套甲方自己那侧的环境(名字不展开)。注册中心按环境分组,命名空间按环境隔离,配置的分组独立于注册分组——这三件事分开以后,"改了配置没生效"才成为一个能定位的问题,而不是一句得靠人猜的抱怨。
路由表放进配置中心,是这套设计里最值得留下的一个决定。 新增一个业务服务,只改配置中心那个 data-id,网关不重新打包、不重启。在二十多个仓的交付节奏上,它意味着"接一个新服务"不再需要排队占发版窗口——这条能力在小系统上无所谓,在多团队并行的项目上直接换成进度。
顺带一条纪律:带凭据的配置不进共享层。那一层 data-id 少、改动频繁、改的人多,看着最像"公共配置",实际暴露面最大。
五、鉴权在网关做一次,服务侧再做一次
网关上挂的是响应式过滤器:登录态校验加一份匿名路径白名单。同一时间,公共库里还有一层服务侧拦截器做几乎同样的事。
登录态本身也不只来自账号密码。鉴权那侧还接了一个第三方企业身份的扫码登录,第三方账号与内部账号的绑定关系单独一张表、带租户列——同一套令牌签发要同时兜住两种身份来源,而网关只认那一种令牌。这是它能把鉴权收成一处的前提:下游二十多个仓不需要知道这个人是从哪个入口登进来的。
看起来重复,其实分工不同:网关挡的是"没登录的人",服务侧挡的是"绕过网关的调用"。 内网里服务端口是直连的,容器编排一改、调试脚本一跑,请求就可能不经过网关。这一层存在的价值,就是让"绕过网关"这件事不发生权限提升。八十人的团队里,一定有人会在某个下午直连端口调试——架构要为这件事提前准备好答案,而不是事后追究。
网关上还集中挂了这些,都是各业务域不需要重复实现的能力:限流(按 IP、按用户、按 API Key 三种维度分别解析)、XSS 与注入特征清洗、接口签名校验。三件事写在网关一次,二十多个仓里一行都不用出现。这就是第一节那条拆分判据在安全能力上的同一用法。
六、全局事务只钉在开单和结算这类入口
Seata 引进了十八个 pom,@GlobalTransactional 一共一百八十多次,集中在交易、财务、库存这几个仓的创建与结算方法上。
这不是"全库铺开",而且我认为不该铺开:
- 读路径不需要全局事务,加了只是排队等锁;
- 单服务内部的写走本地事务,本来就不该被全局事务管;
- 全局锁的成本随参与方数量涨,一次开单跨三个服务是可控的,跨七个就是性能问题的开始。
所以判据是:一次业务动作跨了两个以上服务的写、而且中间态不能被人读到,才上全局事务。 剩下的跨服务写走"本地事务 + 消息 + 对账",那是另一篇的话题。
这条判据让"要不要强一致"在每个入口上都有答案,评审时不再逐条争论——一致性这件事从个人判断变成了一句可以对照的规矩。
全局超时和锁等待这两档参数,我放在配置中心的应用层而不是代码里,线上要调当天就能改,不用等发版。这类参数属于"上线第一天就得能改"的那一类,位置比取值更重要。
七、审批流:仓库里零个流程定义文件
整个仓库没有一个 .bpmn 文件。所有流程定义在后台画、后台发布,运行时通过 repositoryService.createDeployment() 部署。
驱动力很实际:审批流程的变更频率远高于代码。流程定义若是文件,每改一次审批要走一次发版;不是文件,业务人员在后台点一下就生效。这条设计把审批变更的交付周期从"跟着版本走"降到"当天",而审批变更恰恰是这类系统上线后最高频的需求之一。
这些定义存在流程引擎自己的表里,跨环境同步走导入导出。换到这笔交换的这边看:审批变更不再排队等版本,代价只是环境之间的流程定义要对一次齐。
单据跟流程实例怎么挂上:单据表一列流程实例 ID,这个字段名在两百多个文件里出现。这意味着"单据"和"审批"是两个独立生命周期,于是有三件事必须显式处理——单据作废时流程实例怎么办、流程实例被误删时单据怎么显示、同一张单据重新提交时是复用还是新起一个实例。我们给的答案是作废保留流程记录、查不到实例时单据仍能正常显示、重新提交新起实例并在单据上留历史。这三个答案没有一个是优雅的,但每一个都挡住了一类上线后无法解释的工单。
另外,各业务域有两百多个状态枚举。状态多不是问题,问题在于状态与流程经常被混起来:流程表达的是"下一步该谁做",状态表达的是"这张单子现在是什么"。 用状态机去模拟审批流转,或者反过来用流程节点名当业务状态给报表用,半年内就会长出一堆没人能解释的判断分支。这条区分是我在评审里最先看的东西之一。
八、交付:三十一条流水线,规矩写在流水线里
每个服务仓一份流水线定义,一共三十一条,另一套甲方环境另有九份变体。流程一致:拉代码 → Maven 构建 → 打镜像 → 推私有仓库 → 目标机上 compose 重建。
分支规则是 master 出生产镜像,其余分支出开发镜像——这一条写在流水线文件里,不靠人记。靠人记的规矩一定会在某一次赶版本时被破;写进流水线之后,"这个镜像从哪个分支来"变成了流水线自己知道的事。
三十一条流水线跑几年的意义不在于"我们自动化了",而在于任何一个新团队进来,构建和发布的路径是同一个,没人需要问人。这和第二节那份骨架是同一件事的两个面:结构靠复制,流程靠脚本。
收口:三条可以直接抄走的
搭新系统讨论得最热闹的,是用哪个注册中心、哪个流程引擎。这些几乎不影响项目死活。几年下来真正决定成本的是这三条,它们都不依赖任何具体中间件:
- 一条判据切服务,而不是一个组织架构表。 "两个以上业务域都要用才独立成服务"——把它贴在评审文档第一页,能省掉后面所有关于"这个要不要单拆"的争论。
- 约定要做成可以复制的东西,或者写成会失败的检查。 六层骨架、三句降级规矩、超时放配置中心、分支规则写进流水线,全都是把"标准"从人的记忆搬到了机器和目录里。写在文档里的标准会在第三个月失效,复制目录结构带来的标准能活到最后一天。
- 失败必须有一个可判定的形态。 降级返回什么、超时怎么设、日志里能不能一眼看出是被降级了——这类设计全部要在出事之前决定,出事之后再补就成了事故复盘报告。
如果只想带走一件立刻能用的:给新服务做一份骨架,让所有人从它复制,然后开始记录谁在评审上争论过分层。 争论越多的团队,说明骨架还没生效。
我做的另一件事是把这套方法用在了自己正在重写的系统上——那一跑六周的真实数字在《六周重写一套 ERP》里。
本文作者:Aryee 发布时间:2026-09-27 10:18
本文出处:ARYEE.cn 固定链接:https://www.aryee.cn/archives/microservice-architecture-anatomy.html
转载或引用请连这一行一起带走;文中的代码与结论按当时环境成立,组件升级后请以官方文档为准。