【seata】为什么需要分布式事务?(2PC、TCC)

2023-10-07 04:30

本文主要是介绍【seata】为什么需要分布式事务?(2PC、TCC),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 背景
  • 1. Seata简介
    • 1.1 分布式事务产生背景
      • 1.1.1 数据库的水平分割
      • 1.1.2 微服务化
  • 2. 分布式事务理论基础
    • 2.1 两阶段提交(2pc)
    • 2.2 TCC
      • 2.2.1 基本原理
      • 2.2.2 幂等控制
      • 2.2.3 空回滚
      • 2.2.4 防悬挂
  • 参考

背景

分布式事务实现方案从类型上去分刚性事务、柔型事务。刚性事务:通常无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。柔性事务:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。

  • 刚性事务:XA 协议(2PC、JTA、JTS)、3PC

  • 柔型事务:TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)

1. Seata简介

Seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴开源的分布式事务中间件,以高效并且对业务 0 侵入的方式,解决微服务场景下面临的分布式事务问题。

附上项目github链接 https://github.com/seata

1.1 分布式事务产生背景

讲到事务,又得搬出经典的银行转账问题了,下面以实例说明:

假设银行(bank)中有两个客户(name)张三和李四,我们需要将张三的1000元存款(sal)转到李四的账户上,目标就是张三账户减1000,李四账户加1000,不能出现中间步骤(张三减1000,李四没加)
在这里插入图片描述
假设dao层代码如下:

public interface BankMapper {/*** @param userName 用户名* @param changeSal 余额变动值*/public void updateSal(String userName,int changeSal);
}

对应xml中sql如下:

<update id="updateSal">update bank SET sal = sal+#{changeSal}  WHERE name = #{userName}</update>

如果两个用户对应的银行存款数据在一个数据源中,即一个数据库中,那么service层代码可以如下编写:

/*** @param fromUserName 转账人* @param toUserName 被转账人* @param changeSal 转账额度*/@Transactional(rollbackFor = Exception.class)public void changeSal(String fromUserName,String toUserName,int changeSal) {bankMapper.updateSal(fromUserName, -1 * changeSal);bankMapper.updateSal(toUserName, changeSal);}

通过spring框架下的@Transactional注解来保证单一数据源增删改查的一致性

但是随着业务的不断扩大,用户数在不断变多,几百万几千万用户时数据可以存一个库甚至一个表里,假设有10个亿的用户?

1.1.1 数据库的水平分割

为了解决数据库上的瓶颈,分库是很常见的解决方案,不同用户就可能落在不同的数据库里,原来一个库里的事务操作,现在变成了跨数据库的事务操作。
在这里插入图片描述
此时@Transactional注解就失效了,这就是跨数据库分布式事务问题

1.1.2 微服务化

当然,更多的情形是随着业务不断增长,将业务中不同模块服务拆分成微服务后,同时调用多个微服务所产生的

微服务化的银行转账情景往往是这样的:

  • 调用交易系统服务创建交易订单;
  • 调用支付系统记录支付明细;
  • 调用账务系统执行 A 扣钱;
  • 调用账务系统执行 B 加钱;

在这里插入图片描述
如图所示,每个系统都对应一个独立的数据源,且可能位于不同机房,同时调用多个系统的服务很难保证同时成功,这就是跨服务分布式事务问题。

2. 分布式事务理论基础

2.1 两阶段提交(2pc)

2.2 TCC

2.2.1 基本原理

TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。

在这里插入图片描述
其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

下面还是以银行转账例子来说明

假设用户user表中有两个字段:可用余额(available_money)、冻结余额(frozen_money)
A扣钱对应服务A(ServiceA)
B加钱对应服务B(ServiceB)
转账订单服务(OrderService)
业务转账方法服务(BusinessService)

ServiceA,ServiceB,OrderService都需分别实现try(),confirm(),cancle()方法,方法对应业务逻辑如下:
在这里插入图片描述
步骤一 业务调用方BusinessService中就需要调用:
ServiceA.try()
ServiceB.try()
OrderService.try()

步骤二:
1、当所有try()方法均执行成功时,对全局事物进行提交,即由事物管理器调用每个微服务的confirm()方法
2、 当任意一个方法try()失败(预留资源不足,抑或网络异常,代码异常等任何异常),由事物管理器调用每个微服务的cancle()方法对全局事务进行回滚:

引用网上一张TCC原理的参考图片:
在这里插入图片描述

2.2.2 幂等控制

使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,网络原因,或者重试操作都有可能导致这几个操作的重复执行
在这里插入图片描述
业务实现过程中需重点关注幂等实现,讲到幂等,以上述TCC转账例子中confirm()方法来说明

在confirm()方法中:
余额-1000,冻结余额-1000,这一步是实现幂等性的关键,你会怎么做?

大家在自己系统里操作资金账户时,为了防止并发情况下数据不一致的出现,肯定会避免出现这种代码:

//根据userId查到账户
Account account = accountMapper.selectById(userId);
//取出当前资金
int availableMoney = account.getAvailableMoney();
account.setAvailableMoney(availableMoney-1000);
//更新剩余资金
accountMapper.update(account);

因为这本质上是一个 读-改-写的过程,不是原子的,在并发情况下会出现数据不一致问题

所以最简单的做法是:

update account set available_money = available_money-1000 where user_id=#{userId}

这利用了数据库行锁特性解决了并发情况下的数据不一致问题,但是TCC中,单纯使用这个方法适用么?

答案是不行的,该方法能解决并发单次操作下的扣减余额问题,但是不能解决多次操作带来的多次扣减问题,假设我执行了两次,按这种方案,用户账户就少了2000块

那么具体怎么做?上诉转账例子中,可以引入转账订单状态来做判断,若订单状态为已支付,则直接return:

if( order!=null && order.getStatus().equals("转账成功")){return;
}

当然,新建一张去重表,用订单id做唯一建,若插入报错返回也是可以的,不管怎么样,核心就是保证,操作幂等性

2.2.3 空回滚

如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;

TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;
在这里插入图片描述

如上图,阶段1消息丢失,阶段二收到cancel消息

那么具体代码里怎么做呢?
分析下,如果try()方法没执行,那么订单一定没创建,所以cancle方法里可以加一个判断,如果上下文中订单编号orderNo不存在或者订单不存在,直接return

if(orderNo==null || order==null){return;
}

核心思想就是 回滚请求处理时,如果对应的具体业务数据为空,则返回成功

当然这种问题也可以通过中间件层面来实现,如,在第一阶段try()执行完后,向一张事务表中插入一条数据(包含事务id,分支id),cancle()执行时,判断如果没有事务记录则直接返回,但是现在还不支持

2.2.4 防悬挂

如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况

用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;
在这里插入图片描述
这里又怎么做呢?

可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段回滚操作已经执行,不再执行try方法;

参考

Quick Start
Quick Start Source
Seata实现分布式事务
Seata实现2PC事务控制
Seata实战-分布式事务简介及demo上手 参考主体

这篇关于【seata】为什么需要分布式事务?(2PC、TCC)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/156031

相关文章

MYSQL事务死锁问题排查及解决方案

《MYSQL事务死锁问题排查及解决方案》:本文主要介绍Java服务报错日志的情况,并通过一系列排查和优化措施,最终发现并解决了服务假死的问题,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录问题现象推测 1 - 客户端无错误重试配置推测 2 - 客户端超时时间过短推测 3 - mysql 版本问

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

Golang使用etcd构建分布式锁的示例分享

《Golang使用etcd构建分布式锁的示例分享》在本教程中,我们将学习如何使用Go和etcd构建分布式锁系统,分布式锁系统对于管理对分布式系统中共享资源的并发访问至关重要,它有助于维护一致性,防止竞... 目录引言环境准备新建Go项目实现加锁和解锁功能测试分布式锁重构实现失败重试总结引言我们将使用Go作

Redis分布式锁使用及说明

《Redis分布式锁使用及说明》本文总结了Redis和Zookeeper在高可用性和高一致性场景下的应用,并详细介绍了Redis的分布式锁实现方式,包括使用Lua脚本和续期机制,最后,提到了RedLo... 目录Redis分布式锁加锁方式怎么会解错锁?举个小案例吧解锁方式续期总结Redis分布式锁如果追求

SpringBoot嵌套事务详解及失效解决方案

《SpringBoot嵌套事务详解及失效解决方案》在复杂的业务场景中,嵌套事务可以帮助我们更加精细地控制数据的一致性,然而,在SpringBoot中,如果嵌套事务的配置不当,可能会导致事务不生效的问题... 目录什么是嵌套事务?嵌套事务失效的原因核心问题:嵌套事务的解决方案方案一:将嵌套事务方法提取到独立类

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

集中式版本控制与分布式版本控制——Git 学习笔记01

什么是版本控制 如果你用 Microsoft Word 写过东西,那你八成会有这样的经历: 想删除一段文字,又怕将来这段文字有用,怎么办呢?有一个办法,先把当前文件“另存为”一个文件,然后继续改,改到某个程度,再“另存为”一个文件。就这样改着、存着……最后你的 Word 文档变成了这样: 过了几天,你想找回被删除的文字,但是已经记不清保存在哪个文件了,只能挨个去找。真麻烦,眼睛都花了。看

MySql 事务练习

事务(transaction) -- 事务 transaction-- 事务是一组操作的集合,是一个不可分割的工作单位,事务会将所有的操作作为一个整体一起向系统提交或撤销请求-- 事务的操作要么同时成功,要么同时失败-- MySql的事务默认是自动提交的,当执行一个DML语句,MySql会立即自动隐式提交事务-- 常见案例:银行转账-- 逻辑:A给B转账1000:1.查询