【中】CQRS读视图,数据聚合方案选型
系统中有四种类型的订单,A、B、C、D,原本它们是单独的业务,在微服务架构中也是单独的服务,都有各自的业务表。现在业务希望在客户端的一个页面按照订单的时间来进行排序展示。
一、开始
这是一个典型的数据聚合需求,常用的解决方案如下
| 方案 | 实时性 | 开发成本 | 查询性能 | 架构清晰度 | 描述 |
|---|---|---|---|---|---|
| 方案一:BFF 聚合层 | 实时 | 中 | 差 | ★★★★★ | 在客户端服务查询一个用户的所有数据,然后内存分组展示 |
| 方案二:服务扩展 | 实时 | 低 | 中 | ★★ | 相当于让A服务做聚合层了 |
| 方案三:CQRS 读模型 | 最终一致 | 高 | 高 | ★★★★ | 维护一个就诊订单数据 |
| 方案四:网关聚合 | 实时 | 中 | 中 | ★★★★ | 和方案一类似 |
| 方案五:方案一+方案四 | 实时+最终一致 | 极高 | 高 | ★★★★ | 1. 进行中的就数据用方案一 2. 历史中的数据用方案四 |
- 客户端查询订单肯定是基于用户来的,如果业务上一个用户的订单最多不超过2k,那一次性加载到内存是可行的,然后内存分页。【方案一】 是最好的选择,成本低,维护性好,业务自己去迭代自己的逻辑,这里是需要查询就好了。
- 如果数据量多到必须用真实的分页(或者还有管理后台的需求),那就得用【方案三】,把真实的业务数据都聚合到实实在在的表里面去。
- 其它几个方案,其实都是 【方案一】和【方案三】的扩展
二、BFF 聚合层 VS CQRS 读模型
| 维度 | 方案一:BFF 聚合层 | 方案四:CQRS 读模型 |
|---|---|---|
| 一致性 | 强一致,实时反映最新状态 | 最终一致(通过MQ监听数据变更) |
| 用户感知 | 操作后立刻看到新状态 | 操作后秒级更新 |
| 数据排查 | 直接看下游服务,链路清晰 | 需排查:binlog 消费→聚合表写入→查询结果,链路略长 |
| 新增下游业务 | 加一路 RPC 调用 | 加一个 Canal 数据源 + 映射规则 |
| 下游字段变更 | 改 BFF 聚合逻辑 | 改映射规则 + 聚合表加字段(需变更表结构) |
| 查询性能上限 | 受限于下游服务响应和内存合并 | 单表查询,性能天花板极高 |
| 核心难点及问题 | 每次都必须查询当前用户的所有业务的全部数据,内存做分组(进行中、历史)、分页 | 1. 要维护数据同步中间件 2. 历史数据回刷(业务有变动可能涉及历史数据回刷) 3. 被监听的相关表处理历史的时候要谨慎,不然很容易消息堆积导致数据不一致 |
| 架构清晰度 | ★★★★★ | ★★★★ |
| 适用场景 | 订单量少(单用户百条以内)、强一致性要求、快速上线 | 订单量大(千条以上)、复杂查询、对下游服务稳定性有顾虑 |
如果业务上可以用方案一,那自然是最好了,成本和风险都低。但大多数时候是要用方案三,那么来基于方案三进行设计。
三、CQRS 读模型
3-1、 数据存储方案
| 维度 | MySQL | ES |
|---|---|---|
| 存储源 | MySQL | ES |
| 优点 | 数据可靠性强(ACID)、运维简单、团队熟悉、复用现有实例零成本、精确查询和分页性能好 | 全文检索极快、模糊匹配强、海量数据下查询性能天花板高 |
| 缺点 | 不支持全文检索,模糊查询慢,大数据量时性能有上限 | 数据可靠性弱于 MySQL(异步刷盘)、运维复杂、存储成本高、Schema 变更麻烦 |
| 总结 | 目前的需求和性能 MySQL 都满足,但后续扩展不如 ES |
3-2、数据同步方式及问题
| 维度 | 方案一:CDC | 方案二:开发一个 starter,用观察者模式监控指定表变更,发送 MQ | 方案三:业务埋点 | 方案四:定时轮询 |
|---|---|---|---|---|
| 实时性 | 秒级 | 秒级 | 秒级 | 分钟级 |
| 侵入性 | 零 | 中 | 高 | 零 |
| 可靠性 | 高 | 高 | 中 | 较高 |
| 额外组件 | Canal/Maxwell + MQ | MQ | MQ | xxl-job |
| 复杂度 | 高 | 高 | 高 | 低 |
| 优点 | 1. 实时性高 2. 不影响数据库的性能 | 1. 实时性高 2. 不用新加中间件 | 1. 实现上简单 | |
| 缺点 | 要多维护一个中间件 | 1. 代码有侵入性 2. 稍微影响原本业务的性能 | 1. 实时性低 2. 完全依赖业务数据的 updateTime 3. 会影响业务数据库 |
- 这里的方案并不是择其一,可以组合,最好的方案就是 CDC + 定时轮询,这样实时性有保障,也都对帐兜底策略。
- CDC简单来说就是订阅数据库的日志,比如MySQL的binlog,然后同步到消息队列里面去,再去消费处理。
四、一些重要的问题
4-1、CDC的选择
- 能用云就用云,阿里云腾讯云都有自己的数据同步DTS。但是会有一些个限制,比如腾讯云只能把订阅发送到固定的kafka,消费去接入消费,不支持自己的mq。
- 开源的产品有 canal、maxwell 等等
4-2、大批量数据更新如何处理?
CDC是基于binlog,定时任务是基于表最后更新时间, 如果同一时间大批量更新,就会出现
- 自建的canal内存溢出
- 消息延迟消费
要提前暂停CDC和定时任务的同步。在基于业务表更新的字段,判断是否对新表有影响,如果有是需要手动处理数据的。如果判断数据量没那么大,也可以在晚上通过对帐任务慢慢处理。
4-3、如何权衡业务复杂度和查询性能
冗余的字段越多,性能越好,因为不用再去查询了,但是冗余的字段多就复杂,如何业务变更大,就会经常涉及到历史数据的处理问题。
- 目前我只冗余了基础的数据,比如id,状态,等几个列表需要的字段,详情要的字段再去查询
- 如果涉及的数据多,在查询的时候得考虑多线程查询,多线程要考虑超时和线程池策略的问题
4-4、定时轮询
对于定时轮询的思路就是,每次去查询某段时间内的变更数据同步过来,但还是有一些需要注意的点。
- 系统肯定是要记录最后更新时间,然后沦陷的时候把最后更新时间作为开始时间去查询变更数据,建议把这个时间往前移动一点防止有数据漏掉了
- 如果只是拿时间段去分页,其实还存在,在查询的时候数据变动导致分页漏掉了一些数据,这属于比较极端的情况,如果数据量不大不管也可以,处理的话可以用时间段 + 最后更新时间大于上一次的查询时间,这样不用分页,直到没有数据即可
4-5、历史数据处理
如果可以用写一个 insert into select是最好的,但是要注意分批插入,可以用id > 上一次的id这样的方式。分批可以使用代码也可以使用存储过程,在我们的服务器下, 650w数据的插入350s。
4-6、扩大并发度
不管是kafka还是rocketmq,数据都是投送到队列里面去,想要消息消费均衡可以把消息按照表明+id来hash这样可以把消息均衡的投入到队列中去,在消费组,我们可以增加消费者来快速消费,但主要消费者不要大于队列的数量,不然是无效的。
可以优先把队列放大一些,然后消费者可以基于不同情况来动态扩容。
4-N、抉择
数据实时同步只有订阅binlog和业务埋点这两种方案,它们的优缺点也很显而易见,但要说那种才是更好的呢?只能说仁者见仁,智者见智了,也要基于当前的业务场景来。
binlog最大的问题就是批量处理业务数据的时候,CDC能不能承接那么大的数据,以及那么大的数据会导致后续任务阻塞的问题。如果业务可控选择埋点也不是不可以。还要思考业务方处理数据的时候会不会通知到你?
但不管哪种方式,定时任务的补偿机制一定要有,不然突发的同步中断会直接让系统瘫痪。