分布式事务解决方案-seata

一、事务

事务

事务在一般情况下都是指代数据库事务,它是由系统对数据进行访问和更新的操作所组成的一个逻辑单元,具备原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )、持久性( Durability )四大特征,用来确保无论发生什么情况数据都处在一个合理的状态,在日常开发过程中能够不需要考虑网络波动、服务器宕机等问题。所以从某种角度来看,事务是用来服务应用层的。

为什么需要分布式事务

单体应用可以依赖数据库事务,将一系列操作限制在一个会话之中同时成功或失败,但是在分布式系统中,每个服务本身的数据库会话之间是隔离的,就无法单纯的依赖数据库事务,在这种场景下想要保证数据的一致性,就需要引入分布式事务。

二、Seata

Seata 的构成

TC ( Transaction Coordinator ) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM ( Transaction Manager ) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM ( Resource Manager ) - 资源管理器

管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata 保证分布式事务的基础是基于 2PC 协议,将事务拆分为准备阶段和提交阶段。在准备阶段,TM 给每个 RM 发送消息, RM 在本地执行事务,此时事务没有提交,在提交阶段, TM 收到了 RM 执行失败或超时则给每个 RM 发送回滚信息,否则则发送提交消息。 RM 根据 TM 发送的指令进行提交或回滚消息。

Seata 的四种模式

AT 模式

AT 模式是 Seata 的默认模式,保证最终一致性,没有业务入侵。

在一阶段中 Seate 会拦截业务 Sql ,解析 Sql 找到数据,在数据更新之前,保存 UndoLog 加行锁,二阶段负责业务的整体回滚和提交,如果一阶段中有本地的事务没有通过,就执行全局回滚,否则执行全局提交,回滚用到的就是一阶段记录的 UndoLog ,生成反向 Sql 。

开启方式

开启 AT 模式只需要在方法上添加注解 @GlobalTransactional 即可

typescript 复制代码
  @GlobalTransactional
    public void method(PurchaseCommodityParam param) {
        //......
    }

AT 模式的脏写问题

两个 Seata 管理的事务对同一条数据进行修改,事务 A 的 RM( Commodity )执行成功,本地事务提交,此时库存为 999 ,事务 B 减少 100 的库存,库存为 899 ,此时事务 A 的 RM( Order )执行失败,事务 A 整体回滚,库存回滚到 1000 ,那么事务 B 的的操作就被覆盖了。

对于这种情况 Seata 的解决办法是引入了全局锁的概念,一阶段本地事务提交前,需要确保先拿到全局锁 ,拿不到全局锁不能提交本地事务。

参考官网给出的案例,TX1、TX2 都是由 Seata 管理的事务,分别对 m 字段进行更新操作,m 的初始值 1000。TX1 先执行,获取数据库锁,对数据进行更新 m = 1000 - 100 = 900,在本地事务提交前,先获取由 Seata 管理的全局锁,本地事务提交释放数据库锁。此时 TX2 开始执行,获取数据库锁后对数据进行更新操作 m = 900 - 100 = 800,在提交前同样去获取 Seata 的全局锁,在 TX1 提交前,字段 m 的全局锁被 TX1 持有,TX2 无法对数据进行更新。

Seata 通过在 'lock_table' 中记录数据库连接信息来实现全局锁,当 RM 提交或回滚后删除锁记录

全局锁是基于事务都是 Seata 管理的事务,如果此时有非 Seata 管理的事务对数据改动, Seata 无法完成回滚,此时需要根据日志监控来人工介入。

同时在引入全局锁之后会可能会由于锁资源产生死锁的问题 ,TX1 如果在二阶段需要全局回滚,则 TX1 需要重新获取数据库锁回滚数据,才能释放全局锁,但是此时数据库锁被 TX2 所持有,TX2 需要获取全局锁才能提交本地事务,这里就产生了死锁,直到 TX2 全局锁等待超时,放弃获取全局锁并回滚本地事务,TX1 才可以回滚。

AT 模式的脏读问题

Seata 在一阶段执行过程中部分 RM 提交本地事务,二阶段事务发生回滚,在一阶段和二阶段之间如果有业务对数据进行查询读到的就是脏数据。Seata 的 AT 模式是保证分布式事务的最终一致性,在过程中无法保证数据的隔离,脏读是无法避免的问题。

XA 模式

Seata 的 XA 模式是基于数据库本身的特性来实现的,是一种强一致性的事务。

在一阶段中 TC 向 RM 通知执行业务,但是不提交,将执行结果交给事务协调者,如果没有出现问题则进行第二阶段,在二阶段中 RM 一起提交事务。如果第一阶段其中一个事务出现了错误,那么其他的事务都进行回滚。XA 与 AT 模式相比在一阶段中 XA 不会提交本地事务,就可以避免保证只保证最终一致性所引发的脏读问题,同样的由于本地事务不提交 XA 模式也会占用更多的数据库资源。

开启方式

可以在添加配置,设置为 XA 模式

yaml 复制代码
seata:
  data-source-proxy-mode: XA

也可以通过添加数据源代理的方式开启

kotlin 复制代码
public class XADataSourceConfig {
  
    @Bean("dataSource")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxyXA(druidDataSource);
    }
}

XA 模式的死锁问题

RM 服务在执行过程中挂了, TC 收不到 RM 的结束命令,那么其他的RM都会持有数据库资源不释放,这里就产生了死锁,直到等待超时 TC 才会向 RM 下达回滚命令。

TCC 模式

TCC 与 AT 模式类似,AT 模式基于 支持本地 ACID 事务的关系型数据库:

一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。

二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。

二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

一阶段 prepare 行为:调用自定义的 prepare 逻辑。

二阶段 commit 行为:调用自定义的 commit 逻辑。

二阶段 rollback 行为:调用自定义的 rollback 逻辑。

总的来说 TCC 相比于 AT 由于是根据业务来自己实现 Commit 和 Rollback 所以不受数据库类型的限制 ,AT模式支持的数据包括 MySQL、Oracle、PostgreSQL、TiDB、MariaDB,TCC 模式在 1.4.2 版本及之前是不依赖数据源的,从 1.5.1 版本开始增加了防悬挂措施 , 就需要数据源的支持。XA 模式只支持实现了 XA 协议的数据库,包括 MySQL、Oracle、PostgreSQL 和 MariaDB 。

开启方式

less 复制代码
public interface TCCService {
​
    @TwoPhaseBusinessAction(name = "prepareMethod",commitMethod = "commit",rollbackMethod = "rollback")
    public void prepareMethod(@BusinessActionContextParameter(paramName = "param") MethodParam param);
​
    public boolean commit(BusinessActionContext businessActionContext);
​
    public boolean rollback(BusinessActionContext businessActionContext);
  
}

TC 的空回滚、业务悬挂、超时重试

TM 下发命令给 RM 其中有部分 RM 执行阻塞,TC 此时感知异常发出回滚命令,执行过 Perpare 的 RM 回滚是没问题的,但是没有执行过 Perpare 的 RM 也要执行 Rollback 就造成了空回滚,空回滚之后 RM 再次执行 Perpare 就会成为业务悬挂。 在 Commit Rollback 阶段 TC 没有收 RM 的执行响应需要进行 Perpare 的重试,就是超时重试。

空回滚、业务悬挂、超时重试的产生原因本质上就是 RM 没有对 TC 的消息顺序做校验以及幂等,在1.4.2版本及之前,需要自己在业务中处理这些问题,在 Seata 的 1.5.1 版本之后,Seata 增加了一张事务控制表来解决这个问题,通过全局 ID 以及状态来对 RM 的执行过程进行控制。

Saga 模式

Seata 的 Saga 模式是基于状态机引擎实现的编排式分布式事务,状态机最小的执行单位是节点,对应一个服务调用,可以对其配置补偿节点,通过链路的串联编排出调用流程。 Seata 的 Saga 模式与 XA 、 AT 、 TCC 模式相比较于复杂,可以参考 Seata 官网提供的 Demo 来进行学习。

三、总结

在分布式系统中能否保证数据一致性是一项技术难题,Seata 在解决了分布式事务的场景同时也带来了编码复杂度的上升以及性能下降等等问题,也可能由于中间件本身引发一些问题。在开发生涯中不是每一个项目都适合引入分布式事务的解决方案,作为一名开发者需要在这之间有所取舍,本文更多是给大家提供一份解决问题的思路。

参考资料:

推荐阅读

浅析ThreadLocal

图可视化探索与实践

ES亿级商品索引拆分实战

在 ARM 环境下搭建原生 Hadoop 集群

利用流量保障搜索质量的实践

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
DT辰白3 小时前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构
道一云黑板报3 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
老猿讲编程5 小时前
技术发展历程:从 CORBA 到微服务
微服务·云原生·架构
飞来又飞去5 小时前
kafka sasl和acl之间的关系
分布式·kafka
MZWeiei6 小时前
Zookeeper的监听机制
分布式·zookeeper
莹雨潇潇6 小时前
Hadoop完全分布式环境部署
大数据·hadoop·分布式
浩哲Zhe7 小时前
RabbitMQ
java·分布式·rabbitmq
明达技术7 小时前
分布式 IO 模块:赋能造纸业,革新高速纸机主传动
分布式
Allen Bright8 小时前
RabbitMQ中的Topic模式
分布式·rabbitmq
time_silence9 小时前
微服务——服务通信与接口设计
微服务