文章目的:本文旨在提炼一套分布式幂等问题的思考框架,而非解决某个具体的分布式幂等问题。在这个框架体系内,会有一些方案举例说明。
文章目标:但愿读者能经过这套思考框架设计出符合本身业务的完备的幂等解决方案。
文章内容:
(1)背景介绍,为何会有幂等。
(2)什么是幂等,这个定义很是重要,决定了整个思考框架。
(3)解决幂等问题的三部曲,也是做者的思考框架。
(4)总结
前端
一 背景
分布式系统由众多微服务组成,微服务之间必然存在大量的网络调用。下图是一个服务间调用异常的例子,用户提交订单以后,请求到A服务,A服务落单以后,开始调用B服务,可是在A调用B的过程当中,存在不少不肯定性,例如B服务执行超时了,RPC直接返回A请求超时了,而后A返回给用户一些错误提示,但实际状况是B有可能执行是成功的,只是执行时间过长而已。redis
用户看到错误提示以后,每每会选择在界面上重复点击,致使重复调用,若是B是个支付服务的话,用户重复点击可能致使同一个订单被扣屡次钱。不只仅是用户可能触发重复调用,定时任务、消息投递和机器从新启动均可能会出现重复执行的状况。在分布式系统里,服务调用出现各类异常的状况是很常见的,这些异常状况每每会使得系统间的状态不一致,因此须要容错补偿设计,最多见的方法就是调用方实现合理的重试策略,被调用方实现应对重试的幂等策略。sql
二 什么是幂等
对于幂等,有一个很常见的描述是:对于相同的请求应该返回相同的结果,因此查询类接口是自然的幂等性接口。举个例子:若是有一个查询接口是查询订单的状态,状态是会随着时间发生变化的,那么在两次不一样时间的查询请求中,可能返回不同的订单状态,这个查询接口仍是幂等接口吗?数据库
幂等的定义直接决定了咱们如何去设计幂等方案,若是幂等的含义是相同请求返回相同结果,那实际上只须要缓存第一次的返回结果,便可在后续重复请求时实现幂等了。但问题真的有这么简单吗?后端
笔者更赞同这种定义:幂等指的是相同请求(identical request)执行一次或者屡次所带来的反作用(side-effects)是同样的。缓存
引自:https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. In other words, an idempotent method should not have any side-effects (except for keeping statistics).安全
这个定义有必定的抽象,归纳性比较强,在设计幂等方案时,其实就是将抽象部分具化。例如:什么是相同的请求?哪些状况会有反作用?该如何避免反作用?且看三部曲。微信
三 解决方案三部曲
很多关于幂等的文章都称本身的方案是通用解决方案,但笔者却认为,不一样的业务场景下,相同请求和反作用都是有差别性的,不一样的反作用须要不一样的方案来解决,不存在彻底通用的解决方案。而三部曲旨在提炼出一种思考模式,并举例说明,在该思考模式下,更容易设计出符合业务场景的幂等解决方案。网络
第一部曲:识别相同请求
幂等是为了解决重复执行同一请求的问题,那如何识别一个请求有没有和以前的请求重复呢?有的方案是经过请求中的某个流水号字段来识别的,同一个流水号表示同一个请求。也有的方案是经过请求中某几个字段甚至所有字段进行比较,从而来识别是否为同一个请求。因此在方案设计时,明肯定义具体业务场景下什么是相同请求,这是第一部曲。架构
方案举例:token机制识别前端重复请求
在一条调用链路的后端系统中,通常均可以经过上游系统传递的reqNo+source来识别是不是为重复的请求。以下图,B系统是依赖于A系统传递的reqNo+source来识别相同请求的,可是A系统是直接和前端页面交互的系统,如何识别用户发起的请求是相同的呢?好比用户在支付界面上点击了屡次,A系统怎么识别这是一次重复操做呢?
前端能够在第一次点击完成时,将按钮设置为disable,这样用户没法在界面上重复点击第二次,但这只是提高体验的前端解决方案,不是真正安全的解决方案。
常见的服务端解决方案是采用token机制来实现防重复提交。以下图,
(1)当用户进入到表单页面的时候,前端会从服务端申请到一个token,并保存在前端。
(2)当用户第一次点击提交的时候,会将该token和表单数据一并提交到服务端,服务端判断该token是否存在,若是存在则执行业务逻辑。
(3)当用户第二次点击提交的时候,会将该token和表单数据一并提交到服务端,服务端判断该token是否存在,若是不存在则返回错误,前端显示提交失败。
这个方案结合先后端,从前端视角,这是用于防止重复请求,从服务端视角,这个用于识别前端相同请求。服务端每每基于相似于redis之类的分布式缓存来实现,保证生成token的惟一性和操做token时的原子性便可。核心逻辑以下。
// SETNX keyName value: 若是key存在,则返回0,若是不存在,则返回1
// step1. 申请token
String token = generateUniqueToken();
// step2. 校验token是否存在
if(redis.setNx(token, 1) == 1){
// do business
} else {
// 幂等逻辑
}
第二部曲:列出并减小反作用的分析维度
相同的请求重复执行业务逻辑,若是处理不当,会给系统带来反作用。那什么是反作用?从技术的角度理解就是返回结果后还致使某些“系统状态”发生变化,无反作用的函数称之为纯函数,体现到业务的角度就是业务没法接受的非预期结果。最多见的有重复入库、数据被错误变动等,大多数幂等方案就是围绕解决这类问题来设计的。而系统每每可能在多个维度都存在反作用,例如:
(1)调用下游维度:重复调用下游会怎样?若是下游没有幂等,重复调用会带来什么反作用?
(2)返回上游维度:例如第一次返回上游异常,第二次返回上游被幂等了?会给上游带来什么反作用?
(3)并发执行维度:并发重复执行会怎样?会有什么反作用?
(4)分布式锁维度:引入分布式锁来防止并发执行?可是若是锁出现不一致性,会有什么反作用?
(5)交互时序维度:有没有异步交互,是否存在时序问题?会有什么反作用?
(6)客户体验维度:从数据不一致到最终一致,必须在多少时间内完成?若是该时间内没有完成,会有什么反作用?例如大量客诉(秉承客户第一的原则,在支付宝,客诉量太大会定级为生产环境故障)。
(7)业务核对维度:重复调用是否存在覆盖核对标识的状况,带来没法正常核对的反作用?在金融系统中,资金链路没法核对是没法接受的。
(8)数据质量维度:是否存在重复记录?若是存在会有什么反作用?
上面是一些常见的分析维度,不一样行业的系统中会存在不同的维度,尽量地总结出这些维度,并列入系统分析时的checklist中,可以更好地完善幂等解决方案。没有反作用才算是完备的幂等解决方案,可是反作用的维度太多,会提升幂等方案的复杂度。因此在可以达成业务的前提下,减小一些分析维度,可以使得幂等方案实现起来更加经济有效。例如:若是有专门的幂等表存储返回给上游的幂等结果,第(2)维度不用考虑了,若是用锁来防止并发,第(3)个维度不考虑了,若是用单机锁代替分布式锁,第(4)个维度不考虑了。
这是解决幂等问题的第二部曲:列出并减小反作用的分析维度。在这部曲中,涉及的解决方案每每是解决某一个维度的反作用问题,适合以通用组件的形式存在,做为团队内部的一个公共技术套路。
方案举例:加锁避免并发重复执行
不少幂等解决方案都和防并发有关,那么幂等和并发到底有什么关联呢?二者的联系是:幂等解决的是重复执行的问题,重复执行既有串行重复执行(例如定时任务),也有并发重复执行。若是重复执行的业务逻辑没有共享变量和数据变动操做时,并发重复执行是没有反作用的,能够不考虑并发的问题。对于包含共享变量、涉及变动操做的服务(实际上这类服务居多),并发问题可能致使乱序读写共享变量,重复插入数据等问题。特别是并发读写共享变量,每每都是发生生产故障后才被感知到。
因此在并发执行的维度,将并发重复执行变成串行重复执行是最好的幂等解决方案。支付宝最多见的方法就是:一锁二判三更新,以下图。当一个请求过来以后:一锁,锁住要操做的资源;二判,识别是否为重复请求(第一部曲要定义的问题)、判断业务状态是否正常;三更新:执行业务逻辑。
Q&A
小A:锁可能形成性能影响,先判后锁再执行,能够提高效能。
大明:这样可能会失去防并发的效果。还记得double check实现单例模式吗?在加锁前判断了下,那加锁后为啥还要判断下?实际上第二次check才是必须的。想一想看?
小A画图思考中...
小A:明白了,一锁二判三更新,锁和判的顺序是不能变的,若是锁冲突比较高,能够在锁以前判断下,提升效率,因此称之为double check。
大明:是的,聪明。这两个场景不同,但并发思路是同样的。
private volatile static Girl theOnlyGirl;
// 实现单例时作了 double check
public static Girl getTheOnlyGirl() {
if (theOnlyGirl == null) { // 加锁前check
synchronized (Girl.class) {
if (theOnlyGirl == null) { // 加锁后check
theOnlyGirl = new Girl(); // 变动执行
}
}
}
return theOnlyGirl;
}
锁的实现能够是分布式锁,也是能够是数据库锁。分布式锁自己会带来锁的一致性问题,须要根据业务对系统稳定性的要求来考量。支付宝的不少系统是经过在业务数据库中新建一个锁记录表来实现业务锁组件,其分表逻辑和业务表的分表逻辑一致,就能够实现单机数据库锁。若是没有锁组件,悲观锁锁住业务单据也是能够知足条件的,悲观锁要在事务中用select for update来实现,要注意死锁问题,且where条件中必须命中索引,不然会锁表,不锁记录。
并发维度几乎是一个分布式幂等的通用分析维度,因此一个通用的锁组件是颇有必要的。但这也只是解决了并发这一个维度的反作用。虽然没有了并发重复执行的状况,但串行重复执行的状况依旧存在,重复执行才是幂等核心要解决的问题,重复执行若是还存在其它反作用,幂等问题就是没有解决掉。
加锁后业务的性能会下降,这个怎么解决?笔者认为,大多数状况下架构的稳定性比系统性能的优先级更高,何况对于性能的优化有太多地方能够去实现,减小坏代码、去除慢SQL、优化业务架构、水平扩展数据库资源等方式。经过系统压测来实现一个知足SLA的服务才是评估全链路性能的正确方法。
第三部曲:识别细粒度反作用,针对性设计解决方案
在解决了部分维度的反作用以后,就须要针对剩余维度存在的细粒度反作用进行逐一识别并解决了。在数据质量维度上,最大的一个反作用是重复数据。在交互维度上,最大的一个反作用是业务乱序执行。通常这类问题不设计成通用组件,能够开发人员自由发挥。本节用两个常见方案作为例子。
方案举例1:惟一性约束避免重复落库
在数据表设计时,设计两个字段:source、reqNo,source表示调用方,seqNo表示调用方发送过来的请求号。source和reqNo设置为组合惟一索引,保证单据不会重复落两次。若是调用方没有source和reqNo这两个字段,能够根据业务实际状况将请求中的某几个业务参数生成一个md5做为惟一性字段落到惟一性字段中来避免重复落库。
核心逻辑以下:
try {
dao.insert(entity);
// do business
} catch (DuplicateKeyException e) {
dao.select(param);
// 幂等返回
}
这里直接insert单据,若果成功则表示没请求过,举行执行业务逻辑,若是抛出DuplicateKeyException异常,则表示已经执行过,作幂等返回,简单的服务经过这种方式也能够识别是否为重复请求(第一部曲)。
利用数据库惟一索引来避免重复记录,须要注意如下几个问题:
(1)由于存在读写分离的设计,有可能insert操做的是主库,但select查询的倒是从库,若是主备同步不及时,有可能select查出来也是空的。
(2)在数据库有Failover机制的状况下,若是一个城市出现天然灾害,极可能切换到另一个城市的备用库,那么惟一性约束可能就会出现失效的状况,好比并发场景下第一次insert是在杭州的库,而后此时failover将库切到上海了,再一次一样的请求insert也是成功的。
(3)数据库扩容场景下,由于分库规则发生变化,有可能第一次insert操做是在A库,第二次insert操做是在B库,惟一索引一样不起做用。
(4)有的系统catch的是SQLIntegrityConstraintViolationException,这个是完整性约束,包含了惟一性约束,若是未给一个必填字段设值,也会抛这个异常,因此应该catch键重复异常DuplicateKeyException。
对于第(1)个问题,将insert 和select放在同一个事务中便可解决,对于(2)和(3),支付宝内部为了应对容量暴涨和FO,设计了一套基于数据复制技术的分布式数据平台,这个case笔者了解不深,后续有机会再讨论。
小A:若是我用惟一性约束来保证不会落重复数据,是否是能够不加锁防并发了?
大明:二者没有直接关系,加锁防并发解决的是并发维度的反作用问题,惟一性约束只是解决重复数据这单个反作用的问题。若是没有惟一性约束,串行重复执行也会致使insert重复落数据的问题,惟一性约束本质上解决的是重复数据问题,不是并发问题。
方案举例2:状态机约束解决乱序问题
一个业务的生命周期每每存在不一样的状态,用状态机来控制业务流程中的状态转换是不二之选。在实际业务中单向的状态机是比较经常使用的,当状态机处于下一个状态时,是不能回到前面的状态的。如下场景常常会用到状态机作校验:
(1)调用方调用超时重试。
(2)消息投递超时重试。
(3)业务系统发起多个任务,可是期待按照发起顺序有序返回。
对于这种类问题,通常是在处理前先判断状态是否符合预期,若是符合预期再执行业务。当业务执行完成后,变动状态时还会采起相似于于乐观锁的方式兜底校验,例如,M状态只能从N状态转换而来,那么更新单据时,会在sql中作状态校验。
update apply set status = 'M' where status = 'N'
若是状态被设计成可逆的,就有可能产生ABA问题。即在update以前,状态有可能作过这样的变动:N -> M -> N。因此状态机设成单向流转是比较合理的。
四 总结
本文首先引出了幂等的定义:相同请求无反作用,而后提出了设计幂等方案的三部曲,并举例说明。设计者要可以清晰地定义相同请求,而且采用通用组件减小一些反作用的分析维度,再针对具体的反作用设计相应的解决方案,直至没有任何反作用,才是真正完备的幂等解决方案。在实际业务中,实现三部曲不必定是严格的前后顺序,但只要按照这三部曲来构思方案,必能开拓思路,化繁为简。
本文分享自微信公众号 - 技术原始积累(gh_805ebfd2deb0)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。