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

一个操做需要在四个服务上写数据,这四个服务对应不同的db;aMysql,bMysql,cMysql,dMysql;

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

A服务在写sql的同时,调用服务B,这两个操做在一个事务里面。B服务也可以这样处理。

 

如果接口不能保证幂等性,数据的唯一性将很难保证。 数据的唯一性。

 

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

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

 

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

 

业务逻辑保证幂等,如果业务逻辑无法保证幂等,则要增加一个去重表或者类似的实现。

 

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

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

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

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

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

 

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

1、一致性又可以分为强一致性与弱一致性。

2、强一致性可以理解为在任意时刻,所有节点中的数据是一样的。同一时间点,你在节点A中获取到key1的值与在节点B中获取到key1的值应该都是一样的。

3、弱一致性包含很多种不同的实现,目前分布式系统中广泛实现的是最终一致性

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

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

 

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

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

 

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

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

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

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

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

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

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

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

 

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

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

 

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

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

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

 

实践 1、重试

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

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

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

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

 

2、幂等

幂等的数学定义为 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实现,这种方案要注意几点 : 1、这里校验token是否可使用和设置token为已使用,是一个CAS原子操作,需要确保在一个原子操作中。 2、如果token存储使用的是Redis,那么验证token的CAS操作可以使用原子自增操作incr,如果Redis值大于1则token不可使用,反之可使用。还有一种实现方式是token生成系统将token预先写入Redis,用删除操作来校验token是否被使用,删除成功代表token未被使用可执行操作。 3、如果token存储使用的是MySQL,根据token分库分表和建惟一索引,同时通过insert语句来判断token是否存在,如果insert失败则token不可使用,反之可使用。

 

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

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

 

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

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

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

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

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

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

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

 

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

 

4、恢复日志

恢复日志是程序现场的记录,也是业务数据恢复的重要依据。恢复日志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

恢复日志是系统从不一致的状态恢复到一致状态的重要数据,丢失恢复日志,意味着不一致可能无法恢复。为什么是可能,因为有时可以通过状态机对不一致的状态进行恢复。

 

5、异步校验

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

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

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

 

总结

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

 

一、查询模式:数据增删改操作,对每个请求返回一个流水号,可以通过流水号查询增删改操作的结果。

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

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

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

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

 

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

补偿操作根据发起形式分为以下几种。

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

2)通知运营:如果程序无法自动恢复,并且设计时考虑到了不一致的场景,则可以提供运营功能,通过运营手工进行补偿。

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

 

三、异步确保模式

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

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

 

四、定期校对模式

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

 

五、可靠消息模式

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

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

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

 

六、缓存一致性模式

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

1)如果性能要求不是非常高,则尽量使用分布式缓存,而不要使用本地缓存。

2)写缓存时数据一定要完整,如果缓存数据的一部分有效,另一部分无效,则宁可在需要时回源数据库,也不要把部分数据放入缓存中。

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

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