文章目录
是一个分布式事务的解决方案,
分布式事务中的一些概念,也是seata中的概念:
seata安装:
-
下载安装seata的安装包
进入 Seata 官网下载地址或者 Seata Github下载地址,选择 Seata 1.3.0 版本下载。
期间遇到了很多bug:参考这篇博客配置https://blog.csdn.net/fsdad/article/details/108711252
解压后
为了解决bin目录下闪退问题,需要在bin同级目录下建logs文件夹,在里面建seata_gc.log
如下双击闪退
*注:seata官方从1.0
版本后不再提供sql脚本,以及nacos推送配置脚本,需要从0.9.0
的版本复制*
将红色框框的四个文件复制到1.3.0/conf
目录下
注意lib目录下需要复制粘贴自己的mysql驱动的对应版本,否则报错
2. 修改1.3.0版本的file.conf
3 . mysql建库建表
1,上面指定了数据库为seata,所以创建一个数据库名为seata
2,建表,在seata的安装目录下有一个**db_store.sql,**刚才从0.9版本复制过来,运行即可
4. 继续修改配置文件,修改registry.conf
修改type
为nacos
,此处是声明你使用的注册中心
application
为seata启动后注册到nacos的服务名(愿意改就改,不愿意改就默认就行)
*注:group默认为:SEATA_GROUP
,可以更改DEFAULT_GROUP
*,或者使用默认分组也可以
5. 修改从0.9.0
版本拉过来的config.txt
文件,注意这个需要和nacos-config.sh
上下级目录
//config.txt文件
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
store.mode=db /*此处修改为db*/
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver /*自定义修改*/
store.db.url=jdbc:mysql://192.168.204.201:3306/seata?useUnicode=true /*自定义修改*/
store.db.user=root /*自定义修改*/
store.db.password=root /*自定义修改*/
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
6. 启动
先启动nacos
在启动seata-server(运行安装目录下的,seata-server.bat)
在创建微服务前,这里需要 将参数配置到Nacos配置中心
修改完毕后执行1.3.0/conf
下的nacos-config.sh
命令为:sh nacos-config.sh 127.0.0.1
为nacos地址,按实际情况修改即可
执行完毕后打开nacos 配置中心 会看到
业务说明
下单—>库存—>账号余额
- 创建三个数据库
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
2. 创建对应的表
/*分别对应创建表*/
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;
/*--------------------------------------------------------------------*/
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');
/*--------------------------------------------------------------------*/
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')
- 创建回滚日志表,方便查看
注意每个库都要执行一次这个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;
- 每个业务都创建一个微服务,也就是要有三个微服务,订单,库存,账号
订单,seta-order-2001
- 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>1.2.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>8.0.18</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>
-
配置文件
server: port: 2001 spring: application: name: seata-order-service cloud: nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata?useUnicode=true&charcaterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: jh7851192 type: com.alibaba.druid.pool.DruidDataSource druid: # 数据源其他配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall #,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: mapper-locations: classpath:mapper/*.xml seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group enable-auto-data-source-proxy: true service: vgroup-mapping: my_test_tx_group: default grouplist: default: localhost:8091 enable-degrade: false disable-global-transaction: false config: type: nacos nacos: namespace: serverAddr: localhost:8848 group: SEATA_GROUP userName: "" password: "" registry: type: nacos nacos: application: seata-server # 此处名称徐鹤 seata-server application一致 server-addr: localhost:8848 namespace: userName: "" password: ""
还要额外创建其他配置文件,创建一个file.conf:
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup # 事务组名称 vgroup_mapping.fsp_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db #mode = "file" mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "root" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }
创建registry.conf:
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { #serverAddr = "localhost" serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
实际上,就是要将seata中的我们之前修改的两个配置文件复制到这个项目下
-
主启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class,args);
}
}
-
service层
public interface OrderService { /** * 创建订单 * @param order */ void create(Order order); }
@FeignClient(value = "seata-storage-service") public interface StorageService { /** * 减库存 * @param productId * @param count * @return */ @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
@FeignClient(value = "seata-account-service") public interface AccountService { /** * 减余额 * @param userId * @param money * @return */ @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private AccountService accountService; @Resource private StorageService storageService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说: * 下订单->减库存->减余额->改状态 * GlobalTransactional seata开启分布式事务,异常时回滚,name保证唯一即可 * @param order 订单对象 */ @Override ///@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) public void create(Order order) { // 1 新建订单 log.info("----->开始新建订单"); orderDao.create(order); // 2 扣减库存 log.info("----->订单微服务开始调用库存,做扣减Count"); storageService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,做扣减End"); // 3 扣减账户 log.info("----->订单微服务开始调用账户,做扣减Money"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,做扣减End"); // 4 修改订单状态,从0到1,1代表已完成 log.info("----->修改订单状态开始"); orderDao.update(order.getUserId(), 0); log.info("----->下订单结束了,O(∩_∩)O哈哈~"); } }
-
dao层,也就是接口
@Mapper public interface OrderDao { /** * 1 新建订单 * @param order * @return */ int create(Order order); /** * 2 修改订单状态,从0改为1 * @param userId * @param status * @return */ int update(@Param("userId") Long userId, @Param("status") Integer status); }
在resource下创建mapper文件夹,编写mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.eiletxie.springcloud.alibaba.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.eiletxie.springcloud.alibaba.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"></id> <result column="user_id" property="userId" jdbcType="BIGINT"></result> <result column="product_id" property="productId" jdbcType="BIGINT"></result> <result column="count" property="count" jdbcType="INTEGER"></result> <result column="money" property="money" jdbcType="DECIMAL"></result> <result column="status" property="status" jdbcType="INTEGER"></result> </resultMap> <insert id="create" parameterType="com.eiletxie.springcloud.alibaba.domain.Order" useGeneratedKeys="true" keyProperty="id"> insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0); </insert> <update id="update"> update t_order set status =1 where user_id =#{userId} and status=#{status}; </update> </mapper>
-
controller层
@RestController public class OrderController { @Resource private OrderService orderService; /** * 创建订单 * * @param order * @return */ @GetMapping("/order/create") public CommonResult create(Order order) { orderService.create(order); return new CommonResult(200, "订单创建成功"); } }
-
entity类(也叫domain类)
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code, message, null); } }
7 . config配置类
@Configuration
@MapperScan({"com.eiletxie.springcloud.alibaba.dao"}) 指定我们的接口的位置
public class MyBatisConfig {
}
/**
* @Author EiletXie
* @Since 2020/3/18 21:51
* 使用Seata对数据源进行代理
*/
@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 druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources(mapperLocations));
return bean.getObject();
}
}
==库存==,seta-storage-2002
看脑图
- pom
- 配置文件
- 主启动类
- service层
- dao层
- controller层
==账号==,seta-account-2003
看脑图
- pom
- 配置文件
- 主启动类
- service层
- dao层
- controller层
5 . 全局创建完成后,首先测试不加seata
6. 使用seata:
在订单模块的serviceImpl类中的create方法添加启动分布式事务的注解
/**
这里添加开启分布式事务的注解,name指定当前全局事务的名称
rollbackFor表示,发生什么异常需要回滚
noRollbackFor:表示,发生什么异常不需要回滚
*/
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
///@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
// 1 新建订单
log.info("----->开始新建订单");
orderDao.create(order);
// 2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减End");
// 3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减End");
// 4 修改订单状态,从0到1,1代表已完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
-
此时在测试
发现,发生异常后,直接回滚了,前面的修改操作都回滚了