20191127 分布式系统下最终一致性解决方案

分布式事务+(幂等、重试、状态机、恢复日志、异步校验)java

一个操作须要在四个服务上写数据,这四个服务对应不一样的db;aMysql,bMysql,cMysql,dMysql;mysql

如何保证同时成功,同时失败?程序员

A服务在写sql的同时,调用服务B,这两个操作在一个事务里面。B服务也能够这样处理。redis

 

若是接口不能保证幂等性,数据的惟一性将很难保证。 数据的惟一性。算法

 

帐户表数据重复:接口进行幂等性处理,分布式环境下使用redis锁,惟一索引。spring

把无效数据迁移到另外一张表中,对用户的手机号作惟一索引。sql

 

如何保证消息投递的可靠性? 使用mysql进行保证,发送消息前保存消息状态,消费端接收完成或者消费完成后,在修改消息状态。配合定时任务处理。从新发送。数据库

 

业务逻辑保证幂等,若是业务逻辑没法保证幂等,则要增长一个去重表或者相似的实现。后端

 

要保证数据的一致性,首先数据不一致有哪些出现的场景?缓存

1)接口没作幂等,数据重复。

2)消息丢失,致使订单支付状态不一致等。

3)异步处理数据,不知道数据是否执行成功。

4)在四个db中须要落库,如何保证都执行成功。 分布式事务框架。

 

多副本数据的强一致性,弱一致性和最终一致性

一、一致性又能够分为强一致性与弱一致性。

二、强一致性能够理解为在任意时刻,全部节点中的数据是同样的。同一时间点,你在节点A中获取到key1的值与在节点B中获取到key1的值应该都是同样的。

三、弱一致性包含不少种不一样的实现,目前分布式系统中普遍实现的是最终一致性

四、所谓最终一致性,就是不保证在任意时刻任意节点上的同一份数据都是相同的,可是随着时间的迁移,不一样节点上的同一份数据老是在向趋同的方向变化。也能够简单的理解为在一段时间后,节点间的数据会最终达到一致状态。

BASE原则发展自CAP定理,舍弃了系统的强一致性而选择AP,但每一个应用能够根据自身的业务特色,采用适当的方式来使系统达到最终一致性。用较通俗的话来描述就是 : “过程宽松,结果严格,你的老板不关心过程,只看结果”。

 

在我原来所作系统中,要根据不一样维度记录用户的帐本,如根据用户名、银行卡号记录月帐、年帐。因为数据量大,需对记帐数据进行分库分表,一致性的问题就产生了(数据库分表分库之后就会产生一致性问题)。

建立一张重试表,在记帐操做重试过若干次(3次)还不成功的状况下,给运营人员发送短信,由人为介入的方式进行相应的调帐。至此,可保证记帐操做的最终一致性。

 

如何实现分布式系统的最终一致性

在大型分布式企业级应用中,分布式最终一致性方案须要根据系统自身特色量身定制,是系统设计的重点。

大部分业务流程都须要跨多微服务的调用来协做完成,而且要求系统确保分布式最终一致性。

能够选择分布式事务框架方案,目前主流的分布式事务框架大体可分为3类实现 :

一、基于XA协议的两阶段提交(2PC)方案(两阶段提交协议,三阶段提交协议

二、基于支付宝最先提出的TCC(Try、Confirm、Cancel)方案

三、基于ebay最先提出的消息队列异步确保方案

此外还有较轻的解决方案,业务系统能够根据自身须要,选择经过幂等/重试、状态机、恢复日志、异步校验等技术来确保最终一致性。

 

采用分布式事务框架的方案,最终一致性由分布式事务框架保证,业务程序员对框架细节彻底透明。

选择这种方案,须要注意几个点。首先,因为分阶段提交协议自己的脆弱性,主流分阶段提交协议如2PC,3PC, TCC都没法彻底确保最终一致性,要采用异步校验的手段兜底。其次,分阶段提交协议带来的高延迟,屡次协议通讯RTT带来的时间损耗。第三,基于消息队列异步确保的分布式事务框架实现,须要考虑消息可靠性和业务侵入问题。分布式事务框架也有巨大的优点,首先,分布式事务被框架封装成切面,业务开发只需关心纯业务。其次,分布式事务的代码开发量大大减小。对一致性和代码质量有极高要求的银行、金融领域,分布式事务框架是最佳选择。

 

不一样于采用分布式事务框架的最终一致性方案,程序员也能够选择经过幂等/重试、状态机、恢复日志、异步校验等技术来确保最终一致性。这种方案不受限于平台和框架,系统较精简灵活,初期业务系统大都基于这种分布式一致性解决方案。不过这种方案对业务开发的要求更高,分布式一致性逻辑要业务程序员代码实现,容易出现bug。

其实,主流的分布式事务框架也是经过这些基本的系统机制如幂等/重试、状态机、恢复日志、异步校验等来确保的最终一致性,对比两种方案,下文主要围绕后一种展开论述,讨论5点使系统达成分布式最终一致性的技术实践。

分布式事务框架;(幂等/重试、状态机、恢复日志、异步校验);

 

实践 一、重试

重试机制可使分布式不一致数据自动恢复,前提是重试接口要提供幂等保证。重试机制是达成分布式最终一致性的重要手段。例如,超时重传是TCP协议保证数据可靠性的一个重要机制,核心思想其实就是重试。

1)同步重试 : 在上次请求失败或超时,程序再次发起同步调用请求。后端程序不推荐同步重试,其一由于同步等待占用系统线程资源,其二由于重试引发的流量放大,可能致使系统雪崩。

2)异步重试 : 经过异步系统(消息队列或调度中间件)对失败或超时请求再次发起调用。推荐这种方式的重试,重试的时间间隔能够设置为根据重试次数指数增加,超太重试阈值仍未成功,能够报警通知并由人工订正

重试也是提升系统可用性的一种有效手段。若是一个服务的可用性为98%(有1个9),1次重试以后其可用性可达到99.96%(3个9),2次重试能够达到99.9992%(5个9)。

重试机制   1)保证接口幂等   2)重试最大次数,默认次数。   3)重试是否有可能形成服务器端压力增大?

若是接口调用失败了,是否进行请求重试;若是进行重试,默认重试几回;屡次重试是否会对服务器端形成压力?

 

 

二、幂等

幂等的数学定义为 f(f(x)) = f(x)

用通俗的话来讲就是 : 相同的操做执行屡次和执行一次产生的效果是同样的。有的操做是自然幂等的,如查询、删除操做。有的操做是人为使其幂等,例如TCP的超时重传操做就是幂等的,不管客户端将一个seq字节传送多少次,服务端窗口只会用一次该字节。幂等实现方式有不少 :

1)基于记录的悲观锁,MySQL中经过SELECT FOR UPDATE语句实现。这种实现方式要设置AUTOCOMMIT=0,加锁和更新记录在同一个事务中,长时间锁定记录会下降系统的TPS,高并发场景不推荐使用。

2)基于记录版本号或状态机的乐观锁方案,适用于更新数据场景。例如,用户下单购买一个商品的扣库存操做实现幂等,能够用以下SQL语句实现 : UPDATE stocktable SET stock = stock - 1, version = version + 1 WHERE product_id = 123 and version = 1

3)基于数据库惟一索引的去重表,适用于插入和更新数据的场景,由数据库唯一索引确保屡次插入和更新操做只有一次生效。

4)基于全局惟一标识token实现,这种方案要注意几点 : 一、这里校验token是否可以使用和设置token为已使用,是一个CAS原子操做,须要确保在一个原子操做中。 二、若是token存储使用的是Redis,那么验证token的CAS操做可使用原子自增操做incr,若是Redis值大于1则token不可以使用,反之可以使用。还有一种实现方式是token生成系统将token预先写入Redis,用删除操做来校验token是否被使用,删除成功表明token未被使用可执行操做。 三、若是token存储使用的是MySQL,根据token分库分表和建唯一索引,同时经过insert语句来判断token是否存在,若是insert失败则token不可以使用,反之可以使用。

 

三、状态机(正常状态,状态机,异常状态)

状态机是表示实体的状态根据条件转移的数学模型。经过状态机模型,系统能够判断当前不一致状态,以及如何校订不一致状态到一致状态。这样说可能比较抽象,咱们拿发微信群红包的例子来讲明。当你点开发红包按钮,输入总金额、红包个数、标题,点击支付成功后。其实根据时间前后红包系统后台至少经历过这样一个状态机 :

 

1)、当输入总金额、红包个数、标题点击提交,首前后台建立一个初始化状态(INIT)红包

2)、接着系统将根据你输入的总金额和个数n将红包拆分红n分,此时红包的状态为拆分红功(SPLITTED)

3)、此时红包后台会监听异步支付消息,若是支付成功则将红包置为支付成功(PAID)

4)、以后红包系统会通知微信IM系统,发送消息通知群里的用户,此时红包状态为(NOTIFIED)

5.1)、群里的用户把红包抢光了,红包状态被系统置为已抢光(RUNOUT)

5.2)、还有一种可能,若是群里都是程序员,忙着撸代码,没时间抢红包,必定时间后红包自动退款到支付帐号,红包状态便为(REFUND)。

 

这只是一个正常业务流程的红包状态机,异常状况如拆分失败、支付失败、通知失败、退款失败等状况也同理存在一个状态机器。为了方便业务实体状态回滚和校订,状态机要尽可能设计精简,转移到下一个状态的边尽量的只有一条路径(终结状态会例外),这样在回滚和校订时可以明确前一个状态和后一个状态。举个例子,若是系统发现红包一直处于PAID状态,而并无流转到NOTIFIED状态,可以判断是通知群用户出现异常,能够根据实际状况从新通知群用户或者将超期红包退款。

 

四、恢复日志

恢复日志是程序现场的记录,也是业务数据恢复的重要依据。恢复日志log要求全局惟一的requestId来标示请求(实际的业务场景可采用不会重复有含义的业务id),出现异常,能够根据requestId维度redo和undo业务操做,恢复日志具体可分为三部分 :

1)requestId请求开始时,记录REQUEST START requestId

2)本地修改时,记录所有的(requestId,x,originalValue, destValue)四元组,x表明操做对象,修改前x的值为originalValue,本次修改的目的操做值为destValue。

3)requestId结束时,记录REQUEST End requestId

恢复日志是系统从不一致的状态恢复到一致状态的重要数据,丢失恢复日志,意味着不一致可能没法恢复。为何是可能,由于有时能够经过状态机对不一致的状态进行恢复。

 

五、异步校验

完全解决分布式一致性问题,有著名的Paxos算法,经过该算法分布式系统自发达成一致性。而在具体的业务场景,彻底不须要系统自发的达成共识,咱们只要在业务系统外部加上严格的业务约束,用来仲裁业务系统的状态。经过异步校验,能够发现分布式系统中的异常状态,并经过恢复日志进行脚本批量恢复或者人工处理恢复,根据校验的粒度有 :

  • 根据业务实体id校验,使用消息队列,将须要校验业务id投递给校验系统,进行异步校验。
  • 根据时间维度批量校验,使用异步调度框架,根据时间粒度批量获取进行异步校验。

此外,并非全部系统都有可靠消息队列和调度服务支撑,业务系统能够增长一个本地业务id校验回执字段,校验系统根据校验步骤回调设置校验回执字段,并对校验未经过的数据进行重校验或者订正。

 

总结

分布式最终一致性问题,后端程序员在实际开发中常常遇到。在实际系统开发中为了确保最终一致性,每每须要组合多个技术点打出组合拳,由于招数是死的,程序员是活的。总结上面提到的技术点,咱们能够经过幂等和重试机制,使得不一致数据可以自动恢复经过异步校验机制发现业务系统的不一致数据经过状态机和恢复日志,纠正不一致的业务数据

 

1、查询模式:数据增删改操做,对每一个请求返回一个流水号,能够经过流水号查询增删改操做的结果。

表设计: id,请求url及参数,status,createTime,createId; 异步校验机制

订单服务,支付服务(是否支付成功,有订单编号和支付结果,可能存在消息丢失,订单关闭)

过了十五分钟,订单服务没收到支付服务的mq消息,订单服务主动去查一下支付服务支付状态。这就是异步校验机制。(定时任务,异步处理,订单超时管理)。

mq消息丢失(支付状态),致使订单服务和支付服务的数据不一致

 

2、补偿模式:经过修复使整个分布式系统达到一致。为了让系统最终达到一致性状态而作的努力都叫作补偿。

补偿操做根据发起形式分为如下几种。

1)自动恢复:程序根据发起不一致的环境,经过继续进行未完成的操做,或者回滚已经完成的操做,来自动达到一致性状态。

2)通知运营:若是程序没法自动恢复,而且设计时考虑到了不一致的场景,则能够提供运营功能,经过运营手工进行补偿。

3)技术运营:若是很不巧,系统没法自动恢复,又没有运营功能,那么必须经过技术手段来解决,技术手段包括进行数据库变动或者代码变动,这是最糟的一种场景,也是咱们在生产中尽可能避免的场景。

 

3、异步确保模式

异步确保模式是补偿模式的一个典型案例,常常应用到使用方对响应时间要求不过高的场景中,一般把这类操做从主流程中摘除,经过异步的方式进行处理,处理后把结果经过通知系统通知给使用方。这个方案的最大好处是可以对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入帐等。

在实践中将要执行的异步操做封装后持久入库,而后经过定时捞取未完成的任务进行补偿操做来实现异步确保模式,只要定时系统足够健壮,则任何任务最终都会被成功执行。

 

4、按期校对模式

按期校对模式多应用于金融系统中。金融系统因为涉及资金安全,须要保证准确性,因此须要多重的一致性保证机制,包括商户交易对帐、系统间的一致性对帐、现金对帐、帐务对帐、手续费对帐等,这些都属于按期校对模式。顺便说一下,金融系统与社交应用在技术上的本质区别为社交应用在于量大,而金融系统在于数据的准确性。

 

5、可靠消息模式

在分布式系统中,对于主流程中优先级比较低的操做,大多采用异步的方式执行,也就是异步确保模型,为了让异步操做的调用方和被调用方充分解耦,也因为专业的消息队列自己具备可伸缩、可分片、可持久等功能,咱们一般经过消息队列实现异步化。对于消息队列,咱们须要创建特殊的设施来保证可靠的消息发送及处理机的幂等性。

消息的可靠发送能够认为是尽最大努力发送消息通知,在发送消息以前将消息持久到数据库,状态标记为待发送,而后发送消息,若是发送成功,则将消息改成发送成功。定时任务定时从数据库捞取在必定时间内未发送的消息并将消息发送。经过mysql保证消息发送的可靠性。

一些公司把消息的可靠发送实如今了中间件里,经过Spring的注入,在消息发送时自动持久消息记录,若是有消息记录没有发送成功,则定时补偿发送。

 

6、缓存一致性模式

在大规模、高并发系统中的一个常见的核心需求就是亿级的读需求,显然,关系型数据库并非解决高并发读需求的最佳方案,互联网精单作法就是使用缓存来抗住读流量。下面是使用缓存来保证一致性的最佳实践。

1)若是性能要求不是很是高,则尽可能使用分布式缓存,而不要使用本地缓存。

2)写缓存时数据必定要完整,若是缓存数据的一部分有效,另外一部分无效,则宁肯在须要时回源数据库,也不要把部分数据放入缓存中。

3)使用缓存牺牲了一致性,为了提升性能,数据库与缓存只须要保持弱一致性,而不须要保持强一致性,不然违背了使用缓存的初衷。

4)读的顺序是先读缓存,后读数据库,写的顺序要先写数据库,后写缓存。

 

单数据库状况下的事务

若是应用系统是单一的数据库,那么这个很好保证,利用数据库的事务特性来知足事务的一致性,这时候的一致性是强一致性的。对于java应用系统来说,不多直接经过事务的start和commit以及rollback来硬编码,大多经过spring的事务模板或者声明式事务来保证。

 

基于事务型消息队列的最终一致性

借助消息队列,在处理业务逻辑的地方,发送消息,业务逻辑处理成功后,提交消息,确保消息是发送成功的,以后消息队列投递来进行处理,若是成功,则结束,若是没有成功,则重试,直到成功,不过仅仅适用业务逻辑中,第一阶段成功,第二阶段必须成功的场景。对应上图中的C流程。