为什么须要分布式事务
分布式的微服务通常各自有本人的数据源,单机的事务由本机的数据源实现;当一个业务波及多个微服务的多个数据源时,很难保障多个数据源同时胜利、同时失败。
比方:支付宝 转账到 余额宝 1000元
支付宝:
- 账户A(id, user_id, amount)
- 领取服务pay,转出:update A set amount=amount-1000 where user_id=1;
余额宝:
- 账户B(id, user_id, amount)
- 余额宝服务balance,转入:update B set amount=amount+1000 where user_id=1;
领取服务pay和余额宝服务balance,只有同时执行胜利,才算本次转账胜利;只有有一方执行失败,则本次转账失败。
分布式事务的解决方案--2PC
2PC(Two Phase Commit protocol)两阶段提交,是强一致性的分布式事务实现形式。
2PC波及的角色:
- 协调者:coordinator,协调多个参与者进行投票、提交或回滚;
- 参与者:participants,本地事务的执行者;
2PC的解决部署:
投票阶段
- 协调者告诉参与者执行本地事务,而后进入表决过程;
- 参与者执行本地事务,但不提交,将执行后果回复协调者;
提交阶段
- 协调者收到参与者的执行后果,若均执行胜利,则向所有参与者发送commit;否则,向所有参与者发送rollback;
2PC的毛病:
- 协调者是个单点,存在单点故障;
- 事务提交之前,资源被预留锁定,因为波及多节点的网络交互,导致锁工夫较长,影响并发;
分布式事务的解决方案--TCC
TCC(Try Confirm Cancel),是业务层面的分布式事务,TCC要求所有的事务参与者都要实现3个接口:
- Try: 预处理;
- Confirm: 确认;
- Cancel: 勾销;
TCC的执行过程:
- Client向所有事务参与者发送Try操作;
- 若所有事务参与者的Try均胜利,则Client向所有事务参与者发送Confirm,否则发送Cancel;
TCC要求事务参与方,都要当时实现Try/Confirm/Cancel接口,对业务代码的侵入性较强。
TCC存在的问题:
空回滚
- 第一阶段的Try因为音讯失落而产生网络超时,触发第二阶段的Cancel;
- 事务参与方在没有收到Try的状况下,收到了Cancel音讯,称为“空回滚”;
- “空回滚”的存在,要求事务参与方在实现Cancel接口时,思考未收到Try的状况;
防悬挂
- 第一阶段的Try因为音讯失落而产生网络超时,触发第二阶段的Cancel;
- 事务参与方在没有收到Try的状况下,收到了Cancel音讯,执行“空回滚”;
- 此时,第一阶段的Try音讯又达到,该场景称为“防悬挂”;
解决办法:
- 执行“空回滚”的时候,插入1条记录,状态为已回滚;
- 当Try又回来时,先查问记录,若已存在且已回滚,则不再执行该Try;
分布式事务的解决方案--最终一致性
基本思路是,将事务音讯进行长久化(Transaction outbox),通过binlog推送给上游,上游生产胜利后,调用callback到上游,通知上游事务处理完毕。
没有事务的回退流程,通过重试,尽最大致力交付,实质上是最终一致性的解决方案。
事务音讯长久化:Transaction outbox
支付宝pay服务执行本地事务,进行A用户扣款,同时记录音讯数据msg,该msg表与pay业务数据在同一个DB中。
支付宝pay服务的本地事务保障,只有实现扣款,msg肯定能保留下来。
begin transaction update A set amount = amount - 1000 where user_id=1; insert into msg(user_id, amount, status) values(1, 1000, 1)end transactioncommit
通过binlog推送给上游:Transaction log tailing
支付宝的msg表被Canal订阅,而后发送到kafka。通过Canal异步订阅的形式,将两边解耦。
余额宝balance服务生产kafka音讯,将B的amount+1000。
尽最大致力交付:best-effort
余额宝balance本地事务执行胜利后,向余额宝pay发送/callback,告诉它事务已处理完毕,批改msg的status=0。
若balance发送/callback的过程中,pay服务挂了,那么balance将每隔一段时间,再次发动/callback,直到pay回复。
若pay胜利接管并解决balance发送的/callback,然而向balance回复/callback的过程中,balance挂掉了,那么pay将每隔一段时间,再次发送/callback回复,直至胜利。
不论是pay还是balance,都是通过重试,尽最大致力交付,这要求在业务中思考幂等,避免多+余额的状况。比方,balance生产完音讯当前,发送success给pay,失常状况下pay收到音讯后将msg.status=0,但若此时pay服务挂了,重启当前发现msg.status=1,则持续发送音讯给balance,balance就会再次生产该音讯。
业务幂等
业务幂等的解决办法:全局惟一ID+去重表
在balance侧减少音讯生产状态表msg_apply,艰深来说就是个账本,记录音讯的生产状况,每次来1个音讯,在真正执行之前,先去msg_apply中查问,如果是反复音讯,则不再生产。
for each msg in queue begin transaction select count(*) as cnt from msg_apply where msg_id = msg.id; if cnt == 0 then //没有生产过 update B set amount=amount+10000; insert into msg_apply(msg) values(msg.id); end transactioncommit;