一、所需环境
win10、idea、maven3.6.0、jdk1.8、nacos-server-1.4.4、seata-server-1.5.1、mysql5.7.26
二、为什么需要Seata?
-
Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务;
-
在我们没有使用分布式服务的时候,我们程序与数据库大多是1:1,或者1:N的,也就是说,一个程序对应一个数据库或一个程序对应一个数据库集群,通常这个时候,我们需要对多张表进行修改操作时,我们都会为其增添事务,以达到要么都成功,要么都失败的目的;
-
而当我们引用分布式服务的时候,我们程序与数据库的关系将变得复杂,每一个服务都将会拥有属于自己的数据库,这时候就形成了N : N的关系,也就是多个微服务,对应多个数据库;
-
在这样的情况下,当我们的逻辑业务需要调用到其它微服务的时候,如何保证这一业务逻辑的事务就变成了一个问题,例如;
@Override public Integer createOrder(Order order) { // 这里使用了OpenFeign调用了storage微服务,用来调用storage微服务的扣减商品库存的api log.info("*****deduction of goods : {} begins",order.getCommodityCode()); storageService.reduceStorage(order.getCommodityCode(),order.getCount()); log.info("*****deduction of goods : {} ends",order.getCommodityCode()); // 这里使用了OpenFeign调用了accout微服务,用来调用accout微服务的扣减余额的api log.info("****deduction of balance : {} begins",order.getUserId()); accountService.deductAccount(order.getUserId(),order.getMoney()); log.info("****deduction of balance : {} ends",order.getUserId()); // 手动造成一个 by zero异常 int age = 10 /0 ; // 创建订单 log.info("****order creation begins"); Integer orderId = orderMapper.createOrder(order); log.info("****order : {} creation ends",orderId); return orderId; }
- 以上案例存在一个非常致命的问题,在没有使用事务的时候,storage微服务的商品库存会被减去,accout微服务的用户会被扣钱,但是order微服务的订单,却因为中途发生了异常,而没有生成对应的订单,也就是说,商品扣了,钱给了,但是没有订单,不给发货,这是一个很致命的问题;
- 可能会有同学说,那就加事务呗,@Transaction多简单呐,加上不就得了嘛;
- 想法没有错,但是这里的情况是分布式微服务,@Transaction已经爱莫能助了;
- 想直接使用@Transaction来控制多个微服务的事务,无异于,你搁某宝买东西发生了异常,却要求人某东一起跟着回滚,没这个理昂;
- 所以,为了解决上述问题,Seata出现了;
- Seata的详解请移步官网,在这里一一写下的话,篇幅就太多了;
-
三、如何整合Seata与Nacos
- 下载seata-server,我这里下载的是seata的1.5.1版本,算是比较新的了;
- 下载nacos-server,我下载的是1.4.4版本;
- 分别解压以上两个服务的压缩包;
1.nacos配置并启动
- 进入nacos-server的conf目录,打开application.properties文件,在33-41行处添加如下信息;
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
注意数据库用户名和密码,修改为自己的;
-
创建nacos数据库,并执行conf文件夹下的nacos-mysql.sql文件,生成对应的表;
-
进入nacos目录下的bin目录,修改startup.cmd文件;
-
将第26行修改为
set MODE="standalone"
默认为cluster;
-
-
修改后,直接双击startup.cmd即可启动nacos;
-
启动成功后,进入控制台页面;
-
http://localhost:8848/nacos
账户名和密码皆为 nacos
-
2.seata配置并启动
-
进入seata的conf目录,并打开application.yaml文件,配置如下;
server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: 127.0.0.1:4560 kafka-appender: bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: # support: nacos, consul, apollo, zk, etcd3 # 注意事项,官方建议nacos版本在1.2.0及以上版本 type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: # 不写则默认为public名称空间 group: SEATA_GROUP username: nacos # nacos的账号密码 password: nacos ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: "" #secret-key: "" data-id: seataServer.properties registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server # 指定注册至nacos注册中心的服务名,需要和${spring.application.name} 保持一致 server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: cluster: default # 指定seata注册值nacos的集群名为default username: nacos password: nacos ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: "" #secret-key: "" store: # support: file 、 db 、 redis # 注意数据库版本为5.7.26 , 使用8.0.12时报错Could not retrieve transation read-only status server mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&useUnicode=true user: root password: root min-conn: 5 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 100 max-wait: 5000 # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
注意,在store存储的地方,将数据库用户名和密码修改为自己的;
- 若本地没有mysql5.7.26版本,且启动seata时会报错Could not retrieve transation read-only status server;
- 则将store下的mode的值改为file即可;
-
在数据库中创建seata数据库(若store存储模式不是db的则可以跳过该步骤)
-
执行以下脚本,生成对应数据表;
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- 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 = InnoDB DEFAULT 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 = InnoDB DEFAULT 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_and_branch_id` (`xid` , `branch_id`) ) ENGINE = InnoDB DEFAULT 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 = InnoDB DEFAULT 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);
-
在seata目录下的bin目录中,双击seata-server.bat即可启动seata,注意,在启动seata前一定要先启动nacos;
-
启动成功后,访问控制台页面;
-
http://localhost:7091/#/login
用户名和密码皆为seata
-
且nacos的控制台中可以看到我们所注册的seata服务;
-
四、springcloud整合seata、nacos案例演示
1、创建一个maven父工程springcloud-seata-demo
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wp.springcloud</groupId> <artifactId>springcloud</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>Maven</name> <!-- 统一jar包版本 --> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>4.12</junit.version> <lombok.version>1.16.18</lombok.version> <log4j.version>1.2.17</log4j.version> <mysql.version>5.1.47</mysql.version> <druid.version>1.1.16</druid.version> <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version> </properties> <!-- 子模块继承之后,提供作用:锁定版本+子module不用写groupId和version --> <dependencyManagement> <dependencies> <!-- spring boot 2.2.2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring cloud Hoxton.SR1 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR1</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring cloud alibaba 2.1.0.RELEASE --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <optional>true</optional> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
2、在父工程下创建cloud-api-commons
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud</artifactId> <groupId>com.wp.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-api-commons</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.1.0</version> </dependency> </dependencies> </project>
-
创建实体类
package com.wp.www.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @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); } }
此工程用来统一返回所需的实体类;
3、创建cloudalibaba-seata-order8001
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud</artifactId> <groupId>com.wp.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-seata-order</artifactId> <dependencies> <!--这里将以下两个包从starter中排除并重新引入的原因是,starter中的版本,与我们自己所下载的seata服务版本不一致--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.5.1</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.5.1</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- SpringBoot整合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> <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.wp.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
-
application.yaml
server: port: 8001 spring: application: name: cloudalibaba-seata-order cloud: nacos: discovery: # 服务注册中心 server-addr: 127.0.0.1:8848 alibaba: seata: # 大坑,只有在此处配置tx-service-group才能生效 tx-service-group: my-test-group datasource: username: root # 数据库用户名和密码修改为自己的 password: root url: jdbc:mysql://127.0.0.1:3306/seata_order driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource seata: service: vgroup-mapping: my-test-group: default # 指定事务分组至集群映射关系,即映射到哪一个seata-server集群上,需与server端注册到Nacos的cluster保持一致 registry: type: nacos nacos: server-addr: 127.0.0.1:8848 application: seata-server group: SEATA_GROUP enable-auto-data-source-proxy: true mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml
-
主启动类
package com.wp.www; import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @MapperScan(value = {"com.wp.www.repository"}) @EnableFeignClients @EnableAutoDataSourceProxy public class OrderMain8001 { public static void main(String[] args) { SpringApplication.run(OrderMain8001.class,args); } }
-
业务类
-
repository包下
package com.wp.www.repository; import com.wp.www.entity.order.Order; public interface OrderMapper { /* 创建订单并返回主键ID */ Integer createOrder(Order order); }
-
service报下
-
interface
package com.wp.www.service; import com.wp.www.entity.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import java.math.BigDecimal; @FeignClient(value = "cloudalibaba-seata-accout") public interface AccountService { @GetMapping("/account/{userId}/{money}") public CommonResult deductAccount(@PathVariable("userId") String userId, @PathVariable("money") BigDecimal money); }
package com.wp.www.service; import com.wp.www.entity.order.Order; public interface OrderService { Integer createOrder(Order order); }
package com.wp.www.service; import com.wp.www.entity.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "cloudalibaba-seata-storage") public interface StorageService { @GetMapping("/storage/{commodityCode}/{count}") public CommonResult reduceStorage(@PathVariable("commodityCode") String commodityCode, @PathVariable("count") Integer count); }
-
impl
package com.wp.www.service.impl; import com.wp.www.entity.order.Order; import com.wp.www.repository.OrderMapper; import com.wp.www.service.AccountService; import com.wp.www.service.OrderService; import com.wp.www.service.StorageService; import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private AccountService accountService; @Resource private StorageService storageService; @GlobalTransactional(name = "create-order",rollbackFor = {Exception.class}) @Override public Integer createOrder(Order order) { log.info("*****全局事务xid:{}", RootContext.getXID()); log.info("*****deduction of goods : {} begins",order.getCommodityCode()); storageService.reduceStorage(order.getCommodityCode(),order.getCount()); log.info("*****deduction of goods : {} ends",order.getCommodityCode()); log.info("****deduction of balance : {} begins",order.getUserId()); accountService.deductAccount(order.getUserId(),order.getMoney()); log.info("****deduction of balance : {} ends",order.getUserId()); // int age = 10 /0 ; // 若需要测试异常,将此异常打开即可 log.info("****order creation begins"); Integer orderId = orderMapper.createOrder(order); log.info("****order : {} creation ends",orderId); return orderId; } }
在service中的AccountService与StorageService是结合OpenFeign来调用其它微服务的;
-
-
controller包下
package com.wp.www.controller; import com.wp.www.entity.CommonResult; import com.wp.www.entity.order.Order; import com.wp.www.service.OrderService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class OrderController { @Resource private OrderService orderService; @PostMapping("/order") public CommonResult<Order> creatOrder(@RequestBody Order order){ orderService.createOrder(order); return new CommonResult(200,"订单创建成功",order); } }
-
-
在资源文件夹下创建mapper文件夹
-
OrderMapper.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.wp.www.repository.OrderMapper"> <insert id="createOrder" useGeneratedKeys="true" keyProperty="orderId"> insert into order_tbl values (null,#{userId},#{commodityCode},#{count},#{money}) </insert> </mapper>
-
4、另外根据cloudalibaba-seata-order8001分别创建两个微服务
-
cloudalibaba-seata-account8002
-
cloudalibaba-seata-storage8003
具体创建细节不再写在该篇幅中
案例代码已上传至gitee