乾县做网站,手机免费制作微信小程序,青岛seo外包公司,wordpress生活类主题在本篇文章中我们在SpringCloud环境下通过使用Seata来模拟用户购买商品时由于用户余额不足导致本次订单提交失败#xff0c;来验证下在MySQL数据库内事务是否会回滚。
本章文章只涉及所需要测试的服务列表以及Seata配置部分。
用户提交订单购买商品大致分为以下几个步骤来验证下在MySQL数据库内事务是否会回滚。
本章文章只涉及所需要测试的服务列表以及Seata配置部分。
用户提交订单购买商品大致分为以下几个步骤
减少库存扣除金额提交订单
1. 准备环境 Seata Server 如果对Seata Server部署方式还不了解请访问http://blog.yuqiyu.com/seata-init-env.html Eureka Server 服务注册中心如果对Eureka Server部署方式还不了解请访问http://blog.yuqiyu.com/eureka-server.html
2. 准备测试服务
为了方便学习的同学查看源码我们本章节源码采用Maven Module多模块的方式进行构建。
我们用于测试的服务所使用的第三方依赖都一致各个服务的pom.xml文件内容如下所示 dependencies!--Web--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--openfeign接口定义--dependencygroupIdorg.minbox.chapter/groupIdartifactIdopenfeign-service/artifactIdversion0.0.1-SNAPSHOT/version/dependency!--公共依赖--dependencygroupIdorg.minbox.chapter/groupIdartifactIdcommon-service/artifactIdversion0.0.1-SNAPSHOT/version/dependency!--seata--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactId/dependencydependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependency!--Eureka Client--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId/dependencydependencygroupIdcom.zaxxer/groupIdartifactIdHikariCP/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependencydependencygroupIdorg.minbox.framework/groupIdartifactIdapi-boot-starter-mybatis-enhance/artifactId/dependency
/dependencies2.1 Openfeign接口定义模块
由于我们服务之间采用的Openfeign方式进行相互调用所以创建了一个模块openfeign-service来提供服务接口的定义。
账户服务提供的接口定义
账户服务对外所提供的Openfeign接口定义如下所示 /*** 账户服务接口** author 恒宇少年*/
FeignClient(name account-service)
RequestMapping(value /account)
public interface AccountClient {/*** 扣除指定账户金额** param accountId 账户编号* param money 金额*/PostMappingvoid deduction(RequestParam(accountId) Integer accountId, RequestParam(money) Double money);
}商品服务提供的接口定义 商品服务对外所提供的Openfeign接口定义如下所示 /*** 商品服务接口定义** author 恒宇少年*/FeignClient(name good-service)RequestMapping(value /good)public interface GoodClient {/*** 查询商品基本信息** param goodId {link Good#getId()}* return {link Good}*/GetMappingGood findById(RequestParam(goodId) Integer goodId);/*** 减少商品的库存** param goodId {link Good#getId()}* param stock 减少库存的数量*/PostMappingvoid reduceStock(RequestParam(goodId) Integer goodId, RequestParam(stock) int stock);}2.2 公共模块
公共模块common-service内所提供的类是共用的各个服务都可以调用其中最为重要的是将Seata所提供的数据源代理DataSourceProxy实例化配置放到了这个模块中数据库代理相关配置代码如下所示 /*** Seata所需数据库代理配置类** author 恒宇少年*/
Configuration
public class DataSourceProxyAutoConfiguration {/*** 数据源属性配置* {link DataSourceProperties}*/private DataSourceProperties dataSourceProperties;public DataSourceProxyAutoConfiguration(DataSourceProperties dataSourceProperties) {this.dataSourceProperties dataSourceProperties;}/*** 配置数据源代理用于事务回滚** return The default datasource* see DataSourceProxy*/PrimaryBean(dataSource)public DataSource dataSource() {HikariDataSource dataSource new HikariDataSource();dataSource.setJdbcUrl(dataSourceProperties.getUrl());dataSource.setUsername(dataSourceProperties.getUsername());dataSource.setPassword(dataSourceProperties.getPassword());dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());return new DataSourceProxy(dataSource);}
}该配置类在所需要的服务中使用Import注解进行导入使用。
2.3 账户服务 服务接口实现 账户服务用于提供接口的服务实现通过实现openfeign-service内提供的AccountClient服务定义接口来对应提供服务实现实现接口如下所示 /*** 账户接口实现** author 恒宇少年*/RestControllerpublic class AccountController implements AccountClient {/*** 账户业务逻辑*/Autowiredprivate AccountService accountService;Overridepublic void deduction(Integer accountId, Double money) {accountService.deduction(accountId, money);}}服务配置application.yml# 服务名spring:application:name: account-service# seata分组cloud:alibaba:seata:tx-service-group: minbox-seata# 数据源datasource:url: jdbc:mysql://localhost:3306/testusername: rootpassword: 123456type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driver# eurekaeureka:client:service-url:defaultZone: http://service:nodev210.180.98.83:10001/eureka/通过spring.cloud.alibaba.seata.tx-service-group我们可以指定服务所属事务的分组该配置非必填默认为spring.application.name配置的内容加上字符串-fescar-service-group如account-service-fescar-service-group详见com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration配置类源码。 在我本地测试环境的Eureka Server在10.180.98.83服务器上这里需要修改成你们自己的地址数据库连接信息也需要修改成你们自己的配置。 导入Seata数据源代理配置/*** author 恒宇少年*/SpringBootApplicationImport(DataSourceProxyAutoConfiguration.class)public class AccountServiceApplication {/*** logger instance*/static Logger logger LoggerFactory.getLogger(AccountServiceApplication.class);public static void main(String[] args) {SpringApplication.run(AccountServiceApplication.class, args);logger.info(账户服务启动成功.);}}通过Import导入我们common-service内提供的Seata数据源代理配置类DataSourceProxyAutoConfiguration。
2.4 商品服务 服务接口实现 商品服务提供商品的查询以及库存扣减接口服务实现openfeign-service提供的GoodClient服务接口定义如下所示 /*** 商品接口定义实现** author 恒宇少年*/RestControllerpublic class GoodController implements GoodClient {/*** 商品业务逻辑*/Autowiredprivate GoodService goodService;/*** 查询商品信息** param goodId {link Good#getId()}* return*/Overridepublic Good findById(Integer goodId) {return goodService.findById(goodId);}/*** 扣减商品库存** param goodId {link Good#getId()}* param stock 减少库存的数量*/Overridepublic void reduceStock(Integer goodId, int stock) {goodService.reduceStock(goodId, stock);}}服务配置application.ymlspring:application:name: good-servicecloud:alibaba:seata:tx-service-group: minbox-seatadatasource:url: jdbc:mysql://localhost:3306/testusername: rootpassword: 123456type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Drivereureka:client:service-url:defaultZone: http://service:nodev210.180.98.83:10001/eureka/server:port: 8081导入Seata数据源代理配置/*** author 恒宇少年*/SpringBootApplicationImport(DataSourceProxyAutoConfiguration.class)public class GoodServiceApplication {/*** logger instance*/static Logger logger LoggerFactory.getLogger(GoodServiceApplication.class);public static void main(String[] args) {SpringApplication.run(GoodServiceApplication.class, args);logger.info(商品服务启动成功.);}}2.5 订单服务 服务接口 订单服务提供了下单的接口通过调用该接口完成下单功能下单接口会通过Openfeign调用account-service、good-service所提供的服务接口来完成数据验证如下所示 /*** author 恒宇少年*/RestControllerRequestMapping(value /order)public class OrderController {/*** 账户服务接口*/Autowiredprivate AccountClient accountClient;/*** 商品服务接口*/Autowiredprivate GoodClient goodClient;/*** 订单业务逻辑*/Autowiredprivate OrderService orderService;/*** 通过{link GoodClient#reduceStock(Integer, int)}方法减少商品的库存判断库存剩余数量* 通过{link AccountClient#deduction(Integer, Double)}方法扣除商品所需要的金额金额不足由account-service抛出异常** param goodId {link Good#getId()}* param accountId {link Account#getId()}* param buyCount 购买数量* return*/PostMappingGlobalTransactionalpublic String submitOrder(RequestParam(goodId) Integer goodId,RequestParam(accountId) Integer accountId,RequestParam(buyCount) int buyCount) {Good good goodClient.findById(goodId);Double orderPrice buyCount * good.getPrice();goodClient.reduceStock(goodId, buyCount);accountClient.deduction(accountId, orderPrice);Order order toOrder(goodId, accountId, orderPrice);orderService.addOrder(order);return 下单成功.;}private Order toOrder(Integer goodId, Integer accountId, Double orderPrice) {Order order new Order();order.setGoodId(goodId);order.setAccountId(accountId);order.setPrice(orderPrice);return order;}}服务配置application.ymlspring:application:name: order-servicecloud:alibaba:seata:tx-service-group: minbox-seatadatasource:url: jdbc:mysql://localhost:3306/testusername: rootpassword: 123456type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Drivereureka:client:service-url:defaultZone: http://service:nodev210.180.98.83:10001/eureka/server:port: 8082启用Openfeign 导入Seata数据源代理配置/*** author 恒宇少年*/SpringBootApplicationEnableFeignClients(basePackages org.minbox.chapter.seata.openfeign)Import(DataSourceProxyAutoConfiguration.class)public class OrderServiceApplication {/*** logger instance*/static Logger logger LoggerFactory.getLogger(OrderServiceApplication.class);public static void main(String[] args) {SpringApplication.run(OrderServiceApplication.class, args);logger.info(订单服务启动成功.);}}我们仅在order-service调用了其他服务的Openfeign接口所以我们只需要在order-service内通过EnableFeignClients注解启用Openfeign接口实现代理。
3. 服务连接Seata Server
服务想要连接到Seata Server需要添加两个配置文件分别是registry.conf、file.conf。 registry.conf 注册到Seata Server的配置文件里面包含了注册方式、配置文件读取方式内容如下所示 registry {# file、nacos、eureka、redis、zk、consultype filefile {name file.conf}}config {type filefile {name file.conf}}file.conf 该配置文件内包含了使用file方式连接到Eureka Server的配置信息以及存储分布式事务信息的方式如下所示 transport {# tcp udt unix-domain-sockettype TCP#NIO NATIVEserver NIO#enable heartbeatheartbeat true#thread factory for nettythread-factory {boss-thread-prefix NettyBossworker-thread-prefix NettyServerNIOWorkerserver-executor-thread-prefix NettyServerBizHandlershare-boss-worker falseclient-selector-thread-prefix NettyClientSelectorclient-selector-thread-size 1client-worker-thread-prefix NettyClientWorkerThread# netty boss thread size,will not be used for UDTboss-thread-size 1#auto default pin or 8worker-thread-size 8}}## transaction log storestore {## store mode: file、dbmode file## file storefile {dir sessionStore# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size 16384# globe session size , if exceeded throws exceptionsmax-global-session-size 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size 16384# when recover batch read sizesession.reload.read_size 100# async, syncflush-disk-mode async}## database storedb {datasource druiddb-type mysqldriver-class-name com.mysql.jdbc.Driverurl jdbc:mysql://10.180.98.83:3306/iot-transactionaluser devpassword dev2019.}}service {vgroup_mapping.minbox-seata defaultdefault.grouplist 10.180.98.83:8091enableDegrade falsedisable false}client {async.commit.buffer.limit 10000lock {retry.internal 10retry.times 30}}配置文件内service部分需要注意我们在application.yml配置文件内配置了事务分组为minbox-seata在这里需要进行对应配置vgroup_mapping.minbox-seata default,通过default.grouplist 10.180.98.83:8091配置Seata Server的服务列表。 将上面两个配置文件在各个服务resources目录下创建。 4. 编写下单逻辑
在前面说了那么多只是做了准备工作我们要为每个参与下单的服务添加对应的业务逻辑。 账户服务 在account-service内添加账户余额扣除业务逻辑类AccountService如下所示 /*** 账户业务逻辑处理** author 恒宇少年*/ServiceTransactional(rollbackFor Exception.class)public class AccountService {Autowiredprivate EnhanceMapperAccount, Integer mapper;/*** {link EnhanceMapper} 具体使用查看ApiBoot官网文档http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html** param accountId {link Account#getId()}* param money 扣除的金额*/public void deduction(Integer accountId, Double money) {Account account mapper.selectOne(accountId);if (ObjectUtils.isEmpty(account)) {throw new RuntimeException(账户 accountId 不存在.);}if (account.getMoney() - money 0) {throw new RuntimeException(账户 accountId 余额不足.);}account.setMoney(account.getMoney().doubleValue() - money);mapper.update(account);}}商品服务 在good-service内添加查询商品、扣减商品库存的逻辑类GoodService如下所示 /*** 商品业务逻辑实现** author 恒宇少年*/ServiceTransactional(rollbackFor Exception.class)public class GoodService {Autowiredprivate EnhanceMapperGood, Integer mapper;/*** 查询商品详情** param goodId {link Good#getId()}* return {link Good}*/public Good findById(Integer goodId) {return mapper.selectOne(goodId);}/*** {link EnhanceMapper} 具体使用查看ApiBoot官网文档http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html* 扣除商品库存** param goodId {link Good#getId()}* param stock 扣除的库存数量*/public void reduceStock(Integer goodId, int stock) {Good good mapper.selectOne(goodId);if (ObjectUtils.isEmpty(good)) {throw new RuntimeException(商品 goodId ,不存在.);}if (good.getStock() - stock 0) {throw new RuntimeException(商品 goodId 库存不足.);}good.setStock(good.getStock() - stock);mapper.update(good);}}5. 提交订单测试
我们在执行测试之前在数据库内的seata_account、seata_good表内对应添加两条测试数据如下所示 -- seata_good
INSERT INTO seata_good VALUES (1,华为Meta 30,10,5000.00); -- seata_account
INSERT INTO seata_account VALUES (1,10000.00,2019-10-11 02:37:35,NULL);增加SEATA恢复表
DROP SCHEMA IF EXISTS zeroa;CREATE SCHEMA zeroa;USE zeroa;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,ext varchar(100) DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY ux_undo_log (xid,branch_id)) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;DROP SCHEMA IF EXISTS zerob;CREATE SCHEMA zerob;USE zerob;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,ext varchar(100) DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY ux_undo_log (xid,branch_id)) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;
5.1 启动服务
将我们本章所使用good-server、order-service、account-service三个服务启动。
5.2 测试点正常购买
我们添加的账户余额测试数据够我们购买两件商品我们先来购买一件商品验证下接口访问是否成功通过如下命令访问下单接口 ~ curl -X POST http://localhost:8082/order\?goodId\1\accountId\1\buyCount\1
下单成功.通过我们访问/order下单接口根据响应的内容我们确定商品已经购买成功。
通过查看order-service控制台内容 2019-10-11 16:52:15.477 INFO 13142 --- [nio-8082-exec-4] i.seata.tm.api.DefaultGlobalTransaction : [10.180.98.83:8091:2024417333] commit status:Committed
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid10.180.98.83:8091:2024417333,branchId2024417341,branchTypeAT,resourceIdjdbc:mysql://localhost:3306/test,applicationDatanull
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler : Branch committing: 10.180.98.83:8091:2024417333 2024417341 jdbc:mysql://localhost:3306/test null
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed我们可以看到本次事务已经成功Committed。
再去验证下数据库内的账户余额、商品库存是否有所扣减。
5.3 测试点库存不足
测试商品添加了10个库存在之前测试已经销售掉了一件商品我们测试购买数量超过库存数量时是否有回滚日志执行如下命令 ~ curl -X POST http://localhost:8082/order\?goodId\1\accountId\1\buyCount\10
{timestamp:2019-10-11T08:57:13.7750000,status:500,error:Internal Server Error,message:status 500 reading GoodClient#reduceStock(Integer,int),path:/order}在我们good-service服务控制台已经打印了商品库存不足的异常信息 java.lang.RuntimeException: 商品1库存不足.at org.minbox.chapter.seata.service.GoodService.reduceStock(GoodService.java:42) ~[classes/:na]....我们再看order-service的控制台打印日志 Begin new global transaction [10.180.98.83:8091:2024417350]
2019-10-11 16:57:13.771 INFO 13142 --- [nio-8082-exec-5] i.seata.tm.api.DefaultGlobalTransaction : [10.180.98.83:8091:2024417350] rollback status:Rollbacked通过日志可以查看本次事务进行了回滚。
由于库存的验证在账户余额扣减之前所以我们本次并不能从数据库的数据来判断事务是真的回滚。
5.4 测试点余额不足
既然商品库存不足我们不能直接验证数据库事务回滚我们从账户余额不足来下手在之前成功购买了一件商品账户的余额还够购买一件商品商品库存目前是9件我们本次测试购买5件商品这样就会出现购买商品库存充足而余额不足的应用场景执行如下命令发起请求 ~ curl -X POST http://localhost:8082/order\?goodId\1\accountId\1\buyCount\5
{timestamp:2019-10-11T09:03:00.7940000,status:500,error:Internal Server Error,message:status 500 reading AccountClient#deduction(Integer,Double),path:/order}我们通过查看account-service控制台日志可以看到 java.lang.RuntimeException: 账户1余额不足.at org.minbox.chapter.seata.service.AccountService.deduction(AccountService.java:33) ~[classes/:na]已经抛出了余额不足的异常。
通过查看good-service、order-serivce控制台日志可以看到事务进行了回滚操作。
接下来查看seata_account表数据我们发现账户余额没有改变账户服务的事务回滚验证成功。
查看seata_good表数据我们发现商品的库存也没有改变商品服务的事务回滚验证成功。
6. 总结
本章主要来验证分布式事务框架Seata在MySQL下提交与回滚有效性是否能够完成我们预期的效果Seata作为SpringCloud Alibaba的核心框架更新频率比较高快速的解决使用过程中遇到的问题是一个潜力股不错的选择。
由于本章设计的代码比较多请结合源码进行学习。
7. 本章源码
请访问a hrefhttps://gitee.com/hengboy/spring-cloud-chapter target_blankhttps://gitee.com/hengboy/spring-cloud-chapter/a查看本章源码建议使用git clone https://gitee.com/hengboy/spring-cloud-chapter.git将源码下载到本地。
Giteehttps://gitee.com/hengboy/spring-boot-chapter坑点一 如果你的项目采用是spring-cloud-alibaba-seata 0.9.0版本或以下的话它集成了fescar-spring的0.4.2版本如果你的seata-server服务端是采用0.5.0以上建议还是降低版本采用0.4.2版本。因为0.4.2版本解压是fescar-server名字意不意外。这就是坑。而且项目引入seata依赖会与旧版本的fescar依赖冲突。 dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-alibaba-seata/artifactId version0.9.0.RELEASE/version /dependency 如果你的项目采用是spring-cloud-alibaba-seata 0.9.1(这个的seata为0.5.2)版本以上的话那恭喜你。你可以使用seata-server的0.5.2以上的版本了。只需要在依赖这样引入 dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-alibaba-seata/artifactId version0.9.1.BUILD-SNAPSHOT/version exclusions exclusion groupIdio.seata/groupId artifactIdseata-spring/artifactId /exclusion /exclusions /dependency dependency groupIdio.seata/groupId artifactIdseata-all/artifactId versionseata-server对应的版本/version /dependency 坑点二 需要在每个服务中的resources文件中添加两个文件file.conf和registry.conf 具体以seata-server中的file.conf和registry.conf为准。
坑点三 如果你的项目采用是spring-cloud-alibaba-seata 0.9.0版本或以下的话因为它集成了fescar-spring的0.4.2版本如果你是使用nacos来配置参数的话建议使用seata-server 0.4.2不然引入seata0.5.0以上的版本依赖会混淆容易报以下错误
org.springframework.beans.factory.BeanCreationException: Error creating bean with name globalTransactionScanner defined in class path resource [org/springframework/cloud/alibaba/seata/GlobalTransactionAutoConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ExceptionInInitializerError Caused by: java.lang.ExceptionInInitializerError: null Caused by: java.lang.NullPointerException: Name is null at java.lang.Enum.valueOf(Enum.java:236) ~[na:1.8.0_201] at com.alibaba.fescar.core.rpc.netty.TransportProtocolType.valueOf(TransportProtocolType.java:25) ~[fescar-core-0.4.2.jar:na] at com.alibaba.fescar.core.rpc.netty.NettyBaseConfig.clinit(NettyBaseConfig.java:114) ~[fescar-core-0.4.2.jar:na] ... 23 common frames omitted 坑点四 数据源需要引入druid依赖
/** * author lgt */ Configuration public class DatabaseConfiguration { Bean ConfigurationProperties(prefix spring.datasource) public DruidDataSource druidDataSource() { return new DruidDataSource(); } /** * 需要将 DataSourceProxy 设置为主数据源否则事务无法回滚 * * param druidDataSource The DruidDataSource * return The default datasource */ Primary Bean(dataSource) public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); }
} ps:上面是 seata 数据源的配置数据源采用 druid 的DruidDataSource但实际 jdbcTemplate 执行时并不是用该数据源而用的是 seata 对DruidDataSource的代理DataSourceProxy所以与 RM 相关的代码逻辑基本上都是从DataSourceProxy这个代理数据源开始的。
坑点五 0.6.1及之前版本的启动命令是sh seata-server.sh 8091 file 127.0.0.1 0.7.0 及之后版本的启动命令是sh seata-server.sh -p 8091 -h 127.0.0.1 -m file 0.5.2及之前的版本的数据库和之后版本数据库是不一样详细以github的文件一致 ———————————————— 版权声明本文为CSDN博主「sbit_」的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明。 原文链接https://blog.csdn.net/sbit_/article/details/96112393
作者恒宇少年 链接https://www.jianshu.com/p/0a92b7c97c65 来源简书 著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。