使用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了,启动服务命令是:
sh bin/startup.sh -m standalone
上面这种未做任何配置调整就启动服务的方式,nacos会使用内置的Derby数据库,但是考虑到将来服务的扩展,我们一般会使用关系型数据库管理服务数据,比如MySQL。为了安全我们也要开启nacos的鉴权功能,这就要对nacos的配置文件进行调整,调整配置文件信息:application.properties。
- 修改数据库配置,所有的配置信息都保存到数据库中,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
- 开启鉴权,其他的服务端注册或者拉取配置信息都需要身份认证,保证系统的安全:
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
- 初始化数据库脚本:
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
);
以上三个步骤执行完成后,就可以启动服务了,服务启动后看到控制台输出信息:
通过浏览器访问:http://192.168.56.101:8848/nacos 看到下面这个信息就表示nacos配置成功了!!!
二、seata服务端
将上面下载好的seata包解压:
tar -zvxf apache-seata-2.1.0-incubating-bin.tar.gz
接下来进入解压后的目录修改seata配置:
- 修改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
- 运行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);
- 因为使用的是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
脚本执行完成后可以看到下面的输出:
- 以上所有都完成后,启动服务:
sh seata-server.sh -m db
如果看到控制台输出下面内容表示服务启动成功!!!
三、事务模式使用
seata官方文档介绍了它支持四种事务:AT、XA、TCC、SAGA。下面分别简单示例一下每种模式的使用方式和适用场景。
一般讲到事务就会涉及到付款场景,示例也是通过支付和扣款这两种场景介绍分布式事务的使用,假定一共有三个微服务:seata-demo-account 用于处理账户数据; seata-demo-order 用于处理订单数据; seata-demo 是前置接口服务,通过调用上面两个服务处理数据,三个服务都启动成功后nacos注册中心将三个服务都注册进来:
1、AT模式
seata分布式事务默认开启的模式,有关AT模式实现原理参考seata官方介绍的非常详细,这里只是简单说一下过程:首先要在操作的数据库下面创建一个 undo_log
表,将被执行的sql语句修改前的状态保存到这个 undo_log
表,然后执行每一个子事务,子事务执行完后就提交事务,当每个子事务都执行成功了直接删除 undo_log
表记录;如果其中某一个子事务失败,则所有子事务都按照 undo_log
记录的sql回滚执行。
- 在要使用的数据库下创建
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;
- 在需要事务控制的方法上添加
@GlobalTransactional
注解。
以支付场景为例,在我的demo示例代码中是需要 seata-demo
服务分别调用 seata-demo-account
和 seata-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模式的分布式事务控制,下面是我验证事务控制的过程:
当两个子事务都正常执行时数据库会按照请求更新数据,如果代码中有异常时会回滚数据库里面的数据:
(1)异常发生前子事务都提交,同时记录提交前的sql语句到undo_log表:
(2)代码有异常发生后发现数据库里面的数据回滚:
AT模式每个子事务单独控制,这样就不会长时间占用数据库资源,AT模式需要关系性数据库的支持。
2、XA模式
AT模式是事务的最终一致性,这样就会导致数据的中间状态可以被其他线程读到;如果要实现事务的强一致性,那么就要使用数据库都支持的二阶段提交,整个过程如下图:
预提交阶段检查资源是否都已经准备好,如果各个子事务都已经反馈能够提交事务了,在由TC告知各个子事务统一提交或回滚操作,由于在这个过程中会占用数据库资源,性能不如AT模式高,但是这种模式会保证事务的强一致性。
3、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模式适用性非常好,因为事务的控制都是我们自己代码实现,他并不依赖底层数据库支持,但是它需要我们自己编写大量的代码实现事务控制。
4、SAGA模式
saga事务可以分为两部分:正向服务和反向补偿服务,正向是事务正常执行时,每一个节点提交本地事务;一旦某个节点失败了,就反向执行补偿。
seata中的saga是通过状态机来管理的,个人感觉这个模式比较复杂,并且在项目中很少用到,基本上前三种模式已经足够了。
在使用OpenFeign时,注意要关闭熔断和降级CircuitBreaker,因为在不做处理的情况下会导致分布式事务ID无法传递到下一个服务,导致分布式事务失效。
相关文章:

使用seata管理分布式事务
做应用开发时,要保证数据的一致性我们要对方法添加事务管理,最简单的处理方案是在方法上添加 Transactional 注解或者通过编程方式管理事务。但这种方案只适用于单数据源的关系型数据库,如果项目配置了多个数据源或者多个微服务的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外链作为一种短网址服务,具备多项功能和技术优势,适用于多种场景,以下是其主要特点和优势: 短域名与高级设置:W外链提供了非常短的域名,这有助于提高用户体验,使其在社交媒体分享时更加便捷。…...

深入理解Spring Cache:加速应用性能的秘钥
一、什么是Spring Cache? Spring Cache是Spring框架中的一部分,它为应用提供了一种统一的缓存抽象,可以轻松集成各种缓存提供者(如Ehcache、Redis、Caffeine等)。通过使用Spring Cache,开发者可以在方法上…...

C语言入门基础题(力扣):完成旅途的最少时间(C语言版)
1.题目: 给你一个数组 time ,其中 time[i] 表示第 i 辆公交车完成 一趟旅途 所需要花费的时间。 每辆公交车可以 连续 完成多趟旅途,也就是说,一辆公交车当前旅途完成后,可以 立马开始 下一趟旅途。每辆公交车 独立 …...

基于LORA的一主多从监测系统_0.96OLED
关联:0.96OLED hal硬件I2C LORA 在本项目中每个节点都使用oled来显示采集到的数据以及节点状态,OLED使用I2C接口与STM32连接,这个屏幕内部驱动IC为SSD1306,SSD1306作为从机地址为0x78 发送数据:起始…...

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

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

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

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

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

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

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

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

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

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

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

【每日刷题】Day135
【每日刷题】Day135 🥕个人主页:开敲🍉 🔥所属专栏:每日刷题🍍 🌼文章目录🌼 1. LCR 011. 连续数组 - 力扣(LeetCode) 2. 【模板】二维前缀和_牛客题霸_牛客…...

Linux运维01:VMware创建虚拟机
视频链接:05.新建VM虚拟机_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1nW411L7xm/?p14&spm_id_from333.880.my_history.page.click&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.点击“创建虚拟机” 2.选择“自定义(高级࿰…...

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

Vue入门-指令学习-v-show和v-if
v-show: 作用:控制元素的显示隐藏 语法:v-show"表达式" 表达式值true显示,false隐藏 v-if 作用:控制元素的显示隐藏(条件渲染) 语法: vif"表达式" 表达式tr…...

nacos多数据源插件介绍以及使用
概述 在微服务架构中,服务配置的集中管理和动态调整是至关重要的。Nacos 提供了配置管理和服务发现的功能,其中配置管理支持动态数据源的切换,增强了其在复杂环境中的适用性。默认情况下,Nacos 支持 MySQL 和Derby,但…...

国庆档不太热,影视股“凉”了?
今年国庆档票房止步21亿元,属实有点差强人意。 根据国家电影局统计,2024年国庆档(2024年10月1日至7日)全国电影票房为21.04亿元,观影人次为5209万,总票房成绩、观影总人次同比均有所下滑。 作为传统观影高…...

QtDesign预览的效果与程序运行的结果不一致的解决方法
存在的问题 使用Qt designer软件设计出来的界面,与转换成python程序运行出来的结果不一致,具体看下图 Qt designer预览结果 程序运行出来的结果 原因分析 我自己的电脑是2560*1600分辨率的屏幕,采用的是200%的缩放比例,出现这种…...

模运算和快速幂
文章目录 模运算快速幂 模运算 模运算是大数运算中的常用操作。如果一个数太大,无法直接输出,或者不需要直接输出,则可以对它取模,缩小数值再输出。取模可以防止溢出,这是常见的操作。 取模运算一般要求a和m的符号一…...

【机器学习】——神经网络与深度学习:从基础到应用
文章目录 神经网络基础什么是神经网络?神经网络的基本结构激活函数 深度学习概述什么是深度学习?常见的深度学习算法 深度学习的工作流程深度学习的实际应用结论 引言 近年来,神经网络和深度学习逐渐成为人工智能的核心驱动力。这类模型模仿人…...

Unity各个操作功能+基本游戏物体创建与编辑+Unity场景概念及文件导入导出
各个操作功能 部分功能 几种操作游戏物体的方式: Center:有游戏物体父子关系的时候,中心点位置 Global/Local:世界坐标系方向/自身坐标系方向 :调试/暂停/下一帧 快捷键 1.Alt鼠标左键:可以实现巡游角度查看场景 2.鼠标滚轮…...

QT入门教程攻略 QT入门游戏设计:贪吃蛇实现 QT全攻略心得总结
Qt游戏设计:贪吃蛇 游戏简介 贪吃蛇是一款经典的休闲益智类游戏,玩家通过控制蛇的移动来吃掉地图上的食物,使蛇的身体变长。随着游戏的进行,蛇的移动速度会逐渐加快,难度也随之增加。当蛇撞到墙壁或自己的身体时&…...

Linux No space left on device分析和解决
报错解释: "No space left on device" 错误表示你的Linux设备(通常是磁盘分区)上没有剩余空间了。这可能是因为磁盘已满,或者inode已满。磁盘空间是指磁盘上的实际空间,而inode是用来存储文件元数据的数据结…...

Qt实现Halcon窗口显示当前图片坐标
一、前言 Halcon加载图片的窗口,不仅能放大和缩小图片,还可以按住Ctrl键显示鼠标下的灰度值,这种方式很方便我们分析缺陷的灰度和对比度。 二、实现方式 ① 创建显示坐标和灰度的widget窗口 下图的是widget部件,使用了4个label控…...