声明:本文仅用于技术交流,部分内容已作脱敏。
一、背景
1.1 为什么要做服务拆分?
在探讨寄件系统的服务拆分方案之前,我们先从系统、业务和开发三个不同视角来理解服务拆分的必要性。
系统视角:服务拆分是提升系统质量的重要手段。首先,当系统面临可用性或性能瓶颈时,将业务流程拆分可以针对性地解决问题;其次,当系统内功能耦合度较高时,某个功能异常往往会影响其他功能,拆分能有效隔离故障;最后,当服务体量过大时,纵向增加资源的边际收益递减,横向拆分可以更高效地利用系统资源。
业务视角:服务拆分是支持业务增长的必然选择。首先,假设每日寄件订单量达到 1000 万,且按每年 30% 的速度增长,分而治之的策略才能满足这种规模的业务需求;其次,业务方希望快速上线新需求,服务轻量化能显著提升迭代的敏捷性。
开发视角:服务拆分是提升研发效率的关键举措。重量级服务给功能开发和代码维护带来了诸多阻碍,拆分后每个服务更轻量,也便于团队协作和知识传承。
1.2 寄件系统现状及目标
当前面临的问题:
寄件系统目前主要面临三类核心问题:
- 高可用性挑战:服务体量庞大,核心与非核心场景耦合在同一服务中,给业务带来可用性隐患。
- 功能耦合度高:代码改动牵一发而动全身,往往涉及多个业务场景,存在研发质量隐患。
- 开发难度大:编译启动需要 10 分钟以上,单元测试难度大,严重影响研发效率。
从数据层面看,寄件系统有 130 多张数据表,全部放在一个数据库中,数据表与服务耦合严重。由于功能依赖于数据,要想实现功能的解耦,数据层也必须同步进行拆分。
基于以上分析,对寄件服务和数据表进行拆分已成为必然选择。
建设目标:
通过此次服务拆分,我们期望达成三个核心目标:
- 提高寄件系统可用性,减少业务损失
- 提升业务迭代敏捷性,降低开发工时
- 提高研发质量,降低缺陷率
二、拆分方案
2.1 寄件流程概览
在制定拆分方案之前,我们首先梳理寄件业务的完整生命周期,如下图所示:

寄件完整流程主要分为三个关键环节:
第一步:寄件下单

第二步:投递入柜

第三步:快递员揽收

2.2 场景分析
基于上述业务流程,我们从三个维度进行场景分析:业务流(站在用户视角梳理完整流程)、能力(围绕业务流程,列举系统需要提供的能力)、事件(围绕业务流程,列举过程中产生的系统事件)。
场景一:寄件下单

场景二:投递入柜

场景三:快递员揽收

2.3 寄件领域模型
将上述三个场景的分析结果进行汇总,我们可以得到更全面的视图:
- 绿色:业务核心(实体/业务动作)
- 蓝色:相关能力(接口服务)
- 红色:业务事件

基于汇总分析,我们最终得到寄件领域模型:

2.4 服务拆分
在进行服务拆分时,我们主要考虑两个核心原则:职责单一性 (每个服务只负责一个明确的业务子域)和软件包大小(控制服务的复杂度和规模)。基于这两个原则,我们将寄件系统拆分为四个独立服务:
- 下单服务
- 投递服务
- 揽收服务
- 基础数据服务(以下简称基础服务)
下单服务能力范围:
| 类别 | (主要)接口/能力 |
|---|---|
| 用户 | 查询寄件人实名、查询寄件人登录手机号、实名认证 |
| 快递公司 | 查询快递公司列表 |
| 订单 | 下单、查询订单、查询订单详情、修改订单 |
| 支付 | 预锁格口支付、上门件支付、超重支付、微信支付分支付、支付回调 |
| 寄件码 | 生成寄件码 |
| 地址 | 保存地址 |
| 柜机 | 查看附近柜机、预锁格口、更换格口 |
| 其它场景 | 取消订单、用户评价 |
投递服务能力范围:
| 类别 | (主要)接口/能力 |
|---|---|
| 寄件码 | 查询寄件码(查下单服务) |
| 柜机 | 查询可用格口 |
| 支付 | 查询寄件钱包、查询优惠券、柜机支付、支付回调、柜机支付查询、退款、退款回调 |
| 运单 | 运力下单、物流轨迹注册、接收物流轨迹 |
| 订单 | 取消寄件、核验手机尾号、转单 |
| 其它场景 | 用户取消投递、用户柜机取消、用户取返 |
揽收服务能力范围:
| 类别 | (主要)接口/能力 |
|---|---|
| 订单 | 查询待揽收列表、核验手机尾号、取消收件、接收巴枪揽收 |
| 运单 | 接收运费/重量 |
| 支付 | KA支付推送 |
| 结算 | 结算 |
| 其它场景 | 快递员返件、快递员取消返件 |
基础服务能力范围:
| 类别 | (主要)数据表 |
|---|---|
| 区域 | 省市区表、字典表 |
| 基础能力 | 熔断表、重试任务表 |
2.5 数据层拆分
服务拆分的同时,数据层也需要同步进行拆分,以实现数据与服务的解耦。
AS-IS(拆分前) :所有数据表都集中在一个数据库中

TO-BE(拆分后) :数据按服务边界进行拆分,每个服务拥有独立的数据库

三、架构设计
基于上述拆分方案,我们进一步设计完整的系统架构。
3.1 业务架构
业务架构从业务视角展现系统的整体结构:

3.2 应用架构
应用架构从技术视角展现服务的组织方式:

四、工程代码落地
有了清晰的架构设计,接下来我们来看如何在工程代码层面实现这些设计。
4.1 工程清单
每个服务都采用统一的工程结构,便于团队协作和代码维护。
css
# 下单服务
order-service
|- order-service-api rpc接口定义
|- order-service-app-server web 模块,http接口接入层
|- order-service-biz biz 模块,存放核心业务逻辑
|- order-service-dao 数据处理层
# 投递服务
post-service
|- post-service-api rpc接口定义
|- post-service-app-server web 模块,http接口接入层
|- post-service-biz biz 模块,存放核心业务逻辑
|- post-service-dao 数据处理层
# 揽收服务
pick-service
|- pick-service-api rpc接口定义
|- pick-service-app-server web 模块,http接口接入层
|- pick-service-biz biz 模块,存放核心业务逻辑
|- pick-service-dao 数据处理层
# 基础服务
basic-service
|- basic-service-api rpc接口定义
|- basic-service-app-server web 模块,http接口接入层
|- basic-service-biz biz 模块,存放核心业务逻辑
|- basic-service-dao 数据处理层
每个服务内部采用统一的分层结构:
css
{服务名}
|- {服务名}-api [rpc接口定义]
|- enums 枚举类
|- event 消息事件
|- facade Dubbo接口
|- model 接口请求响应对象
|- {服务名}-app-server [web 模块,http接口接入层]
|- async 消息处理
|- rpc Dubbo接口实现
|- web Web接口实现
|- {服务名}-biz [biz 模块,存放核心业务逻辑]
|- bo 业务对象
|- enums 枚举类
|- exception 异常类
|- remote 外部http远程调用
|- service 业务实现
|- util 工具类
|- {服务名}-dao [数据处理层]
|- entity 实体类
|- mapper MyBatis Mapper
|- util 工具类
工程依赖关系:

4.2 通用功能包
为了避免重复建设,我们将各服务通用的功能抽取到公共模块中:
perl
send-common
|- send-common-app 应用通用包
|- send-common-db db模块通用包
|- send-common-test 单元测试工具包
send-common-app 提供的特性:
- 字典表缓存、渠道表缓存、区域表缓存
- 参数检验过滤器(RequestValidationFilter)
- 接口访问日志(Http/Dubbo)
- 多数据源(MySQL/Redis)
send-common-db 提供的特性:
- 动态切换数据源
- 数据加解密
- 影子库
4.3 使用示例
下面通过几个实际例子展示如何使用这些通用功能。
示例一:多数据源配置
ini
#主库
send.main.datasource.url=jdbc:mysql://localhost:3306/send?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
send.main.datasource.username=root
send.main.datasource.password=[ENCRYPT]974UEpo2O6+KF79hjq7JCu9D+6OM/hk2
#备库1
send.backup.datasource.url=jdbc:mysql://localhost:3306/send?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
send.backup.datasource.username=root
send.backup.datasource.password=[ENCRYPT]974UEpo2O6+KF79hjq7JCu9D+6OM/hk2
#备库2
send.report.datasource.url=jdbc:mysql://localhost:3306/send?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
send.report.datasource.username=root
send.report.datasource.password=[ENCRYPT]974UEpo2O6+KF79hjq7JCu9D+6OM/hk2
#影子库
send.shadow.datasource.url=jdbc:mysql://localhost:3306/send?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
send.shadow.datasource.username=root
send.shadow.datasource.password=[ENCRYPT]974UEpo2O6+KF79hjq7JCu9D+6OM/hk2
示例二:动态切换数据源
java
@Test
public void query_switchDataSource() {
DictionaryEntity dictionary = SwitchDataSourceHelper.start(DynamicDataSourceType.SOURCE_SALVE_01)
.execute(() -> dictionaryMapper.selectByPrimaryKey(2));
Assert.assertEquals("17:00:00", dictionary.getDictValue());
}
示例三:单元测试
在进行单元测试时,我们可以通过以下方式灵活配置环境:
- 可通过配置
spring.profiles.active=dev来运行 dev 环境的单元测试; @SpringBootTest中的配置项可覆盖 Disconf 的配置,下面示例中切换了备库的数据源;
less
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// 这里的配置项可覆盖 Disconf 的配置
"spring.profiles.active=uat6",
// 将备库地址换成其他实例
"send.backup.datasource.url=jdbc:mysql://localhost:3306/send?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false",
"send.backup.datasource.username=root",
"send.backup.datasource.password=[ENCRYPT]974UEpo2O6+KF79hjq7JCu9D+6OM/hk2"
})
public class SendDictionaryServiceImplTest {
@Autowired
private FcboxSendDictionaryMapper dictionaryMapper;
@Test
public void query_switchDataSource() {
FcboxSendDictionary dictionary = SwitchDataSourceHelper.start(DynamicDataSourceType.SOURCE_SALVE_01)
.execute(() -> dictionaryMapper.selectByPrimaryKey(2));
Assert.assertEquals("17:00:00", dictionary.getDictValue());
}
}

封面
