1

一、场景示例

本文的标题不是很顾名思义,但是你一定在日常的开发工作中遇到过类似场景。

1. 场景一

假设公司从事电商业务,除了电商系统外,公司内部一般也会有一个客户关系系统(CRM)。如果CRM系统希望获取到电商系统注册的客户信息,以完成自己的后续业务。该如何实现?

2. 场景二

依然假设公司从事电商业务,电商系统中通常有订单模块,模块中会有订单表。为了提高查询性能避免连表Join,订单表除了存储用户id数据外也存储用户名作为冗余字段。用户侧业务用户名可以被更改,用户一旦更改了自己的用户名,订单表的数据也需要被更新。如何实现?

场景一是新增数据同步场景,场景二是更新数据同步场景。

3. 概念导入

在继续正文之前,我们再细化两个概念。

3.1 如何定义“系统”

标题中的“系统”是被打上了引号,表明它其实一个很宽泛的定义,大到可以是一个业务功完整的应用系统平台(电商系统、客户关系系统),小到可以是一个单体应用中的业务模块(用户模块、订单模块)。这里的“系统”不是按复杂程度、代码规模和实现方式划分的。我们将存储状态(数据)的业务组件定义为“系统”

根据这个定义,一个微服务体系中微服务组件(用户中心、订单中心、库存中心、评价中心),分布式系统中主节点、从节点(各类中间件的架构)都是我们文中的“系统”。因为两个系统存储着状态(数据),所以“系统”间有可能遇到需要状态(数据)同步的场景。

3.2 数据同步方法

“系统”间同步数据的方式主要有三种:

  • 接口调用方式
  • 消息队列方式
  • 数据库或文件方式(场景比较小众,本文不展开描述)

三、接口调用方式实现

我们使用接口调用的方式实现场景一、二中需求。

1. 场景一的实现方式

首先,如果电商系统和CRM不是同时期上线的话,我们要考虑电商系统历史存量数据问题,通常情况历史数据在首次数据同步的时候离线一次性导入。存量数据解决之后我们就需要考虑电商系统的增量数据即新注册用户。根据接口调用方向有两种方式,数据推送模式和数据拉取模式。

1.1 数据推送模式


数据推送是一种相对符合直觉的方式,这是方式的语言描述就是“当你有新数据的时候告诉我一声”,这里的“告诉”指的就是接口调用,就是电商系统在有新用户注册的时候调用CRM提供的接口,将新增用户信息以接口入参的形式传递。

1.2 数据拉取模式


就像推和拉是一对反义词一样,数据拉取与数据推送在接口调用方向上是相反,由CRM调用电商系统提供的接口,获取新增用户信息。

1.2.1 接口调用时机

因为CRM不知道电商系统何时会有新用户注册,所以它只能采用轮询的方式定期调用接口。

1.2.2 获取数据方式

如何判断新增数据?就是在某个基线节点之后增加的数据,这个基线节点可以是时间这个天然单调递增的数值也可以是其他单调递增的数值(数据库自增ID)。所以电商系统需要提供这样一个接口,以时间(或自增ID)为入参,批量查询出入参时间(自增ID)之后的所有数据。这也就要求电商系统和CRM都要存储用户的创建时间(或自增ID)参数用于接口调用。

注:如果我们用时间作为接口参数,极端情况下可能出现数据缺失问题,读者可以自己想一想原因

2. 场景二的实现方式

和场景一一样,如果前期没有考虑过数据同步,也存在历史存量数据问题,也需要一次性批量同步。我们重点关注增量数据,即后期产生的用户名更改数据同步问题。依然有两种方式:

2.1 数据推送模式


和场景一的实现类似,只不过这次的语言描述变成了“当你有数据变化的时候告诉我一声”,由用户模块调用订单模块接口,将用户名变化后的信息以接口入参的形式传递。

2.2 数据拉取模式


由订单模块调用用户模块提供的接口,获取更新用户的信息。

2.2.1 接口调用时机

和场景一相同,订单模块也不知道用户模块的数据何时被更新,所以它也只能采用轮询的方式定期调用接口。

2.2.2 获取数据方式

这里和场景一有些不同,场景一我们获取的是新增数据,这里我们要获取的是数据变化情况,所以有两种实现方式

逐条查询

按照用户唯一标识(用户ID)查询用户详情,判断订单模块中冗余的用户名和查询出来的值比较,不同就标明用户名已改变,进行更新操作。此时只需要用户模块提供一个根据用户唯一标识(用户ID)查询用户详情的接口(更简单专用的接口就是根据ID查询用户名)。显然在需要大量数据同步的场景下这不是一个好的实现方式。

批量查询

和场景一类似,用户模块需要提供一个以时间为入参,批量查询出来入参时间之后发生变动的所有数据的接口。这时就需要用户模块和订单模块都存储更新时间。(在这里在稍微深入一下,通常用户表都会存储更新时间,但是这个更新时间在用户任意属性变更的时候都会被更新,这样我们会获取到所有的用户属性有变更数据而不只是用户名更改的数据。要保证获取数据精准,除非我们在用户表单独存储用户名更新时间,显然这样得不偿失)

注:和场景一一样,使用时间作为接口参数,极端情况下也可能出现数据缺失问题。

通过对场景一、二的实现方法的描述我们可见。推送和拉取行为是相对数据方提供方和数据需求方而言,从数据提供方调用数据需求方接口就是数据推送,反之就是数据拉取,虽然两种方式接口的调用方向不同,但是数据的流向是相同的,都是通过数据提供方想数据需求方转移。

3. 推送模式和拉取模式的区别

接下来我们从两个方面,描述推送和拉取的区别:

3.1 系统间依赖关系

推送的方式下,数据提供方依赖数据需求方,场景一中电商系统调用CRM系统的接口,所以电商系统对CRM系统产生了依赖,同理场景二中用户模块也依赖了订单模块。在这种依赖关系我们嗅到了“坏味道”。

首先,直觉告诉我们数据提供方不应该依赖数据需求方。是你需要我的数据,我反而要调用你的接口并处理异常(异常如何处理也是有需求方业务决定的,例如是否可以重试,是否允许数据丢失),一旦你的业务发生变化导致接口或者异常处理逻辑变化,我不得不修改代码以应对。

其次,随着业务演进,未来可能有更多的数据需求方加入,每增加一个数据需求方都会造成数据提供方的代码层面修改,这也是我们不能接受的。

反观拉取的方式,因为是数据需求方调用数据提供方的接口,依赖方向转变了,无论数据需求方业务如何变化或者增加了更多的数据需求方,数据提供方均不受影响。

通常不好的耦合关系不会影响当前系统实现和运行,而是影响系统对未来业务变化的扩展性。如果我们能确定未来业务不会变化,也就不需要关心这类耦合关系,显然这种绝对稳定的系统少之又少。

3.2 数据同步的时效性和精确性

时效性方面

推送模式下,数据提供方可以在数据变化时同时调用数据需求方接口传递变化。而在拉取模式下,需求方无法感知数据何时变化,只能定期轮询接口。时效性和接口轮序频率成正比,为了增加时效性,只能提高接口轮询频率。但是过高的频率通常不可取:一来在数据变化不频繁的场景下,频繁的接口调用是一种浪费,二来过于频繁的接口调用也会个被调用方带来比较大的性能压力。所以我们需要在时效性和性能权衡,找到一个平衡点。

精确性方面

推送模式下,数据提供方可以精确的感知自身哪些数据发生变化并通过调用接口传递给数据需求方。而在拉取模式下,数据需求方无法感知哪些数据变化,只能通过双方保存数据变化状态(新增时间、更新时间)、增加查询条件的方式进行范围查询或者采用全量查询后逐个比对的方式(不适用新增数据场景)。相较于数据推送模式,数据拉取模式很难及时的获取到精准的同步数据信息。

4. 两种模式如何选择

两种模式各有利弊,从系统间依赖关系角度考虑,采用拉取模式的系统有更好的业务扩展性、能更好的应对未来需求变化。但是拉取模式在数据同步时效性、精确性和系统性能上有天然的劣势,实现起来也更加复杂。所以在具体方案选择上我们需要根据具体场景判断。数据同步时效性和系统性能要求不高的场景可以采用拉取模式以提高系统对未来业务的扩展能力,反之需要采用推送的方式以系统扩展性换取数据同步时效性和系统性能。

5. 推送模式优化

让我们在回看一下推送模式的问题,既然推送模式场景下数据需求方的业务变化可能对数据提供方造成影响。我们可不可以对推送模式进行优化以解决一个系统变化影响到另一个系统的问题。我们可以借鉴设计模式中“策略模式”的思想,将系统分为固定部分和变化部分,对变化的部分进行一定维度的抽象然后在将这种抽象分离出去独立实现。这种将固定逻辑和变化逻辑分离的做法,一定程度上可以解决我们遇到的问题。

5.1 如何改造

将变化进行抽象

首先,我们将数据提供方中变化的逻辑,如调用哪些数据需求方的接口、调用接口签名(名称、地址、入参出参)是什么、如何处理调用过程中的异常等这些有可能变化并且需要代码实现的逻辑抽象为通过自定义配置的方式实现,形成一个“接口配置中心”的概念。

对抽象进行实现

接下来,我们就需要编码实现“接口配置中心”的逻辑。配置中的主要职责就是对数据提供方提供统一的数据同步接口,一旦数据提供方调用数据同步接口,“配置中心”根据提前定义的配置调用后端数据需求方的接口完成数据同步工作,同时会根据配置处理接口调用过程中产生的各种异常情况。


注:图中箭头表示接口调用方向和依赖关系方向。

改造后,数据提供方调用接口中心的接口传递需要同步的数据,有接口配置中心负责调用个数据需求方的接口进行二次传递。数据提供方只依赖配置中心,不再依赖各个数据需求方,接口配置中心虽然依赖后端的各个数据需求方,但是数据需求方的种种变化已经通过配置来实现,所以无论数据需求方业务如何变化,我们只需要调整配置中心配置即可,这种变化并不能传导到数据需求方。通过引入配置中心,我们有效隔离了数据需求方对数据提供方的影响。

5.2 改造后的优点和不足

优点

解决了拉取模式下数据需求方影响数据提供方的问题。

缺点

增加了系统实现复杂度,显然“接口配置中心”实现起来并容易。

  • 如何接口调用的相关行为抽象为可以配置的方式并加以实现
  • 如何处理接口调用过程中遇到的种种异常情况
  • 如何解决接口调用的性能问题

降低了系统稳定性,提高了运维难度。更多的模块意味着更多的问题点,我们需要更多运维监控手段在第一时间发现并解决问题,以确保系统整体稳定运行。

5.3 使用建议

是否要使用这种方式,还需要综合考虑。在一个业务逻辑相对简单的小型系统,我们没有必要引入如此复杂的实现方式,根据场景需要从上面介绍的拉取模式和推送模式选择即可,而对于业务逻辑较复杂的大型系统,可以引入这种方式以降低系统间复杂的依赖关系。

四、消息队列模式实现


注:图中箭头表示接口调用方向和依赖关系方向。

数据提供方以消息的方式向数据需求方传递需要同步数据信息,数据提供发向消息队列发送一条消息,消息队列负责将消息发送给数据需求方。数据提供方和数据需求方都只依赖消息队列,他们两者之间没有依赖也就不会互相影响。

相信大家对消息队列并不陌生,在此我也不再赘述。你有没有发现消息队列很像同我们前文所述“接口配置中心”模式,只是实现略有不同。

  • “接口配置中心”中接口签名转换成了消息队列中的主题、消息概念
  • “接口配置中心”中接口调用行为就像消息队列中的发布订阅模式
  • “接口配置中心”中异常处理机制就是消息队列中的保证投递机制
  • “接口配置中心”中的性能要求也可以通过消息队列中的削峰作用来满足

所以消息队列可以完全代替我们上文所说的“接口配置中心”功能。免去我们重复造轮子之苦。

可能有熟悉和使用过消息队列的同学看到文中场景之后第一反应就是使用消息队列来实现。为什么我要放在最后才介绍这种方式呢。其实就是想让大家体验一下解决问题的基本方式——由渐入深、层层递进。首先,遇到一个问题,先从简单的方案入手,如果简单方案能解决你的问题就没有必要采用复杂的方式。我们虽然有大炮,但不能要来打苍蝇,有时候苍蝇拍更好使。其次,以一个简单方案作为突破口可以将一个大问题分解为一个个小问题,随着我们方案的优化调整逐个解决。

优点和不足

消息队列模式除了实现简单、开箱即用外,还能降低系统耦合度、提高传输数据的时效性和精准度,提高系统性能和吞吐量。但是因为引用了更多的中间件,也增加了系统运维复杂度。

五、总结

最后让我们来总结一下。本文介绍了“系统”间数据同步的场景和各种实现方式。主要介绍了接口方式和消息队列方式,接口方式分为推送模式、拉取模式、“接口配置中心”模式。实际项目中ongoing如何选择实现方式,需要结合场景综合考虑。以下是我的建议:

业务规模不复杂的单体应用系统或小型系统或者大型系统中的简单业务模块、可以选择相对简单的接口同步方式。具体使用推送模式还是拉取模式,需要根据未来业务扩展性、数据同步时效性和性能权衡利弊。推送模式在同步数据时效性和性能有优势,而在未来业务扩展性略显不足。拉取模式相反。

业务规模较复杂的分布式系统或微服务系统,可以选择消息队列模式。系统越复杂未来需求变化的可能性越大,应对未来需求变化是我们的第一要务,不要存在侥幸心理而选择简单的方式,切记。

除非现有市面上的消息队列中间件不满足自身需求,可以考虑定制开发,但是开发前一定要谨慎,重复造轮子的工作需要充分的理由。

你也可以不遵循上面的建议,只要你的选择符合你的场景即可。但是,一名架构师的的职责就是利用最小的成本构建出一套可运行并易于扩展的系统,并随着业务变化持续保证系统扩展性。所以我还是希望你能关注未来的系统扩展性,因为这是你的长期职责,即使它总和老板的短期目标相违背。


JerryTse
771 声望126 粉丝