新系统从 0 到 1 怎么搭:一套上线几年的微服务架构,逐层拆开

2026-09-27 Aryee 10

「新业务系统的设计与架构」这一档,我按自己交付过的那套来写。

它上线了,跑了几年,二十多个服务仓,三个研发团队、八十人左右一起往里写代码,业务覆盖商贸、仓储、生产、物流四条线。 技术栈是真名——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 两种注册方式都在用。每个降级实现把接口整个实现一遍,方法不抛异常,一律返回统一响应体里的失败状态。

选它是因为网关之后还有别的调用方,抛异常会让一次远程故障沿着调用链变成一堆看不懂的堆栈。但光这么做不够,所以我配套定了三句规矩——这三句现在是我给新项目做代码检查时的固定项:

  1. 契约方法的返回类型一律包在统一响应里,禁止裸 boolean 和裸 List。裸 boolean 在降级时只能返回 false,而 false 在那个语境里是"业务上不允许",不是"我调不通"——一个值承担两个意思,故障就会伪装成业务结论。
  2. 调用方拿到非成功状态时,要么明确处理,要么立刻抛出去,不允许"取不到就当空"。这条挡住的是最贵的那类事故:远程超时被读成"这个客户没有数据",于是它不进报警队列,而是被当成一次正常空结果写进报表。
  3. 降级类里的日志按方法名打点。故障排查时一眼能看出这条结果是降级的还是业务的。

有了这三句,"降级"从一个可能吞故障的黑洞变成了一次可定位、可报警的显式分支。这套架构里我最愿意直接卖给下一个项目的,就是这三句。

四、配置分三层,路由也放配置中心:接一个新服务不占发版窗口

服务多的系统,配置最先烂掉。我们的分法:

哪一层 放什么 谁读
共享层 数据源公共参数、日志级别、MQ 连接、序列化开关 所有服务,改一次全体生效
应用层 这个服务自己的参数,按环境分 单个服务
路由层 网关的路由与过滤器定义 只有网关

一共五套环境:本地、开发、测试、准生产,再加一套甲方自己那侧的环境(名字不展开)。注册中心按环境分组,命名空间按环境隔离,配置的分组独立于注册分组——这三件事分开以后,"改了配置没生效"才成为一个能定位的问题,而不是一句得靠人猜的抱怨。

路由表放进配置中心,是这套设计里最值得留下的一个决定。 新增一个业务服务,只改配置中心那个 data-id,网关不重新打包、不重启。在二十多个仓的交付节奏上,它意味着"接一个新服务"不再需要排队占发版窗口——这条能力在小系统上无所谓,在多团队并行的项目上直接换成进度。

顺带一条纪律:带凭据的配置不进共享层。那一层 data-id 少、改动频繁、改的人多,看着最像"公共配置",实际暴露面最大。

五、鉴权在网关做一次,服务侧再做一次

网关上挂的是响应式过滤器:登录态校验加一份匿名路径白名单。同一时间,公共库里还有一层服务侧拦截器做几乎同样的事。

登录态本身也不只来自账号密码。鉴权那侧还接了一个第三方企业身份的扫码登录,第三方账号与内部账号的绑定关系单独一张表、带租户列——同一套令牌签发要同时兜住两种身份来源,而网关只认那一种令牌。这是它能把鉴权收成一处的前提:下游二十多个仓不需要知道这个人是从哪个入口登进来的。

看起来重复,其实分工不同:网关挡的是"没登录的人",服务侧挡的是"绕过网关的调用"。 内网里服务端口是直连的,容器编排一改、调试脚本一跑,请求就可能不经过网关。这一层存在的价值,就是让"绕过网关"这件事不发生权限提升。八十人的团队里,一定有人会在某个下午直连端口调试——架构要为这件事提前准备好答案,而不是事后追究。

网关上还集中挂了这些,都是各业务域不需要重复实现的能力:限流(按 IP、按用户、按 API Key 三种维度分别解析)、XSS 与注入特征清洗、接口签名校验。三件事写在网关一次,二十多个仓里一行都不用出现。这就是第一节那条拆分判据在安全能力上的同一用法。

六、全局事务只钉在开单和结算这类入口

Seata 引进了十八个 pom,@GlobalTransactional 一共一百八十多次,集中在交易、财务、库存这几个仓的创建与结算方法上。

这不是"全库铺开",而且我认为不该铺开:

  • 读路径不需要全局事务,加了只是排队等锁;
  • 单服务内部的写走本地事务,本来就不该被全局事务管;
  • 全局锁的成本随参与方数量涨,一次开单跨三个服务是可控的,跨七个就是性能问题的开始。

所以判据是:一次业务动作跨了两个以上服务的写、而且中间态不能被人读到,才上全局事务。 剩下的跨服务写走"本地事务 + 消息 + 对账",那是另一篇的话题。

这条判据让"要不要强一致"在每个入口上都有答案,评审时不再逐条争论——一致性这件事从个人判断变成了一句可以对照的规矩。

全局超时和锁等待这两档参数,我放在配置中心的应用层而不是代码里,线上要调当天就能改,不用等发版。这类参数属于"上线第一天就得能改"的那一类,位置比取值更重要。

七、审批流:仓库里零个流程定义文件

整个仓库没有一个 .bpmn 文件。所有流程定义在后台画、后台发布,运行时通过 repositoryService.createDeployment() 部署。

驱动力很实际:审批流程的变更频率远高于代码。流程定义若是文件,每改一次审批要走一次发版;不是文件,业务人员在后台点一下就生效。这条设计把审批变更的交付周期从"跟着版本走"降到"当天",而审批变更恰恰是这类系统上线后最高频的需求之一。

这些定义存在流程引擎自己的表里,跨环境同步走导入导出。换到这笔交换的这边看:审批变更不再排队等版本,代价只是环境之间的流程定义要对一次齐。

单据跟流程实例怎么挂上:单据表一列流程实例 ID,这个字段名在两百多个文件里出现。这意味着"单据"和"审批"是两个独立生命周期,于是有三件事必须显式处理——单据作废时流程实例怎么办、流程实例被误删时单据怎么显示、同一张单据重新提交时是复用还是新起一个实例。我们给的答案是作废保留流程记录、查不到实例时单据仍能正常显示、重新提交新起实例并在单据上留历史。这三个答案没有一个是优雅的,但每一个都挡住了一类上线后无法解释的工单。

另外,各业务域有两百多个状态枚举。状态多不是问题,问题在于状态与流程经常被混起来:流程表达的是"下一步该谁做",状态表达的是"这张单子现在是什么"。 用状态机去模拟审批流转,或者反过来用流程节点名当业务状态给报表用,半年内就会长出一堆没人能解释的判断分支。这条区分是我在评审里最先看的东西之一。

八、交付:三十一条流水线,规矩写在流水线里

每个服务仓一份流水线定义,一共三十一条,另一套甲方环境另有九份变体。流程一致:拉代码 → Maven 构建 → 打镜像 → 推私有仓库 → 目标机上 compose 重建。

分支规则是 master 出生产镜像,其余分支出开发镜像——这一条写在流水线文件里,不靠人记。靠人记的规矩一定会在某一次赶版本时被破;写进流水线之后,"这个镜像从哪个分支来"变成了流水线自己知道的事。

三十一条流水线跑几年的意义不在于"我们自动化了",而在于任何一个新团队进来,构建和发布的路径是同一个,没人需要问人。这和第二节那份骨架是同一件事的两个面:结构靠复制,流程靠脚本。

收口:三条可以直接抄走的

搭新系统讨论得最热闹的,是用哪个注册中心、哪个流程引擎。这些几乎不影响项目死活。几年下来真正决定成本的是这三条,它们都不依赖任何具体中间件:

  1. 一条判据切服务,而不是一个组织架构表。 "两个以上业务域都要用才独立成服务"——把它贴在评审文档第一页,能省掉后面所有关于"这个要不要单拆"的争论。
  2. 约定要做成可以复制的东西,或者写成会失败的检查。 六层骨架、三句降级规矩、超时放配置中心、分支规则写进流水线,全都是把"标准"从人的记忆搬到了机器和目录里。写在文档里的标准会在第三个月失效,复制目录结构带来的标准能活到最后一天。
  3. 失败必须有一个可判定的形态。 降级返回什么、超时怎么设、日志里能不能一眼看出是被降级了——这类设计全部要在出事之前决定,出事之后再补就成了事故复盘报告。

如果只想带走一件立刻能用的:给新服务做一份骨架,让所有人从它复制,然后开始记录谁在评审上争论过分层。 争论越多的团队,说明骨架还没生效。

我做的另一件事是把这套方法用在了自己正在重写的系统上——那一跑六周的真实数字在《六周重写一套 ERP》里。

你手上那套系统,卡在哪一步?

留个手机号和方便的时间,我打过来先听你说现状与约束,再给可执行的判断:这套系统是该继续修、该动哪里,还是干脆重做一版设计更省。

约一次沟通不接纯 UI 外包,只做系统层面的问题。