学习孙玄老师的公开课,企业级分布式事务,笔记。
使用场景
一次请求涉及数据分布多个存储系统:
- 多个DB;
- DB和Redis;
- DB和MQ;
- …
业务场景容忍度:
-
购买商品,商品、订单、支付;不能失败,必须用分布式事务。
–金融场景,要么都成功,要么都失败
–要求最终一致性 -
朋友圈发布信息,发布信息,朋友圈小红点;允许失败,对用户伤害不大,用分布式事务,那不是给自己找困难吗?
– 社交场景
– BASE理论即可满足(CAP理论不太适合,最终还是要数据一致性)
分布式事务的本质
以购买商品为例:
-
同步场景;
– 减库存,建立订单
– 前台支付 -
异步场景;
– 前台超时未支付,普适的操作,使用MQ延迟消息(创建订单的时候,发一个延迟消息,查看订单状态)
基本上所有分布式事务场景都能满足AP,主要是CP。
分布式事务普适方法论(DB MQ Redis ES)
方法论:
- 拆分;A -> DB, B -> MQ, C -> ES, D -> Cache
– 分布式事务,也叫长事务
– 本地事务,也叫短事务
方法论就是把长事务拆分成短事务,把上面的ABCD拆分开来,每个都成功了,分布式事务就成功了,一个失败就都失败了。
那失败了怎么办?补偿
- 补偿,人为规定一个补偿步骤
– AB成功,C失败
– 补偿AB,不用补偿C
分布式事务普适方法论的业务场景
– AB成功,C失败
– 补偿AB,不用补偿C
-
场景1 多个DB
– C是本地事务,直接就rollback了;AB的补偿要注意幂等 -
场景2 DB和Redis
– Redis setNX 在指定的key不存在时,为key设置指定的值
– SETNX Key Value 设置成功返回1,失败返回0
– EXPIRE Key seconds 设置生存时间
– MULTI 执行多个命令,EXEC,实现如下:Redis本身就支持事务,使用LUA实现:
if
redis.calll(
'setnx',KEYS[1],ARGV[1] )== 1
then
redis.call(
'expire',KEYS[1],ARGV[2]
)
return 1 else return 0
end
这个lua脚本如何使用java调用:
在这里插入代码片
-
场景3 DB和RocketMQ,异步的情况,先放DB,再放延迟MQ
-
场景4 DB和ES
– ES不提供本地事务,AP CP模型使用分布式事务,ES不提供分布式事务,ES retry几次仍然失败就扔了,或者记录log,再重试。
–也就是选择了ES,就满足不了分布式事务,最多记录一下,看一下失败的概率。
分布式事务设计
异步场景 前台超时未支付
场景描述:
下单记录DB,发送MQ到延时消息。
保证事务的方法1:×
1)改成一个本地事务:把保存DB和发送MQ放在一个事务里,try catch,如果发送消息失败,回滚DB操作。
有漏洞!:如果发送消息是Timeout,实际发送消息成功,此时回滚DB操作,就完蛋了。
方法2: ×
2) 调整时序:先发消息,在写DB。
有漏洞!:写DB失败了,但是消息已经发出去了,也完蛋了。
那怎么办?两阶段提交!
过程:1)应用程序发起事务,commit请求。2)事务协调者发起prepare投票。3)事务参与者都同意后,事务协调者再发起commit。4)commit过程出现异常,服务重启后,再次进行commit。
核心思路:协商!一旦协商成功后,就没有回头路了。不断try,直到成功。
如何设计和落地
1)2PC落地实践
需要业务系统提供下单成功的回查接口,MQ要支持prepare消息和超时机制回查业务系统。整体设计太过复杂。在工业体系很难落地。不优雅。
2)2PC落地实践优化(消息表)认知,格局,思维模型
第一阶段,消息落地到DB
第二阶段,消息发送MQ,读取消息表中信息发出去,失败了一直重试,直到成功为止。
第二阶段落地关键点:
1)发送端消息无法保证幂等,只能保证至少发送一次
2)订单业务逻辑冗余部署,如果他们都去读取未读消息来发送,很有可能会发重复。正常情况下,每台服务器只处理请求自己的请求。会有一种情况发送重复,就是服务重启后,未处理消息同时被多台机器获取(解决方式:加锁)。
3)业务线程不要阻塞,发消息失败就放入到时间轮转动线程(重试线程),要打印日志。
4)补偿线程(MsgTask)失败也无所谓,因为失败了就不会改DB状态,不改状态,还会再次重试。(其中一台机器处理就可以,方式:加锁)
5)清理线程,定时清理已发送的消息,疑问:发送或补偿成功,就删除不好吗?(此时只标记,不删除)
6)持锁线程,工具类,强锁,给补偿线程和清理线程用。
读操作不需要去重,但写操作一定去重。
异步场景分布式事务不存在性能瓶颈。
吞吐量不会有瓶颈,因为DB写入量大,可以拆库;读库量大,可以多加从库,线性扩展;业务逻辑更是可以横向扩展;MQ可以拆分多个topic。每一个环节都是可扩展的。
响应延迟不会有瓶颈,同步路径短,毫秒级别,用户能接受,异步处理环节和用户无关。
大前提,分布式事务,一定满足CAP模型,要么CP,要么AP,这种场景一般量都不大,所以不会有性能瓶颈。
锁不会影响性能,锁的粒度可以小一些,就是拆分,锁也可以有集群。但不会无线扩展,ROI和性能的平衡。
同步场景 SAGAS
tcc 对业务侵入太大
Seata 对业务的打包,不算严格的框架
关键点:
- 异步补偿,TM(分布式事务补偿服务)
- 补偿api
- T1->Tn 记录请求参数,写在TDB里
拦截设计:
1)在事务开始时,生成事务唯一标识,事务id
2)事务中,每个步骤执行时,都要去TDB中记录自己的入参
Java代码:
1)在Java中以动态代理的形式实现,invoke() 前后增加事务
补偿设计:
1)事务组表 txId status ts
2)事务调用组表 txId actionId调用步骤顺序 callMethod补偿方法 ptype params补偿参数
3)补偿策略,事务执行失败,修改事务组状态,分布式事务异步调用补偿服务