当前位置: 首页 > news >正文

使用seata管理分布式事务

做应用开发时,要保证数据的一致性我们要对方法添加事务管理,最简单的处理方案是在方法上添加 @Transactional 注解或者通过编程方式管理事务。但这种方案只适用于单数据源的关系型数据库,如果项目配置了多个数据源或者多个微服务的rpc调用,就会导致传统的事务管理方案失效,这就涉及到 分布式事务 管理,下面介绍一个非常好用的分布式事务管理框架:seata。通过seata了解如何在业务中管理分布式事务。
要使用seata首先需要到seata的官方网站下载服务端的安装包,当前最新的版本是2.1.0:apache-seata-2.1.0-incubating-bin.tar.gz 。seata支持多种部署方式:单机部署、高可用部署、docker部署、k8s部署、raft部署。根据自己的业务量选择一种部署方式,这里演示如何使用seata就选择最简单的单机部署方式,所有服务都在虚拟机中启动。

一、配置中心和注册中心

如果我们的项目是使用微服务架构,那么注册中心和配置中心是必不可少的两个组件,seata可以通过配置中心管理配置,为了简化代码选择nacos作为注册中心和配置中心,使用微服务模拟分布式事务场景,微服务选择SpringCloud,服务间调用使用OpenFeign。为了完成这个示例功能,首选就要搭建一个nacos服务:
首先下载nacos安装包:2.4.2.zip
将下载好的nacos解压到服务器的某个目录:

unzip nacos-server-2.4.2.zip
cd nacos

解压nacos

如果只是演示使用,不需要调整任何配置可以直接启动服务开始使用nacos了,启动服务命令是:

sh bin/startup.sh -m standalone

上面这种未做任何配置调整就启动服务的方式,nacos会使用内置的Derby数据库,但是考虑到将来服务的扩展,我们一般会使用关系型数据库管理服务数据,比如MySQL。为了安全我们也要开启nacos的鉴权功能,这就要对nacos的配置文件进行调整,调整配置文件信息:application.properties。

  1. 修改数据库配置,所有的配置信息都保存到数据库中,nacos开启高可用后就可以使用统一的配置信息:
spring.datasource.platform=mysql
spring.sql.init.platform=mysqldb.num=1db.url.0=jdbc:mysql://192.168.56.101:3306/nacos?characterEncoding=utf8&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=nacos@Test
  1. 开启鉴权,其他的服务端注册或者拉取配置信息都需要身份认证,保证系统的安全:
nacos.core.auth.enabled=truenacos.core.auth.server.identity.key=nacosTest
nacos.core.auth.server.identity.value=nacosTest2024nacos.core.auth.plugin.nacos.token.secret.key=SecretKeyh6k53by4u8ct9gd6of5vvgcvgpxf4u5g605ks7hzkknd7uiuim00co9gqocf
  1. 初始化数据库脚本:
CREATE TABLE `config_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) DEFAULT NULL COMMENT 'group_id',`content` longtext NOT NULL COMMENT 'content',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',`c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',`c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',`effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',`type` varchar(64) DEFAULT NULL COMMENT '配置的类型',`c_schema` text COMMENT '配置的模式',`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';CREATE TABLE `config_info_aggr` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',`content` longtext NOT NULL COMMENT '内容',`gmt_modified` datetime NOT NULL COMMENT '修改时间',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';CREATE TABLE `config_info_beta` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL COMMENT 'content',`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';CREATE TABLE `config_info_tag` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL COMMENT 'content',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';CREATE TABLE `config_tags_relation` (`id` bigint(20) NOT NULL COMMENT 'id',`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',`nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',PRIMARY KEY (`nid`),UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';CREATE TABLE `group_capacity` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';CREATE TABLE `his_config_info` (`id` bigint(20) unsigned NOT NULL COMMENT 'id',`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL COMMENT 'content',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',`op_type` char(10) DEFAULT NULL COMMENT 'operation type',`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',PRIMARY KEY (`nid`),KEY `idx_gmt_create` (`gmt_create`),KEY `idx_gmt_modified` (`gmt_modified`),KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';CREATE TABLE `tenant_capacity` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';CREATE TABLE `tenant_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`kp` varchar(128) NOT NULL COMMENT 'kp',`tenant_id` varchar(128) default '' COMMENT 'tenant_id',`tenant_name` varchar(128) default '' COMMENT 'tenant_name',`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';CREATE TABLE `users` (`username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username',`password` varchar(500) NOT NULL COMMENT 'password',`enabled` boolean NOT NULL COMMENT 'enabled'
);CREATE TABLE `roles` (`username` varchar(50) NOT NULL COMMENT 'username',`role` varchar(50) NOT NULL COMMENT 'role',UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);CREATE TABLE `permissions` (`role` varchar(50) NOT NULL COMMENT 'role',`resource` varchar(128) NOT NULL COMMENT 'resource',`action` varchar(8) NOT NULL COMMENT 'action',UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

以上三个步骤执行完成后,就可以启动服务了,服务启动后看到控制台输出信息:
nacos服务启动输出

通过浏览器访问:http://192.168.56.101:8848/nacos 看到下面这个信息就表示nacos配置成功了!!!
nacos登录页
nacos主页面

二、seata服务端

将上面下载好的seata包解压:

tar -zvxf apache-seata-2.1.0-incubating-bin.tar.gz

seata解压

接下来进入解压后的目录修改seata配置:

  1. 修改seata的注册中心和配置中心,存储就选择MySQL:
console:user:username: seatapassword: seata
seata:config:type: nacosnacos:server-addr: 127.0.0.1:8848namespace:group: SEATA_GROUPcontext-path:username: nacospassword: nacosdata-id: seata-server.propertiesregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848group: SEATA_GROUPnamespace:cluster: defaultcontext-path:username: nacospassword: nacosstore:mode: dbdb:datasource: druiddb-type: mysqldriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.56.101:3306/seata?rewriteBatchedStatements=true&characterEncoding=utf8&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTCuser: rootpassword: nacos@Testmin-conn: 10max-conn: 100global-table: global_tablebranch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error
  1. 运行script目录下的sql脚本,这里支持多种数据库,由于我使用的是mysql,下面执行的就是mysql的脚本:
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(`xid`                       VARCHAR(128) NOT NULL,`transaction_id`            BIGINT,`status`                    TINYINT      NOT NULL,`application_id`            VARCHAR(32),`transaction_service_group` VARCHAR(32),`transaction_name`          VARCHAR(128),`timeout`                   INT,`begin_time`                BIGINT,`application_data`          VARCHAR(2000),`gmt_create`                DATETIME,`gmt_modified`              DATETIME,PRIMARY KEY (`xid`),KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(`branch_id`         BIGINT       NOT NULL,`xid`               VARCHAR(128) NOT NULL,`transaction_id`    BIGINT,`resource_group_id` VARCHAR(32),`resource_id`       VARCHAR(256),`branch_type`       VARCHAR(8),`status`            TINYINT,`client_id`         VARCHAR(64),`application_data`  VARCHAR(2000),`gmt_create`        DATETIME(6),`gmt_modified`      DATETIME(6),PRIMARY KEY (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key`        VARCHAR(128) NOT NULL,`xid`            VARCHAR(128),`transaction_id` BIGINT,`branch_id`      BIGINT       NOT NULL,`resource_id`    VARCHAR(256),`table_name`     VARCHAR(32),`pk`             VARCHAR(36),`status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',`gmt_create`     DATETIME,`gmt_modified`   DATETIME,PRIMARY KEY (`row_key`),KEY `idx_status` (`status`),KEY `idx_branch_id` (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;CREATE TABLE IF NOT EXISTS `distributed_lock`
(`lock_key`       CHAR(20) NOT NULL,`lock_value`     VARCHAR(20) NOT NULL,`expire`         BIGINT,primary key (`lock_key`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
  1. 因为使用的是nacos注册中心,我们还需要执行一下配置中心的初始化脚本文件,文件在解压后目录下的 script/config-center/nacos 子目录下,在执行脚本之前需要先调整上一层目录下的文件 config.txt 根据自己使用的配置中心和存储方式调整相关内容:
调整存储方式:store.mode=db
store.lock.mode=db
store.session.mode=db调整数据库连接:store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driverstore.db.url=jdbc:mysql://192.168.56.101:3306/seata?useUnicode=true&rewriteBatchedStatements=true&allowMultiQueries=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
store.db.user=root
store.db.password=nacos@Test
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

执行脚本之前需要删除未使用的空配置,比如不使用redis就删除相关空配置,其他的配置可以保留。进入nacos脚本目录执行初始化脚本:

sh nacos-config.sh -h 192.168.56.101 -p 8848 -g SEATA_GROUP -u nacos -w nacos

脚本执行完成后可以看到下面的输出:
seata配置nacos
seata配置成功

  1. 以上所有都完成后,启动服务:
sh seata-server.sh -m db

如果看到控制台输出下面内容表示服务启动成功!!!
seata登录
seata主页面

三、事务模式使用

seata官方文档介绍了它支持四种事务:AT、XA、TCC、SAGA。下面分别简单示例一下每种模式的使用方式和适用场景。
一般讲到事务就会涉及到付款场景,示例也是通过支付和扣款这两种场景介绍分布式事务的使用,假定一共有三个微服务:seata-demo-account 用于处理账户数据; seata-demo-order 用于处理订单数据; seata-demo 是前置接口服务,通过调用上面两个服务处理数据,三个服务都启动成功后nacos注册中心将三个服务都注册进来:
nacos注册中心

1、AT模式

seata分布式事务默认开启的模式,有关AT模式实现原理参考seata官方介绍的非常详细,这里只是简单说一下过程:首先要在操作的数据库下面创建一个 undo_log 表,将被执行的sql语句修改前的状态保存到这个 undo_log 表,然后执行每一个子事务,子事务执行完后就提交事务,当每个子事务都执行成功了直接删除 undo_log 表记录;如果其中某一个子事务失败,则所有子事务都按照 undo_log 记录的sql回滚执行。

  1. 在要使用的数据库下创建 undo_log 表:
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  1. 在需要事务控制的方法上添加 @GlobalTransactional 注解。

以支付场景为例,在我的demo示例代码中是需要 seata-demo 服务分别调用 seata-demo-accountseata-demo-order ,这样要保证账户余额和支付订单两个服务数据一致性,就必须要对两个操作添加事务控制,由于两个操作分别在两个服务中,那么就需要使用分布式事务实现控制:
seata-demo中的代码:

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xingo.client.AccountClient;
import org.xingo.client.PayOrderClient;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/27*/
@RestController
public class PayController {@Autowiredprivate PayOrderClient payOrderClient;@Autowiredprivate AccountClient accountClient;@GetMapping("/pay")@GlobalTransactionalpublic String pay(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay) {String rs1 = accountClient.addBalance(userId, pay);System.out.println("acount: " + rs1);String rs2 = payOrderClient.payOrder(userId, pay);System.out.println("order: " + rs2);return "ok";}
}

两个远程调用是我使用open-feign封装的客户端:

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;
import java.sql.Timestamp;/*** @Author xingo* @Date 2024/9/27*/
@Component
@CircuitBreaker(name = "default")
@FeignClient(contextId = "payOrderClient", name = "seata-demo-order", path = "/payorder", fallbackFactory = PayOrderClient.PayOrderClientImpl.class)
public interface PayOrderClient {@GetMapping("/add")String payOrder(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay);@Componentpublic static class PayOrderClientImpl implements FallbackFactory<PayOrderClient> {@Overridepublic PayOrderClient create(Throwable cause) {return new PayOrderClient() {@Overridepublic String payOrder(int userId, BigDecimal pay) {return this.fail();}private String fail() {return "error, ts: " + new Timestamp(System.currentTimeMillis());}};}}}
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;
import java.sql.Timestamp;/*** @Author xingo* @Date 2024/9/27*/
@Component
@CircuitBreaker(name = "default")
@FeignClient(contextId = "accountClient", name = "seata-demo-account", path = "/account", fallbackFactory = AccountClient.PayOrderClientImpl.class)
public interface AccountClient {@GetMapping("/add")BigDecimal add(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay);@GetMapping("/addbalance")String addBalance(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay);@Componentpublic static class PayOrderClientImpl implements FallbackFactory<AccountClient> {@Overridepublic AccountClient create(Throwable cause) {return new AccountClient() {@Overridepublic BigDecimal add(int userId, BigDecimal pay) {return null;}@Overridepublic String addBalance(int userId, BigDecimal pay) {return this.fail();}private String fail() {return "error, ts: " + new Timestamp(System.currentTimeMillis());}};}}}

seata-demo-account中实现账户管理

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xingo.mapper.AccountMapper;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/27*/
@RestController
@RequestMapping("/account")
public class AccountController {@Autowiredprivate AccountMapper accountMapper;@GetMapping("/addbalance")@GlobalTransactionalpublic String addBalance(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay) {int rs = accountMapper.addAmount(pay, userId);return rs > 0 ? "ok|" + accountMapper.selectBalance(userId) : "fail";}
}

seata-demo-order中实现订单管理

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xingo.common.JacksonUtils;
import org.xingo.domain.SeataPayOrder;
import org.xingo.mapper.PayOrderMapper;import java.math.BigDecimal;
import java.time.LocalDateTime;/*** @Author xingo* @Date 2024/9/27*/
@RestController
@RequestMapping("/payorder")
public class PayOrderController {@Autowiredprivate PayOrderMapper payOrderMapper;@GetMapping("/add")@GlobalTransactionalpublic String payOrder(@RequestParam(value = "userId") int userId,@RequestParam(value = "pay") BigDecimal pay) {SeataPayOrder order = SeataPayOrder.builder().userId(userId).pay(pay).createTime(LocalDateTime.now()).modifyTime(LocalDateTime.now()).build();payOrderMapper.insert(order);return "ok|" + JacksonUtils.toJSONString(order);}
}

上面这些方法就已经实现了AT模式的分布式事务控制,下面是我验证事务控制的过程:
AT模式事务
当两个子事务都正常执行时数据库会按照请求更新数据,如果代码中有异常时会回滚数据库里面的数据:
(1)异常发生前子事务都提交,同时记录提交前的sql语句到undo_log表:
子事务都提交数据
记录undo日志

(2)代码有异常发生后发现数据库里面的数据回滚:
事务回滚
AT模式每个子事务单独控制,这样就不会长时间占用数据库资源,AT模式需要关系性数据库的支持。

2、XA模式

AT模式是事务的最终一致性,这样就会导致数据的中间状态可以被其他线程读到;如果要实现事务的强一致性,那么就要使用数据库都支持的二阶段提交,整个过程如下图:
两阶段提交
预提交阶段检查资源是否都已经准备好,如果各个子事务都已经反馈能够提交事务了,在由TC告知各个子事务统一提交或回滚操作,由于在这个过程中会占用数据库资源,性能不如AT模式高,但是这种模式会保证事务的强一致性。
XA管理事务

3、TCC模式

TCC事务

无论是AT或者XA模式,都需要底层关系型数据库的事务支持,对于不支持事务的数据库,我们要保证数据的一致性,就需要自己实现事务的管理,在seata中可以通过TCC实现,要实现TCC事务管理,首先要定义一个接口,在接口上面添加 @LocalTCC 注解,并且声明三个方法,分别对应处理数据的方法,提交数据的方法,回滚数据的方法,使用注解 @TwoPhaseBusinessAction(name = "addPreOrder", commitMethod = "confirmPreOrder", rollbackMethod = "cancelPreOrder") 标记,大致定义如下:

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/27*/
@LocalTCC
public interface PreOrderService {/*** 订单预付* @param userId* @param pay* @param orderNo* @return*/@TwoPhaseBusinessAction(name = "addPreOrder", commitMethod = "confirmPreOrder", rollbackMethod = "cancelPreOrder")boolean addPreOrder(int userId, BigDecimal pay, String orderNo);/*** 事务的commit方法* @param ctx* @return*/boolean confirmPreOrder(BusinessActionContext ctx);/*** 事务的rollback方法* @param ctx* @return*/boolean cancelPreOrder(BusinessActionContext ctx);}

接下来就是要实现上面定义的接口:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xingo.domain.SeataPreOrder;
import org.xingo.mapper.PreOrderMapper;
import org.xingo.service.PreOrderService;import java.math.BigDecimal;
import java.time.LocalDateTime;/*** @Author xingo* @Date 2024/9/27*/
@Service
public class PreOrderServiceImpl implements PreOrderService {@Autowiredprivate PreOrderMapper preOrderMapper;@Override@Transactional(rollbackFor = Exception.class)public boolean addPreOrder(int userId, BigDecimal pay, String orderNo) {System.out.println("try-xid: " + RootContext.getXID());LambdaQueryWrapper<SeataPreOrder> wrapper = Wrappers.<SeataPreOrder>lambdaQuery().eq(SeataPreOrder::getOrderNo, orderNo);SeataPreOrder find = preOrderMapper.selectOne(wrapper);if(find == null) {SeataPreOrder order = SeataPreOrder.builder().userId(userId).pay(pay).createTime(LocalDateTime.now()).modifyTime(LocalDateTime.now()).orderNo(orderNo).xid(RootContext.getXID()).build();preOrderMapper.insert(order);return true;}return false;}@Override@Transactional(rollbackFor = Exception.class)public boolean confirmPreOrder(BusinessActionContext ctx) {System.out.println("confirm-xid: " + ctx.getXid());int rs = preOrderMapper.clearXid(ctx.getXid());return rs > 0;}@Override@Transactional(rollbackFor = Exception.class)public boolean cancelPreOrder(BusinessActionContext ctx) {System.out.println("cancel-xid: " + ctx.getXid());int rs = preOrderMapper.delByXid(ctx.getXid());return rs > 0;}
}

定义好上面的类信息后就声明了一个TCC事务,调用方式跟上面使用AT或XA一样,只需要在请求方法上面添加 @GlobalTransactional 注解就可以管理事务了,如果没有异常情况输出日志大致如下:
tcc-ok
如果代码中有异常抛出,那么事务回滚:
tcc-rollback
TCC模式适用性非常好,因为事务的控制都是我们自己代码实现,他并不依赖底层数据库支持,但是它需要我们自己编写大量的代码实现事务控制。

4、SAGA模式

SAGA事务逻辑
saga事务可以分为两部分:正向服务和反向补偿服务,正向是事务正常执行时,每一个节点提交本地事务;一旦某个节点失败了,就反向执行补偿。
seata中的saga是通过状态机来管理的,个人感觉这个模式比较复杂,并且在项目中很少用到,基本上前三种模式已经足够了。
在使用OpenFeign时,注意要关闭熔断和降级CircuitBreaker,因为在不做处理的情况下会导致分布式事务ID无法传递到下一个服务,导致分布式事务失效。

相关文章:

使用seata管理分布式事务

做应用开发时&#xff0c;要保证数据的一致性我们要对方法添加事务管理&#xff0c;最简单的处理方案是在方法上添加 Transactional 注解或者通过编程方式管理事务。但这种方案只适用于单数据源的关系型数据库&#xff0c;如果项目配置了多个数据源或者多个微服务的rpc调用&…...

浏览器指纹

引言 先看下 官网 给的定义。 WebAssembly (abbreviatedWasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server …...

W外链平台有什么优势?

W外链作为一种短网址服务&#xff0c;具备多项功能和技术优势&#xff0c;适用于多种场景&#xff0c;以下是其主要特点和优势&#xff1a; 短域名与高级设置&#xff1a;W外链提供了非常短的域名&#xff0c;这有助于提高用户体验&#xff0c;使其在社交媒体分享时更加便捷。…...

深入理解Spring Cache:加速应用性能的秘钥

一、什么是Spring Cache&#xff1f; Spring Cache是Spring框架中的一部分&#xff0c;它为应用提供了一种统一的缓存抽象&#xff0c;可以轻松集成各种缓存提供者&#xff08;如Ehcache、Redis、Caffeine等&#xff09;。通过使用Spring Cache&#xff0c;开发者可以在方法上…...

C语言入门基础题(力扣):完成旅途的最少时间(C语言版)

1.题目&#xff1a; 给你一个数组 time &#xff0c;其中 time[i] 表示第 i 辆公交车完成 一趟旅途 所需要花费的时间。 每辆公交车可以 连续 完成多趟旅途&#xff0c;也就是说&#xff0c;一辆公交车当前旅途完成后&#xff0c;可以 立马开始 下一趟旅途。每辆公交车 独立 …...

基于LORA的一主多从监测系统_0.96OLED

关联&#xff1a;0.96OLED hal硬件I2C LORA 在本项目中每个节点都使用oled来显示采集到的数据以及节点状态&#xff0c;OLED使用I2C接口与STM32连接&#xff0c;这个屏幕内部驱动IC为SSD1306&#xff0c;SSD1306作为从机地址为0x78 发送数据&#xff1a;起始…...

C#系统学习路线

分享一个C#程序员的成长学习路线规划&#xff0c;希望能够帮助到想从事C#开发的你。 我一直在想&#xff0c;初学者刚开始学习编程时应该学些什么&#xff1f;学习到什么程度才能找到工作&#xff1f;才能在项目中发现和解决Bug&#xff1f; 我不知道每位初学者在学习编程时是…...

UI开发:从实践到探索

UI开发&#xff1a;从实践到探索 参考博客文章&#xff1a;https://blog.jim-nielsen.com/2024/sanding-ui/ 在现代web开发中&#xff0c;用户界面&#xff08;UI&#xff09;的重要性不言而喻。一个优秀的UI不仅能提升用户体验&#xff0c;还能直接影响产品的成功。 UI开发…...

操作系统 | 学习笔记 | 王道 | 3.1 内存管理概念

3 内存管理 3.1 内存管理概念 3.1.1 内存管理的基本原理和要求 内存可以存放数据&#xff0c;程序执行前需要先放到内存中才能被CPU处理—缓和cpu和磁盘之间的速度矛盾 内存管理的概念 虽然计算机技术飞速发展&#xff0c;内存容量也在不断扩大&#xff0c;但仍然不可能将所有…...

Unity射线之拾取物体

实现效果&#xff1a; 可以移动场景内物品放置到某个位置。通过射线检测&#xff0c;点击鼠标左键&#xff0c;移动物体&#xff0c;再点击左键放下物体。 效果&#xff1a; 移动物体 实现思路&#xff1a; 通过射线检测&#xff0c;将检测到的物体吸附到摄像机前的一个空物…...

Python的numpy库矩阵计算(数据分析)

一、创建矩阵 import numpy as np#创建矩阵anp.arange(15).reshape(3,5) bnp.arange(15,30).reshape(3,5) 使用arrange和reshape创建的二维数组就可以看成矩阵。 此时a和b存储的是&#xff1a; [[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14]] [[15 16 17 18 19]…...

R语言的基本语句及基本规则

0x01 赋值语句 使用 “<-” 或 “” 进行赋值。例如&#xff1a; x <- 5 # 将数值 5 赋值给变量 x y 10 # 另一种赋值方式0x02 输出语句 使用 print() 函数输出内容。例如&#xff1a; print("Hello, R!") print(x)0x03 注释语句 任何在 #之后的内容在…...

网络受限情况下安装openpyxl模块提示缺少Jdcal,et_xmlfile

1.工作需要处理关于Excel文件内容的东西 2.用公司提供的openpyxl模块总是提示缺少jdcal文件,因为网络管控,又没办法直接使用命令下载&#xff0c;所以网上找了资源&#xff0c;下载好后上传到个人资源里了 资源路径 openpyxl jdcal et_xmlfile 以上模块来源于&#xff1a;Py…...

【算法】- 查找 - 散列表查询(哈希表)

文章目录 前言一、哈希表的思想二、哈希表总结 前言 散列技术&#xff1a;在记录的存储位置和它的关键字之间建立一个确定的对应关系f&#xff0c;使得每个关键字key对应一个存储位置f(key) 哈希表&#xff1a;采用散列技术将记录存储在一块连续的存储空间中&#xff0c;这块连…...

货币政策工具

本文为个人学习笔记&#xff0c;内容源于教材&#xff1b;整理记录的同时也作为一种分享。 1. 简介 货币政策工具作为央行实现货币政策目标的经济手段&#xff0c;以期达到最终目标&#xff0c;即物价稳定&#xff0c;充分就业&#xff0c;经济增长&#xff0c;国际收支平衡。…...

std::async概念和使用方法

std::async是 C 标准库中的一个函数模板&#xff0c;用于启动一个异步任务&#xff0c;并返回一个std::future对象&#xff0c;该对象可用于获取异步任务的结果。 1、概念 std::async允许你以异步的方式执行一个函数或者可调用对象&#xff0c;它会在后台启动一个新的线程或者…...

Chatgpt 原理解构

一、背景知识 1. 自然语言处理的发展历程 自然语言处理在不同时期呈现出不同的特点和发展态势。萌芽期&#xff0c;艾伦・图灵在 1936 年提出 “图灵机” 概念&#xff0c;为计算机诞生奠定基础&#xff0c;1950 年他提出著名的 “图灵测试”&#xff0c;预见了计算机处理自然…...

【每日刷题】Day135

【每日刷题】Day135 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. LCR 011. 连续数组 - 力扣&#xff08;LeetCode&#xff09; 2. 【模板】二维前缀和_牛客题霸_牛客…...

Linux运维01:VMware创建虚拟机

视频链接&#xff1a;05.新建VM虚拟机_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1nW411L7xm/?p14&spm_id_from333.880.my_history.page.click&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.点击“创建虚拟机” 2.选择“自定义&#xff08;高级&#xff0…...

服务器平均响应时间和数据包大小关系大吗?

服务器的平均响应时间与数据包大小有一定的关系&#xff0c;但这只是影响响应时间的众多因素之一。具体来说&#xff0c;数据包大小对服务器响应时间的影响可以从以下几个方面来理解&#xff1a; 1. 数据传输时间 影响: 较大的数据包需要更多的时间在网络上传输&#xff0c;因此…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

基于matlab策略迭代和值迭代法的动态规划

经典的基于策略迭代和值迭代法的动态规划matlab代码&#xff0c;实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

LRU 缓存机制详解与实现(Java版) + 力扣解决

&#x1f4cc; LRU 缓存机制详解与实现&#xff08;Java版&#xff09; 一、&#x1f4d6; 问题背景 在日常开发中&#xff0c;我们经常会使用 缓存&#xff08;Cache&#xff09; 来提升性能。但由于内存有限&#xff0c;缓存不可能无限增长&#xff0c;于是需要策略决定&am…...

提升移动端网页调试效率:WebDebugX 与常见工具组合实践

在日常移动端开发中&#xff0c;网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时&#xff0c;开发者迫切需要一套高效、可靠且跨平台的调试方案。过去&#xff0c;我们或多或少使用过 Chrome DevTools、Remote Debug…...

什么是VR全景技术

VR全景技术&#xff0c;全称为虚拟现实全景技术&#xff0c;是通过计算机图像模拟生成三维空间中的虚拟世界&#xff0c;使用户能够在该虚拟世界中进行全方位、无死角的观察和交互的技术。VR全景技术模拟人在真实空间中的视觉体验&#xff0c;结合图文、3D、音视频等多媒体元素…...

Ubuntu系统复制(U盘-电脑硬盘)

所需环境 电脑自带硬盘&#xff1a;1块 (1T) U盘1&#xff1a;Ubuntu系统引导盘&#xff08;用于“U盘2”复制到“电脑自带硬盘”&#xff09; U盘2&#xff1a;Ubuntu系统盘&#xff08;1T&#xff0c;用于被复制&#xff09; &#xff01;&#xff01;&#xff01;建议“电脑…...