cap
ddatsh
分布式系统的事务一致性是一个技术难题
OLTP系统领域,很多业务场景都会面临事务一致性方面的需求,最经典转账的案例
传统企业开发,系统往往是以单体应用形式存在,没有横跨多个数据库
通常只需借助开发平台中特有数据访问技术和框架(Spring、JDBC、ADO.NET),结合rdbms自带事务管理机制来实现事务性的需求(ACID)
互联网平台往往由一系列分布式系统构成,开发语言平台和技术栈也相对比较杂,尤其微服务架构盛行,看起来简单的功能,内部可能需要调用多个“服务”并操作多个数据库或分片来实现,情况复杂很多
单一技术手段和解决方案,无法应对和满足这些复杂的场景
分布式系统中,同时满足“CAP定律”三者不可能,比现实中找对象需同时满足“高、富、帅”或“白、富、美”更加困难
互联网领域绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可
分布式事务
分布式系统,必然要提分布式事务
理解分布式事务,得介绍两阶段提交协议
简单但不精准的例子来说明:
第一阶段,张老师作为“协调者”,给小强和小明(参与者、节点)发微信,组织他们俩明天8点在学校门口集合,一起去爬山,然后开始等待小强和小明答复
第二阶段,如果小强和小明都回答没问题,那么大家如约而至。如果小强或者小明其中一人回答说“明天没空,不行”,那么张老师会立即通知小强和小明“爬山活动取消”
这个过程中可能有很多问题。小强没看手机,张老师会一直等着答复,小明可能在家里把爬山装备都准备好了却一直等着张老师确认信息
更严重的是,如果到明天8点小强还没有答复,那么就算“超时”了,那小明到底去还是不去集合爬山呢?
这就是两阶段提交协议的弊病,业界引入三阶段提交协议解决该类问题
两阶段提交协议在主流开发语言平台,数据库产品中都有广泛应用和实现的
XOpen组织的DTP模型图
XA协议指TM(事务管理器)和RM(资源管理器)间的接口
主流RDBMS都实现了XA接口
JTA符合X/Open DTP模型,事务管理器和资源管理器之间也使用了XA协议
本质上也是借助两阶段提交协议来实现分布式事务
XA事务成功和失败的模型图
WebLogic、Webshare提供了JTA的实现和支持。Tomcat没有实现的(不是JavaEE应用服务器),需要第三方的框架Jotm、Automikos等来实现,均支持spring事务整合
.NET平台,借助ado.net中TransactionScop API编程实现,还必须配置和借助MSDTC服务
mysql部署在Linux平台,无法支持分布式事务
总结:
这种方式实现难度不算太高,比较适合传统的单体应用,同一个方法中存在跨库操作的情况
但分布式事务对性能的影响较大,不适合高并发和高性能要求场景
提供回滚接口
服务化架构,功能X,需要去协调后端的A、B甚至更多的原子服务
如A和B其中一个调用失败了,怎么办?
往往提供一个BFF层来协调调用A、B服务
如果有些需要同步返回结果,尽量按照“串行”的方式去调用
调A失败,不去调B
调A成功,调用B失败,尝试去回滚刚刚对A的调用操作
有时不必严格提供单独对应的回滚接口,可以通过传递参数实现
尽量把可提供回滚接口的服务放在前面。举例:
论坛网站,每天登录成功后奖励5个积分,但积分和用户是两套独立的子系统服务,对应不同DB,控制起来比较麻烦。解决思路:
- 登录和加积分的服务调用放在BFF层一个本地方法中
- 请求登录接口时,先加积分操作,成功后再执行登录操作
- 登录成功,最好,积分也加成功。登录失败,则调用加积分对应的回滚接口(执行减积分的操作)
总结
缺点比较多,复杂场景不推荐使用,适用简单场景,容易提供回滚,依赖的服务也非常少的情况
代码量庞大,耦合性高。非常有局限性,很多的业务无法简单实现回滚,串行的服务很多,回滚的成本实在太高
本地消息表
思路源于ebay,后支付宝等公司布道,业内广泛使用
基本设计思想,将远程分布式事务拆分成一系列的本地事务
经典跨行转账的例子
第一步,扣款1W,通过本地事务保证凭证消息插入到消息表
begin transaction
update A set amount=amount-1000 where userId=1;
insert into message(userId,amount,status) values(1,1000,1);
end transaction
commit;
第二步,通知对方银行账户上加1W。如何通知到对方?
通常采用两种方式:
- 时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件
- 定时轮询扫描,去检查消息表的数据
各有利弊,仅靠MQ,可能通知失败;频繁定时轮询,90%无用功。一般两种方式结合使用
解决了通知的问题,还有消息有重复消费问题,往用户帐号上多加了钱
其实可以在消息消费方,也通过“消费状态表”记录消费状态
执行“加款”操作之前,检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地事务控制来更新这个“消费状态表”。避免重复消费的问题
总结
基本避免分布式事务,实现“最终一致性”
但 RDBMS吞吐量和性能存在瓶颈,频繁读写消息给数据库造成压力
真正的高并发场景下,也会有瓶颈和限制
MQ(非事务消息)
非事务消息支持的MQ产品,难将业务操作与MQ操作放在一个本地事务域中管理
跨行转账,难保证扣款完成后对MQ投递消息一定能成功。一致性似乎很难保证。
先从消息生产者这端来分析
public void trans(){
try{
bool result=dao.update(model);
if(result){
mq.send(model);
}
}catch(Exception e){
rollback();
}
}
- DB成功,MQ投递也成功,皆大欢喜
- DB失败,不向MQ发消息
- DB成功,但MQ投递失败,向外抛出异常,DB回滚
没有问题
消费端
- 消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
- 尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果
如何保证消息与业务操作一致,不丢失?
主流MQ都有持久化消息的功能。消费者宕机或消费失败,都可执行重试机制(甚至自定义重试次数)
如何避免消息被重复消费造成的问题?
- 保证消费者调用业务的服务接口的幂等性
- 通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)
总结
比较常见,性能和吞吐量优于RDBMS消息表方案
MQ自身和业务都具有高可用性,理论上可满足大部分业务场景
没有充分测试的情况下,不建议在交易业务中直接使用
MQ(事务消息)
Bob向Smith转账 ,到底先发送消息,还是先执行扣款操作?
都可能会出问题
先发消息,扣款失败,Smith会多出一笔钱
反过来,先扣款,后发消息,可能扣款成功但是消息没发出去,Smith收不到钱
除异常捕获和回滚方式外
RocketMQ设计和实现思路
第一阶段发送Prepared消息时,拿到消息地址
第二阶段执行本地事物
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态
但如果确认消息发送失败怎么办?
RMQ定期扫描消息集群中的事物消息,发现了Prepared消息,会向消息发送者确认,Bob的钱到底是减了还是没减呢?
如果减了是回滚还是继续发送确认消息?
RMQ根据发送端设置的策略来决定是回滚还是继续发送确认消息
保证了消息发送与本地事务同时成功或同时失败
总结
各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”
适合的业务场景广泛,而且比较可靠。技术实现难度比较大
主流开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,需二次开发或者新造轮子
RMQ 事务消息部分的代码也并未开源,需要自己去实现
其他补偿方式
支付宝交易接口,一般会在支付宝的回调接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功
同时,只有当回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。
这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似
一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,这种重试机制非常有效
极端的场景,系统自身有bug或者程序逻辑有问题,重试1W次也无济于事
“明明已经付款,却显示未付款不发货”类似的悲剧
为了交易系统更可靠,一般会在类似交易这种高级别的服务代码中,加入详细日志记录,系统内部引发类似致命异常,会有邮件通知
同时,后台定时任务扫描和分析此类日志,检查出这种特殊的情况,尝试通过程序来补偿并邮件通知相关人员
某些特殊的情况下,还会有“人工补偿”,最后一道屏障