💥 破局“大泥球”!COLA+CQRS让财务凭证推送飞起来(附源码思路)

374 阅读14分钟

还在忍受动辄千行的 God Service类?每次新增下游都像在代码废墟里“考古”?财务推送高峰期一个慢查询就能堵死核心流程? 本文分享团队一次成功的架构升级:用 COLA架构+CQRS模式重构“财务凭证推送”系统,使得Service瘦身90%,高频查询提速5倍+,推送吞吐怒涨40% ,,并建立可持续扩展的架构体系。

关键词: #架构设计 #DDD #COLA架构 #CQRS #事件驱动 #财务凭证推送 #Java #代码重构

一、痛点:深陷“三层架构”泥潭的财务凭证推送

  1. 财务凭证推送需将财务凭证实时、准确、可靠分发至资金、报表等下游系统。旧系统采用“经典”三层架构(Controller-Service-Dao),随着业务膨胀,系统逐渐演变为典型的 “大泥球”架构
  1. 💣 千行Service类“考古”难:凭证生成、校验、路由、推送等逻辑全塞进巨型Service类。修改代码如同“考古挖掘”,新增下游需在数千行代码中定位插入点,稍有不慎就引爆连锁BUG
  2. 🚦 读写 互斥 拖垮性能:高频状态查询与核心推送共用同一数据库。在月初财务做账推送高峰期,一个全表扫描直接打满DB连接池,导致推送线程阻塞超时,业务投诉激增
  3. 🧩 扩展性举步维艰: 新增下游需修改核心Service逻辑及数据库表,牵一发而动全身
  4. 🌀 领域逻辑湮没细节: 凭证的核心规则(借贷平衡、科目校验)和状态机(等待生成→生成→推送→成功/失败)散落在流程代码中,新人理解成本极高
  5. 🕵️ 故障定位如大海捞针: 推送链路跨多个系统,定位失败原因耗时耗力,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 天然适合与领域事件结合,是实现最终一致性的理想伙伴
  1. 为什么选择 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层的适配器推送至下游系统,并更新凭证状态。核心:异步 解耦 ,事件驱动

四、核心实现:领域、命令、查询与推送的优雅解耦

  1. 领域建模:让财务凭证“活”起来

核心:建立 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:凭证状态变化时发布,包含voucherIdoldStatusnewStatus// 用于追踪、更新视图、触发后续操作

仓储接口: 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处理:清爽的“四步操作法”

目标:处理创建/修改凭证的请求,执行业务规则,发布事件

流程:

  1. Controller (Adapter): 接收创建凭证的请求 (e.g., HTTP POST /vouchers)

  2. Command: 将请求参数封装为 GenerateVoucherCommand (DTO)

  3. Command Handler (App Layer):

    1. 转换DTO为领域模型 (组装 FinanceVoucher 聚合根,可能调用VoucherGenerationService)
    2. 调用聚合根方法执行业务逻辑 (voucher.generate() - 核心规则在此执行)
    3. 通过 VoucherRepository (Infra Impl) 保存聚合根到写库
    4. 发布 VoucherGeneratedEvent 领域事件 (异步解耦后续推送)
  4. 返回操作结果

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 处理流程:专库专用的“闪电查询”

目标:高效查询凭证状态(读),使用优化视图

流程:

  1. Controller (Adapter): 接收查询请求 (e.g., HTTP GET /vouchers/{id}/status)

  2. Query: 封装为 GetVoucherStatusQuery (包含 voucherId)

  3. Query Handler (App Layer):

    1. 调用专用的 VoucherStatusViewRepository (Infra Impl)
    2. 查询为状态展示优化的视图库 (如单独的状态表、ES索引、Redis缓存)
  4. 组装结果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. ## 事件驱动推送:插件化设计的精髓

目标:监听凭证生成/状态变更事件,异步、可靠地推送到下游系统

流程:

  1. MQ Listener (Adapter): 监听消息队列 (e.g., RocketMQ) 中的 VoucherGeneratedEvent / VoucherStatusChangedEvent

  2. 推送处理服务 (App Layer):

    1. 根据事件中的voucherIdVoucherRepository 加载凭证聚合根 (或部分信息)

    2. 调用 voucher.markAsPushing() 更新状态为推送中 (需保存)

    3. 调用路由引擎 (VoucherRouter) 根据凭证类型、账簿规则等确定目标下游系统列表

    4. 调用凭证转换器 (VoucherConverter) 将领域模型转换为下游系统要求的特定格式 (DTO)。(利用工厂VoucherConverterFactory按目标系统获取转换器)

    5. 遍历目标系统列表:

      • 获取对应的推送适配器 (PushAdapter) (利用注册表PushAdapterRegistry)
      • 调用适配器的push方法执行实际推送 (HTTP/Feign/MQ等)
      • 根据推送结果调用 voucher.markAsSuccess(targetSystem)voucher.markAsFailed(targetSystem, reason) 更新状态 (需保存)
    6. (可选) 发布新的 VoucherStatusChangedEvent

  3. 推送适配器 (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()));
    }
}

五、效果:从性能到扩展性的全面提升

指标重构前重构后涨幅
状态查询延迟220ms28ms↓ 87%
推送吞吐量120 TPS170 TPS↑ 40%
核心Service行数3200行200行↓ 94%
新增下游耗时18人日3人日↓ 83%
  1. 代码清晰度:从“泥潭”到“清泉”

    • 领域逻辑内聚: FinanceVoucher聚合根治好了“贫血模型”,核心校验规则和状态机修改集中在1个文件,不再散落各处。
    • 分层清晰: COLA强制分层让代码各归其位,“Controller乱跳Service,Service乱调Dao”成为历史。新人也能快速定位逻辑
    • 扩展丝滑: 新增下游系统?只需实现新的 VoucherConverter PushAdapter,并在路由引擎中配置。 核心业务流程 (VoucherPushService, CommandHandler) 纹丝不动,风险极低
  1. 性能优化:读写分离的胜利

    • 高频状态查询: 迁移到专门优化的状态视图表后,响应时间平均从 200ms+ 降至 30ms 以下 (提升5倍+),彻底消除对写库的压力
    • 核心推送(写): 摆脱了查询干扰,推送吞吐量提升约40%,高峰期稳定性显著增强
  1. 扩展性显著增强

    • 新增下游: 实现 Converter + Adapter → 注册 → 配置路由,核心代码零改动!
    • CQRS分离为未来Query端深度优化(如迁移至Elasticsearch)扫清障碍
    • 事件驱动使得推送流程天然异步解耦。新增一个凭证事件消费者(如审计日志服务)只需监听MQ并实现逻辑,对主流程0侵入
  1. 领域知识沉淀

    • FinanceVoucher聚合根及其行为、状态机、领域事件,成为团队理解“财务凭证”业务的统一语言(Ubiquitous Language)

六、踩坑实录:这样解决才靠谱

  1. 最终一致性问题:业务与技术妥协

症状:事件驱动和读写分离必然带来数据暂时不一致(如刚生成的凭证,状态视图可能还未更新)

解法:

  • 业务妥协: 产品接受状态延迟≤3秒

  • 技术双保险: 重试机制:指数退避 + 最大重试次数

    •   // 事件消费更新视图(主渠道)
        @EventListener
        public void updateView(VoucherStatusChangedEvent e) {
            statusViewRepo.updateStatus(e.getVoucherId(), e.getNewStatus());
        }
      
        // Binlog兜底(防事件丢失)
        @DtsListener(table="voucher")
        public void binlogFallback(List<DtsRecord> records) {...}
      
  • 运营兜底: 开发管理后台供人工重试/标记

  1. DDD学习曲线:聚合根边界之争

症状: 团队成员争论“聚合根边界在哪”

解法:

  • 举办DDD工作坊: 组织多场DDD工作坊和COLA规范解读,在白板画聚合根边界

  • 代码评审重点: 在代码评审中重点指导领域模型设计,严查领域逻辑泄漏到App层

  • 建示范项目: 建立了一个精简的示例项目供参考,新人2小时看懂结构

  1. 视图数据同步:如何保证可靠

如何可靠、及时地将Commnd端的数据变更同步到Query端的视图库

  • 主要方式: 监听 VoucherStatusChangedEvent ,由专门的服务消费事件更新视图库。//我们当前方案

  • 兜底方式: 使用 DTS 监听数据库binlog 作为补充,确保极端情况下视图最终一致。

七、总结:清晰胜于混沌

这次重构用 COLA + CQRS 组合拳 精准打击了“大泥球”架构:

  • 以领域模型为核心: 真正聚焦“财务凭证”的业务本质,内聚规则,沉淀知识。
  • 明确分层职责: COLA的强制分层让代码各司其职,结构一目了然。
  • 果断读写分离: CQRS彻底解耦读写,扫除了性能瓶颈,为独立优化铺路。
  • 拥抱事件驱动: 利用领域事件实现流程解耦、最终一致性和强大的可追溯性。

架构没有银弹,但清晰胜于混沌!

🚀 如果你的系统正在经历:

  • Service类突破千行还在膨胀
  • 读写互相伤害引发线上故障
  • 不敢动“祖传代码”的窒息时刻

别犹豫!这套组合拳,值得你全力一试。