前两天被问到一个问题:在公司中,有不同的组和服务接口。我们正常的事务规则是是如果出现问题的话,对该Service下的方法进行事务处理,回滚什么的。但是如果是一个Service调用了很多的服务,如dubbo服务。那就出问题了。甚至有些dubbo本身就套了更多的Dubbo服务。
比如吧,一个转钱的服务,先从A服务那边调取用户'jack'的信息,如果钱够的话,就从jack的账户提取500块钱,然后如果成功返回给调用方一个唯一标识。再调用B服务,把唯一标识和转的钱传给B服务,标记钱已经增加了。
这个赚钱的服务,如果调用服务A的时候钱已经转出去了,但是调用B的时候出错了,比如说,连接不上,连接超时,或者B拒绝转入进来钱。这就遇到问题了。所以了解分布式服务下的事务控制问题还是很有必要的。
我们有一些解决办法,但是不会存在完美的解决办法,都是在合适的时候去使用对应的解决办法的。
事务的ACID特性
原子性(A)
所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
一致性(C)
事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
隔离性(I)
所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
持久性(D)
所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。
其实一切的解决办法都是围绕或者为了实现事务的ACID特性。
解决办法:
1.异步消息。
当遇到报出问题或者请求失败,如果是非正常的原因,可以考虑发送一个异常消息,来进行一个请求回滚或者重复请求。比如说,在上面这个服务中,可以发送一个消息给自己的一个mq队列接口,在mq队列中完成一个默认后续操作一定会成功的操作。异步确保型)。或者在mq队列中进行一个等待重发,看看是否是并发问题导致暂时性的请求失败,循环一定的时间后在看(最大可能性努力型)
2.事务补偿机制
即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。在上述案例中,则是如果B服务调用失败或者请求出现问题,则执行补偿机制,比如请求服务A,把钱重新入账,并标注后续请求异常
3.两阶段提交、三阶段提交
TCC分别对应Try、Confirm和Cancel三种操作,这三种操作的业务含义如下:
- Try:预留业务资源
- Confirm:确认执行业务操作
- Cancel:取消执行业务操作
- 完成所有业务检查(一致性):检查A、B、C的帐户状态是否正常,帐户A的余额是否不少于30元,帐户B的余额是否不少于50元。
- 预留必须业务资源(准隔离性):帐户A的冻结金额增加30元,帐户B的冻结金额增加50元,这样就保证不会出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,帐户A和B的可用余额不够的情况。
2、Confirm:确认执行业务。
- 真正执行业务:如果Try阶段帐户A、B、C状态正常,且帐户A、B余额够用,则执行帐户A给账户C转账30元、帐户B给账户C转账50元的转帐操作。
- 不做任何业务检查:这时已经不需要做业务检查,Try阶段已经完成了业务检查。
- 只使用Try阶段预留的业务资源:只需要使用Try阶段帐户A和帐户B冻结的金额即可。
3、Cancel:取消执行业务
- 释放Try阶段预留的业务资源:如果Try阶段部分成功,比如帐户A的余额够用,且冻结相应金额成功,帐户B的余额不够而冻结失败,则需要对帐户A做Cancel操作,将帐户A被冻结的金额解冻掉。