共计 2419 个字符,预计需要花费 7 分钟才能阅读完成。
为什么须要分布式事务
分布式的微服务通常各自有本人的数据源,单机的事务由本机的数据源实现;当一个业务波及多个微服务的多个数据源时,很难保障多个数据源同时胜利、同时失败。
比方:支付宝 转账到 余额宝 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;