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