为什么需要分布式事务
分布式的微服务通常各自有自己的数据源,单机的事务由本机的数据源实现;当一个业务涉及多个微服务的多个数据源时,很难保证多个数据源同时成功、同时失败。
比如:支付宝 转账到 余额宝 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 transaction
commit
通过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 transaction
commit;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。