本文介绍了微服务的数据库设计,内容非常详细。感兴趣的朋友可以参考一下,希望对你有所帮助。
单独的数据库:
微服务设计的一个关键点是数据库设计。基本原理是每个服务都有自己独立的数据库,只有微服务本身可以访问这个数据库。它基于以下三个原因。
优化服务接口:微服务之间的接口越小越好。最好只有服务调用接口(RPC或消息),没有其他接口。如果微服务不能享受自己的数据库,那么数据库就成为接口的一部分,这就大大扩展了接口的范围。
错误诊断:生产环境中的大多数错误都与数据库有关,要么是数据有问题,要么是数据库的使用有问题。当你不能完全控制对数据库的访问时,就会出现各种各样的错误。可能是其他程序直接连接到你的数据库,或者其他部门直接用客户端访问数据库中的数据,但这些在程序中找不到,增加了错误检查的难度。如果是程序中的问题,只要修改了代码,那么这个错误就不会再发生了。而上面提到的错误,你永远无法预测它们何时会再次发生。
性能调整:性能调整是一样的。您需要完全控制数据库以确保其性能。如果其他部门必须访问数据库并且只查询它,那么您可以创建另一个只读数据库,让他们在另一个库中查询,以免影响您的库。
理想的设计是只有你的服务可以访问你的数据库,你只调用自己数据库中的数据,其他微服务的所有访问都是通过服务调用实现的。当然,在实际应用中,简单的服务调用可能无法满足性能或其他需求,不同的微服务在一定程度上需要共享一些数据。
共享数据:
在微服务之间共享数据有四种方式。
静态表:
有一些静态的数据库表,比如国家,可能会被很多程序使用,需要在程序内部加入国家的表来生成最终用户的显示数据,这样微服务调用效率低,影响性能。一种方法是在每个微服务中配置这样一个只读的表,以便建立数据库连接。当然,您需要确保数据同步。这种方案在大多数情况下是可以接受的,因为有以下两点:
鸿蒙系统正式战略合作,打造——HarmonyOS技术社区。
静态数据库表结构基本不变:因为一旦表结构发生变化,不仅要改变所有微服务的数据库表,还要改变所有微服务的程序。
数据库中的数据变化不频繁:所以数据同步的工作量不大。此外,当您同步数据库时,总是会有延迟。如果数据很少更改,您可以选择许多同步方法。
只读业务数据访问:
如果需要从其他数据库中读取动态业务数据,理想的方法是服务调用。如果只是调用其他微服务做一些计算,性能一般是可以接受的。如果需要做数据连接,可以用程序代码代替SQL语句。如果测试后性能达不到要求,可以考虑在自己的数据库中设置一组只读数据表。同步数据大致有两种方法。如果是事件驱动模式,则通过发送消息进行同步;如果是RPC模式,将由数据库本身或第三方同步软件提供的同步模式进行同步。
通常,您可能只需要其他数据库中的几个表,每个表只需要几个字段。此时,其他数据库是最终的数据源,控制着所有的写操作和相应的业务验证逻辑,我们称之为主表。您的只读库可以称为从表。当一条数据被写入主表时,将发送一条广播消息,所有带有从表的微服务将监听该消息并更新只读表中的数据。但是这个时候你必须非常小心,因为它比静态手表危险得多。首先,它的表结构会发生更频繁的变化,它的变化完全不受你的控制。与静态表不同,第二个业务数据经常更新,这需要更高的数据同步。需要根据具体的业务需求来决定多少延迟是可以接受的。
此外,它还有两个问题:
鸿蒙系统正式战略合作,打造——HarmonyOS技术社区。
数据容量:数据库中的数据量是影响性能的主要因素。因为这个数据是外部的,不利于掌握其交通规律,难以规划容量和更好的调优性能。
接口泄漏:最初,微服务之间的接口只是服务调用接口。此时,您可以对内部程序和数据库进行任何更改,而不会影响其他服务。现在数据库表结构已经成为接口的一部分。界面一旦发布,基本上是不可更改的,这大大限制了你的灵活性。幸运的是,由于另一组表带有缓冲区,当主表被修改时,从表可能不需要同步更新。
除非可以通过服务调用的方式完成所有功能(无需本地只读数据库),否则无论是使用RPC还是事件驱动的方法集成微服务,上述问题都是不可避免的。但是,您可以通过合理规划数据库更改来减少上述问题的影响,这将在下面详细解释。
读写业务数据访问:
这是最复杂的情况。通常,您有一个主表,而其他表是从表。主表包含主信息,主信息被复制到从表。
,但微服务会有额外字段需要写入从表。这样本地微服务对从表就既有读也有写的操作。而且主表和从表有一个先后次序的关系。从表的主键来源于主表,因此一定先有主表,再有从表。
上图是例子。假设我们有两个与电影有关的微服务,一个是电影论坛,用户可以发表对电影的评论。另一个是电影商店。“movie”是共享表,左边的一个是电影论坛库,它的“movie”表是主表。右边的是电影商店库,它的“movie”表是从表。它们共享“id”字段(主键)。主表是数据的主要来源,但从表里的“quantity”和“price”字段主表里面没有。主表插入数据后,发消息,从表接到消息,插入一条数据到本地“movie”表。并且从表还会修改表里的“quantity”和“price”字段。在这种情况下,要给每一个字段分配一个唯一源头(微服务),只有源头才有权利主动更改字段,其他微服务只能被动更改(接收源头发出的更改消息之后再改)。在本例子中, “quantity”和“price”字段的源头是右边的表,其他的字段的源头都是左边的表。本例子中“quantity”和“price”只在从表中存在,因此数据写入是单向的,方向是主表到从表。如果主表也需要这些字段,那么它们还要被回写,那数据写入就变成双向的。
直接访问其它数据库:
这种方式是要绝对禁止的。生产环境中的许多程序错误和性能问题都是由这种方式产生的。上面的三种方式由于是另外新建了本地只读数据库表,产生了数据库的物理隔离,这样一个数据库的性能问题不会影响到另一个。另外,当主库中的表结构更改时,你可以暂时保持从库中的表不变,这样程序还可以运行。如果直接访问别人的库,主库一修改,别的微服务程序马上就会报错。
向后兼容的数据库更新:
从上面的论述可以看出,数据库表结构的修改是一个影响范围很广的事情。在微服务架构中,共享的表在别的服务中也会有一个只读的拷贝。现在当你要更改表结构时,还需要考虑到对别的微服务的影响。当在单体(Monolithic)架构中,为了保证程序部署能够回滚,数据库的更新是向后兼容的。需要兼容性的另一个原因是支持蓝绿发布(Blue-Green Deployment)。在这种部署方式中,你同时拥有新旧版本的代码,由负载均衡来决定每一个请求指向那个版本。它们可以共享一个数据库(这就要求数据库是向后兼容的),也可以使用不同的数据。数据库的更新简单来讲有以下几种类型:
-
增加表或字段:如果字段可取空值,这个操作是向后兼容的。如果是非空值就要插入一个缺省值。
-
删除表或字段:可先暂时保留被删除表或字段,经过几个版本之后再删除。
-
修改字段名:新增加一个字段,把数据从旧字段拷贝到新字段,用数据库触发器(或程序)同步旧字段和新字段(供过渡时期使用)。 然后再在几个版本之后把原来的字段删除。
-
修改表名:如果数据库支持可更新视图,最简单的办法是先修改表的名字,然后创建一个可更新视图指向原来的表。如果数据库不支持可更新视图,使用的方法与修改字段名相似,需要创建新的表并做数据同步。
-
修改字段类型:与修改字段名几乎相同,只是在拷贝数据时,需要做数据类型转换。
向后兼容的数据库更新的好处是,当程序部署出现问题时,如需进行回滚。只要回滚程序就行了,而不必回滚数据库。回滚时一般只回滚一个版本。凡是需要删除的表或字段在本次部署时都不做修改,等到一个或几个版本之后,确认没有问题了再删除。它的另一个好处就是不会对其他微服务中的共享表产生立刻的直接影响。当本微服务升级后,其他微服务可以评估这些数据库更新带来的影响再决定是否需要做相应的程序或数据库修改。
跨服务事物:
微服务的一个难点是如何实现跨服务的事物支持。两阶段提交(Two-Phase Commit)已被证明性能上不能满足需求,现在基本上没有人用。被一致认可的方法叫Saga。它的原理是为事物中的每个操作写一个补偿操作(Compensating Transaction),然后在回滚阶段挨个执行每一个补偿操作。示例如下图,在一个事物中共有3个操作T1,T2,T3。每一个操作要定义一个补偿操作,C1,C2,C3。事物执行时是按照正向顺序先执行T1,当回滚时是按照反向顺序先执行C3。 事物中的每一个操作(正向操作和补偿操作)都被包装成一个命令(Command),Saga执行协调器(Saga Execution Coordinator (SEC))负责执行所有命令。在执行之前,所有的命令都会按顺序被存入日志中,然后Saga执行协调器从日志中取出命令,依次执行。当某个执行出现错误时,这个错误也被写入日志,并且所有正在执行的命令被停止,开始回滚操作。
Saga放松了对一致性(Consistency)的要求,它能保证的是最终一致性(Eventual Consistency),因此在事物执行过程中数据是不一致的,并且这种不一致会被别的进程看到。在生活中,大多数情况下,我们对一致性的要求并没有那么高,短暂的不一致性是可以接收的。例如银行的转账操作,它们在执行过程中都不是在一个数据库事物里执行的,而是用记账的方式分成两个动作来执行,保证的也是最终一致性。
Saga的原理看起来很简单,但要想正确的实施还是有一定难度的。它的核心问题在于对错误的处理,要把它完全讲明白需要另写一遍文章,我现在只讲一下要点。网络环境是不可靠的,正在执行的命令可能很长时间都没有返回结果,这时,第一,你要设定一个超时。第二,因为你不知道没有返回值的原因是,已经完成了命令但网络出了问题,还是没完成就牺牲了,因此不知道是否要执行补偿操作。这时正确的做法是重试原命令,直到得到完成确认,然后再执行补偿操作。但这对命令有一个要求,那就是这个操作必须是幂等的(Idempotent),也就是说它可以执行多次,但最终结果还是一样的。
另外,有些操作的补偿操作比较容易生成,例如付款操作,你只要把钱款退回就可以了。但有些操作,像发邮件,完成之后就没有办法回到之前的状态了,这时就只能再发一个邮件更正以前的信息。因此补偿操作不一定非要返回到原来的状态,而是抵消掉原来操作产生的效果。
微服务的拆分:
我们原来的程序大多数都是单体程序,但现在要把它拆分成微服务,应该怎样做才能降低对现有应用的影响呢?
我们用上面的图来做例子。它共有两个程序,一个是“Styling app”,另一个是“Warehouse app”,它们共享图中下面的数据库,库里有三张表,“core client”,“core sku”,“core item”。
假设我们要拆分出来一个微服务叫“client-service”,它需要访问“core client”表。第一步,我们先把程序从原来的代码里拆分出来,变成一个服务. 数据库不动,这个服务仍然指向原来的数据库。其他程序不再直接访问这个服务管理的表,而是通过服务调用或另建共享表来获取数据。
第二步,再把服务的数据库表拆分出来,这时微服务就拥有它自己的数据库了,而不再需要原来的共享数据库了。这时就成了一个真正意义上的的微服务。
上面只讲了拆分一个微服务,如果有多个需要拆分,则需一个一个按照上面讲的方法依次进行。
另外,Martin Fowler在他的文章"Break Monolith into Microservices"里有一个很好的建议。那就是,当你把服务从单体程序里拆分时,不要只想着把代码拆分出来。因为现在的需求可能已经跟原来有所不同,原先的设计可能也不太适用了。而且,技术也已更新,代码也要作相应的改造。更好的办法是重写原来的功能(而不是重写原来的代码),把重点放在拆分业务功能上,而不是拆分代码上,用新的设计和技术来实现这个业务功能。
数据库设计是微服务设计的一个关键点,基本原则是每个微服务都有自己单独的数据库,而且只有微服务本身可以访问这个数据库。微服务之间的数据共享可以通过服务调用,或者主、从表的方式实现。在共享数据时,要找到合适的同步方式。在微服务架构中,数据库的修改影响广泛,需要保证这种修改是向后兼容的。实现跨服务事物的标准方法是Saga。当把单体程序拆分成微服务时,可以分步进行,以减少对现有程序的影响。
关于微服务的数据库设计是怎样的就分享到这里了,希望
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/130921.html