问题前因
比如一个供应链系统存在 商品,销售订单,采购三个主要服务,数据结构如下:
商品表
ID | 商品名称 | 分类 | 型号 | 生产年份 | 编码 |
---|---|---|---|---|---|
01 | 手机 | 数码产品 | 小米 | 2020 | 61381 |
订单和子订单
订单id | 下单时间 | 客户 | 总金额 | 子订单id | 商品id | 单价 | 数量 |
---|---|---|---|---|---|---|---|
采购单和子订单
采购单id | 下单时间 | 供应商 | 总金额 | 采购子订单id | 商品id | 单价 | 数量 |
---|---|---|---|---|---|---|---|
这样做正常保证了每个数据,字段的独立性,符合三范式等。这该系统中需要满足以下两个需求:
- 根据商品型号/分类/生成年份/编码等查找订单。
- 根据商品型号/分类/生成年份/编码等查找采购订单。
在初期可以这样设计:
严格按照微服务的划分将商品职责存在商品服务中,查找订单和采购订单时,如果查找订单包含型号/分类/生成年份/编码字段,就需要按照以下顺序去查询:
- 现根据型号/分类/生成年份/编码商品字段,调用商品服务,返回相应商品信息;
- 在订单中,通过商品信息的id,使用in语句查询对应订单。
这样做初期是没有问题的,运行一段时间会发生如下问题:
- 随着商品数量新增的越来越多,匹配的商品也越多,订单服务中包含IN语句的查询效率越来越慢;
- 商品作为核心服务,依赖他的服务越来愈多,随着商品数量增长,商品服务会存在请求超时;
- 由于商品服务超时,相关服务处理请求经常请求失败。
结果就是每次查询订单,只要带上关键字,查询效率就会很慢而且总失败。
例如:
- 查询一个含有手机关键字的订单,首先去调用商品服务:查询含有手机的商品,结果有1000个。
- 在订单服务中使用IN去查询这些订单,必然会很慢。
解决 1、数据冗余方案
说白了就是在订单,采购单中保存一些商品字段的信息,这样查询订单时,再出现关键字就不用调用商品服务了。
比如:在订单和子订单结构上新增商品名称、商品分类id、商品型号、生产批次id,采购单和子订单同理。
这样的话新问题来了,新商品发布或者更新是怎么同步冗余的数据字段呢?有两种解决方案:
- 每次更新商品,先调用订单服务,再更新冗余数据
- 每次更新商品,先发布一条消息,订单与采购服务各自订阅这条消息,再更新冗余数据
那这两种方案会出现哪些问题呢?
如果每次更新商品,先调用订单服务,再更新冗余数据,则会有以下两种问题。
- 数据一致性问题:如果订单冗余数据更新失败了,整个业务需要回滚,负责维护商品服务的开发人员肯定不乐意,因为冗余数据不是商品服务的核心需求,不能因为边缘流程阻断了自身的核心流程。
- 依赖问题:从职责来说,商品服务应该只关注商品本身,但是现在商品还需要调用订单与采购服
务。而且,依赖商品这个核心服务的服务实在是太多了,也就导致后续商品服务每次更新商品时,
都需要调用更新订单冗余数据、更新采购冗余数据、更新门店库存冗余数据、更新运营冗余数据等
一大堆服务。那么商品到底是下游服务还是上游服务?还能不能安心当底层核心服务?
因此,第一个解决办法直接被我们否决了,即我们采取的第二个解决办法——通过消息发布订阅的方
案,因为它存在如下 2 点优势:
- 商品无须调用其他服务,它只需要关注自身逻辑即可,顶多多生成一条消息送到 MQ。
- 如果订单、采购等服务的更新冗余数据失败了,我们使用消息重试机制就可以了,最终能保证数据
的一致性。
这个方案看起来已经挺完美了,而且市面上基本也是这么做的,不过该方案存在如下几个问题。
-
在这个方案中,仅仅保存冗余数据还远远不够,我们还需要将商品分类与生产批号的清单进行关联查
询。也就是说,每个服务不只是订阅商品变更这一种消息,还需要订阅商品分类、商品生产批号变更等
消息。
事实上,商品表中还有很多字段存在冗余,比如保修类型、包换类型
等。为了更新这些冗余数据,采购服务与订单服务往往需要订阅近十种消息,因此,我们基本上需要把
商品的一小半逻辑复制过来。 -
每个依赖的服务需要重复实现冗余数据更新同步的逻辑。前面我们讲了采购、订单及其他服务都需要
依赖商品数据,因此每个服务需要将冗余数据的订阅、更新逻辑做一遍,最终重复的代码就会很多。 -
MQ 消息类型太多了:联调时最麻烦的是 MQ 之间的联动,如果是接口联调还好说,因为调用哪个
服务器的接口相对可控而且比较好追溯;如果是消息联调就比较麻烦,因为我们常常不知道某条消息被
哪台服务节点消费了,为了让特定的服务器消费特定的消息,我们就需要临时改动双方的代码。不过联
调完成后,我们经常忘了改回原代码。
2、解耦业务逻辑的数据同步方案
解耦业务逻辑的数据同步方案的设计思路是这样的:
- 将商品及商品相关的一些表(比如分类表、生产批号表、保修类型、包换类型等)实时同步到需要
依赖使用它们的服务的数据库,并且保持表结构不变; - 在查询采购、订单等服务时,直接关联新同步过来的商品相关表;
- 不允许采购、订单等服务修改商品相关表。
以上方案就能轻松解决如下两个问题:
- 商品无须依赖其他服务,如果其他服务的冗余数据同步失败,它也不需要回滚自身的流程;
- 采购、订单等服务无须关注冗余数据的同步。
不过,该方案的“缺点”是增加了订单、采购等数据库的存储空间(因为增加了商品相关表)。
仔细计算后,我们发现之前数据冗余的方案中每个订单都需要保存一份商品的冗余数据,假设订单总数
是 N,商品总数是 M,而 N 一般远远大于 M。因此,在之前数据冗余的方案中,N 条订单就会产生
N 条商品的冗余数据。相比之下,解耦业务逻辑的数据同步方案更省空间,因为只增加了 M 条商品的
数据。
此时问题又来了,如何实时同步相关表的数据呢?
我们直接找一个现成的开源中间件就可以了,不过它需要满足支持实时同步、支持增量同步、不用写业
务逻辑、支持 MySQL 之间同步、活跃度高这五点要求。
比较贴近我们需求的开源中间件是 Bifrost,原因如下:
- 它的界面管理不错;
- 它的架构比较简单,出现问题后,我们可以自行调查,之后就算作者不维护了也可以自我维护,
相对比较可控。 - 作者更新活跃;
- 自带监控报警功能。
3、最终效果
整个架构方案上线后,商品数据的同步还算比较稳定,此时商品服务的开发人员只需要关注自身逻辑,
无须再关注使用数据的人。如果需要关联使用商品数据的订单,采购服务的开发人员也无须关注商品数
据的同步问题,只需要在查询时加上关联语句即可,实现了双赢。
然而,唯一让我们担心的是 Bifrost 不支持集群,没法保障高可用性。不过,到目前为止,它还没有出
现宕机的情况,反而是那些部署多台节点负载均衡的后台服务常常会出现宕机。
最终,我们总算解决了服务之间数据依赖的问题。