最终一致性解决方案
1. 两阶段提交
2.异步确保(没有事务消息)
3.异步确保(事务消息)
4. 补偿交易(Compensating Transaction)
5.消息重试
6.幂等(接口支持重入)
A给B转100元。
1. 两阶段提交
有协调者,协调操作流程。Pre commit锁资源,commit或rollback时提交或释放资源。
调用A的commit接口超时了,继续重试。要求下游接口幂等。下游故障,短时重试不能解决,定时处理中间状态:扔到重试MQ。重试策略,失败返回
2.异步确保(没有事务消息)
不只为一致性,考虑响应时间,下游稳定性等
关键要有消息表。一般有队列(不丢消息,但不支持事务消息),基本思路:
生产方:消息表记录消息发送状态,和数据一个事务提交(存储耦合)。只是增加一个字段跟业务强耦合,处理不同交易数据可通用处理。
消费方:需处理消息成功,给生产方confirm。失败放MQ。支持重试省事儿,不支持放回队尾或建队列处理。如成功才继续,block(不会这么做)。Kafka lowlevel接口是支持自己设置offset的,可实现block。
生产方定时扫描本地消息表,没处理完的消息由发送一遍。自动对账补账,这一步可省略。丢消息或者下游处理失败场景少。看业务上能不能容忍不一致到对账补账周期。
ps:不要MQ。脚本处理低频场景,离线扫表让人不爽。业务量初期也可以
一致性要求不高兜底方案(对账补账),不需要confirm,扔给消息万事大吉
3.异步确保(事务消息)
理想:消息扔到MQ,肯定被消费成功。不用担心失败,丢失。
现实:处理失败,继续消费,直到成功为止
大部分MQ都不支持事务消息比如kafka。RocketMQ号称支持,事务消息关键封装消息状态和重发等。没成熟事务消息MQ。网传RMQ提供2PC提交接口。
1.生产方发送prepared消息给RMQ。失败返回。
2.执行本地事务,成功发Confirm消息给RMQ。失败,调用RMQ cancel接口。
3.步超时如何处理呢?第四步骤
4.生产方实现check接口,告知RMQ自己本地事务是否执行成功(第4步)。定时轮训pre消息,调用check接口,决定是否可提交。
5.可能失败。这时候需要RMQ支持消息重试。处理失败的消息果断时间再进行重试,直到成功为止(超过重试次数后会进死信队列,可能得人肉处理了,因为没用过所以细节不是很了解)。
支持消息重试
P.S. 阿里内部因历史原因,用notify比RMQ要多,原理类似。
4. 补偿交易(Compensating Transaction)
和操作本身一个事务里完成。跟2PC比,核心价值少锁代价。
如A:-100 B:+100。如B:+100失败,补偿A:+100。看起来跟注册个单库事务一样简单。做到业务无感知。
5.消息重试
事务消息解决生产者和MQ之间一致性,重试确保消费者和MQ之间的一致性
pull,push模式。失败,放重试队列。延迟时间固定(2s),队首消息,时间到达才被消费。
时间为水位,期望执行时间大于当前时间的消息高于水位。其他消息对consumer不可见。如消息延迟时间不一样:
(1)基于队列方案:按秒建多个队列。按执行时间入不同队列,一天86400个队列(一般丑陋)。按时间消费不同队列。
(2)基于DB:不依赖队列,支付时消息进去时候,设置下次执行时间,对时间做索引
(3)redis的延时队列,支持重试。zset按处理时间排序。遇到的持久化问题,内存数据丢失问题,重试次数控制,消息追溯等
总结:MQ提供消息重试最好
6.幂等(接口支持重入)
没有MQ,重试也是无处不在的。幂等怎么做?
insert,依赖唯一键,异常回滚事务。
update,那么状态机控制和版本控制异常重要。这里要多加小心。
引入log表。该log对操作id(消息id?)插入log失败整个回滚
不能查log表或者用redis,加锁。除非在事务里查。唯一键冲突回滚掉就好
用唯一键挡重入是目前为止个人觉得最有安全感的方式。当然对数据库会有一些额外性能损耗。问题就变成了有多大的并发,其中又有多大是需要重试的?
Fasion IO卡+分库分表之后,不会到db性能瓶颈(对金融类场景)。
后记
最终一致性问题,万恶之源是RPC本身会失败。生涯大部分时间都会跟各种失败和timeout搏斗了。用MQ实现最终一致性原因:
1. MQ强大,可不丢数据。对事务消息更普及。
2. 异步处理能力(响应时间、吞吐量)和稳定性(99.99%的服务依赖99.9%的服务)服务之间解耦
https://zhuanlan.zhihu.com/p/25933039