当前位置: 首页 > 科技观察

微服务的数据库设计

时间:2023-03-19 15:27:16 科技观察

分离数据库:微服务设计的关键之一是数据库设计。基本原理是每个服务都有自己独立的数据库,只有微服务本身才能访问这个数据库。它基于以下三个原因。优化服务接口:微服务之间的接口越小越好。最好只有服务调用接口(RPC或消息)而没有其他接口。如果微服务不能独占自己的数据库,那么数据库也成为接口的一部分,极大地扩展了接口的范围。错误诊断:生产环境中的大部分错误都与数据库有关,要么是数据有问题,要么是数据库的使用方式有问题。当您不能完全控制数据库访问时,可能会发生各种错误。可能是其他程序直接连接到你的数据库或者其他部门直接用客户端访问数据库中的数据,而这些在程序中是查不到的,增加了排查的难度。如果是程序的问题,只要修改代码,就不会再出现这个错误了。而上面提到的错误,你永远无法预测它们什么时候会再次发生。性能调优:性能调优也是如此,你需要对数据库进行完全的控制,以保证其性能。如果其他部门一定要访问数据库,只能查询,那你可以再建一个只读数据库,让他们在别的图书馆查询,以免影响你的图书馆。理想的设计是你的数据库只能被你的服务访问,你只调用自己数据库中的数据,其他微服务的访问都是通过服务调用来实现的。当然,在实际应用中,简单的服务调用可能无法满足性能或其他方面的要求,不同的微服务需要在一定程度上共享一些数据。共享数据:微服务之间共享数据有四种方式。静态表:有一些静态的数据库表,比如country,很多程序可能会用到,程序需要加入country表来生成最终用户展示数据,所以使用微服务调用的方式效率不高,影响性能。一种方法是在每个微服务中配置一个这样的表,它是只读的,这样就可以建立数据库连接。当然,您需要保持数据同步。这种方案在大多数情况下是可以接受的,因为以下两点:静态数据库表结构基本不变:因为一旦表结构发生变化,不仅要更改所有微服务的数据库表,还要修改所有微服务的微服务程序.数据库表中的数据变化不频繁:数据同步的工作量不大。此外,当您同步数据库时,总会有延迟,如果数据不经常更改,那么您有很多同步选项可供选择。只读业务数据访问:如果需要读取其他数据库中的动态业务数据,最理想的方式是调用服务。如果只是调用其他微服务做一些计算,性能一般是可以接受的。如果你需要做数据连接,那么你可以用程序代码来做,而不是SQL语句。如果测试后性能不能满足要求,那么可以考虑在自己的数据库中建立一套只读的数据表。同步数据的方式大致有两种。如果是事件驱动的方式,使用发送消息的方式进行同步。如果是RPC方式,使用数据库本身提供的同步方式或者第三方同步软件。通常情况下,你可能只需要其他数据库的几个表,每个表只需要几个字段。这个时候其他数据库就是最终的数据来源,控制着所有的写操作和相应的业务验证逻辑,我们称之为主表。您的只读库可以称为从表。当一条数据写入主表时,会发送一个广播消息,所有有从表的微服务都会监听该消息,更新只读表中的数据。但是这个时候你必须非常小心,因为它比静态表要危险得多。首先,它的表结构变化比较频繁,而且它的变化完全不是你能控制的。第二个业务数据不像静态表,经常更新,所以对数据同步的要求比较高。多少延迟是可以接受的取决于具体的业务需求。此外,它还有两个问题:数据容量:数据库中的数据量是影响性能的主要因素。因为这个数据是外部的,不利于掌握它的流量规律,难以进行容量规划,也不可能进行更好的性能调优。接口泄漏:微服务之间的接口本来只是一个服务调用接口。此时,您可以对内部程序和数据库进行任何更改,而不会影响其他服务。数据库表结构现在也是界面的一部分。接口一旦发布,基本上就不能再更改了,这就大大限制了你的灵活性。幸运的是,因为已经建了另外一套表,有了buffer,当master表修改的时候,slave表可能不需要同步更新。除非你可以使用服务调用(没有本地只读数据库)来完成所有功能,否则无论你使用RPC还是事件驱动的微服务集成,上面提到的问题都是不可避免的。但是你可以通过合理规划数据库变更来减少上述问题的影响,下面会详细说明。读写业务数据访问:这是最复杂的情??况。通常,您有一个表是主表,其他表是从表。主表包含主信息,这些主信息被复制到从表,但是微服务会有额外的字段需要写入从表。这样,本地微服务对从表既有读写操作。而且,主表和从表是有顺序关系的。从表的主键来自主表,所以必须先有主表,再有从表。上图就是一个例子。假设我们有两个与电影相关的微服务,一个是电影论坛,用户可以在这里发表对电影的评论。另一个是电影店。“movie”是共享表,左边那个是电影论坛库,它的“movie”表是主表。右边的是电影存储库,它的“电影”表是从表。他们共享“id”字段(主键)。主表是主要的数据来源,但副表中的“数量”和“价格”字段不在主表中。向主表插入数据后,发送消息,从表接收消息,向本地“电影”表插入一条数据。而从表也会修改表中的“数量”和“价格”字段。在这种情况下,必须为每个字段分配一个唯一的来源(微服务)。只有源头有权主动更改该字段,其他微服务只能被动更改(在收到源头发送的更改消息后)。在本例中,“数量”和“价格”字段的来源是右表,其他字段的来源是左表。本例中“数量”和“价格”只存在于从表中,所以数据写入是单向的,方向是从主表到从表。如果主表也需要这些字段,那么就得回写,数据写入就变成了双向的。直接访问其他数据库:绝对禁止这种方式。生产环境中的许多错误和性能问题都是这样产生的。以上三种方法新建本地只读数据库表,造成数据库物理隔离,这样一个数据库的性能问题不会影响到另一个。另外,当主库中的表结构发生变化时,可以暂时保持从库中的表不变,这样程序仍然可以运行。如果直接访问别人的库,一旦修改了主库,其他微服务程序会立即报错。向后兼容的数据库更新:从上面的讨论可以看出,修改数据库表结构是一件影响面很广的事情。在微服务架构中,共享表在其他服务中也有一个只读副本。现在当你要改变表结构的时候,还需要考虑对其他微服务的影响。在Monolithic架构中,为了保证程序部署可以回滚,数据库的更新是向下兼容的。兼容性的另一个原因是支持蓝绿部署。在这种部署方式中,你同时拥有新旧版本的代码,负载均衡决定了每个请求指向哪个版本。它们可以共享一个数据库(这需要数据库向后兼容),也可以使用不同的数据。有几种类型的数据库更新:添加表或字段:如果字段可以取空值,则此操作是向后兼容的。如果它不为空,则插入默认值。删除表或字段:可以暂时保留已删除的表或字段,等多个版本后再删除。修改字段名:增加一个新字段,将旧字段的数据复制到新字段,使用数据库触发器(或程序)同步旧字段和新字段(过渡期使用)。然后在几个版本之后删除原来的字段。修改表名:如果数据库支持可更新视图,最简单的方法是先修改表名,然后创建指向原表的可更新视图。如果数据库不支持可更新视图,使用的方法类似于修改字段名,需要新建一张表并同步数据。修改字段类型:与修改字段名几乎一样,只是在复制数据时需要进行数据类型转换。向后兼容的数据库更新的好处是,当程序的部署出现问题时,必要时可以回滚。只回滚程序,而不是数据库。回滚时,一般只回滚一个版本。所有需要删除的表或字段都不会在本次部署过程中被修改。一个或几个版本后,确认没有问题再删除。另一个好处是它不会对其他微服务中的共享表产生直接的直接影响。当微服务升级时,其他微服务可以评估这些数据库更新的影响,然后决定是否进行相应的程序或数据库修改。跨服务事物:微服务的难点之一是如何实现跨服务事物支持。两阶段提交(Two-PhaseCommit)在性能方面已经被证明不能满足要求,现在基本没人用了。一致认可的方法称为Saga。它的原理是为事务中的每一个操作写一个补偿操作(CompensatingTransaction),然后在回滚阶段一个一个地执行每一个补偿操作。例如下图,一个事务中有T1、T2、T3三个操作。每个操作定义一个补偿操作,C1、C2、C3。事情执行时,顺序先执行T1,回滚时,逆序先执行C3。Thing中的每一个操作(前向操作和补偿操作)都被封装成一个命令(Command),由Saga执行协调器(SagaExecutionCoordinator(SEC))负责执行所有的命令。在执行之前,所有的命令都会依次存储在日志中,然后Saga执行协调器会从日志中取出命令,依次执行。当执行出现错误时,错误也会被写入日志,并停止所有正在执行的命令,并开始回滚操作。Saga放宽了对一致性(Consistency)的要求,它能保证的是最终一致性(EventualConsistency),所以在事物执行过程中数据是不一致的,这种不一致会被其他进程看到。在生活中,大多数情况下,我们对一致性的要求并没有那么高,短期的不一致是可以接受的。例如,银行转账操作在执行过程中不是在一个数据库事务中执行,而是以记账的形式分两次执行,保证了最终的一致性。Saga的原理看似简单,但要正确实现还是有难度的。它的核心问题在于错误的处理。要完全解释它需要另一篇文章。我现在只讲要点。网络环境不可靠,正在执行的命令可能长时间没有返回结果。这时候,首先,你需要设置一个超时时间。第二,因为你不知道没有返回值的原因是命令已经完成但是网络有问题,或者还没完成就牺牲了,所以你不知道是否要进行补偿操作。这时正确的做法是重试原来的命令,直到得到完成确认,再进行补偿操作。但是对命令有一个要求,就是操作必须是幂等的(Idempotent),也就是说可以执行多次,但是最后的结果还是一样的。另外,有些操作的补偿操作是比较容易产生的,比如支付操作,你只需要退钱即可。但是对于一些操作,比如发送邮件,完成之后是没有办法返回到之前的状态的。这时,您只能再发一封邮件,更正之前的信息。因此,补偿操作不一定要回到原来的状态,而是要抵消原来操作的效果。微服务的拆分:我们原来的程序大部分都是单体程序,现在想拆分成微服务,应该怎么做才能减少对现有应用的影响?我们以上图为例。它有两个程序,一个是“Stylingapp”,另一个是“Warehouseapp”。他们共享下图的数据库。仓库中有“coreclient”、“coresku”、“coreitem”三个表。假设我们要拆分出一个名为“client-service”的微服务,需要访问“coreclient”表。第一步是将程序从原始代码中拆分出来,并将其变成一项服务。数据库没有动,服务还是指向原来的数据库。其他程序不再直接访问这个服务管理的表,而是通过服务调用或者另一个共享表来获取数据。第二步是拆分服务的数据库表。这时微服务有了自己的数据库,不再需要原来的共享数据库。这时候,就成为了真正意义上的微服务。上面只讲了拆分一个微服务。如果有多个微服务需要拆分,需要按照上面提到的方法一个一个的进行。此外,MartinFowler在他的文章“BreakMonolithintoMicroservices”中有一个很好的建议。也就是说,当你把服务从单体程序中拆分出来时,不要只想着把代码拆分出来。因为现在的要求可能已经和原来的不一样了,原来的设计不一定合适。此外,技术已更新,代码已相应调整。更好的办法是重写原来的功能(而不是重写原来的代码),着重于拆分业务功能而不是拆分代码,用新的设计和技术来实现这个业务功能。结论:数据库设计是微服务设计的一个重点。基本原理是每个微服务都有自己独立的数据库,只有微服务本身才能访问这个数据库。微服务之间的数据共享可以通过服务调用,或者主从表来实现。共享数据时,找到合适的同步方法。在微服务架构中,数据库的修改影响面很广,需要保证这种修改是向后兼容的。实现跨服务事务的标准方式是Saga。在将单体程序拆分成微服务时,可以分步进行,减少对现有程序的影响。