上一篇 《Dubbo 入门:什么是 RPC 与服务治理?》 讲了 Dubbo 是什么。这一篇进入实战最头疼的问题:分布式改造时,系统该怎么拆?
子系统划分:为什么拆不对就全盘皆输
把单体拆成多个服务,第一刀如果切错了,后面所有的 Dubbo 配置、集群容错、链路追踪都是在错误的地基上盖楼。当年踩坑总结出的四大原则,每一条都是血泪教训。
原则一:不要出现跨库 SQL 查询
不要出现 A 服务的 SQL 需要连接查询 B 服务中的表。
拆分的依据是数据而不是代码。每个服务拥有自己独立的数据库,服务 A 只能通过 RPC 调用服务 B 的接口拿数据,绝不能直接连 B 的库做 JOIN。
❌ 错误:A 服务的 SQL 直接 join B 服务的表
A服务(库A) SELECT * FROM a_tbl JOIN b_tbl ...
✅ 正确:A 服务通过 RPC 调 B 服务的接口
A服务 --dubbo调用--> B服务.getXxx() --> 查库B
为什么这条最重要?因为垂直拆库(把单库拆成多个独立库)是服务化的必然结果。如果服务间的数据耦合剪不断,将来做垂直拆库时所有跨库 JOIN 全部报错,到时候再回头拆服务,成本翻十倍。
判断标准:两个业务模块的表之间是否存在强关联查询?如果有,要么合并成一个服务,要么通过接口把数据”取过来再组装”。
原则二:避免环状依赖调用
子系统间避免环状依赖调用(A 服务调用 B,B 调用 C,C 调用 A)。
❌ 环状依赖(无法初始化,也无法升级)
A ---调用---> B
^ |
| v
+---调用------- C
✅ 分层依赖(单向依赖,边界清晰)
A ---> B ---> C
环状依赖的危害:
- 启动死锁:A 启动时要初始化 B 的引用,B 启动时要初始化 C,C 又要 A——互相等,服务起不来。
- 升级困难:改 A 会影响 B,改 B 会影响 C,改 C 又回到 A,牵一发动全身。
- 故障传播:任何一个环节出问题,调用链会绕圈打转,故障定位极其困难。
解决思路:把环打断,让依赖变成单向分层。如果确实存在双向业务需求,通过引入中间层(如消息队列、公共服务)解耦。
原则三:服务间关系链不要过长
服务间关系链不要过长(A 调用 B,B 调用 C,C 调用 D…)。
❌ 调用链过长:一次请求穿 5 个服务
A -> B -> C -> D -> E
✅ 调用链尽量短:聚合服务把结果组装好再返回
A -> B(B 内部聚合 C、D)
链路过长带来的问题:
- 响应时间累加:每一跳都有网络开销 + 序列化开销,5 跳就是 5 倍延迟,用户等不起。
- 故障放大:链路上任何一个服务慢或挂,整个请求失败;越长的链路,故障概率越高。
- 排查困难:出了问题要顺着链路一个一个查,日志分散在多个服务里。
解决思路:优先保证关键链路短。把多个服务合并成”聚合服务”,或者让中间服务并行调用多个下游再组装结果。
原则四:尽量避免分布式事务
尽量避免分布式事务。
单体时代一个事务搞定的事,拆分后变成了跨服务的数据一致性难题。分布式事务方案(两阶段提交、TCC、最终一致性)要么性能差,要么实现复杂,都是当年踩过的坑。
❌ 跨服务强一致:A 服务扣库存 + B 服务扣余额,要求同时成功
✅ 最终一致:A 先成功,通过消息队列通知 B 异步处理,失败补偿
解决思路:
- 能不分就不分:把强一致相关的数据留在同一个服务、同一个库,用本地事务解决。
- 必须分时用最终一致性:本地事务 + 消息队列 + 补偿机制,允许短暂不一致,最终达到一致。
- 实在要强一致:才考虑 TCC 等方案,但要把性能开销和实现复杂度算清楚。
接口设计规范
子系统拆完之后,Dubbo 接口的设计直接影响后续的维护成本。当年总结的经验:
接口要面向业务设计
- 一个接口对应一个业务语义,不要为了”通用”设计出 God Interface(上帝接口)。
- 方法粒度适中:太粗(一个方法做十件事)难复用,太细(十个小方法)增加调用次数。
参数对象化
RPC 接口的参数尽量用对象而不是散落的多个基本类型:
// ❌ 参数散乱,新增字段就要改接口
User getUser(String name, Integer age, String phone, String email);
// ✅ 参数对象化,新增字段不影响已有调用方
User getUser(QueryUserRequest request);
参数对象要实现 Serializable
public class QueryUserRequest implements Serializable {
private static final long serialVersionUID = 1L; // 显式声明,防止序列化兼容问题
private String name;
// getter/setter ...
}
Dubbo 的远程调用基于序列化传输,所有跨服务的参数对象和返回对象必须实现 Serializable,否则运行时抛 NotSerializableException。
接口版本管理
接口一旦发布,就永远不要修改它——升级必须通过版本号。
<!-- 提供方声明版本 -->
<dubbo:service interface="com.example.UserService" ref="userServiceImpl" version="1.0.0"/>
<!-- 消费方引用指定版本 -->
<dubbo:reference id="userService" interface="com.example.UserService" version="1.0.0"/>
服务升级时,新旧版本可以同时存在(新版本发布、旧版本下线前给调用方迁移时间),这就是 Dubbo 接口版本管理最大的价值。
超时与重试配置
服务拆分后,网络调用有不确定性,超时重试必须显式配置(当年不配超时导致请求卡死的问题太常见):
<!-- 推荐在服务端配置超时 -->
<dubbo:provider timeout="3000" retries="2"/>
<!-- 消费方可覆盖 -->
<dubbo:reference interface="com.example.UserService" timeout="3000" retries="2"/>
配置建议:
- 超时:根据接口的耗时特征设置,一般 1~3 秒;写操作可以更长,但不能不设。
- 重试:幂等接口可以重试(
retries="2");非幂等的写操作(下单、扣款)重试次数设为 0,否则重复调用会造成重复扣款——这是最经典的线上事故。 - 服务端配置优先级高于消费端,团队约定在服务端统一配置,避免各消费方各配各的。
小结
- 拆分四大原则:不跨库 SQL、不环状依赖、链路不过长、尽量避免分布式事务。
- 接口规范:面向业务设计、参数对象化、实现 Serializable、用版本号管理接口演进。
- 超时重试:必须显式配置;写操作重试设 0,防止重复扣款。
下一篇 《Dubbo 注册中心与直连调试:只订阅、只注册、多注册中心》 讲开发调试阶段怎么连服务。