总结分布式服务事务的基本概念
首先需要对分布式服务有一个概念:其中核心为同一网络下不同组件,通过网络进行通信和协调,对外一个如同一个整体。然后事务的需求就是:不同的服务使用的是不同的数据库,这就导致不同服务数据库的一致性不能保证。举个例子你就会明白了:a服务会调用b和c服务(b,c都会修改数据库),当a调用服务时,bc都会修改各自的数据库。但是如果出现突发情况,导致b失败,与此同时c依然在执行,并且成功。就会出现如下情况:a服务调用失败,可这次失败却导致了c服务数据库的改变,且a无法操作数据库让其回归正常水平,这在开发过程中是完全无法接受的。所有我们就需要进行处理这个问题,那么处理的方案就叫——分布式事务
接下来我们就来讲讲分布式服务的基本构成:
1.事务协调者transaction coordinator 简写TC:外部引用组件
作用:协调所有参与者,管理全局事务
常用组件:Seata TC(最常用,国内微服务首选),Atomikos Transaction,Narayana,Bitronix Transaction Manager
2.事务发起者transaction manager 简写TM:全局事务发起者
作用:发起事务
3.事务参与者resource mananger 简写RM:事务参与者
作用:参与事务
主体有了接下来就是关于事务是如何运转的了:
首先TM需要向TC进行交互,TC会得到本次事务的一个ID,接下来RM会从TM拿到ID,这样事务的绑定就完成了。
RM会正常执行指令 ,在这个过程中会出现两种情况:
如果所有RM都完成,所有事务就会提交
如果有任一RM失败,所有事务都会进行回滚
这就是事务基本流程,根据不同市场需求会进行很多变种,当万变不离其宗,整体思路是不会变的。然后还有一个小概念的分类:
全局事务,分布式事务,包含所有的分支事务
分支事务,又称为本地事务,某个服务的本地数据库事务
这是个很容易记住的一个小概念,理解型记忆,没什么难的
分布式事务解决方案
市面上的解决方案有很多,但是我到今天为止知道的只有4种,后续可能会增加,也可能不会,谁知道呢。
1.XA协议(数据库协议)
1991 年提出的一个协议方案,由于当年的网络并不发达,导致该协议并不能在高并发的情况下运行,多用于远古项目和传统金融核心、银行、保险、财务系统
其步骤为
1.TM向TC发起事务开始声明,并给予执行ID
2.RM得到ID进行执行,执行完成后不提交,保持数据库连接
3.TC会等待所有RM执行完毕后进行汇总,根据结果进行提交/回滚。
总结:由于其时代背景,其解决了分布式服务的功能需求,但在当今网络已经不太适应,只有少量项目在苦苦支撑。时代的洪流滚滚而去,又谁记得他曾经的辉煌呢?反正我记不到。
Saga 模式
Saga的核心思想:把一个全局事务拆成一串本地子事务,每一步子事务都有对应的补偿动作(回滚逻辑)
其核心步骤:有a,b,c3个事务
当有任一事务失败时候,a执行a1回滚a,b执行b1回滚b,c执行c1回滚c
TCC(Try-Confirm-Cancel)
XA 在互联网大规模分布式系统里太重,数据库层锁阻塞、性能差;应当把分布式事务提升到业务层,由业务做资源预留,而不是数据库锁。所有TCC模式应运而生,他解决了数据库连接挂起的情况在互联网支付、钱包、账户系统(支付宝、微信支付、各类支付平台)等新支付模式下如同一枚新星冉冉升起。
其核心分为4步两阶段走:
1.TC调用所有接口,并汇总业务结果
2.Try(一阶段):业务校验 + 预留 / 冻结资源(本地事务提交),直接释放数据库资源
3.Confirm(二阶段 - 提交):全部 Try 成功,协调器调用 Confirm,确认执行业务,使用预留资源,幂等
4.Cancel(二阶段 - 回滚):任意 Try 失败,协调器调用 Cancel,释放冻结资源,幂等
总结:解决前面数据库连接占用的痛点,处理灵活,但其confirm和cancel需要进行构建和思考,对于业务能力要求高
Seata AT模式
Apache Seata(incubating) 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务
实现方式:
1.在各个事务的数据库中建表 undo_log表
2.直接执行业务,seata代理数据源(DataSource),拦截sql执行,提取sql执行前后的数据镜像,将前后数据镜像转化成一条sql,加入到当前本地事务,执行提交,插入到undo_log表中
3.如果所有参与者都处理成功,TC 协调所有参与者直接删除undo_log记录,如果存在参与者处理失败,TC协调所有的参与者回滚,参与者从undo_log表中提取前后镜像,计算得到逆向补偿sql,执行sql实现补偿
总结:简单,通用,性能高,解决数据库挂起痛点,但缺点很明显对于复杂业务无法处理
Seata的AT模式全局事务执行过程
重点扩展一下Seata AT模式
其运行流程如下:
1.@GlobalTransactional开启全局事务,TM 向 TC 申请全局事务,TC 生成全局唯一 XID,XID 通过 RPC 传播到所有参与的微服务分支。
2.RM 拦截业务 SQL,在执行更新前,查询原始数据生成before image(前镜像)
3.执行业务 ,更新数据库业务数据
4.更新完成后,查询更新后的数据生成after image(后镜像)更新完成后,查询更新后的数据生成 after image(后镜像)
5.在同一个本地数据库事务内:写入 undo_log(存放前后镜像) + 向 TC 申请全局锁
6.拿到全局锁后,提交本地数据库事务,释放本地行锁、数据库连接
后续根据结果分为2种情况执行:
A:全局事务正常提交
1.TM通知TC 全局事务提交
2.TC下发commit 指令给所有 RM 分支
3.RM收到commit,异步任务批量删除当前分支的 undo_log,立刻返回成功
B:全局事务回滚(某分支失败)
1.TM通知TC 全局回滚
2.TC下发rollback 指令给 RM
3.RM查询undo_log,对比after image 和 当前数据库数据(防脏写校验)
4.如果数据没有被别的事务修改,根据 before 镜像生成补偿 SQL 执行回滚
5.回滚成功后删除 undo_log;如果校验失败(数据已被外部修改),进入人工告警处理。
AT 模式的全局锁与本地锁
修改同一行数据的多个全局事务,串行执行,不能并发写 规则:本地事务提交之前,必须先拿到 TC 上该行记录的全局锁;拿不到全局锁,不允许提交本地事务,不断重试,超时直接回滚本地事务,普通本地事务(没有 @GlobalTransactional),直接操作数据库,不受 Seata 全局锁控制,可以修改数据,会破坏隔离。
缓存与数据库不同步问题
更新 DB 和操作缓存是两个独立操作,原子性无法保证,只能做到最终一致性
Cache Aside旁路缓存(工业最常用)
先更新数据库,再删除缓存。先删缓存再更新数据库:会出现,读请求在 DB 更新前命中旧数据,写入缓存,永久脏数据的并发问题