赞
踩
这篇文章请结合往期文章分布式事务解决方案Seata入门介绍、分布式事务解决方案Seata搭建
本篇文章实际操作Seata解决分布式事务问题,这里会创建3个服务,一个订单服务,一个库存服务,一个账户服务
业务流程如下:当用户下单是,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过远程调用服务来扣减用户账户里的余额,最后在订单服务中修改订单状态为已完成。该操作跨越3个数据库,两次远程调用,很明显存在分布式事务问题!
下订单->减库存->扣余额->改(订单)状态
1.建库
CREATE DATABASE seata_order;//订单库
CREATE DATABASE seata_storage;//库存库
CREATE DATABASE seata_account;//账户库
2.在订单库seata_order建订单表t_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:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
3.在库存库seata_storage建库存表t_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 '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
4.在账户库seata_account建账户表t_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 '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000')
SELECT * FROM t_account;
5.订单-库存-账户3个库下都需要建各自的回滚日志表
seata-server-0.9.0\seata\conf\db_undo_log.sql
DROP TABLE `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,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
建设完成后的效果
这里seata库是在文章开头那篇文章中添加的
新建订单Order-Module
1.新建seata-order-service2001
2.添加pom
<dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <artifactId>seata-all</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--web-actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--mysql-druid--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
这里需要注意一下seata版本,如果版本不一致需要手动剔除从新引入
3.添加yml
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: #自定义事务组名称需要与seata-server中的对应,也就是file.conf文件中service中vgroup_mapping.fsp_tx_group值 tx-service-group: fsp_tx_group nacos: discovery: server-addr: 192.168.0.177:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.0.177:3307/seata_order username: root password: root feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
4.复制file.conf、registry.conf文件
直接从seata-server-0.9.0\seata\conf目录下将file.conf、registry.conf文件拷贝到resources目录下
5.数据源代理
import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.transaction.SpringManagedTransactionFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; @Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
或者seata的github配置
然后就是实际业务了,然后根据上面的步骤搭建出订单服和账户服,完整的测试demogithub地址
项目搭建的步骤跳过,若有需要自行到我的github地址去克隆!
我的demo项目中加了锁刚开始启动的时候可以先注释掉!下面会解释道为什么要加锁!,我的demo中已经加好了全局事务,先注释掉全局事务!看看没有加全局事务的效果!
然后还一个就是2003服务中的一个手动报错,也注释!
将代码更改为上图!然后我们启动Nacos和Seata,Nacos搭建和Seata搭建请看我的往期文章,需要的自己去找吧!访问测试
http://localhost:2001/order/create?userId=1&productId=1&count=1&money=1
请求通了,没问题!
手动报错一下!
重启服务!最好是全部重启3台服务!数据库回归!再次请求
手动报错请求情况
服务调用者
服务提供者
数据库情况
这里在账户服务中手动报错了,那么这里订单确实创建了,库存也扣了,但是在扣款的时候就失败了,然后导致数据库没有扣款,账户微服务报错,导致2001服务调用者卡在扣款这步,从而也导致了订单状态卡死,无法更改,那么这就是一个经典的分布式事务问题!那么我们前面准备了这么多Seata的准备,现在我们就来开启Seata这个全局事务!
将2001微服务Seata全局事务管理注解放开!如上图!重启服务器!数据看回归!
开启Seata全局事务遇到异常事务情况
微服务调用者
微服务提供者
数据库情况
扣款服务我们手动报错了,整个调用链报错了,但是数据都是正常的,那么这里Seata对全局事务就起作用了!
注意:到这里并没有结束!别忘了demo中有加锁的情况!下面就开始演示为什么这里要加锁!将2003也就是账户服务手动报错注释掉
然后将openfeign超时时间恢复到默认1S,不要管为什么,照做就是!
数据库回归,重启服务!上Jmeter并发测试一下!
关于Jmeter安装使用,我的往期往期文章中也有讲解!若有需要自行查找!Jmeter发送请求,这里我们同时模拟100并发请求看看Seata事务情况!
Seata并发事务情况
微服务调用者
微服务调用者跑着跑着就报下面的错了
库存服务,这里貌似都没有处理完成!,有点奇怪,文章下面会解释
账户服里面是干干净净的,也有点小奇怪,文章下面会解释
数据库情况
看看Jmeter情况
全部报错!这里貌似是feign调用超时报错了!
那么我们将feign调用超时时间设长一点!
重启服务!Jmeter请求结果清空!再次尝试!
微服务调用者
io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout at io.seata.rm.datasource.exec.LockRetryController.sleep(LockRetryController.java:50) ~[seata-all-0.9.0.jar:0.9.0]
库存服
也是一样的异常信息,这个异常就是获取锁的问题,也就是多个业务请求相互争抢全局锁和本地锁引起的超时情况!在往期文章中AT模式中有介绍当前这种情况!
openfeign超时
这种情况是建立在开启seata全局事务,服务器刚启动,没有一个请求访问,注意没有一个请求访问,直接并发请求,那么就会报错openfeign的超时异常!
Global lock wait timeout
这种情况是也是建立在开启seata全局事务系统启动后,先发起一个请求,将系统跑通后,然后再次并发请求进去,那么就会出现全局锁等待超时问题,这里并发其实并不高,Jmeter同时发送两个请求就会出现,AT模式中有介绍当前这种异常的原因
这里只是处理这个全局锁等待超时,并不是决绝,因为我目前并没有找到完美的解决方案!在上面关联之前的一篇文章中有解释道参数的原因就是多个业务在阶段一和阶段二争抢全局锁和本地锁的时候的问题,那么我这里的处理方案就是限制并发,做个锁!
其他地方不变!数据库回归,开启Jmeter测试一下!直接发送10个并发请求进去!
10个并发请求测试
微服务调用者
库存服务
账户服
数据库情况
那么我们把并发搞大点,数据库回归一下,服务器不动
50、60、70并发这里设置这些是由于我们的环境不一样,可能你们的数据库在本地,或者电脑配置不一样的,这些情况可能就要去更改并发数。
微服务调用者
库存服
账号服
数据看情况
事务还是控制住了,那么这里微服务调用者报的异常execute executeAutoCommitTrue error:io.seata.core.exception.RmTransactionException: Response[ TransactionException[Could not register branch into global session xid = 192.168.184.1:8091:2064089468 status = TimeoutRollbacki ]
这个异常我尝试了不同的并发数量,发现并发执行到1分钟的时候开始出现这个异常,所以上面在写了50、60、70并发数,就是为了体现出这个异常,然后再seata库中的global_table
表中的timeout
时间刚好是1分钟,seata默认的超时时间就是1分钟
然后顺着这个思路设置了一下超时时长timeoutMills = 300000
这里网上还有另一种解决方案,我里用的seata版本为0.9.0,可以将seata版本切换为1.2试试!
再次发送同样并发量的请求或者直接将并发加到100,数据库回归!
并发请求过程中seata库中的timeout字段时间为5分钟!
等待100并发业务处理完成!
100并发请求全部成功!
服务器也正常!
注意,我这种只是处理方法,并不是完美的解决方案!更加完美的解决方案还在寻找中!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。