为什么需要分布式事务

分布式的微服务通常各自有自己的数据源,单机的事务由本机的数据源实现;当一个业务涉及多个微服务的多个数据源时,很难保证多个数据源同时成功、同时失败。

比如:支付宝 转账到 余额宝 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,本地事务的执行者;

image.png

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接口,对业务代码的侵入性较强。

image.png

TCC存在的问题:

  • 空回滚

    • 第一阶段的Try由于消息丢失而产生网络超时,触发第二阶段的Cancel;
    • 事务参与方在没有收到Try的情况下,收到了Cancel消息,称为“空回滚”;
    • “空回滚”的存在,要求事务参与方在实现Cancel接口时,考虑未收到Try的情况;
  • 防悬挂

    • 第一阶段的Try由于消息丢失而产生网络超时,触发第二阶段的Cancel;
    • 事务参与方在没有收到Try的情况下,收到了Cancel消息,执行“空回滚”;
    • 此时,第一阶段的Try消息又到达,该场景称为“防悬挂”;
    • 解决方法:

      • 执行“空回滚”的时候,插入1条记录,状态为已回滚;
      • 当Try又回来时,先查询记录,若已存在且已回滚,则不再执行该Try;

分布式事务的解决方案--最终一致性

基本思路是,将事务消息进行持久化(Transaction outbox),通过binlog推送给下游,下游消费成功后,调用callback到上游,告诉上游事务处理完毕。

image.png

没有事务的回退流程,通过重试,尽最大努力交付,本质上是最终一致性的解决方案。

事务消息持久化: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;

a朋
63 声望38 粉丝