赞
踩
目录
一致性问题,“万恶之源”是数据冗余和分布并通过网络交互+网络异常是常态。
所谓分布式服务,就是把之前通过本地接口交互的模块,拆分成单独的应用独立部署,并通过远程接口和网络消息交互。且不管这样说严不严密,正不正确,理解就好。本文的重点也不是这个话题。简单画一张图辅助理解,如图。集中式架构,要想保证订单表和库存表的一致性,只要一个本地事务(ACID)就能保证两者的强一致性。分布式架构,订单表由订单服务操作,库存表由库存服务操作。要想保证订单表和库存表的一致性,那么就必须保证订单服务对订单表的操作和库存服务对库存表的操作同事成功。之前的一个本地事务就变成了一个分布式事务。由于服务之间通过网络交互+网络异常是常态,就会产生服务间数据不一致的情况。这就涉及一个分布式事务一致性的问题。
模式分析:A服务同步调用B服务的接口并等待结果返回,后续的流程会依赖B服务的返回结果。这种交互模式下,A服务得到的结果细分有三种情况。
业务场景:适用于大规模、高并发的短小操作且依赖返回值的场景。例如,交易服务和库存服务(卡券服务、红包服务等)的交互、用户登录和准入服务的交互等。
解决方案:方案一,服务调用方查询重试方案;
方案二,TCC方案。
1. 服务调用方查询重试方案,适合一个从业务服务场景。
- 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服务处理请求要做幂等。
2. TCC方案,适合多个从业务服务场景。TCC是阿里在二阶段提交协议的基础上提出的一种解决分布式事务一致性的协议,原理图如下。其对应的产品是DTX(老版是DTS)。DTS中有个快速开始的例子看明白了,TCC就基本OK了。在蚂蚁金服内部被广泛地应用于交易、转账、红包等核心资金链路,服务于亿级用户的资金操作。
注:关于TCC,个人认为,理解原理很重要。工作中遇到吻合的场景可以根据原理自行实现,满足业务即可。
模式分析: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:Quick Start - Apache RocketMQ。
注:保证幂等性的方法很多,根据具体的业务场景,总能很容易找到保证幂等性的方法。
注:如果,以上场景和解决方案,没能包含您工作中遇到的场景,欢迎交流,并共同讨论解决方案。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。