一致性问题,“万恶之源”是数据冗余和分布并经过网络交互+网络异常是常态。html
所谓分布式服务,就是把以前经过本地接口交互的模块,拆分红单独的应用独立部署,并经过RPC和MQ交互。拿电商中的订单和库存举例(新增一条订单记录,库存就要-1),集中式架构中,要想保证订单表和库存表的一致性,只要一个本地事务(ACID)就能保证二者的强一致性;而分布式架构中,订单表由订单服务操做,库存表由库存服务操做。要想保证订单表和库存表的一致性,那么就必须保证订单服务对订单表的操做和库存服务对库存表的操做同时成功。以前的一个本地事务就变成了一个分布式事务。因为服务之间经过网络交互+网络异常是常态,就会产生服务间数据不一致的状况。这就涉及一个分布式事务一致性的问题。git
模式分析:A服务同步调用B服务的接口并等待结果返回,后续的流程会依赖B服务的返回结果。这种交互模式下,A服务获得的结果细分有三种状况。github
业务场景:适用于大规模、高并发的短小操做且依赖返回值的场景。例如,交易服务和库存服务(卡券服务、红包服务等)的交互、用户登陆和准入服务的交互等。数据库
解决方案:方案一,服务调用方查询重试方案;方案二,TCC方案。apache
注:这两种方案,保证数据一致最终仍是靠“异步”,只不过须要快速校准,准实时。缓存
1 下单减库存方法() { 2 // 1.准备操做 3 // 2.重试调用B服务 4 result = RetryUtil { 5 while(重试次数 < 最大重试次数) { 6 try { 7 if (重试次数 != 0) { 8 // case1:网络超时或异常(catch分支) 9 // case2:查询到扣减库存操做,result=成功(return) 10 // case3:查不到扣减库存操做,result=失败(继续下面操做) 11 result = rpc.查询扣减库存是否成功(); 12 if (result == 成功) { 13 return result; 14 } 15 } 16 // case1:网络超时或异常(catch分支) 17 // case2:扣减库存成功,result=成功(return) 18 // case3:扣减库存失败,result=失败(return) 19 return rpc.扣减库存(); 20 } catch (Exception e) { 21 if (重试次数 = 最大重试次数) { 22 // 报警,人工处理或者(近实时)对帐系统自动校准 23 // 抛出异常,中断后续流程 24 throw 自定义异常; //或者result封装异常 25 } 26 } 27 } 28 }; 29 // 3.后续操做 30 }
注:1) 查询重试后依然失败(极少),报警,人工处理或者准实时对帐系统自动校准;服务器
2) 重试次数不宜多,甚至只重试一次;网络
3) B服务处理请求要作幂等。架构
注:1)关于TCC,我的认为,理解原理很重要。工做中遇到吻合的场景能够根据原理自行实现,知足业务便可;并发
2)一个开源实现:tcc-transaction;
模式分析:A服务调用B服务,B服务先受理请求并落库,状态是待处理。B服务处理请求很耗时,或者要依赖其余的服务。B服务处理完后通知A服务或者A服务定时去查询B服务的处理结果。这种交互模式下,对于CASE-1,第1步和第2步同接口同步调用模式,第3步同消息异步处理模式;对于CASE-2,至关于两次接口同步调用模式。
业务场景:适用于非核心链路上负载较高的处理环节,这个环节常常耗时较长,而且对时效性要求不高。例如,用户提现时,帐户系统和提现系统的交互(CASE-1);提现系统和三方系统(银行系统或者三方托管系统)的交互(CASE-2)。
解决方案:服务被调方最大努力处理方案。因为B服务中请求有落库,因此能够用定时任务不断重试尽最大努力将请求处理出结果。处理后,将请求状态设置成对应的结果落库。而后再通知A服务或者A服务异步主动查询。
1 受理请求方法() { 2 // 1.请求落库,状态为待处理 3 // 2.返回受理结果 4 if (落库成功) { 5 // 返回受理成功 6 } else { 7 // 返回受理失败 8 } 9 } 10 11 定时任务处理请求方法() { 12 // 1.扫描待处理请求 13 try { 14 // 2.处理请求 15 if (处理成功) { 16 // 设置请求处理状态为处理成功 17 } eles { 18 // 设置请求处理状态为处理失败 19 } 20 } catch (Exception e) { 21 // 不作任何操做,请求状态依旧为待处理 22 } 23 // 3.消息通知A服务处理结果或者等待A服务查询处理结果 24 }
注:1) B服务一般都是接受请求并持久化后才返回A服务受理成功。避免服务进程被杀掉而致使请求丢失。
2) 不论是第(1,2)两步仍是CASE-2中的第(3,4)两步,若是查询重试失败,能够落库,用定时任务处理,知道成功。反正不像接口同步调用模式,A服务不须要实时的结果。
模式分析:A服务将B服务须要的信息经过消息中间件传递给B服务,A服务无需知道B服务的处理结果。这种交互模式下,消息生产者要确保消息发送成功;消息消费者要确保消息消费成功。
业务场景:消息异步处理模式与接口异步调用模式相似,多应用于非核心链路上负载较高的处理环节中,井且服务的上游不关心下游的处理结果,下游也不须要向上游返回处理结果。例如,在电商系统中,用户下订单支付且交易成功后,发送消息给物流系统或者帐务系统进行后续的处理。
解决方案:生产者最大努力通知+消费者最大努力处理方案。
1 交易完成发消息方法() { 2 // 1.设置交易状态为已完成 3 // 2.消息落库,状态为待发送 4 // 可异步发送,也建议异步发送 5 try { 6 // 3.发送消息 7 if (发送成功) { 8 // 设置消息状态为发送成功 9 } 10 } catch (Exception e) { 11 // 不作任何操做,消息状态依旧为待发送 12 } 13 } 14 15 定时任务发送消息方法() { 16 // 1.扫描待发送消息 17 try { 18 // 2.发送消息 19 if (发送成功) { 20 // 设置消息状态为发送成功 21 } 22 } catch (Exception e) { 23 // 不作任何操做,消息状态依旧为待发送 24 } 25 }
注:1) 定时任务重试发送消息和消息服务器重发未ACK的消息通常都是时间阶梯式的(2n*时间间隔);
2) 支持事务消息中间件之RocketMQ。
注:保证幂等性的方法不少,根据具体的业务场景,总能找到保证幂等性的方法。
注:若是,以上场景和解决方案,没能包含您工做中遇到的场景,欢迎交流,并共同讨论解决方案。