关于微服务:如何在微服务下保证事务的一致性

5次阅读

共计 13107 个字符,预计需要花费 33 分钟才能阅读完成。

作者:京东科技 苗元

背景

随着业务的疾速倒退、业务复杂度越来越高,传统单体利用逐步暴露出了一些问题,例如开发效率低、可维护性差、架构扩展性差、部署不灵便、健壮性差等等。而微服务架构是将单个服务拆分成一系列小服务,且这些小服务都领有独立的过程,彼此独立,很好地解决了传统单体利用的上述问题,然而在微服务架构下如何保障事务的一致性呢?

1、事务的介绍

1.1 事务

1.1.1 事务的产生

数据库中的数据是共享资源,因而数据库系统通常要反对多个用户的或不同应用程序的拜访,并且各个拜访过程都是独立执行的,这样就有可能呈现并发存取数据的景象,这里有点相似 Java 开发中的多线程平安问题(解决共享变量平安存取问题),如果不采取肯定措施会呈现数据异样的状况。列举一个简略的经典案例:比方用户用银行卡的钱还京东白条,银行卡扣款胜利了,然而白条因为网络或者零碎问题没有还款胜利,就会出大问题,这时候咱们就须要应用事务。

1.1.2 事务的概念

事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向零碎提交,要么都执行、要么都不执行;事务是一组不可再宰割的操作汇合(工作逻辑单元)。例如:在关系数据库中,一个事务能够是一条 SQL 语句,一组 SQL 语句或整个程序。

1.1.3 事务的个性

事务的四大特色次要是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),这四大特色大家或多或少都据说过,这里我做下简略介绍。

(1)原子性(Atomicity):事务内的操作要么全副胜利,要么全副失败,不会在两头的某个环节完结。如果所有的操作都胜利了,那么事务是胜利的,只有其中任何一个操作失败,那么事务会进行回滚,回滚到操作最后的状态。

begin transaction;

update activity_acount set money = money-100 where name = ‘ 小明 ’;

update activity_acount set money = money+100 where name = ‘ 小红 ’;

commit transaction;

(2)一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,然而对于整个数据的完整性保持稳定。换一种说法是数据依照预期失效,数据的状态是预期的状态。比方数据库在一个事务执行之前和执行之后,都必须处于一致性状态,如果事务执行失败,那么须要主动回滚到原始状态,也就是事务一旦提交,其余事务查看到的后果统一,事务一旦回滚,其余事务也只能看到回滚前的状态。

举个艰深一点的例子:小明给小红转账 100 元,转账前和转账后数据是正确的状态,这叫一致性,如果小红没有收到 100 元或者收到金额少于 100 元,这就呈现数据谬误,就没有达到一致性。

(3)隔离性(Isolation):在并发环境中,不同事务共事批改雷同的数据时,一个未实现的事务不会影响另外一个未实现的事务。

例如当多个用户并发拜访数据库时,比方操作同一张表时,数据库为每一个用户开启的事务,不能被其余事务的操作所烦扰,多个并发事务之间要互相隔离。

(4)持久性(Durability):事务一旦提交,其批改的数据将永远保留到数据库中,扭转是永久性,即便接下来数据库产生故障也不应答其有任何影响。

艰深一点例子:A 卡里有 2000 块钱,当 A 从卡里取出 500,在不思考外界因素烦扰的状况下,那么 A 的卡里只能剩 1500。不存在取了 500 块钱后,卡里一会剩 1400,一会剩 1500,一会剩 1600 的状况。

1.1.4 Mysql 隔离级别

如果不思考事务隔离性产生问题:脏读、不可反复读和幻读。

Mysql 隔离级别分为 4 种:Read Uncommitted(读取未提交的)、Read Committed(读取提交的)、Repeatable Red(可反复读)、Serializaable(串行化)

(1)Read Uncommitted 是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么以后事务读到的数据就是脏数据,这就是脏读(Dirty Read)。

(2)在 Read Committed 隔离级别下,一个事务可能会遇到不可反复读(Non Repeatable Read)的问题。不可反复读是指,在一个事务内,屡次读同一数据,在这个事务还没有完结时,如果另一个事务恰好批改了这个数据,那么,在第一个事务中,两次读取的数据就可能不统一。

(3)在 Repeatable Read 隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查问某条记录,发现没有,然而,当试图更新这条不存在的记录时,居然能胜利,并且,再次读取同一条记录,它就神奇地呈现了,就好象产生了幻觉一样。

(4)Serializable 是最严格的隔离级别。在 Serializable 隔离级别下,所有事务依照秩序顺次执行,因而,脏读、不可反复读、幻读都不会呈现。尽管 Serializable 隔离级别下的事务具备最高的安全性,然而,因为事务是串行执行,所以效率会大大降落,应用程序的性能会急剧升高。如果没有特地重要的情景,个别都不会应用 Serializable 隔离级别。

如果没有指定隔离级别,数据库就会应用默认的隔离级别。在 MySQL 中,如果应用 InnoDB,默认的隔离级别是 Repeatable Read。

1.1.5 启动事务

在阐明启动事务之前,首先大家先想一下事务的流传行为,事务流传行为用于解决两个被事务管理的办法相互调用问题。理论开发中将事务在 service 管制,如以下办法调用存在流传行为,如果 serviceB 也会产生一个代理对象,同时也会进行事务管理,执行 serviceA 和 serviceB 别离开启事务,上边的 serviceA 中 funA 办法内容不处于一个事务中了。

class serviceA{
    // 此办法进行事务管制
    funA(){
        // 在此办法中操作多个 dao 的操作,处于一个事务中
        userDao.insertUser();
        orderDao.insertOrder();
        // 如果在这里调用另一个 service 的办法,此时存在事务流传
        serviceB.funB();}
}
class serviceB{funB(){}}


解决方案就是,在启动类上增加注解 @EnableTransactionManagement,在执行事务的办法下面应用 @Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED)设置隔离界别与事务流传。默认就是 REQUIRED。

Spring 的申明式事务为事务流传定义了几个级别,默认流传级别就是 REQUIRED,它的意思是,如果以后没有事务,就创立一个新事务,如果以后有事务,就退出到以后事务中执行。其余的还有:

1.SUPPORTS:示意如果有事务,就退出到以后事务,如果没有,那也不开启事务执行。这种流传级别可用于查询方法,因为 SELECT 语句既能够在事务内执行,也能够不须要事务;

2.MANDATORY:示意必须要存在以后事务并退出执行,否则将抛出异样。这种流传级别可用于外围更新逻辑,比方用户余额变更,它总是被其余事务办法调用,不能间接由非事务办法调用;

3.REQUIRES_NEW:示意不论以后有没有事务,都必须开启一个新的事务执行。如果以后曾经有事务,那么以后事务会挂起,等新事务实现后,再复原执行;

4.NOT_SUPPORTED:示意不反对事务,如果以后有事务,那么以后事务会挂起,等这个办法执行实现后,再复原执行;

5.NEVER:和 NOT_SUPPORTED 相比,它岂但不反对事务,而且在监测到以后有事务时,会抛出异样拒绝执行;

6.NESTED:示意如果以后有事务,则开启一个嵌套级别事务,如果以后没有事务,则开启一个新事务。

1.2 本地事务

1.2.1 本地事务定义

定义:在单体利用中,咱们执行多个业务操作应用的是同一个连贯,操作同一个数据库,操作不同表,一旦有异样咱们能够整体回滚。

其实在介绍事务的定义中,也介绍了一部分本地事务。本地事务通过 ACID 保证数据的强一致性,在咱们理论开发过程中,咱们或多或少都应用了本地事务。例如,MySQL 事务处理应用 begin 开始事务、rollback 回滚事务、commit 确认事务。事务提交后,通过 redo log 记录变更,通过 undo log 在失败时进行回滚,保障事务原子性。在咱们日常应用 Java 语言开发时,都接触过 Spring,Spring 应用 @Transactional 注解就能够实现事务性能,后面咱们也介绍过了。事实上,Spring 封装了这些细节,在生成相干的 Bean 的时候,在须要注入相干的带有 @Transactional 注解的 Bean 时候用代理去注入,在代理中开启提交 / 回滚事务。

1.2.2 本地事务的毛病

随着业务的高速倒退,面对海量数据,例如,上千万甚至上亿的数据,查问一次所破费的工夫会变长,甚至会造成数据库的单点压力。因而,咱们就要思考分库与分表计划了。分库与分表的目标在于,减小数据库的单库单表累赘,进步查问性能,缩短查问工夫。这里,咱们先来看下单库拆分的场景。事实上,分表策略能够演绎为垂直拆分和程度拆分。垂直拆分,把表的字段进行拆分,即一张字段比拟多的表拆分为多张表,这样使得行数据变小。一方面,能够缩小客户端程序和数据库之间的网络传输的字节数,因为生产环境共享同一个网络带宽,随着并发查问的增多,有可能造成带宽瓶颈从而造成阻塞。另一方面,一个数据块能寄存更多的数据,在查问时就会缩小 I/O 次数。程度拆分,把表的行进行拆分。因为表的行数超过几百万行时,就会变慢,这时能够把一张的表的数据拆成多张表来寄存。程度拆分,有许多策略,例如,取模分表,工夫维度分表等。这种场景下,尽管咱们依据特定规定分表了,咱们依然能够应用本地事务。

然而,库内分表,仅仅是解决了单表数据过大的问题,但并没有把单表的数据扩散到不同的物理机上,因而并不能加重 MySQL 服务器的压力,依然存在同一个物理机上的资源竞争和瓶颈,包含 CPU、内存、磁盘 IO、网络带宽等。对于分库拆分的场景,它把一张表的数据划分到不同的数据库,多个数据库的表构造一样。此时,如果咱们依据肯定规定将咱们须要应用事务的数据路由到雷同的库中,能够通过本地事务保障其强一致性。然而,对于依照业务和性能划分的垂直拆分,它将把业务数据别离放到不同的数据库中。这里,拆分后的零碎就会遇到数据的一致性问题,因为咱们须要通过事务保障的数据扩散在不同的数据库中,而每个数据库只能保障本人的数据能够满足 ACID 保障强一致性,然而在分布式系统中,它们可能部署在不同的服务器上,只能通过网络进行通信,因而无奈精确的晓得其余数据库中的事务执行状况。

此外,不仅仅在跨库调用存在本地事务无奈解决的问题,随着微服务的落地中,每个服务都有本人的数据库,并且数据库是互相独立且通明的。那如果服务 A 须要获取服务 B 的数据,就存在跨服务调用,如果遇到服务宕机,或者网络连接异样、同步调用超时等场景就会导致数据的不统一,这个也是一种分布式场景下须要思考数据一致性问题。

当业务量级扩充之后的分库,以及微服务落地之后的业务服务化,都会产生分布式数据不统一的问题。既然本地事务无奈满足需要,因而就须要分布式事务。

2、分布式事务定义

分布式事务定义:咱们能够简略地了解,它就是为了保障不同数据库的数据一致性的事务解决方案。这里,咱们有必要先来理解下 CAP 准则和 BASE 实践。CAP 准则是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分区容错性)的缩写,它是分布式系统中的平衡理论。在分布式系统中,一致性要求所有节点每次读操作都能保障获取到最新数据;可用性要求无论任何故障产生后都能保障服务依然可用;分区容错性要求被分区的节点能够失常对外提供服务。事实上,任何零碎只可同时满足其中二个,无奈三者兼顾。对于分布式系统而言,分区容错性是一个最根本的要求。那么,如果抉择了一致性和分区容错性,放弃可用性,那么网络问题会导致系统不可用。如果抉择可用性和分区容错性,放弃一致性,不同的节点之间的数据不能及时同步数据而导致数据的不统一。

此时,BASE 实践针对一致性和可用性提出了一个计划,BASE 是 Basically Available(根本可用)、Soft-state(软状态)和 Eventually Consistent(最终一致性)的缩写,它是最终一致性的实践撑持。简略地了解,在分布式系统中,容许损失局部可用性,并且不同节点进行数据同步的过程存在延时,然而在通过一段时间的修复后,最终可能达到数据的最终一致性。BASE 强调的是数据的最终一致性。相比于 ACID 而言,BASE 通过容许损失局部一致性来取得可用性。

当初比拟罕用的分布式事务解决方案,包含强一致性的两阶段提交协定,三阶段提交协定,以及最终一致性的牢靠事件模式、弥补模式,TCC 模式。

3、分布式事务 - 强一致性解决方案

3.1 二阶段提交协定

在分布式系统中,每个数据库只能保障本人的数据能够满足 ACID 保障强一致性,然而它们可能部署在不同的服务器上,只能通过网络进行通信,因而无奈精确的晓得其余数据库中的事务执行状况。因而,为了解决多个节点之间的协调问题,就须要引入一个协调者负责管制所有节点的操作后果,要么全副胜利,要么全副失败。其中,XA 协定是一个分布式事务协定,它有两个角色:事务管理者和资源管理者。这里,咱们能够把事务管理者了解为协调者,而资源管理者了解为参与者。

XA 协定通过二阶段提交协定保障强一致性。

二阶段提交协定,顾名思义,它具备两个阶段:第一阶段筹备,第二阶段提交。这里,事务管理者(协调者)次要负责管制所有节点的操作后果,包含筹备流程和提交流程。第一阶段,事务管理者(协调者)向资源管理者(参与者)发动筹备指令,询问资源管理者(参与者)预提交是否胜利。如果资源管理者(参与者)能够实现,就会执行操作,并不提交,最初给出本人响应后果,是预提交胜利还是预提交失败。第二阶段,如果全副资源管理者(参与者)都回复预提交胜利,资源管理者(参与者)正式提交命令。如果其中有一个资源管理者(参与者)回复预提交失败,则事务管理者(协调者)向所有的资源管理者(参与者)发动回滚命令。举个案例,当初咱们有一个事务管理者(协调者),三个资源管理者(参与者),那么这个事务中咱们须要保障这三个参与者在事务过程中的数据的强一致性。首先,事务管理者(协调者)发动筹备指令预判它们是否曾经预提交胜利了,如果全副回复预提交胜利,那么事务管理者(协调者)正式发动提交命令执行数据的变更。

留神的是,尽管二阶段提交协定为保障强一致性提出了一套解决方案,然而依然存在一些问题。其一,事务管理者(协调者)次要负责管制所有节点的操作后果,包含筹备流程和提交流程,然而整个流程是同步的,所以事务管理者(协调者)必须期待每一个资源管理者(参与者)返回操作后果后能力进行下一步操作。这样就非常容易造成同步阻塞问题。其二,单点故障也是须要认真思考的问题。事务管理者(协调者)和资源管理者(参与者)都可能呈现宕机,如果资源管理者(参与者)呈现故障则无奈响应而始终期待,事务管理者(协调者)呈现故障则事务流程就失去了控制者,换句话说,就是整个流程会始终阻塞,甚至极其的状况下,一部分资源管理者(参与者)数据执行提交,一部分没有执行提交,也会呈现数据不一致性。此时,读者会提出疑难:这些问题应该都是小概率状况,个别是不会产生的?是的,然而对于分布式事务场景,咱们不仅仅须要思考失常逻辑流程,还须要关注小概率的异样场景,如果咱们对异样场景不足解决计划,可能就会呈现数据的不一致性,那么前期靠人工干预解决,会是一个老本十分大的工作,此外,对于交易的外围链路兴许就不是数据问题,而是更加重大的资损问题。

3.2 三阶段提交协定

二阶段提交协定诸多问题,因而三阶段提交协定就要登上舞台了。三阶段提交协定是二阶段提交协定的改进版本,它与二阶段提交协定不同之处在于,引入了超时机制解决同步阻塞问题,此外退出了准备阶段尽可能提前发现无奈执行的资源管理者(参与者)并且终止事务,如果全副资源管理者(参与者)都能够实现,才发动第二阶段的筹备和第三阶段的提交。否则,其中任何一个资源管理者(参与者)回复执行失败或者超时期待,那么就终止事务。总结一下,三阶段提交协定包含:第一阶段准备,第二阶段筹备,第二阶段提交。

这里可能大家有点蒙,我再具体解说一下三阶段提交的整体流程。

3PC 次要是为了解决两阶段提交协定的单点故障问题和放大参与者阻塞范畴。引入参加节点的超时机制之外,3PC 把 2PC 的筹备阶段分成事务询问(该阶段不会阻塞)和事务预提交, 则三个阶段别离为 CanCommit、PreCommit、DoCommit。

(1)第一阶段(CanCommit 阶段)

相似于 2PC 的筹备(第一)阶段。协调者向参与者发送 commit 申请,参与者如果能够提交就返回 Yes 响应,否则返回 No 响应。

1. 事务询问:协调者向参与者发送 CanCommit 申请。询问是否能够执行事务提交操作。而后开始期待参与者的响应。2. 响应反馈
    参与者接到 CanCommit 申请之后,失常状况下,如果其本身认为能够顺利执行事务,则返回 Yes 响应,并进入准备状态。否则反馈 No。

(2)第二阶段(PreCommit 阶段)

协调者依据参与者的反馈状况来决定是否能够忘性事务的 PreCommit 操作。依据响应状况,有以下两种可能:

如果响应 Yes,则:

1. 发送预提交申请:协调者向参与者发送 PreCommit 申请,并进入 Prepared 阶段。2. 事务预提交
    参与者接管到 PreCommit 申请后,会执行事务操作,并将 undo 和 redo 信息记录到事务日志中。3. 响应反馈
    如果参与者胜利的执行了事务操作,则返回 ACK 响应,同时开始期待最终指令。

如果有任何一个参与者向协调者发送了 No 响应,或者期待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。则有:

1. 发送中断请求:协调者向所有参与者发送 abort 申请。2. 中断事务
    参与者收到来自协调者的 abort 申请之后(或超时之后,仍未收到协调者的申请),执行事务的中断。

(3)第三阶段(doCommit 阶段)

该阶段进行真正的事务提交,也能够分为执行提交和中断事务两种状况。

如果执行胜利,则有如下操作:

1. 发送提交申请
    协调者接管到参与者发送的 ACK 响应,那么它将从预提交状态进入到提交状态。并向所有参与者发送 doCommit 申请。2. 事务提交
    参与者接管到 doCommit 申请之后,执行正式的事务提交。并在实现事务提交之后开释所有事务资源。3. 响应反馈
    事务提交完之后,向协调者发送 ACK 响应。4. 实现事务
    协调者接管到所有参与者的 ACK 响应之后,实现事务。

协调者没有接管到参与者发送的 ACK 响应(可能是接受者发送的不是 ACK 响应,也可能响应超时),那么就会执行中断事务(留神这是没有收到二段段最初的 ACK,这里要了解分明)。则有如下操作:

1. 发送中断请求
    协调者向所有参与者发送 abort 申请

2. 事务回滚
    参与者接管到 abort 申请之后,利用其在阶段二记录的 undo 信息来执行事务的回滚操作,并在实现回滚之后开释所有的事务资源。3. 反馈后果
    参与者实现事务回滚之后,向协调者发送 ACK 音讯

4. 中断事务
    协调者接管到参与者反馈的 ACK 音讯之后,执行事务的中断。

最要害的 在 doCommit 阶段,如果参与者无奈及时接管到来自协调者的 doCommit 或者 rebort 申请时(1、协调者呈现问题;2、协调者和参与者呈现网络故障),会在期待超时之后,会持续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,阐明参与者在第二阶段曾经收到了 PreCommit 申请,那么协调者产生 PreCommit 申请的前提条件是他在第二阶段开始之前,收到所有参与者的 CanCommit 响应都是 Yes。(一旦参与者收到了 PreCommit,象征他晓得大家其实都批准批改了)所以,一句话概括就是,当进入第三阶段时,因为网络超时等起因,尽管参与者没有收到 commit 或者 abort 响应,然而它有理由置信:胜利提交的几率很大)

三阶段提交协定很好的解决了二阶段提交协定带来的问题,是一个十分有参考意义的解决方案。然而,极小概率的场景下可能会呈现数据的不一致性。因为三阶段提交协定引入了超时机制,一旦参与者无奈及时收到来自协调者的信息之后,他会默认执行 commit。而不会始终持有事务资源并处于阻塞状态。然而这种机制也会导致数据一致性问题,因为,因为网络起因,协调者发送的 abort 响应没有及时被参与者接管到,那么参与者在期待超时之后执行了 commit 操作。这样就和其余接到 abort 命令并执行回滚的参与者之间存在数据不统一的状况。

4、分布式事务 - 最终一致性解决方案

4.1 TCC 模式

二阶段提交协定和三阶段提交协定很好的解决了分布式事务的问题,然而在极其状况下依然存在数据的不一致性,此外它对系统的开销会比拟大,引入事务管理者(协调者)后,比拟容易呈现单点瓶颈,以及在业务规模一直变大的状况下,零碎可伸缩性也会存在问题。留神的是,它是同步操作,因而引入事务后,直到全局事务完结能力开释资源,性能可能是一个很大的问题。因而,在高并发场景下很少应用。因而,须要另外一种解决方案:TCC 模式。留神的是,很多读者把二阶段提交等同于二阶段提交协定,这个是一个误区,事实上,TCC 模式也是一种二阶段提交。

TCC 模式将一个工作拆分三个操作:Try、Confirm、Cancel。如果,咱们有一个 func() 办法,那么在 TCC 模式中,它就变成了 tryFunc()、confirmFunc()、cancelFunc() 三个办法。

在 TCC 模式中,主业务服务负责发动流程,而从业务服务提供 TCC 模式的 Try、Confirm、Cancel 三个操作。其中,还有一个事务管理器的角色负责管制事务的一致性。例如,咱们当初有三个业务服务:交易服务,库存服务,领取服务。用户选商品,下订单,紧接着抉择领取形式进行付款,而后这笔申请,交易服务会先调用库存服务扣库存,而后交易服务再调用领取服务进行相干的领取操作,而后领取服务会申请第三方领取平台创立交易并扣款,这里,交易服务就是主业务服务,而库存服务和领取服务是从业务服务。

咱们再来梳理下,TCC 模式的流程。第一阶段主业务服务调用全副的从业务服务的 Try 操作,并且事务管理器记录操作日志。第二阶段,当全副从业务服务都胜利时,再执行 Confirm 操作,否则会执行 Cancel 逆操作进行回滚。

留神 咱们要特地留神操作的幂等性。幂等机制的外围是保障资源唯一性,例如反复提交或服务端的多次重试只会产生一份后果。领取场景、退款场景,波及金钱的交易不能呈现屡次扣款等问题。事实上,查问接口用于获取资源,因为它只是查问数据而不会影响到资源的变动,因而不论调用多少次接口,资源都不会扭转,所以是它是幂等的。而新增接口是非幂等的,因为调用接口屡次,它都将会产生资源的变动。因而,咱们须要在呈现反复提交时进行幂等解决。

那么,如何保障幂等机制呢?事实上,咱们有很多实现计划。其中,一种计划就是常见的创立惟一索引。在数据库中针对咱们须要束缚的资源字段创立惟一索引,能够避免插入反复的数据。然而,遇到分库分表的状况是,惟一索引也就不那么好使了,此时,咱们能够先查问一次数据库,而后判断是否束缚的资源字段存在反复,没有的反复时再进行插入操作。留神的是,为了防止并发场景,咱们能够通过锁机制,例如乐观锁与乐观锁保证数据的唯一性。这里,分布式锁是一种常常应用的计划,它通常状况下是一种乐观锁的实现。然而,很多人常常把乐观锁、乐观锁、分布式锁当作幂等机制的解决方案,这个是不正确的。除此之外,咱们还能够引入状态机,通过状态机进行状态的束缚以及状态跳转,确保同一个业务的流程化执行,从而实现数据幂等。

4.2 弥补模式

咱们提到了重试机制。事实上,它也是一种最终一致性的解决方案:咱们须要通过最大致力一直重试,保障数据库的操作最终肯定能够保证数据一致性,如果最终多次重试失败能够依据相干日志并被动告诉开发人员进行手工染指。留神的是,被调用方须要保障其幂等性。重试机制能够是同步机制,例如主业务服务调用超时或者非异样的调用失败须要及时从新发动业务调用。重试机制能够大抵分为固定次数的重试策略与固定工夫的重试策略。除此之外,咱们还能够借助音讯队列和定时工作机制。音讯队列的重试机制,即音讯生产失败则进行从新投递,这样就能够防止音讯没有被生产而被抛弃,例如 JMQ 能够默认容许每条音讯最多重试 多少 次,每次重试的间隔时间能够进行设置。定时工作的重试机制,咱们能够创立一张工作执行表,并减少一个“重试次数”字段。这种设计方案中,咱们能够在定时调用时,获取这个工作是否是执行失败的状态并且没有超过重试次数,如果是则进行失败重试。然而,当呈现执行失败的状态并且超过重试次数时,就阐明这个工作永恒失败了,须要开发人员进行手工染指与排查问题。

除了重试机制之外,也能够在每次更新的时候进行修复。例如,对于社交互动的点赞数、珍藏数、评论数等计数场景,兴许因为网络抖动或者相干服务不可用,导致某段时间内的数据不统一,咱们就能够在每次更新的时候进行修复,保证系统通过一段较短的工夫的自我复原和修改,数据最终达到统一。须要留神的是,应用这种解决方案的状况下,如果某条数据呈现不一致性,然而又没有再次更新修复,那么其永远都会是异样数据。

定时校对也是一种十分重要的解决伎俩,它采取周期性的进行校验操作来保障。对于定时工作框架的选型上,业内比拟罕用的有单机场景下的 Quartz,以及分布式场景下 Elastic-Job、XXL-JOB、SchedulerX 等分布式定时工作中间件,咱公司有分布式调用平台(https://schedule.jd.com/)。对于定时校对能够分为两种场景,一种是未实现的定时重试,例如咱们利用定时工作扫描还未实现的调用工作,并通过弥补机制来修复,实现数据最终达到统一。另一种是定时核查,它须要主业务服务提供相干查问接口给从业务服务核查查问,用于复原失落的业务数据。当初,咱们来试想一下电商场景的退款业务。在这个退款业务中会存在一个退款根底服务和自动化退款服务。此时,自动化退款服务在退款根底服务的根底上实现退款能力的加强,实现基于多规定的自动化退款,并且通过音讯队列接管到退款根底服务推送的退款快照信息。然而,因为退款根底服务发送音讯失落或者音讯队列在屡次失败重试后的被动抛弃,都很有可能造成数据的不一致性。因而,咱们通过定时从退款根底服务查问核查,复原失落的业务数据就显得特地重要了。

4.3 牢靠事件模式

在分布式系统中,音讯队列在服务端的架构中的位置十分重要,次要解决异步解决、零碎解耦、流量削峰等问题。多个零碎之间如果应用同步通信,则很容易造成阻塞,同时会将这些零碎耦合在一起,因而,引入音讯队列后,一方面解决了同步通信机制造成的阻塞,另一方面通过音讯队列实现了业务解耦。

牢靠事件模式,通过引入牢靠的音讯队列,只有保障以后的牢靠事件投递并且音讯队列确保事件传递至多一次,那么订阅这个事件的消费者保障事件可能在本人的业务内被生产即可。这里是否只有引入了音讯队列就能够解决问题了呢?事实上,只是引入音讯队列并不能保障其最终的一致性,因为分布式部署环境下都是基于网络进行通信,而网络通信过程中,上下游可能因为各种起因而导致音讯失落。

其一,主业务服务发送音讯时可能因为音讯队列无奈应用而产生失败。对于这种状况,咱们能够让主业务服务(生产者)发送音讯,再进行业务调用来确保。个别的做法是,主业务服务将要发送的音讯长久化到本地数据库,设置标记状态为“待发送”状态,而后把音讯发送给音讯队列,音讯队列先向主业务服务(生产者)返回音讯队列的响应后果,而后主业务服务判断响应后果执行之后的业务解决。如果响应失败,则放弃之后的业务解决,设置本地的长久化音讯标记状态为“失败”状态。否则,执行后续的业务解决,设置本地的长久化音讯标记状态为“已发送”状态。

此外,音讯队列接管音讯后,也可能从业务服务(消费者)宕机而无奈生产。JMQ 有 ACK 机制,如果生产失败,会重试,如果胜利,会从音讯队列中删除此条音讯。那么,音讯队列如果始终重试失败而无奈投递,会在肯定次数之后被动抛弃,当然咱们也能够设置为始终重试,这种形式不举荐。咱们须要如何解决呢?咱们在上个步骤中,主业务服务曾经将要发送的音讯长久化到本地数据库。因而,从业务服务生产胜利后,它也会向音讯队列发送一个告诉音讯,此时它是一个音讯的生产者。主业务服务(消费者)接管到音讯后,最终把本地的长久化音讯标记状态为“实现”状态。这就是应用“正反向音讯机制”确保了音讯队列牢靠事件投递。当然,弥补机制也是必不可少的。定时工作会从数据库扫描在肯定工夫内未实现的音讯并从新投递。大家也可能会说,生产胜利之后能够用 RPC 调用主业务服务,首先这样主业务服务要额定提供一个 RPC 的接口;另外也会对从业务服务造成业务的复杂度和耗时影响。这里要留神从业务服务要保障幂等性。

理解了“牢靠事件模式”的方法论后,当初咱们来看一个实在的案例来加深了解。首先,当用户发动退款后,自动化退款服务会收到一个退款的事件音讯,此时,如果这笔退款合乎自动化退款策略的话,自动化退款服务会先写入本地数据库长久化这笔退款快照,紧接着,发送一条执行退款的音讯投递到给音讯队列,音讯队列承受到音讯后返回响应胜利后果,那么自动化退款服务就能够执行后续的业务逻辑。与此同时,音讯队列异步地把音讯投递给退款根底服务,而后退款根底服务执行本人业务相干的逻辑,执行失败与否由退款根底服务自我保障,如果执行胜利则发送一条执行退款胜利音讯投递到给音讯队列。最初,定时工作会从数据库扫描在肯定工夫内未实现的音讯并从新投递。这里,须要留神的是,自动化退款服务长久化的退款快照能够了解为须要确保投递胜利的音讯,由“正反向音讯机制”和“定时工作”确保其胜利投递。此外,真正的退款出账逻辑在退款根底服务来保障,因而它要保障幂等性。当呈现执行失败的状态并且超过重试次数时,就阐明这个工作永恒失败了,须要开发人员进行手工染指与排查问题。

总结一下,引入了音讯队列并不能保障牢靠事件投递,换句话说,因为网络等各种起因而导致音讯失落不能保障其最终的一致性,因而,咱们须要通过“正反向音讯机制”确保了音讯队列牢靠事件投递,并且应用弥补机制尽可能在肯定工夫内未实现的音讯并从新投递。

5、总结

Google Chubby 的作者 Mike Burrows 说过,there is only one consensus protocol, and that’s Paxos”– all other approaches are just broken versions of Paxos. 意思是世上只有一种一致性算法,那就是 Paxos,所有其余一致性算法都是 Paxos 算法的不完整版。下面都是以 Paxos 算法实践为根底具象化的计划。Google 的 Chubby、MegaStore、Spanner 等零碎,ZooKeeper 的 ZAB 协定,还有更加容易了解的 Raft 协定都有 Paxos 算法的影子,感兴趣的能够去看 Paxos 算法具体阐明,这里就不再赘述了。

当初在做流动平台相干我的项目,常常短时间要实现一个流动组件,个别没有残缺的思考微服务下保障事务的一致性或者一套对立的规范,所以须要微服务下保障事务的一致性 SOP,这样每个能够保障每个流动疾速搭建和平安运行。后续会推出流动平台在微服务下保障事务一致性的整体计划。大家可能以前都听过或者在写代码过程中或多或少都思考过,也或多或少应用过后面提到过的这些计划,然而没有一个系统性的理解或者欠缺的计划调研,心愿通过这篇文章能让大家有个略微残缺的理解,文章中有任何有余或者大家有更好的计划,欢送一起独特探讨。

正文完
 0