一、微服务化带来的分布式事务问题

开发当中真实场景:

首先,设想一个传统的单体应用(Monolithic App),通过 3 个 Module,在同一个数据源上更新数据来完成一项业务。

很自然的,整个业务过程的数据一致性由本地事务来保证。

随着业务需求和架构的变化,单体应用被拆分为微服务:原来的 3 个 Module 被拆分为 3 个独立的服务,分别使用独立的数据源。业务过程将由 3 个服务的调用来完成。

此时,每一个服务内部的数据一致性仍由本地事务来保证。而整个业务层面的全局数据一致性要如何保障呢?

这就是微服务架构下面临的,典型的分布式事务需求:我们需要一个分布式事务的解决方案保障业务全局的数据一致性。

需要解决的问题

随着分布式服务架构的流行与普及,原来在单体应用中执行的多个逻辑操作,现在被拆分成了多个服务之间的远程调用。虽然服务化为我们的系统带来了水平伸缩的能力,然而随之而来挑战就是分布式事务问题,多个服务之间使用自己单独维护的数据库,它们彼此之间不在同一个事务中,假如 A 执行成功了,B 执行却失败了,而 A 的事务此时已经提交,无法回滚,那么最终就会导致两边数据不一致性的问题。

二、什么是事务

事务提供一种机制:将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。

事务拥有以下四个特性,习惯上被称为 ACID 特性:

A:原子性(Atomicity),一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency),事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation),指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。

即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

1、本地事务

什么是本地事务(Local Transaction)?本地事务也称为数据库事务或传统事务(相对于分布式事务而言)

本地事务有这么几个特征:

  • 一次事务只连接一个支持事务的数据库(一般来说都是关系型数据库)
  • 事务的执行结果保证ACID
  • 会用到数据库锁

起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。

倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。

2、分布式事务

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用。

一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:

上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。

正常情况下,两个数据库各自更新成功,两边数据维持着一致性。

但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。

3、事务的作用

保证每个事务的数据一致性。

三、分布式事务系统架构

本地事务主要限制在单个会话内,不涉及多个数据库资源。但是在基于 SOA(Service-Oriented Architecture,面向服务架构)的分布式应用环境下,越来越多的应用要求对多个数据库资源,多个服务的访问都能纳入到同一个事务当中,分布式事务应运而生。

1、什么是分布式系统?

部署在不同结点上的系统通过网络交互来完成协同工作的系统。

比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。

2、分布式事务场景

  • 电商系统中的下单扣库存

    电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成

  • 金融系统中的银行卡充值

    在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。

  • 教育系统中下单选课业务

    在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。

  • SNS 系统的消息发送

    在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。

3、单一服务分布式事务

最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。

4、多服务分布式事务

当一个服务操作访问不同的数据库资源,又希望对它们的访问具有事务特性时,就需要采用分布式事务来协调所有的事务参与者。

对于上面介绍的单一服务分布式事务应用架构,尽管一个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。

下图反映了这样一个跨越多个服务的分布式事务:

较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。

四、CAP 定理

1、概念

CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论。

分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。

分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

C (一致性):指数据在多个副本之间能够保持一致的特性(严格的一致性)在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间具有相同的数据)

一致性(Consistency)是指多副本(Replications)问题中的数据一致性。可以分为强一致性、与弱一致性。

① 强一致性

简言之,在任意时刻,所有节点中的数据是一样的。

例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。

② 弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性就属于弱一致性。

A (可用性):指系统提供的服务必须一直处于可用的状态,每次只要收到用户的请求,服务器就必须给出回应。在合理的时间内返回合理的响应(不是错误和超时的响应)

只有非故障节点才能满足业务正常;只有在合理的时间内,用户才能接受;只有返回合理的响应,用户才能接受。

P (网络分区容错性):网络节点之间无法通信的情况下,节点被隔离,产生了网络分区, 整个系统仍然是可以工作的 . 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

什么是分区?

在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域。这就是分区。

  • CAP 原则的精髓就是要么 AP,要么 CP,要么 AC,但是不存在 CAP。

2、CAP 定理的证明

现在我们就来证明一下,为什么不能同时满足三个特性?

假设有两台服务器,一台放着应用 A 和数据库 DB0,一台放着应用 B 和数据库 DB0,他们之间的网络可以互通,也就相当于分布式系统的两个部分。

在满足 C (一致性)的时候,两台服务器 N1 和 N2,一开始两台服务器的数据是一样的,N1 的 DB0=N2 的 DB0。

在满足 A (可用性)的时候,用户不管是请求 N1 或者 N2,都会得到立即响应。

在满足 P (分区容错性)的情况下,N1 和 N2 有任何一方宕机,或者网络不通的时候,都不会影响 N1 和 N2 彼此之间的正常运作。

当用户通过 N1 中的 A 应用请求数据更新到数据库 DB0 后,这时 N1 中的数据库数据从 DB0 更新为 DB1,通过分布式系统的数据同步更新操作,N2 服务器中的数据库也更新为了 DB1,这时,用户通过 B 向数据库发起请求得到的数据就是即时更新后的数据 DB1。

上面是正常运作的情况,但分布式系统中,最大的问题就是网络传输问题,现在假设一种极端情况,N1 和 N2 之间的网络断开了,但我们仍要支持这种网络异常,也就是P (分区容错性),那么这样能不能同时满足C (一致性) 和 A (可用性)呢?

假设 N1 和 N2 之间通信的时候网络突然出现故障,有用户向 N1 发送数据更新请求,那 N1 中的数据 DB0 将被更新为 DB1,由于网络是断开的,N2 中的数据库仍旧是 DB0。

如果这个时候,有用户向 N2 发送数据读取请求,由于数据还没有进行同步,应用程序没办法立即给用户返回最新的数据 DB1,怎么办呢?有二种选择,第一,牺牲数据一致性,响应旧的数据 DB0 给用户;第二,牺牲可用性,阻塞等待,直到网络连接恢复,数据更新操作完成之后,再给用户响应最新的数据 DB1。

上面的过程比较简单,但也说明了要满足分区容错性的分布式系统,只能在**C (一致性)A (可用性)**两者中,选择其中一个。也就是说分布式系统不可能同时满足三个特性。这就需要我们在搭建系统时进行取舍了,那么,怎么取舍才是更好的策略呢?

3、取舍策略

现如今,对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。那么就只能在 C 和 A 之间进行取舍。

因为,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象,随着网络节点出现问题,产生了分区, 这时候其他节点和出错节点的数据必然会不一致,这时候就要面临选择,

是选择停掉所有的服务,等网络节点修复后恢复数据,以此来保证一致性(PC),
还是选择继续提供服务,放弃强一致性的要求,以此来保证整体的可用性(PA)。

所以,最多满足两个条件:

组合分析结果
CA满足原子和可用,放弃分区容错。说白了,就是一个整体的应用。
CP满足原子和分区容错,也就是说,要放弃可用。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。
AP满足可用性和分区容错,当出现分区,同时为了保证可用性,必须让节点继续对外服务,这样必然导致失去原子性。

在分布式系统设计中 AP 的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。

顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。

但是,有个特殊情况需要注意:但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C 必须保证,出现网络故障的话,宁可停止服务,可以在 A 和 P 之间做取舍。

总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。