分布式事务Seata

Seata架构

TC-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

集成Seata步骤

1.下载seata-server
2.解压配置,注册nacos

registry {
	  # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
	  type = "nacos"

	  nacos {
		# seata tc 服务注册到 nacos的服务名称,可以自定义
		application = "seata-tc-server"
		serverAddr = "127.0.0.1:8848"
		group = "DEFAULT_GROUP"
		namespace = ""
		cluster = "SH"
		username = "nacos"
		password = "nacos"
	  }
	}

	config {
	  # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
	  type = "nacos"
	  # 配置nacos地址等信息
	  nacos {
		serverAddr = "127.0.0.1:8848"
		namespace = ""
		group = "SEATA_GROUP"
		username = "nacos"
		password = "nacos"
		dataId = "seataServer.properties"
	  }
	}

3.naocs创建seata的配置文件seataServer.properties

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

4.创建seata数据库表(分支事务表 branch_table,全局事务表global_table)
记录全局事务、分支事务、全局锁信息

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table`  (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` tinyint(4) NULL DEFAULT NULL,
  `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` int(11) NULL DEFAULT NULL,
  `begin_time` bigint(20) NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;

5.启动seata
6.SpringBoot引用依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除-->
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

7.修改配置文件

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server # tc服务在nacos中的服务名称
      cluster: SH
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: SH

Seata提供了四种不同的分布式事务解决方案

XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
TCC模式:最终一致的分阶段事务模式,有业务侵入
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
SAGA模式:长事务模式,有业务侵入

XA模式

强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
场景:对一致性、隔离性有高要求的业务

流程:TM开启全局任务,然后调用分支(微服务),RM向TC注册分支事务,微服务执行业务sql,不提交事务,RM报告事务状态。TM提交/回滚全局事务,检查分支事务状态,进行各个分支的提交/回滚。
在这里插入图片描述
RM一阶段的工作:
注册分支事务到TC
执行分支业务sql但不提交
报告执行状态到TC
TC二阶段的工作:
TC检测各分支事务执行状态
如果都成功,通知所有RM提交事务
如果有失败,通知所有RM回滚事务
RM二阶段的工作:
接收TC指令,提交或回滚事务

XA模式的优点
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
依赖关系型数据库实现事务

实现:
修改application.yml文件(每个参与事务的微服务),开启XA模式:seata: data-source-proxy-mode: XA # 开启数据源代理的XA模式。
给发起全局事务的入口方法添加@GlobalTransactional注解,本例中是OrderServiceImpl中的create方法

AT模式

最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式。缺弥补了XA模型中资源锁定周期过长的缺陷
场景:基于关系型数据库的大多数分布式事务场景都可以

流程:TM开启全局事务,TM调用分支,RM向TC注册分支事务,微服务执行业务sql,执行完进行提交事务,并记录更新前后的快照,RM报告事务状态。TM提交/回滚全局事务,检查分支事务状态,进行各个分支的提交/回滚,然后删除log数据
在这里插入图片描述
AT模式的脏写问题:
在这里插入图片描述

事务1获取DB锁,保存快照1,执行sql,提交事务,释放DB锁。事务2进行操作的时候,也对数据进行修改,但是事务1回滚日志,将事务2的修改作废。

AT模式的写隔离:
在这里插入图片描述
在这里插入图片描述

全局锁:由TC记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。
事务1获取DB锁,保存快照1,执行sql,提交事务,获取全局锁,释放DB锁。事务2获取DB锁,进行sql操作,提交事务的时候需要获取全局锁,重试超时回滚并释放DB锁,事务1等待获取DB锁和全局锁进行回滚,事务2并没有修改数据,不产生脏读。

AT模式的优点:
一阶段完成直接提交事务,释放数据库资源,性能比较好
利用全局锁实现读写隔离
没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
两阶段之间属于软状态,属于最终一致
框架的快照功能会影响性能,但比XA模式要好很多

实现:
1.创建全局锁表和快照表
lock_table导入到TC服务关联的数据库
undo_log表导入到微服务关联的数据库
2.修改application.yml文件,将事务模式修改为AT模式 seata: data-source-proxy-mode: AT

TCC模式

最终一致的分阶段事务模式,有业务侵入。
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try:资源的检测和预留;
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的反向操作。

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
阶段二:假如要提交(Confirm),则冻结金额扣减30
阶段三:如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
Try业务
记录冻结金额和事务状态到account_freeze表,扣减account表可用金额
Confirm业务
根据xid删除account_freeze表的冻结记录
Cancel业务
修改account_freeze表,冻结金额为0,state为2,修改account表,恢复可用金额

如何判断是否空回滚
在这里插入图片描述
cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
如何避免业务悬挂
try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

TCC问题:网络延迟
保证confirm、cancel接口的幂等性
允许空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
拒绝业务悬挂
对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂

使用:
1.创建冻结补偿表:在数据库记录冻结金额的同时,记录当前事务id和执行状态
在这里插入图片描述

2.声明TCC接口

@LocalTCC
public interface TCCService {
				/**Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法*/
			@TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
			void prepare(@BusinessActionContextParameter(paramName = "param") String param);
			/*** 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致 */
			boolean confirm (BusinessActionContext context);
			/*** 二阶段回滚方法,要保证与rollbackMethod一致*/
			boolean cancel (BusinessActionContext context);
}

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0.获取事务id
        String xid = RootContext.getXID();
        // 1.扣减可用余额
        accountMapper.deduct(userId, money);
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id
        String xid = ctx.getXid();
        // 2.根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        // 1.恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

SAGA模式

长事务模式,有业务侵入
场景:业务流程长、业务流程多,参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:
一阶段:直接提交本地事务
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
Saga模式优点:
事务参与者可以基于事件驱动实现异步调用,吞吐高
一阶段直接提交事务,无锁,性能好
不用编写TCC中的三个阶段,实现简单
缺点:
软状态持续时间不确定,时效性差
没有锁,没有事务隔离,会有脏写

四种模式对比

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值