1. 从单体到分布式:为什么架构一定要走到这一步
很多同学第一次听到“分布式架构”这四个字,脑子里蹦出来的画面就是一堆服务器、一堆中间件、一堆看不懂的拓扑图。但真到项目里被逼着做拆分的时候,才发现自己连“为什么要拆”都说不清楚。我在带团队做系统演进的那几年,踩过最大的坑就是:为了分布式而分布式,结果把一个本来跑得好好的单体系统,拆成了一堆互相甩锅的服务,运维成本翻了三倍,故障排查时间从十分钟变成了半天。
所以聊分布式架构的基本概念,我不想一上来就给你讲 CAP、BASE 这些名词。咱们先把场景摆出来:一个电商系统,早期就是一台机器上跑一个 war 包,数据库也是单机。日订单量几千的时候,啥事没有。等到日订单量冲到几万、几十万,问题就来了——数据库连接池被打满、Tomcat 线程数不够用、一次发版全站停机、某个不重要的报表查询把主库拖垮。这时候你才真正需要分布式架构,它的核心目标就三个:提升吞吐量、提升可用性、提升可扩展性。
但这里有个关键认知:分布式架构不是“银弹”,它本质上是用复杂度换能力。你换来的是水平扩展的能力、故障隔离的能力、团队并行开发的能力;你付出的是网络通信的不确定性、数据一致性的难度、运维监控的复杂度。所以一个合格的架构师,在决定要不要分布式之前,一定会先问自己:当前系统的瓶颈到底在哪?是 CPU、内存、磁盘 IO、网络带宽,还是数据库?如果瓶颈在数据库,你拆应用服务是没用的,得先做读写分离或者分库分表。
我个人的经验是,单体优先,按需拆分。一个初创项目,团队就五六个人,你上来就搞微服务、搞服务网格,纯属给自己找罪受。等到业务复杂度上来了,团队规模扩大了,再逐步把边界清晰、变更频繁的模块拆出去。拆分的时机判断,我一般看三个信号:第一,某个模块的发布频率明显高于其他模块,每次发版都要全量回归;第二,某个模块的资源消耗特征和其他模块差异巨大,比如图片处理吃 CPU,订单查询吃数据库连接;第三,团队已经大到沟通成本超过开发成本,一个模块的改动需要跨三个小组协调。
分布式架构的基本概念里,最核心的一个词叫**“远程调用”**。单体系统里,A 方法调 B 方法,就是一次栈帧压栈出栈,纳秒级。到了分布式环境,A 服务调 B 服务,中间隔着网络,一次调用可能几毫秒,也可能几秒,甚至直接超时。这个差异是所有分布式问题的根源。你后面遇到的服务雪崩、重试风暴、数据不一致,追到根上都是因为“网络调用不可靠”。所以理解分布式,第一步就是接受一个事实:网络是不可靠的,远程调用一定会失败。你的架构设计,必须假设每一次远程调用都可能超时、可能丢包、可能重复。
再往下延伸,分布式架构还涉及几个基础概念,我逐个拆开讲。服务注册与发现,解决的是“B 服务在哪”的问题。单体时代,你写个new BService()就完事了;分布式时代,B 服务可能部署在十台机器上,IP 随时会变,你需要一个注册中心来维护这份地址清单。负载均衡,解决的是“选哪台 B 服务”的问题,轮询、随机、最小连接数、一致性哈希,各有各的适用场景。配置中心,解决的是“改个参数要不要重启”的问题,把配置从代码里抽出来,集中管理、动态推送。网关,解决的是“外部流量怎么进来”的问题,做鉴权、限流、路由、协议转换。链路追踪,解决的是“一次请求到底经过了哪些服务”的问题,没有它,线上排查就是盲人摸象。
这些概念不是孤立的,它们共同构成了一套分布式协作的基础设施。你可以把它们想象成一个城市的交通系统:注册中心是地图,负载均衡是导航,配置中心是交通信号灯,网关是收费站,链路追踪是监控摄像头。缺了哪个,城市还能转,但一出问题你就抓瞎。
2. 分布式架构的核心理论:CAP、BASE 与一致性模型
2.1 CAP 理论到底在说什么,别被面试题带偏了
CAP 这三个字母,分布式面试必问,但很多人背了答案却不会用。C 是一致性,A 是可用性,P 是分区容错性。定理说的是:在一个分布式系统中,这三个特性最多同时满足两个。注意,这里的“最多两个”有个前提——只有当网络分区发生时,你才需要做取舍。网络没分区的时候,C 和 A 是可以同时满足的。
那什么是网络分区?简单说,就是机房之间的网络断了,或者交换机挂了,导致一部分节点联系不上另一部分节点。这时候你有两个选择:要么拒绝服务,保证数据一致(选 C 弃 A);要么继续提供服务,但可能返回旧数据(选 A 弃 C)。P 是分布式系统的前提,你只要跨网络部署,P 就是必须接受的,所以真正的取舍是在 C 和 A 之间。
我见过太多团队在这个问题上纠结。其实你不需要在系统级别做统一选择,不同业务模块可以有不同的取舍。比如订单创建、支付扣款,这类涉及钱的场景,必须选 C,宁可短暂不可用,也不能出现数据错乱。而商品详情页、用户评论列表,这类读多写少的场景,选 A 更合适,返回稍微旧一点的数据用户根本感知不到。所以 CAP 不是让你二选一,而是让你在具体场景下做权衡。
这里有个常见的误区:很多人以为选了 AP 就可以完全不管一致性。不是的。AP 系统最终也要达到一致,只是允许中间有一段时间不一致,这就是 BASE 理论里的“最终一致性”。所以 CAP 和 BASE 不是对立的,BASE 是 CAP 中 AP 方向的延伸和补充。
2.2 BASE 理论:妥协的艺术
BASE 是 Basically Available、Soft state、Eventually consistent 的缩写。翻译成人话就是:基本可用、软状态、最终一致。基本可用指的是系统在出现故障时,允许损失部分可用性,比如降级返回兜底数据、限流拒绝部分请求,但核心功能还能用。软状态指的是允许系统中的数据存在中间状态,这个中间状态不影响系统的整体可用性。最终一致指的是经过一段时间后,所有节点的数据最终会达到一致。
我举个实际例子你就明白了。用户下单后,需要扣减库存、生成订单、发送通知。在强一致方案里,这三个操作要么全成功,要么全失败,通常用分布式事务来实现。但分布式事务的性能很差,而且实现复杂。在 BASE 方案里,你可以先扣库存、再生成订单、最后发通知,中间任何一步失败都通过补偿机制来修复。比如订单生成失败,就把库存加回去;通知发送失败,就重试几次,实在不行就记录日志人工处理。用户看到的是“下单成功”,但后台可能还在处理中,这就是软状态。最终所有数据都会对齐,这就是最终一致。
注意:BASE 不是让你放弃一致性,而是把一致性的时间窗口拉长,用异步和补偿来换取性能和可用性。选择 BASE 方案的前提是,你的业务能接受短暂的数据不一致。
2.3 一致性模型的谱系:从强一致到最终一致
一致性模型不是一个非黑即白的东西,它是一个谱系。最左边是强一致性,任何时刻任何节点读到的都是最新写入的值。往右是线性一致性,它比强一致稍弱,要求所有操作看起来像在一个全局时钟下按顺序发生。再往右是顺序一致性,只保证单个节点的操作顺序一致,不保证全局顺序。然后是因果一致性,只保证有因果关系的操作顺序一致。最右边是最终一致性,只保证经过足够长的时间后,所有节点数据一致。
在实际系统里,强一致通常靠分布式共识算法来实现,比如 Paxos、Raft。这些算法的代价是通信轮次多、延迟高,适合元数据管理、配置中心这类场景。最终一致通常靠异步复制、消息队列、定时对账来实现,适合订单状态同步、缓存更新这类场景。
我个人的选型经验是:能用最终一致就不用强一致,除非业务真的不能忍。比如用户注册后发优惠券,晚几秒钟到账完全没问题,用消息队列异步处理就行。但用户余额扣减,必须强一致,那就老老实实走数据库事务或者分布式事务框架。判断标准很简单:如果数据不一致会导致用户投诉、资金损失、法律风险,那就选强一致;否则优先考虑最终一致。
3. 分布式架构的常见模式与落地选型
3.1 服务拆分:怎么拆才不后悔
服务拆分是分布式架构的第一步,也是最容易翻车的一步。我见过把用户服务拆成“用户基本信息服务”“用户扩展信息服务”“用户登录服务”的,结果一个简单的用户详情查询要调三个服务,延迟翻了三倍。拆分的粒度,我的原则是按业务能力拆,不按技术分层拆。一个服务应该是一个完整的业务能力单元,对外提供粗粒度的接口,内部可以有自己的数据库、自己的缓存、自己的逻辑。
具体怎么判断边界?我常用一个方法叫“变更频率聚类”。把系统里所有的功能点列出来,标注每个功能点的变更频率。变更频率相近、且经常一起变更的功能,放在一个服务里。变更频率差异大的,拆开。比如商品管理和订单管理,商品管理经常改价格、改描述,订单管理经常改状态、改流程,两者变更节奏完全不同,就应该拆开。而订单创建和订单查询,虽然读写特征不同,但业务上紧密相关,初期可以放在一个服务里,等性能压力大了再做读写分离。
还有一个经验:拆服务之前先拆数据库。如果你的两个服务还共用一个数据库,那本质上还是单体,只是把代码分开了。服务拆分的真正标志是数据私有化,每个服务有自己的数据库,其他服务只能通过接口访问,不能直接连库。这一步很难,因为涉及到数据迁移、外键去除、事务拆分,但不做这一步,拆分就是自欺欺人。
3.2 通信方式:同步还是异步,RPC 还是消息
服务拆完了,接下来要解决服务之间怎么通信。主流方式就两种:同步调用和异步消息。同步调用常见的是 REST 和 RPC。REST 基于 HTTP,简单通用,但性能一般,适合对外接口和跨团队协作。RPC 通常是二进制协议,性能好,但耦合度高,适合内部服务之间高频调用。我一般建议对外 REST,对内 RPC,兼顾通用性和性能。
异步消息就是通过消息队列来通信,生产者发消息,消费者收消息,两边不需要同时在线。异步的好处是解耦、削峰、最终一致。比如订单创建后,需要通知库存、通知物流、通知积分,这些操作都可以通过消息队列异步处理,订单服务只管发消息,不用等结果。坏处是增加了系统的复杂度,消息可能丢失、可能重复、可能乱序,需要额外处理。
选型上,我的经验是:核心链路用同步,非核心链路用异步。支付扣款必须同步,因为用户等着结果;发送短信通知可以异步,晚几秒没关系。另外,异步消息一定要考虑幂等性,因为消息可能重复投递。幂等的实现方式有很多,比如用唯一业务 ID 做去重、用数据库唯一索引、用 Redis 记录已处理的消息 ID。
3.3 数据一致性方案:从 2PC 到 TCC 到本地消息表
分布式环境下,数据一致性是最头疼的问题。我按复杂度从低到高给你捋一遍常见方案。
本地消息表:最简单,也最常用。在业务数据库里建一张消息表,业务操作和写消息在同一个本地事务里完成。然后有一个定时任务或者异步线程去读消息表,把消息发到消息队列,消费者处理完后回调确认。这个方案的好处是不依赖外部事务协调器,实现简单,坏处是需要自己处理消息重试和幂等。
TCC:Try-Confirm-Cancel 三段式。Try 阶段预留资源,Confirm 阶段确认执行,Cancel 阶段回滚释放。TCC 的优点是性能好,不依赖数据库事务,缺点是业务侵入性大,每个操作都要写三个方法。适合资金类、库存类等对一致性要求极高的场景。
2PC/3PC:两阶段提交和三阶段提交,依赖数据库的 XA 事务。优点是强一致,缺点是性能差、协调者单点问题。现在互联网系统里用得越来越少,更多用在传统企业级中间件里。
Saga:把一个长事务拆成多个本地事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,就反向执行补偿操作。Saga 适合业务流程长、参与方多的场景,比如旅游预订(机票+酒店+租车)。
实操心得:不管选哪种方案,幂等和重试是必须的。网络超时后你不知道对方到底处理了没有,所以重试是常态。重试就必须幂等,否则会出现重复扣款、重复下单。我一般用“业务唯一 ID + 数据库唯一索引”来做幂等,简单可靠。
4. 分布式架构的运维与治理:上线只是开始
4.1 监控体系:没有监控的分布式就是灾难
单体系统挂了,你看日志就行。分布式系统挂了,日志散落在十台机器上,你连从哪看起都不知道。所以分布式架构必须配套一套完整的监控体系。我一般分三层:基础设施监控、应用监控、业务监控。
基础设施监控看 CPU、内存、磁盘、网络,常用 Prometheus + Grafana。应用监控看 QPS、响应时间、错误率、线程池状态,常用 Micrometer + Prometheus 或者 SkyWalking。业务监控看订单量、支付成功率、库存周转,这个需要自己埋点,用 ELK 或者 ClickHouse 做分析。
链路追踪是分布式监控的核心。一次用户请求可能经过网关、订单服务、库存服务、支付服务,每个服务都有自己的日志。链路追踪通过一个全局 Trace ID 把整条链路串起来,你能清楚地看到每个环节的耗时和状态。SkyWalking、Zipkin、Jaeger 都是常用工具。我强烈建议在项目初期就接入链路追踪,不要等到出问题了再补,那时候改造成本很高。
4.2 限流、熔断、降级:保住核心链路
分布式系统里,一个非核心服务挂了,可能拖垮整个系统。比如评论服务响应变慢,导致调用它的商品详情页线程池被占满,进而导致整个商品服务不可用。这就是雪崩效应。防止雪崩的三大手段:限流、熔断、降级。
限流是控制入口流量,超过阈值的请求直接拒绝。常见算法有计数器、滑动窗口、令牌桶、漏桶。我一般用 Sentinel 或者 Resilience4j,配置简单,功能强大。限流的阈值怎么定?压测!没有压测数据的限流都是拍脑袋。先压出单机的极限 QPS,然后按 80% 设置阈值,留 20% 的缓冲。
熔断是当某个服务的错误率超过阈值时,自动切断对该服务的调用,直接返回兜底数据。熔断器有三个状态:关闭、打开、半开。关闭时正常调用,打开时直接拒绝,半开时放少量请求试探,如果成功就恢复关闭状态。熔断的关键是快速失败,不要让请求堆积。
降级是当系统压力过大时,主动关闭一些非核心功能,把资源让给核心功能。比如双十一期间,关闭商品评价、关闭历史订单查询,保证下单和支付可用。降级方案要提前准备好,并且要能动态开关,不能等到出问题了再改代码发版。
4.3 常见问题排查速查表
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 服务调用超时 | 网络抖动、对方服务慢、线程池满 | 看链路追踪、看对方服务监控 | 重试、熔断、扩容 |
| 数据不一致 | 消息丢失、重试失败、并发写 | 对账、查消息队列堆积 | 补偿、幂等、加锁 |
| 服务注册不上 | 网络不通、注册中心挂了 | 看注册中心日志、看网络 | 修复网络、注册中心集群 |
| 配置不生效 | 配置未推送、本地缓存 | 看配置中心推送记录 | 手动刷新、重启 |
| 消息重复消费 | 消费者未幂等、ACK 丢失 | 查消费日志、查消息 ID | 幂等处理、去重表 |
| 数据库连接满 | 慢查询、连接泄漏 | 看慢查询日志、看连接池监控 | 优化 SQL、加大连接池 |
这张表是我这些年排查问题的经验总结,基本上覆盖了 80% 的线上故障。遇到问题先查表,能快速定位方向,然后再深入分析。
避坑技巧:分布式系统里,日志一定要带 Trace ID。没有 Trace ID,你在十台机器的日志里找一次请求,无异于大海捞针。我一般用 MDC 把 Trace ID 放到 ThreadLocal 里,日志框架自动打印,排查效率提升十倍。
5. 从理论到落地:一个模拟项目的架构演进记录
5.1 初始阶段:单体应用快速上线
假设我们有一个模拟的在线课程系统,初期功能很简单:用户注册登录、课程展示、课程购买。团队三个人,两周上线。这个阶段用单体架构最合适:一个 Spring Boot 应用,一个 MySQL 数据库,部署在一台 4C8G 的云服务器上。代码分层清晰,Controller、Service、DAO 各司其职。这个阶段不要想分布式,快速验证业务才是第一优先级。
这个阶段的关键是把代码写好。我见过太多项目,一开始就想着以后要拆,结果代码写得乱七八糟,Service 之间互相调用,数据库表随便关联。等到真要拆的时候,发现根本拆不动。所以单体阶段就要有模块化意识,按业务分包,包与包之间通过接口调用,不要跨包直接访问对方的 DAO。这样以后拆的时候,直接把包拎出来就是一个服务。
5.2 成长阶段:读写分离与缓存引入
用户量涨到十万,课程详情页访问变慢,数据库 CPU 飙升。这时候先别急着拆服务,先做两件事:读写分离和缓存。读写分离就是把读请求分流到从库,主库只负责写。MySQL 主从复制配置很简单,应用层用 ShardingSphere 或者自己写个 AOP 切面,根据方法名判断走主库还是从库。缓存用 Redis,把课程详情、用户会话这些热点数据缓存起来,减少数据库压力。
这个阶段要注意缓存一致性。我一般用“先更新数据库,再删除缓存”的策略,配合延迟双删来降低不一致的概率。如果业务能接受短暂不一致,也可以直接用过期时间兜底。缓存穿透、缓存击穿、缓存雪崩这三个经典问题,解决方案分别是布隆过滤器、互斥锁、过期时间加随机值。
5.3 成熟阶段:服务拆分与治理
用户量破百万,团队扩到二十人,单体应用的发布变成瓶颈。这时候开始拆服务。第一批拆的是用户服务和课程服务,因为这两个模块变更最频繁,而且资源消耗特征不同。用户服务吃 CPU,课程服务吃数据库连接。拆完之后,各自独立部署、独立扩容,发布互不影响。
拆服务的同时,配套的基础设施要跟上:注册中心用 Nacos,配置中心也用 Nacos,网关用 Spring Cloud Gateway,链路追踪用 SkyWalking,限流熔断用 Sentinel。这些组件不需要一次性全上,按需引入。比如先上注册中心和网关,让服务能互相发现、外部流量能进来;再上配置中心,解决配置散落的问题;最后上链路追踪和限流熔断,提升可观测性和稳定性。
这个阶段最大的挑战是分布式事务。用户购买课程,需要扣减用户余额、生成订单、开通课程权限。这三个操作跨三个服务,必须保证一致性。我选的是本地消息表方案:在订单服务里,生成订单和写消息在同一个本地事务里完成,然后异步通知用户服务和课程服务。用户服务和课程服务处理完后回调确认,失败就重试。这个方案实现简单,性能好,适合我们的业务场景。
5.4 高阶阶段:多机房与单元化
用户量破千万,单机房已经扛不住了,开始考虑多机房部署。多机房的核心问题是数据同步和流量调度。数据同步一般用 MySQL 的主从复制或者双向复制,但双向复制会有冲突问题,需要业务层做冲突解决。流量调度用 DNS 或者 GSLB,根据用户地理位置把请求路由到最近的机房。
单元化是更高级的玩法,把用户按 ID 分片,每个单元独立处理一部分用户的所有请求,单元之间不互相调用。这样任何一个单元故障,只影响一部分用户,不会全站崩溃。单元化的实现复杂度很高,需要改造路由层、数据层、缓存层,一般只有超大规模系统才需要。对于大多数团队来说,同城双活已经足够,两个机房同时提供服务,数据同步,流量均分,一个机房挂了另一个能顶上。
6. 我踩过的坑与给你的实操建议
6.1 不要过早优化,但也不要拒绝演进
我见过两个极端。一个是团队三个人,上来就搞微服务、搞服务网格,结果三个月没上线,项目黄了。另一个是系统已经日活百万了,还在用单体,数据库天天报警,每次发版都提心吊胆。这两个极端都不可取。我的建议是:架构演进要跟着业务走,业务没到那个量级,不要上那个量级的架构。但同时在单体阶段就要做好模块化,为未来的拆分留好口子。
判断时机的方法很简单:当你觉得“不改架构就撑不下去了”的时候,就是该改的时候。具体信号包括:发布频率高到影响业务迭代、数据库成为明显瓶颈、团队沟通成本超过开发成本、可用性要求从 99% 提升到 99.99%。这些信号出现一个,就可以开始规划演进了。
6.2 分布式事务能不用就不用
分布式事务是分布式架构里最复杂、最容易出问题的部分。我的经验是:能通过业务设计规避的,就不要用技术方案硬解。比如用户注册送积分,不要做成一个事务,而是注册成功后发消息,积分服务异步处理。积分晚到几秒钟,用户根本感知不到。再比如订单和库存,不要用 TCC,而是用“下单锁库存、支付扣库存、超时释放库存”的流程,把一个大事务拆成多个小事务,用状态机来驱动。
如果业务真的不能容忍不一致,比如支付和订单,那就用本地消息表或者 TCC。但一定要做好幂等和重试,因为网络超时是常态,重试是必须的。幂等的实现,我推荐用“业务唯一 ID + 数据库唯一索引”,简单可靠,不依赖 Redis 的可用性。
6.3 监控和日志是分布式系统的生命线
分布式系统出问题是必然的,区别在于你能不能快速定位和恢复。我见过一个团队,服务拆了二十个,但没有链路追踪,线上出问题只能一个个服务登录上去看日志,平均故障恢复时间四个小时。后来上了 SkyWalking,恢复时间降到二十分钟。这个投入产出比太高了。
我的建议是:项目初期就接入链路追踪和集中式日志。链路追踪用 SkyWalking 或者 Jaeger,日志用 ELK 或者 Loki。Trace ID 要贯穿整个调用链,从网关一直传到数据库。日志格式要统一,包含时间戳、Trace ID、服务名、日志级别、线程名、消息内容。这样排查问题时,你只需要拿到 Trace ID,就能看到整条链路的完整日志。
6.4 团队能力比技术选型更重要
最后说一个容易被忽视的点:分布式架构对团队能力的要求比单体高得多。单体时代,一个开发就能搞定部署和运维。分布式时代,你需要懂注册中心、配置中心、网关、链路追踪、消息队列、分布式事务,还需要有人专门做监控和告警。如果团队没有这些能力,强行上分布式就是给自己挖坑。
我的建议是:先培养人,再上架构。让团队先在小项目里练手,熟悉这些中间件的使用和运维,然后再在核心项目里推广。同时要建立完善的文档和规范,比如服务命名规范、接口定义规范、日志规范、告警规范。这些看起来是小事,但能极大降低协作成本。
最后分享一个小技巧:每次架构演进后,做一次故障演练。故意把某个服务停掉、把网络延迟调高、把数据库主库切走,看看系统能不能自动恢复。这种演练能暴露很多平时发现不了的问题,比如熔断阈值设置不合理、重试次数过多导致雪崩、降级方案没生效。演练一次,比看十篇架构文章都有用。