还在忍受动辄千行的 God Service类?每次新增下游都像在代码废墟里“考古”?财务推送高峰期一个慢查询就能堵死核心流程? 本文分享团队一次成功的架构升级:用 COLA架构+CQRS模式重构“财务凭证推送”系统,使得Service瘦身90%,高频查询提速5倍+,推送吞吐怒涨40% ,,并建立可持续扩展的架构体系。
关键词: #架构设计
#DDD
#COLA架构
#CQRS
#事件驱动
#财务凭证推送
#Java
#代码重构
一、痛点:深陷“三层架构”泥潭的财务凭证推送
- 财务凭证推送需将财务凭证实时、准确、可靠分发至资金、报表等下游系统。旧系统采用“经典”三层架构(Controller-Service-Dao),随着业务膨胀,系统逐渐演变为典型的 “大泥球”架构:
- 💣 千行Service类“考古”难:凭证生成、校验、路由、推送等逻辑全塞进巨型Service类。修改代码如同“考古挖掘”,新增下游需在数千行代码中定位插入点,稍有不慎就引爆连锁BUG
- 🚦 读写 互斥 拖垮性能:高频状态查询与核心推送共用同一数据库。在月初财务做账推送高峰期,一个全表扫描直接打满DB连接池,导致推送线程阻塞超时,业务投诉激增
- 🧩 扩展性举步维艰: 新增下游需修改核心Service逻辑及数据库表,牵一发而动全身
- 🌀 领域逻辑湮没细节: 凭证的核心规则(借贷平衡、科目校验)和状态机(等待生成→生成→推送→成功/失败)散落在流程代码中,新人理解成本极高
- 🕵️ 故障定位如大海捞针: 推送链路跨多个系统,定位失败原因耗时耗力,SLA难以保障
我们迫切需要一场来自架构层面的升级! 经过深入调研和评估,我们团队选择了 COLA架构 + CQRS 模式 作为新财务凭证推送系统的基石
二、破局利器:为什么是COLA+CQRS?
1. COLA架构:根治代码混乱的良方
阿里开源的COLA架构,用强制分层约束代码边界:
- Adapter 层: 适配外部请求(HTTP, RPC, MQ等),做协议转换、基础参数校验,调用App层(对应我们的Controller、API Gateway、MQ Listener)。
- App层 (Application Layer): 协调领域能力,组合业务流程,事务控制在此层。(核心业务流程的编排者,类似优化后的Service层)
- Domain层 (Domain Layer):核心层, 包含领域模型(聚合根(Aggregate Root) 、值对象 ( Value Object ) )、领域服务(Domain Service) 、领域事件(Domain Event) 、仓储接口(Repository Interface)(“财务凭证”的核心业务规则和状态机在这里安家)
- Infrastructure 层: 提供技术细节实现,如数据库访问(Repository Impl)、消息队列发送、缓存、配置等。(纯技术实现,与业务无关)
层级 | 我们的改造重点 | 旧架构痛点 |
---|---|---|
Domain层 | 聚合根治了贫血模型- 借贷平衡校验内聚到FinanceVoucher |
- 状态机流转封装为聚合根方法 | 业务逻辑散落Service- 校验代码重复
- 状态变更分散 | | App层 | Command/ Query 分离,职责清晰 | 一个Service啥都干 | | Infra 层 | 推送 适配器 插件化 | HTTP/Feign代码硬耦合 |
✅ 核心价值:把核心业务规则从Service屎山里挖出来,代码可读性、可维护性飙升!
2. CQRS:读写分离的性能解药
CQRS ( Command Query Responsibility Segregation ) 即 命令查询职责分离 。 其核心思想是将修改状态的操作(Command) 和读取数据的操作( Query ) 分离,使用不同的模型和路径处理
- Command 端: 处理创建、更新、删除等写操作。关注业务规则、数据一致性、事务性。通常操作写库
- Query 端: 处理数据查询操作。关注性能、灵活性、数据展示需求。可以读取专门为查询优化过的视图库(如ES, ClickHouse, Redis缓存),模型可以与Command端完全不同
✅ 在凭证推送中的核心价值:
- 读写 解耦 : 核心的凭证生成和推送(写)不再受复杂查询(读)的干扰,双方可独立优化和扩展
- 性能优化: Query端可以使用更适合的存储(如ES索引),查询效率提升显著(我们实测状态查询速度提升5倍+)
- 模型灵活性: Command模型专注业务变更,Query模型专注展示需求,互不影响
- 事件驱动基础: CQRS 天然适合与领域事件结合,是实现最终一致性的理想伙伴
-
为什么选择 COLA + CQRS?
-
COLA提供了清晰的层次划分和领域建模指导,根治了代码组织混乱和领域逻辑不清晰的问题
-
CQRS精准命中了读写耦合带来的性能瓶颈和扩展性问题,为高性能、高扩展性扫清了障碍
-
1+1 > 2! COLA的清晰领域模型为CQRS的分离提供了良好的基础;CQRS的分离又让COLA各层(尤其是Domain层和App层)的职责更加纯粹。事件驱动(Domain Event)成为连接两者的自然桥梁。两者结合,让我们构建出既清晰表达业务(COLA),又能高性能、高扩展性处理复杂读写场景(CQRS)的系统。
三、架构蓝图:COLA+CQRS如何落地财务凭证推送
下图展示了新系统的整体架构,清晰体现了COLA分层与CQRS分离:
🔴 Command路径 : 生成凭证请求 → CommandHandler → 执行业务规则 → 保存聚合根 → 发布领域事件
处理创建/更新凭证的核心写流程。请求通过Adapter进入App层,由Command Handler协调Domain层(聚合根执行业务逻辑)和Infra层(持久化),并发布领域事件
🔵 Query****路径:接收查询请求 → QueryHandler → 访问优化视图库 → 返回状态数据
处理凭证状态查询。请求通过Adapter进入App层,由Query Handler直接调用Infra层查询专门优化的视图库
🟢 事件驱动路径:监听领域事件 → 异步加载凭证 → 路由转换 → 推送下游 → 更新状态
MQ Listener (Adapter) 监听领域事件,触发App层的推送处理服务。该服务加载凭证、协调转换、路由、调用Infra层的适配器推送至下游系统,并更新凭证状态。核心:异步 解耦 ,事件驱动
四、核心实现:领域、命令、查询与推送的优雅解耦
-
领域建模:让财务凭证“活”起来
核心:建立 FinanceVoucher
聚合根(Aggregate Root),内聚核心业务规则和状态
关键属性:
voucherId
(唯一标识)accountBookCode
(账簿编码)accountingDate
(记账日期)status
(状态:DRAFT
,GENERATED
,PUSHING
,SUCCESS
,FAILED
) // 核心状态机voucherType
(凭证类型)lineItems
(行项目集合,值对象 ****Value Object)sourceSystem
(来源系统)retryCount
(重试计数)pushResults
(记录推送各下游系统的结果)
核心行为(方法): 业务规则内聚在此
generate()
:执行核心校验规则(借贷平衡、科目有效性等),生成唯一凭证号,状态变更为GENERATED
。markAsPushing()
:状态变更为PUSHING
(推送中)。markAsSuccess(String targetSystem)
:记录成功推送的目标系统。markAsFailed(String targetSystem, String reason)
:记录推送失败的目标系统及原因,增加重试计数。canRetry()
:根据重试策略判断是否可重试。
领域服务 (Domain Service ): VoucherGenerationService
处理需要跨多个FinanceVoucher
聚合或外部服务的复杂生成逻辑
领域事件 (Domain Event): 实现解耦的关键
VoucherGeneratedEvent
:凭证成功生成后发布,包含voucherId
,基础信息VoucherStatusChangedEvent
:凭证状态变化时发布,包含voucherId
,oldStatus
,newStatus
// 用于追踪、更新视图、触发后续操作
仓储接口: VoucherRepository
(定义save
, findById
等操作)
领域层代码示例 (简化):
// Domain Layer 示例片段 (简化) public class FinanceVoucher { private String voucherId; private VoucherStatus status; private List<VoucherLineItem> lineItems; private int retryCount; // ... 其他属性和getter/setter public void generate () throws DomainException { // 1. 校验借贷平衡 BigDecimal debitTotal = lineItems.stream().filter(item -> item.getDirection() == Direction.DEBIT) .map(VoucherLineItem::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal creditTotal = lineItems.stream().filter(item -> item.getDirection() == Direction.CREDIT) .map(VoucherLineItem::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); if (debitTotal.compareTo(creditTotal) != 0 ) { throw new DomainException ( "Debit and credit totals must balance." ); } // 2. 其他校验 (科目有效性等) ... // 3. 生成唯一凭证号 (业务规则) ... // 4. 更新状态 this .status = VoucherStatus.GENERATED; } public void markAsPushing () { if ( this .status != VoucherStatus.GENERATED) { throw new IllegalStateException ( "Voucher must be GENERATED to start pushing." ); } this .status = VoucherStatus.PUSHING; } // ... 其他状态变更方法 } // 领域事件 public class VoucherGeneratedEvent implements DomainEvent { private String voucherId; private String accountBookCode; private LocalDate accountingDate; // ... getters, setters, constructor }
2. ## Command处理:清爽的“四步操作法”
目标:处理创建/修改凭证的请求,执行业务规则,发布事件
流程:
-
Controller (Adapter): 接收创建凭证的请求 (e.g., HTTP POST
/vouchers
) -
Command: 将请求参数封装为
GenerateVoucherCommand
(DTO) -
Command Handler (App Layer):
- 转换DTO为领域模型 (组装
FinanceVoucher
聚合根,可能调用VoucherGenerationService
) - 调用聚合根方法执行业务逻辑 (
voucher.generate()
- 核心规则在此执行) - 通过
VoucherRepository
(Infra Impl) 保存聚合根到写库 - 发布
VoucherGeneratedEvent
领域事件 (异步解耦后续推送)
- 转换DTO为领域模型 (组装
-
返回操作结果
Command Handler 代码示例 (App Layer):
// App Layer (Command Handler 示例)
@Service
@RequiredArgsConstructor // Lombok
public class GenerateVoucherCommandHandler {
private final VoucherRepository voucherRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public Response handle(GenerateVoucherCommand command) {
try {
// 1. 转换Command -> 领域模型 (简化,实际可能需要工厂或Builder)
FinanceVoucher voucher = new FinanceVoucher();
voucher.setAccountBookCode(command.getAccountBookCode());
voucher.setAccountingDate(command.getAccountingDate());
// ... 设置其他属性,填充lineItems (需要校验转换)
// 2. 执行业务逻辑 (核心在聚合根内部)
voucher.generate();
// 3. 保存聚合根
voucherRepository.save(voucher);
// 4. 发布领域事件 (异步解耦推送流程)
eventPublisher.publish(new VoucherGeneratedEvent(voucher.getVoucherId(), ...));
return Response.buildSuccess(voucher.getVoucherId());
} catch (DomainException e) {
// 处理业务异常
return Response.buildFailure(e.getMessage());
} catch (Exception e) {
// 处理系统异常
return Response.buildFailure("System error");
}
}
}
3. ## Query 处理流程:专库专用的“闪电查询”
目标:高效查询凭证状态(读),使用优化视图
流程:
-
Controller (Adapter): 接收查询请求 (e.g., HTTP GET
/vouchers/{id}/status
) -
Query: 封装为
GetVoucherStatusQuery
(包含voucherId
) -
Query Handler (App Layer):
- 调用专用的
VoucherStatusViewRepository
(Infra Impl) - 查询为状态展示优化的视图库 (如单独的状态表、ES索引、Redis缓存)
- 调用专用的
-
组装结果DTO并返回
Query Handler 代码示例 (App Layer):
// App Layer - 状态查询Handler(走优化视图库)
@Service
@RequiredArgsConstructor
public class GetVoucherStatusQueryHandler {
private final VoucherStatusViewRepository statusViewRepository; // 查询专用仓储
public VoucherStatusDTO handle(GetVoucherStatusQuery query) {
// 直接查询为读优化的视图表(如ES索引/Redis缓存)
VoucherStatusView view = statusViewRepository.findByVoucherId(query.getVoucherId());
if (view == null) {
throw new NotFoundException("Voucher not found");
}
return new VoucherStatusDTO(view.getVoucherId(), view.getStatus(), view.getLastUpdateTime());
}
}
// Infra Layer - 视图仓储实现(JDBC示例)
@Repository
public class JdbcVoucherStatusViewRepository implements VoucherStatusViewRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public VoucherStatusView findByVoucherId(String voucherId) {
// 高性能查询:无关联表、无业务逻辑,纯粹查状态!
String sql = "SELECT voucher_id, status, last_update_time FROM voucher_status_view WHERE voucher_id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{voucherId}, (rs, rowNum) ->
new VoucherStatusView(
rs.getString("voucher_id"),
VoucherStatus.valueOf(rs.getString("status")),
rs.getTimestamp("last_update_time").toLocalDateTime()
));
}
}
4. ## 事件驱动推送:插件化设计的精髓
目标:监听凭证生成/状态变更事件,异步、可靠地推送到下游系统
流程:
-
MQ Listener (Adapter): 监听消息队列 (e.g., RocketMQ) 中的
VoucherGeneratedEvent
/VoucherStatusChangedEvent
-
推送处理服务 (App Layer):
-
根据事件中的
voucherId
从VoucherRepository
加载凭证聚合根 (或部分信息) -
调用
voucher.markAsPushing()
更新状态为推送中 (需保存) -
调用路由引擎 (
VoucherRouter
) 根据凭证类型、账簿规则等确定目标下游系统列表 -
调用凭证转换器 (
VoucherConverter
) 将领域模型转换为下游系统要求的特定格式 (DTO)。(利用工厂VoucherConverterFactory
按目标系统获取转换器) -
遍历目标系统列表:
- 获取对应的推送适配器 (
PushAdapter
) (利用注册表PushAdapterRegistry
) - 调用适配器的
push
方法执行实际推送 (HTTP/Feign/MQ等) - 根据推送结果调用
voucher.markAsSuccess(targetSystem)
或voucher.markAsFailed(targetSystem, reason)
更新状态 (需保存)
- 获取对应的推送适配器 (
-
(可选) 发布新的
VoucherStatusChangedEvent
-
-
推送适配器 (Infra Layer): 实现类 (
HttpPushAdapter
,DubboPushAdapter
,FtpPushAdapter
等),封装与特定下游系统的通信细节、重试、超时、加密等
推送处理服务核心代码示例 (App Layer):
// App Layer (事件监听处理服务 - 简化核心流程)
@Service
@RequiredArgsConstructor
public class VoucherPushService {
private final VoucherRepository voucherRepository;
private final VoucherRouter voucherRouter; // 路由策略
private final VoucherConverterFactory converterFactory; // 转换器工厂
private final PushAdapterRegistry pushAdapterRegistry; // 适配器注册表
private final DomainEventPublisher eventPublisher;
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务
public void handlePushEvent(VoucherGeneratedEvent event) {
FinanceVoucher voucher = voucherRepository.findById(event.getVoucherId())
.orElseThrow(() -> new NotFoundException("Voucher not found: " + event.getVoucherId()));
// 1. 标记推送中
voucher.markAsPushing();
voucherRepository.save(voucher); // 更新状态
// 2. 确定推送目标 (路由规则)
List<String> targetSystems = voucherRouter.route(voucher);
// 3. 转换 & 推送
for (String targetSystem : targetSystems) {
try {
// 3.1 获取对应转换器
VoucherConverter converter = converterFactory.getConverter(targetSystem);
Object targetDto = converter.convert(voucher); // 转换为下游所需DTO
// 3.2 获取对应推送适配器并推送
PushAdapter adapter = pushAdapterRegistry.getAdapter(targetSystem);
PushResult result = adapter.push(targetDto);
// 3.3 处理结果
if (result.isSuccess()) {
voucher.markAsSuccess(targetSystem);
} else {
voucher.markAsFailed(targetSystem, result.getErrorMessage());
// 根据重试策略,可能触发重试逻辑 (记录失败,稍后重试)
}
} catch (Exception e) {
voucher.markAsFailed(targetSystem, "System error: " + e.getMessage());
}
voucherRepository.save(voucher); // 更新每个目标系统的推送状态
}
// 4. (可选) 发布最终状态事件
eventPublisher.publish(new VoucherStatusChangedEvent(voucher.getVoucherId(), VoucherStatus.GENERATED, voucher.getStatus()));
}
}
五、效果:从性能到扩展性的全面提升
指标 | 重构前 | 重构后 | 涨幅 |
---|---|---|---|
状态查询延迟 | 220ms | 28ms | ↓ 87% |
推送吞吐量 | 120 TPS | 170 TPS | ↑ 40% |
核心Service行数 | 3200行 | 200行 | ↓ 94% |
新增下游耗时 | 18人日 | 3人日 | ↓ 83% |
-
代码清晰度:从“泥潭”到“清泉”
- 领域逻辑内聚:
FinanceVoucher
聚合根治好了“贫血模型”,核心校验规则和状态机修改集中在1个文件,不再散落各处。 - 分层清晰: COLA强制分层让代码各归其位,“Controller乱跳Service,Service乱调Dao”成为历史。新人也能快速定位逻辑
- 扩展丝滑: 新增下游系统?只需实现新的
VoucherConverter
和PushAdapter
,并在路由引擎中配置。 核心业务流程 (VoucherPushService
,CommandHandler
) 纹丝不动,风险极低
- 领域逻辑内聚:
-
性能优化:读写分离的胜利
- 高频状态查询: 迁移到专门优化的状态视图表后,响应时间平均从 200ms+ 降至 30ms 以下 (提升5倍+),彻底消除对写库的压力
- 核心推送(写): 摆脱了查询干扰,推送吞吐量提升约40%,高峰期稳定性显著增强
-
扩展性显著增强
- 新增下游: 实现
Converter
+Adapter
→ 注册 → 配置路由,核心代码零改动! - CQRS分离为未来Query端深度优化(如迁移至Elasticsearch)扫清障碍
- 事件驱动使得推送流程天然异步解耦。新增一个凭证事件消费者(如审计日志服务)只需监听MQ并实现逻辑,对主流程0侵入
- 新增下游: 实现
-
领域知识沉淀
FinanceVoucher
聚合根及其行为、状态机、领域事件,成为团队理解“财务凭证”业务的统一语言(Ubiquitous Language)
六、踩坑实录:这样解决才靠谱
-
最终一致性问题:业务与技术妥协
症状:事件驱动和读写分离必然带来数据暂时不一致(如刚生成的凭证,状态视图可能还未更新)
解法:
-
业务妥协: 产品接受状态延迟≤3秒
-
技术双保险: 重试机制:指数退避 + 最大重试次数
-
// 事件消费更新视图(主渠道) @EventListener public void updateView(VoucherStatusChangedEvent e) { statusViewRepo.updateStatus(e.getVoucherId(), e.getNewStatus()); } // Binlog兜底(防事件丢失) @DtsListener(table="voucher") public void binlogFallback(List<DtsRecord> records) {...}
-
-
运营兜底: 开发管理后台供人工重试/标记
-
DDD学习曲线:聚合根边界之争
症状: 团队成员争论“聚合根边界在哪”
解法:
-
举办DDD工作坊: 组织多场DDD工作坊和COLA规范解读,在白板画聚合根边界
-
代码评审重点: 在代码评审中重点指导领域模型设计,严查领域逻辑泄漏到App层
-
建示范项目: 建立了一个精简的示例项目供参考,新人2小时看懂结构
-
视图数据同步:如何保证可靠
如何可靠、及时地将Commnd端的数据变更同步到Query端的视图库
-
主要方式: 监听 VoucherStatusChangedEvent ,由专门的服务消费事件更新视图库。//我们当前方案
-
兜底方式: 使用 DTS 监听数据库binlog 作为补充,确保极端情况下视图最终一致。
七、总结:清晰胜于混沌
这次重构用 COLA + CQRS 组合拳 精准打击了“大泥球”架构:
- 以领域模型为核心: 真正聚焦“财务凭证”的业务本质,内聚规则,沉淀知识。
- 明确分层职责: COLA的强制分层让代码各司其职,结构一目了然。
- 果断读写分离: CQRS彻底解耦读写,扫除了性能瓶颈,为独立优化铺路。
- 拥抱事件驱动: 利用领域事件实现流程解耦、最终一致性和强大的可追溯性。
架构没有银弹,但清晰胜于混沌!
🚀 如果你的系统正在经历:
- Service类突破千行还在膨胀
- 读写互相伤害引发线上故障
- 不敢动“祖传代码”的窒息时刻
别犹豫!这套组合拳,值得你全力一试。