乐趣区

关于分布式事务:分布式事务及其解决方案

为什么须要分布式事务

分布式的微服务通常各自有本人的数据源,单机的事务由本机的数据源实现;当一个业务波及多个微服务的多个数据源时,很难保障多个数据源同时胜利、同时失败。

比方:支付宝 转账到 余额宝 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;
退出移动版