alibaba使用seata做分布式事务控制
1、数据库准备
分布式事务的话肯定是跟数据库有关的,数据库我这里使用的是mysql8版本
首先新建数据库seata_db_one,seata_db_two,seata_db_three
seata_db_one里面有order表
seata_db_two里面有storage表
seata_db_three里面有account表
2、微服务准备
我们的业务逻辑是首先下订单(订单微服务),然后减库存(库存微服务),然后减账户余额(账户微服务)
新建三个模块做对应的微服务service-seata9290(订单),service-seata9290(库存),service-seata9290(账户)
引入依赖,pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
service-seata9290
配置文件
mybatis:
type-aliases-package: com.zhu.model
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
spring:
datasource:
url: jdbc:mysql://*****:3306/seata_db_one?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: ***
password: ***
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: service-seata9290
cloud:
nacos:
discovery:
server-addr: localhost:8848
server:
port: 9290
启动类添加注解
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@RestController
public class HelloController {
@Resource
private OrderService orderService;
@Resource
private OpenFeign openFeign;
@RequestMapping("/hello")
public String hello(@RequestParam(value = "id",required = false)Long id){
orderService.insert();//添加订单
openFeign.storageHello();//减少库存,每次减1
orderService.update(id);//更改订单状态
return "hello";
}
}
@Mapper
public interface OrderMapper {
public void insert(Order order);
public void update(Order order);
}
@Data
public class Order {
private long id;
private int flag;
private String name;
}
@Component
@FeignClient(value = "service-seata9291")
public interface OpenFeign {
@RequestMapping("/storage/hello")
public void storageHello();
}
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
public void insert(){
Order order =new Order();
order.setFlag(0);
order.setName("订单");
orderMapper.insert(order);
}
public void update(long id){
Order order =new Order();
order.setId(id);
orderMapper.update(order);
}
}
<?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.zhu.mapper.OrderMapper">
<update id="update" parameterType="com.zhu.model.Order">
update `order` set flag = 1 where id=#{id,jdbcType=BIGINT}
</update>
<insert id="insert" parameterType="com.zhu.model.Order">
insert into `order` (flag,`name`) values (#{flag},#{name})
</insert>
</mapper>
service-seata9291
配置文件
mybatis:
type-aliases-package: com.zhu.model
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
spring:
datasource:
url: jdbc:mysql://*****/seata_db_two?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: ***
password: ***
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: service-seata9291
cloud:
nacos:
discovery:
server-addr: localhost:8848
server:
port: 9291
启动类添加注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@Resource
private AccountOpenFeign accountOpenFeign;
@RequestMapping("/storage/hello")
public void storageHello() {
storageService.decri(1);//减少库存
accountOpenFeign.accountMoney();//扣钱
}
}
@Mapper
public interface StorageMapper {
void decri(Storage storage);
}
@Data
public class Storage {
private Long id;
private String name;
private String num;
}
@FeignClient(value = "service-seata9292")
public interface AccountOpenFeign {
@RequestMapping("/accountHello")
public void accountMoney();
}
@Service
public class StorageService {
@Resource
private StorageMapper storageMapper;
public void decri(int num) {
Storage storage=new Storage();
storage.setId(1L);
storageMapper.decri(storage);
}
}
<?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.zhu.mapper.StorageMapper">
<update id="decri" parameterType="com.zhu.model.Storage">
update storage set num=num - 1 where id=#{id,jdbcType=BIGINT}
</update>
</mapper>
service-seata9292
配置文件
mybatis:
type-aliases-package: com.zhu.model
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
spring:
datasource:
url: jdbc:mysql://****:3306/seata_db_three?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: ***
password: ***
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: service-seata9292
cloud:
nacos:
discovery:
server-addr: localhost:8848
server:
port: 9292
启动类注解
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class AccountController {
@Resource
private AccountService accountService;
@RequestMapping("/accountHello")
public void accountMoney() {
accountService.decriMoney();
}
}
@Mapper
public interface AccountMapper {
void decri();
}
@Data
public class Account {
private Long id;
private String name;
private double money;
}
@Service
public class AccountService {
@Resource
private AccountMapper accountMapper;
public void decriMoney(){
accountMapper.decri();
}
}
3、测试
数据库里面记得附上初始值
调用http://localhost:9290/hello?id=14(id是数据库下一条id的值)发现调用成功,数据库也会相应更改
现在在service-seata9292的AccountService类里面的decriMoney方法加上代码,如下
public void decriMoney(){
int a =10/0;//这里会抛异常,那么数据更改就会不一致
accountMapper.decri();
}
重新启动nacos和多个微服务,http://localhost:9290/hello?id=15,发现订单新建了,但是状态并未更改,库存减少了,金额并未减少
显然这样是不行的,我们需要做到共同进退,也就是加上分布式事务
我们在service-seata9290的HelloController类的hello方法上添加@GlobalTransactional
注解
@RequestMapping("/hello")
@GlobalTransactional
public String hello(@RequestParam(value = "id",required = false)Long id){
orderService.insert();//添加订单
openFeign.storageHello();//减少库存,每次减1
orderService.update(id);//更改订单状态
return "hello";
}
再次测试就会是正常的流程了,那怕中途异常,也会全部回滚
seate使用确实是很简单的,只需要在开始加上@GlobalTransactional
注解就行了
从数据库来看,确实是达到了一致性,但是发现后台会一直报错
ERROR 5584 --- [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager : no available service 'null' found, please make sure registry config correct
也就是seata的服务端没有启动,找不到才报的错
4、seata服务端配置
首先官网下载服务https://github.com/seata/seata/releases
下载后解压
我们先修改配置文件
在conf目录下,我们主要是修改file.conf和registry.conf这两个文件
file.conf是修改seata的存储的,这里使用数据库存储
store {
## store mode: file、db、redis
mode = "db" ##选择db,所以只需要更改db的配置
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://xxxx:3306/seata"
user = "root"
password = "xxx"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
registry.conf是服务注册与配置中心的相关的文件
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" ##这里使用的是nacos,下面下面只需要配置nacos
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP" ##分组不同可能会导致服务与客户端无法连接,所以推荐将所有的group设置为一样的
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos" ##配置中心使用的是nacos,下面下面只需要配置nacos
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
group = "SEATA_GROUP" ##分组不同可能会导致服务与客户端无法连接,所以推荐将所有的group设置为一样的
username = "nacos"
password = "nacos"
cluster = "default"
}
}
现按nacos配置中心还没有seata的配置,还需要将seata的配置推送到nacos
下载源码,解压后用idea打开,打开如图所示的config.txt文件,这里面是seata-server的所有配置
修改配置
打开文件夹目录…\seata-1.3.0\script\config-center\nacos,右键点击Git Bash Here,输入sh nacos-config.sh命令
显示一下信息表示配置推送成功
=========================================================================
Complete initialization parameters, total-count:79 , failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.
现在将seata-sever启动起来,进入到D:\seata-server-1.3.0\seata\bin双击seata-server.bat,即可启动服务,启动后我们进入nacos查看(nacos一定是先启动)
服务启动成功,再去配置列表查看配置是否推送成功
去client端配置seata
spring:
datasource:
url: jdbc:mysql://xxx:3306/seata_db_three?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver ##mysql8驱动
application:
name: service-seata9292
cloud:
nacos:
discovery:
server-addr: localhost:8848
group: SEATA_GROUP ##分组不同可能会导致服务与客户端无法连接,所以推荐将所有的group设置为一样的
seata:
application-id: ${spring.application.name}
enabled: true
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
config:
type: nacos
nacos:
namespace: ""
serverAddr: localhost:8848
group: SEATA_GROUP ##分组不同可能会导致服务与客户端无法连接,所以推荐将所有的group设置为一样的
userName: "nacos"
password: "nacos"
registry:
type: nacos
nacos:
application: seata-server #名称与服务端在nacos注册中心的名称一样
serverAddr: localhost:8848
group: SEATA_GROUP ##分组不同可能会导致服务与客户端无法连接,所以推荐将所有的group设置为一样的
namespace: ""
userName: "nacos"
password: "nacos"
cluster: default
再次启动nacos,seata-seaver,service-seata9292,就会发现service-seata9292控制台就不会报错了