定制网站开发的目的是什么,互联网营销有哪些,游戏加盟网,wordpress文章中的相关文章代码在SpringCloudAlibabaSeate处理分布式事务
1、部分面试题 微服务boot/cloud做的项目#xff0c;你不可能只有一个数据库吧#xff1f;那么多个数据库之间如何处理分布式事务的#xff1f; 一个场景#xff1a;在订单支付成功后#xff0c;交易中心会调用订单中心的服务把订…SpringCloudAlibabaSeate处理分布式事务
1、部分面试题 微服务boot/cloud做的项目你不可能只有一个数据库吧那么多个数据库之间如何处理分布式事务的 一个场景在订单支付成功后交易中心会调用订单中心的服务把订单状态更新并调用物流中心的服务通知商品发货同时还要调用积分中心的服务为用户增加相应的积分。如何保障分布式事务一致性成为了确保订单业务稳定运行的核心诉求之一。 阿里巴巴的Seate-AT模式如何做到对业务的无侵入 对于分布式事务问题你知道的解决方案有哪些 2PC两阶段提交3PC三阶段提交TCC方案TCC(Try-Confirm-Cancel)又被称补偿事务类似2PC的柔性分布式解决方案2PC改良版LocalMessage本地消息表;独立消息微服务RabbitMQ/KafKa组件实现可靠消息最终一致性方案;最大努力通知方案
以上的面试题都指向了一个问题一次业务操作需要跨多个数据源或者需要跨多个系统进行远程调用就会产生分布式业务问题但是关系型数据库提供的能力是基于单机事务的一旦遇到了分布式事务场景就需要通过更多其他技术手段来解决。 分布式事务之前是单机单库没这个问题但是表结构关系从1:1-1:N-N:N; 分布式事务之后 单体应用被拆分成微服务应用原来的三个模块被拆分成三个独立的应用分别使用三个独立的数据源业务操作需要调用三个服务来完成。 此时每个服务自己内部的数据一致性由本地事务来保证但是全局的数据一致性问题没法保证。
2、Seata简介
2.1、基本简介
官网Apache Seata
GitHubGitHub
SeataSimple Extensible Autonomous Transaction Architecture是一款开源的分布式事务解决方案致力于在微服务架构下提供高性能和简单易用的分布式事务服务。简单可扩展自治事务框架。 2.2、各事务模式
2.2.1、Seata AT 模式(重点使用这个)
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案Seata 在内部做了对数据库操作的代理层我们使用 Seata AT 模式时实际上用的是 Seata 自带的数据源代理 DataSourceProxySeata 在这层代理中加入了很多逻辑比如插入回滚 undo_log 日志检查全局锁等。
两阶段提交协议的演变
一阶段业务数据和回滚日志记录在同一个本地事务中提交释放本地锁和连接资源。二阶段 提交异步化非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
2.2.2、Seata TCC 模式
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案是继 AT 模式后第二种支持的事务模式最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层不依赖底层数据库可以灵活选择业务资源的锁定粒度减少资源锁持有时间可扩展性好可以说是为独立部署的 SOA 服务而设计的。 2.2.3、Seata Saga 模式
Saga 模式是 SEATA 提供的长事务解决方案在 Saga 模式中业务流程中每个参与者都提交本地事务当出现某一个参与者失败则补偿前面已经成功的参与者一阶段正向服务和二阶段补偿服务都由业务开发实现。 2.2.4、Seata XA 模式
XA 模式是从 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理DTPDistributed Transaction Processing标准。Seata XA 模式是利用事务资源数据库、消息服务等对 XA 协议的支持以 XA 协议的机制来管理分支事务的一种事务模式。 3、Seata工作流程简介
整个分布式事务的管理就是全局事务ID的传递和变更要让开发者无感知。 Seata对分布式事务的协调和控制就是一个XID加3个概念(TCTMRM) XID是全局事务的唯一标识它可以在服务的调用链路中传递绑定到事物的上下文中 TC (Transaction Coordinator)-事务协调者 它就是意义上的Seata维护全局和分支事务的状态驱动全局事务提交或回滚。 TM (Transaction Manager)-事务管理器 标注全局GlobalTransactional启动入口动作的微服务模块(比如订单模块)它是事务的发起者负责定义全局事务的范围并根据TC维护的全局事务和分支事务状态做出开始事务、提交事务、回滚事务的决议。 RM(Resource Manager)-资源管理器就是mysql数据库本身可以是多个RM 管理分支事务处理的资源与TC交谈以注册分支事务和报告分支事务的状态并驱动分支事务提交或回滚。
小总结
三个组件相互协作TC以Seata 服务器(Server)形式独立部署TM和RM则是以Seata Client的形式集成在微服务中运行。 TM 向 TC 申请开启一个全局事务全局事务创建成功并生成一个全局唯一的 XID XID 在微服务调用链路的上下文中传播 RM 向 TC 注册分支事务将其纳入 XID 对应全局事务的管辖 TM 向 TC 发起针对 XID 的全局提交或回滚决议 TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
4、Seata-Server2.0.0安装
官网2.0.0版本的下载地址2.0.0.zip (github.com)
GitHub下载地址seata-server-2.0.0.zip (github.com)
Seata参数配置详情参数配置 | Apache Seata 4.1、mysql8.0数据库建库建表
直接拿到官网的mysql.sql文件mysql.sql (github.com)
先创建数据库
CREATE DATABASE seata;
USE seata;然后再执行sql创建表
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);4.2、更改配置
修改seata-server-2.0.0\conf\application.yml配置文件修改前要备份一下出厂的配置文件。 我们先看一下自带的配置模板看一下官方给的配置样例我们对着改自己的就好。
server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seata
seata:config:# support: nacos, consul, apollo, zk, etcd3#服务配置中心选择nacostype: nacos nacos:server-addr: 127.0.0.1:8848namespace:#后续自己在nacos里面新建,不想新建SEATA_GROUP就写DEFAULT_GROUPgroup: SEATA_GROUP username: nacospassword: nacosregistry:# support: nacos, eureka, redis, zk, consul, etcd3, sofa#服务注册中心选择nacostype: nacos nacos:application: seata-serverserver-addr: 127.0.0.1:8848group: SEATA_GROUPnamespace:cluster: defaultusername: nacospassword: nacosstore:# support: file 、 db 、 redis 、 raft#这里就是我们参考官网的mysql.sql文件中要的条件db使用数据库存储mode: db db:datasource: druiddb-type: mysql#这里我们是8.0版本的驱动换一下driver-class-name: com.mysql.cj.jdbc.Driver #这里说明一下我的MySQL8.0的端口被我改成3307了url: jdbc:mysql://127.0.0.1:3307/seata?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrue user: rootpassword: 123456min-conn: 10max-conn: 100#这里分别对应我们创建的仨表global-table: global_table branch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000# server:# service-port: 8091 #If not configured, the default is ${server.port} 1000security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
测试先启动Nacos 然后启动seata-server找到seata的bin目录找到seata-server.bat启动 然后刷新一下nacos服务列表 访问seata首页localhost:7091账密都是seata 5、Seata案例实战数据库和表准备
我们要准备订单库存账户3个业务数据库准备好
业务说明
当用户下单时会在订单服务中创建一个订单然后通过远程调用库存服务来扣减下单商品的库存再通过远程调用账户服务来扣减用户账户里面的余额最后在订单服务中修改订单状态为已完成
该操作跨越三个数据库有两次远程调用很明显会有分布式事务问题。 由于我们使用的是Seata的AT模式所以在每一个库中都应创建一个undo_log回滚日志表以便在事务失败或者需要回滚时进行回滚。
undo_log回滚日志表在官网中也有给出
GitHub地址incubator-seata/script/client/at/db/mysql.sql at master · apache/incubator-seata · GitHub
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS undo_log
(branch_id BIGINT NOT NULL COMMENT branch transaction id,xid VARCHAR(128) NOT NULL COMMENT global transaction id,context VARCHAR(128) NOT NULL COMMENT undo_log context,such as serialization,rollback_info LONGBLOB NOT NULL COMMENT rollback info,log_status INT(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created DATETIME(6) NOT NULL COMMENT create datetime,log_modified DATETIME(6) NOT NULL COMMENT modify datetime,UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4 COMMENT AT transaction mode undo table;
ALTER TABLE undo_log ADD INDEX ix_log_created (log_created);5.1、seata_order:存储订单的数据库
执行SQL建seata_order库建t_order表undo_log表
CREATE DATABASE seata_order;USE seata_order;CREATE TABLE t_order(id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,user_id BIGINT(11) DEFAULT NULL COMMENT 用户id,product_id BIGINT(11)DEFAULT NULL COMMENT 产品id,count INT(11) DEFAULT NULL COMMENT 数量,money DECIMAL(11,0) DEFAULT NULL COMMENT 金额,status INT(1) DEFAULT NULL COMMENT 订单状态: 0:创建中; 1:已完结)ENGINEINNODB AUTO_INCREMENT1 DEFAULT CHARSETutf8;SELECT * FROM t_order;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS undo_log
(branch_id BIGINT NOT NULL COMMENT branch transaction id,xid VARCHAR(128) NOT NULL COMMENT global transaction id,context VARCHAR(128) NOT NULL COMMENT undo_log context,such as serialization,rollback_info LONGBLOB NOT NULL COMMENT rollback info,log_status INT(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created DATETIME(6) NOT NULL COMMENT create datetime,log_modified DATETIME(6) NOT NULL COMMENT modify datetime,UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4 COMMENT AT transaction mode undo table;
ALTER TABLE undo_log ADD INDEX ix_log_created (log_created);5.2、seata_storage:存储库存的数据库
执行SQL建seata_storage库建t_storage表undo_log表
#storageCREATE DATABASE seata_storage;USE seata_storage;CREATE TABLE t_storage(id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,product_id BIGINT(11) DEFAULT NULL COMMENT 产品id,total INT(11) DEFAULT NULL COMMENT 总库存,used INT(11) DEFAULT NULL COMMENT 已用库存,residue INT(11) DEFAULT NULL COMMENT 剩余库存)ENGINEINNODB AUTO_INCREMENT1 DEFAULT CHARSETutf8;INSERT INTO t_storage(id,product_id,total,used,residue)VALUES(1,1,100,0,100);SELECT * FROM t_storage;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS undo_log(branch_id BIGINT NOT NULL COMMENT branch transaction id,xid VARCHAR(128) NOT NULL COMMENT global transaction id,context VARCHAR(128) NOT NULL COMMENT undo_log context,such as serialization,rollback_info LONGBLOB NOT NULL COMMENT rollback info,log_status INT(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created DATETIME(6) NOT NULL COMMENT create datetime,log_modified DATETIME(6) NOT NULL COMMENT modify datetime,UNIQUE KEY ux_undo_log (xid, branch_id)) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4 COMMENT AT transaction mode undo table;ALTER TABLE undo_log ADD INDEX ix_log_created (log_created);5.3、seata_account:存诸账户信息的数据库
执行SQL建seata_account库建t_account表undo_log表
#accountcreate database seata_account;use seata_account;CREATE TABLE t_account(id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT id,user_id BIGINT(11) DEFAULT NULL COMMENT 用户id,total DECIMAL(10,0) DEFAULT NULL COMMENT 总额度,used DECIMAL(10,0) DEFAULT NULL COMMENT 已用余额,residue DECIMAL(10,0) DEFAULT 0 COMMENT 剩余可用额度)ENGINEINNODB AUTO_INCREMENT2 DEFAULT CHARSETutf8;INSERT INTO t_account(id,user_id,total,used,residue)VALUES(1,1,1000,0,1000);SELECT * FROM t_account;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS undo_log(branch_id BIGINT NOT NULL COMMENT branch transaction id,xid VARCHAR(128) NOT NULL COMMENT global transaction id,context VARCHAR(128) NOT NULL COMMENT undo_log context,such as serialization,rollback_info LONGBLOB NOT NULL COMMENT rollback info,log_status INT(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created DATETIME(6) NOT NULL COMMENT create datetime,log_modified DATETIME(6) NOT NULL COMMENT modify datetime,UNIQUE KEY ux_undo_log (xid, branch_id)) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4 COMMENT AT transaction mode undo table;ALTER TABLE undo_log ADD INDEX ix_log_created (log_created);6、微服务实现
下订单–减库存–扣余额-改订单状态
6.1、使用MyBatis一键生成dao层
修改config.properties
# seata_order
jdbc.driverClass com.mysql.cj.jdbc.Driver
jdbc.url jdbc:mysql://localhost:3307/seata_order?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrue
jdbc.user root
jdbc.password 123456修改generatorConfig.xml
!-- table tableNamet_pay domainObjectNamePay--
!-- generatedKey columnid sqlStatementJDBC/--
!-- /table--!-- seata_order --table tableNamet_order domainObjectNameOrdergeneratedKey columnid sqlStatementJDBC//table刷新maven中mybatis插件 cloud-api-commons新增库存和账户两个Feign接口
StorageFeignApi
FeignClient(value seata-storage-service) //seata-storage-service现在还没有一会建
public interface StorageFeignApi {//扣减库存PostMapping(value /storage/decrease)ResultData decrease(RequestParam(productId) Long productId, RequestParam(count) Integer count);}AccountFeignApi
FeignClient(value seata-account-service)
public interface AccountFeignApi {//扣减账户余额PostMapping(/account/decrease)ResultData decrease(RequestParam(userId) Long userId, RequestParam(money) Long money);
}6.2、新建订单Order微服务seata-order-service2001
导入相关依赖 dependencies!-- nacos --dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency!--alibaba-seata--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactId/dependency!--openfeign--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependency!--loadbalancer--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-loadbalancer/artifactId/dependency!--cloud-api-commons--dependencygroupIdcom.zm.cloud/groupIdartifactIdcloud-api-commons/artifactIdversion1.0-SNAPSHOT/version/dependency!--web actuator--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependency!--SpringBoot集成druid连接池--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactId/dependency!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --dependencygroupIdorg.springdoc/groupIdartifactIdspringdoc-openapi-starter-webmvc-ui/artifactId/dependency!--mybatis和springboot整合--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactId/dependency!--Mysql数据库驱动8 --dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactId/dependency!--通用Mapper4--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactId/dependency!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactId/dependency!-- fastjson2 --dependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactId/dependency!--lombok--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.28/versionscopeprovided/scope/dependency!--test--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build配置application.yml
server:port: 2001spring:application:name: seata-order-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址# applicationName druid-mysql8 driverdatasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/seata_order?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrueusername: rootpassword: 123456
# mybatis
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.zm.cloud.entitiesconfiguration:map-underscore-to-camel-case: true# seata
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组由它获得TC服务的集群名称service:vgroup-mapping: # 点击源码分析default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: AT #不写也是ATlogging:level:io:seata: info对应说明 主启动类
SpringBootApplication
MapperScan(com.zm.cloud.mapper)
EnableDiscoveryClient
EnableFeignClients
public class Main2001 {public static void main(String[] args) {SpringApplication.run(Main2001.class,args);}
}我们把mybatis_generator中自动生成的订单模块dao层的实体类和mapper文件复制过来。 在实体类上打上ToString注解并实现序列化接口。
Table(name t_order)
ToString
public class Order implements Serializable创建OrderService
public interface OrderService {//创建订单void createOrder(Order order);
}创建OrderServiceImp实现类
Slf4j
Service
public class OrderServiceImp implements OrderService {Resourceprivate OrderMapper orderMapper;Resourceprivate StorageFeignApi storageFeignApi;//订单微服务通过openfeign调用库存微服务Resourceprivate AccountFeignApi accountFeignApi;//订单微服务通过openfeign调用账户微服务Overridepublic void createOrder(Order order) {//XID全局事务的检查通过seata的RootContext获取String xid RootContext.getXID();//创建订单log.info(开始新建订单\txid_order: xid\n);//创建之前有订单状态0是创建中1代表创建完成order.setStatus(0);//再开始创建订单拿到返回值进行判断,大于0代表插入一条记录成功int i orderMapper.insert(order);//插入成功之后再获得MySQL的实体对象Order orderFromDB null;if (i0) {orderFromDBorderMapper.selectOne(order);log.info(------- 新建订单成功orderFromDB info: orderFromDB\n);//新订单创建成功后开始调用storageFeignApi减少一个库存log.info(------- 订单微服务开始调用storageFeignApi减少一个库存\n);storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());log.info(------- 订单微服务开始调用storageFeignApi减少一个库存操作完成);//新订单创建成功后开始调用accountFeignApi扣用户的钱log.info(------- 订单微服务开始调用accountFeignApi扣除账户余额\n);accountFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getMoney());log.info(------- 订单微服务开始调用accountFeignApi扣除账户余额操作完成);System.out.println();//订单完成修改状态为1log.info(------- 正在修改订单状态....);orderFromDB.setStatus(1);//构建查询条件Example example new Example(Order.class);Example.Criteria criteria example.createCriteria();criteria.andEqualTo(userId,orderFromDB.getUserId());criteria.andEqualTo(status,0);//使用上面创建的查询条件orderMapper调用updateByExampleSelective来更新数据库中的订单状态。//这个方法会找到所有满足条件的订单将这些订单的status字段更新为orderFromDB中的status值int updateResult orderMapper.updateByExampleSelective(orderFromDB, example);log.info(------- 修改订单状态完成\tupdateResult);log.info(------- orderFromDB info: orderFromDB);}System.out.println();log.info(结束新建订单\txid_order: xid);}
}新建一个controller
RestController
Slf4j
public class OrderController {Resourceprivate OrderService orderService;//创建订单GetMapping(/order/create)public ResultData create(Order order){orderService.createOrder(order);return ResultData.success(order);}
}6.3、新建库存Storage微服务
新建moduleseata-storage-service2002
添加相关依赖 dependencies!-- nacos --dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency!--alibaba-seata--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactId/dependency!--openfeign--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependency!--loadbalancer--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-loadbalancer/artifactId/dependency!--cloud_commons_utils--dependencygroupIdcom.atguigu.cloud/groupIdartifactIdcloud-api-commons/artifactIdversion1.0-SNAPSHOT/version/dependency!--web actuator--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependency!--SpringBoot集成druid连接池--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactId/dependency!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --dependencygroupIdorg.springdoc/groupIdartifactIdspringdoc-openapi-starter-webmvc-ui/artifactId/dependency!--mybatis和springboot整合--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactId/dependency!--Mysql数据库驱动8 --dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactId/dependency!--通用Mapper4--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactId/dependency!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactId/dependency!-- fastjson2 --dependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactId/dependency!--lombok--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.28/versionscopeprovided/scope/dependency!--test--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build配置文件application.yml
server:port: 2002spring:application:name: seata-storage-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址# applicationName druid-mysql8 driverdatasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storage?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrueusername: rootpassword: 123456
# mybatis
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.atguigu.cloud.entitiesconfiguration:map-underscore-to-camel-case: true
# seata
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging:level:io:seata: info主启动类
MapperScan(com.zm.cloud.mapper)
SpringBootApplication
EnableFeignClients
EnableDiscoveryClient
public class Main2002 {public static void main(String[] args) {SpringApplication.run(Main2002.class,args);}
}修改mybatis_generator中的自动生成配置生成出库存的实体类和mapper文件
修改config.properties切换成库存的数据库seata_storage
# seata_storage
jdbc.driverClass com.mysql.cj.jdbc.Driver
jdbc.url jdbc:mysql://localhost:3307/seata_storage?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrue
jdbc.user root
jdbc.password 123456修改generatorConfig.xml切换t_storage
table tableNamet_storage domainObjectNameStoragegeneratedKey columnid sqlStatementJDBC/
/table修改完成之后刷新插件 将生成的实体类和mapper文件拷贝到库存微服务中实体类中打上ToString注解并实现序列化接口。
Table(name t_storage)
ToString
public class Storage implements Serializable StorageMapper中添加自己的扣减库存的方法
public interface StorageMapper extends MapperStorage {//扣减库存void decrease(Param(productId) Long productId, Param(count) Integer count);
}对应着StorageMapper.xml增加方法的实现
?xml version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.zm.cloud.mapper.StorageMapperresultMap idBaseResultMap typecom.zm.cloud.entities.Storage!--WARNING - mbg.generated--id columnid jdbcTypeBIGINT propertyid /result columnproduct_id jdbcTypeBIGINT propertyproductId /result columntotal jdbcTypeINTEGER propertytotal /result columnused jdbcTypeINTEGER propertyused /result columnresidue jdbcTypeINTEGER propertyresidue //resultMapupdate iddecreaseupdate t_storagesetused used #{count},residue residue - #{count}whereproduct_id #{productId}/update
/mapper新建StorageService
public interface StorageService {//扣库存void decrease(Long productId, Integer count);
}实现类StorageServiceImpl
Service
Slf4j
public class StorageServiceImpl implements StorageService {Resourceprivate StorageMapper storageMapper;Overridepublic void decrease(Long productId, Integer count) {log.info(-------storage-service中扣减库存开始);storageMapper.decrease(productId,count);log.info(-------storage-service中扣减库存结束);}
}库存微服务的controller
RestController
public class StorageController {Resourceprivate StorageService storageService;//扣库存RequestMapping(/storage/decrease)public ResultData decrease(Long productId, Integer count) {storageService.decrease(productId, count);return ResultData.success(扣减库存成功!);}
}6.4、新建账户Account微服务
新建模块seata-account-service2003
依赖和上面的库存微服务一样的就不重复了配置文件开始application.yml
server:port: 2003spring:application:name: seata-account-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址# applicationName druid-mysql8 driverdatasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/seata_account?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrueusername: rootpassword: 123456
# mybatis
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.zm.cloud.entitiesconfiguration:map-underscore-to-camel-case: true
# seata
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging:level:io:seata: info主启动类Main2003
MapperScan(com.zm.cloud.mapper)
SpringBootApplication
EnableDiscoveryClient
EnableFeignClients
public class Main2003 {public static void main(String[] args) {SpringApplication.run(Main2003.class,args); }
}修改mybatis_generator中的自动生成配置文件生成出账户的实体类和mapper文件
修改config.properties切换成库存的数据库seata_account
# seata_account
jdbc.driverClass com.mysql.cj.jdbc.Driver
jdbc.url jdbc:mysql://localhost:3307/seata_account?characterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrueallowPublicKeyRetrievaltrue
jdbc.user root
jdbc.password 123456修改generatorConfig.xml切换t_account
!--seata_account--
table tableNamet_account domainObjectNameAccountgeneratedKey columnid sqlStatementJDBC/
/table修改完成更新一下自动生成然后将生成的实体类和mapper文件拷贝到Account微服务中。
实体类中打上ToString注解并实现序列化接口。
Table(name t_account)
ToString
public class Account implements Serializable AccountMapper中添加扣减余额的方法
public interface AccountMapper extends MapperAccount {//扣减账户余额void decrease(Param(userId) Long userId,Param(money) Long money);
}在AccountMapper中添加实现
?xml version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.zm.cloud.mapper.AccountMapperresultMap idBaseResultMap typecom.zm.cloud.entities.Account!--WARNING - mbg.generated--id columnid jdbcTypeBIGINT propertyid /result columnuser_id jdbcTypeBIGINT propertyuserId /result columntotal jdbcTypeDECIMAL propertytotal /result columnused jdbcTypeDECIMAL propertyused /result columnresidue jdbcTypeDECIMAL propertyresidue //resultMapupdate iddecreaseupdate t_accountset residue residue - #{money},used used #{money}where user_id #{userId}/update
/mapperAccountService
public interface AccountService {//扣减余额void decrease(Param(userId) Long userId, Param(money) Long money);
}接口实现类AccountServiceImpl
Service
Slf4j
public class AccountServiceImpl implements AccountService {Resourceprivate AccountMapper accountMapper;Overridepublic void decrease(Long userId, Long money) {log.info(-------account-service中扣减账户余额开始);accountMapper.decrease(userId,money);//myTimeOut();//int age 10/0;log.info(-------account-service中扣减账户余额结束);}//我们要模拟事务处理失败要回滚的场景private static void myTimeOut(){try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }}
}AccountController RestController
public class AccountController {Resourceprivate AccountService accountService;//扣减账户余额RequestMapping(/account/decrease)ResultData decrease(RequestParam(userId) Long userId, RequestParam(money) Long money){accountService.decrease(userId,money);return ResultData.success(扣减账户余额成功);}
}6.5、测试
nacos和seata全部启动起来然后把2001、2002、2003微服务启动起来。 此时我们没有在订单模块添加GlobalTransactional注解浏览器请求一个下个订单代表1号用户花了100块钱买了10个1号产品。
localhost:2001/order/create?userld1productld1count10money100
然后你就会发现报错了~ 这个就跟我们的springbootspringcloud版本太高导致和阿里巴巴Seata不兼容有关了去改一下bootcloud的版本。
!--仅为了整合openfeign alibaba seata的案例降低版本处理下--
spring.boot.version3.1.7/spring.boot.version
spring.cloud.version2022.0.4/spring.cloud.version我们到数据库中看一下它有订单的插入但是库存和账户都没有变化。 把这条数据删除后再重启三个服务然后再次访问一次 新建成功查看后台输出情况 再看一下数据库的数据对不对帐。 OK了目前我们没有添加注解GlobalTransactional现在演示我们扣钱过程中如果出现报错我们的事务会不会进行回滚。
超时异常报错
把之前的那个seata-account-service2003微服务中service实现类里的超时方法注释打开测试一下。 重启2003微服务测试开始转圈等待65秒其实这个时候看数据库订单状态为1 后台报错超时但是看到它扣款和减少库存是成功的 到数据库中看库存和金额都是被扣了 by zero错误测试
把刚才的超时方法注掉把那个除以0的打开再来一次。 直接报错再看数据库的数据。 库存减少了钱也扣了结果是个未完成订单这事务压根不回滚。
6.6、添加GlobalTransactional注解测试
我们保留超时的方法然后在订单的service实现类中的createOrder方法上打上GlobalTransactional并为这个全局事务起一个名字zm-transactional-order
GlobalTransactional(name zm-transactional-order,rollbackFor Exception.class)
Override
public void createOrder(Order order)重启服务还是那个localhost:2001/order/create?userId1productId1count10money100
然后开seata的控制台全局事务的信息都有了全局锁会保证当前的操作只有子事务被允许。 这个时候到数据库看一下有压根未完成的订单记录库存也扣除了账户也扣除了还有undo_log表的记录。 等到我们的超时时间一到就会发现上面的所有事务全部都回滚了订单都没有了都是添加注解之前的数据。 undo_log也是空的了 查看控制台输出 undo_log这个表无论数据是否正常插入最后都会自动删除它就起到一个溯源的作用它记录上一步的内容有错误的情况下根据这个内容进行回滚然后自动删除。
既然错误的已经演示了那么再来一个正常情况下的。 查看后台输出 6.7、AT模式如何做到对业务的无侵入
seata的整体机制
两阶段提交协议的演变 一阶段业务数据和回滚日志记录在同一个本地事务中提交释放本地锁和连接资源。 在一阶段Seata会拦截“业务SQL” 解析SQL语义找到“业务SQL”要更新的业务数据在业务数据被更新之前将其保存为“before image” 执行“业务SQL”更新业务数据在业务数据更新之后将其保存为“after image”最后生成行锁 以上操作全部在一个数据库事务内完成这样保证了一阶段的原子性。 二阶段 提交异步化非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
二阶段分为2种情况
正常提交
二阶段如是顺利提交的话因为“业务 SQL”在一阶段已经提交至数据库所以Seata框架只需将一阶段保存的快照数据和行锁删掉完成数据清理即可。 出现异常需要回滚
二阶段如果是回滚的话Seata 就需要回滚一阶段已经执行的“业务 SQL”还原业务数据。
回滚方式便是用“before image”还原业务数据但在还原前要首先要校验脏写对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写可以还原业务数据如果不一致就说明有脏写出现脏写就需要转人工处理。