Seata AT分布式事务简单集成

Seata 是什么?

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
关于Seata的介绍和事务的详细流转细节参考 Seata官网

本文采用docker部署seata服务
1、运行镜像

docker run --name seata-server -p 8091:8091 -d seataio/seata-server

2、复制配置文件到主机 当前目录

docker cp seata-server:/seata-server .

3、停止服务

docker stop seata-server

4、删除服务

docker rm seata-server

5、重新运行服务
# 脚本
# BEGIN ANSIBLE MANAGED BLOCK
#!/bin/bash
HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
docker rm -f seata-server;
docker run --name seata-server \
  --restart=always \
  -v $HOME/seata-server:/seata-server \
  -e SEATA_IP=192.168.8.43 \
  -e SEATA_PORT=8091 \
  -p 8091:8091 \
  -d seataio/seata-server
# END ANSIBLE MANAGED BLOCK
6、切换到seata配置文件目录

cd /seata-server/resources
修改register.conf

  • 如果是配置中心是file的话,会使用file.conf里面的配置,如果是其他,使用配置中心的配置
  • 直连 eureka/consul/apollo/etcd/zookeeper/sofa/redis/file
本文采用nacos配置
  • 修改 register.type 和 config.type 为 nacos
    然后修改nacos的相关配置
# 注册中心配置
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos" # 注册类型
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server-lss"
    serverAddr = "123.57.26.81:8848"
    group = "SEATA_GROUP"
    namespace = "lss_test"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
# 配置中心配置
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "123.57.26.81:8848"
    namespace = "lss_test"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}
7、重启seata-server

docker restart seata-server
查看nacos,发现seata 服务端已正常启动

image.png

接下来配置项目

参考seata官网的账户、订单项目

1、新建项目,如下图
image.png
2、pom.xml


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.2.RELEASE




    


    com.lss
    seata_xa
    1.0.0
    pom

    
        1.4.0
        2.2.3.RELEASE
    

    
        business_xa
        order_xa
        account_xa
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.projectlombok
            lombok
            1.18.8
        
        





        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        

        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-seata
        
            
                
                    io.seata
                    seata-spring-boot-starter
                
            
        











        
            io.seata
            seata-spring-boot-starter
            ${seata.version}
        

        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        
        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
            2.1.0.RELEASE
        

        
        
            mysql
            mysql-connector-java
            5.1.48

        
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
            2.1.0.RELEASE
        
    
    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    1.8
                    1.8
                
            
        
    
    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                Hoxton.SR8
                pom
                import
            
            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                ${alibaba.cloud.version}
                pom
                import
            
        
    

3、config.txt 和 nacos-config.sh是从 官网 拷出来的配置,需要上传到nacos,具体也可参考/seata-server/resources/README-zh.md 里面的描述
  • nacos-init.sh 是运行 nacos-config.sh 的脚本

#-h: host, the default value is localhost.
#
#-p: port, the default value is 8848.
#
#-g: Configure grouping, the default value is 'SEATA_GROUP'.
#
#-t: Tenant information, corresponding to the namespace ID field of Nacos, the default value is ''.
#
#-u: username, nacos 1.2.0+ on permission control, the default value is ''.
#
#-w: password, nacos 1.2.0+ on permission control, the default value is ''.

bash nacos-config.sh -h 123.57.26.81 -p 8848 -g SEATA_GROUP -t lss_test -u nacos -w nacos

执行后 bash nacos-init.sh 后,在nacos上可查看到相关配置

image.png

项目整体结构图

image.png
4、business_xa项目
  • bootstrap.yml
server:
  port: 17000

nacos:
  namespace: lss_test
  address: 123.57.26.81:8848

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/xa_order?useSSL=false&serverTimezone=UTC
    username: root
    password: 12345678
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: business_xa
  cloud:
    nacos:
      namespace: ${nacos.namespace}
      server-addr: ${nacos.address}
      config:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
#          file-extension: yml
#          shared-configs:
#            - data-id: common.yml
#              refresh: true
      discovery:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
        register-enabled: true

# seata 配置
seata:
  # 注册信息
  registry:
    type: nacos
    nacos:
      application: seata-server-lss   # 这个是 seata 服务端的应用名称
      server-addr: ${nacos.address}  # nacos 服务地址
      group : "SEATA_GROUP"           # nacos 分组
      namespace: ${nacos.namespace}           # nacos 命名空间
      username: "nacos"
      password: "nacos"
  # 配置信息
  config:
    type: nacos
    nacos:
      server-addr: 123.57.26.81:8848
      group: "SEATA_GROUP"
      namespace: ${nacos.namespace}
      username: "nacos"
      password: "nacos"
  application-id: ${spring.application.name}
  tx-service-group: seata-server_xa_test    # 自定义事物组 tc
  • BusinessController.java
package com.lss.sample.operator;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("api")
@RestController
@Slf4j
public class BusinessController {


    @Autowired
    BusinessService businessService;

    @GetMapping("purchase")
    public String purchase(@RequestParam(value = "type", defaultValue = "1") Integer type ) {

        try {
            businessService.purchase(type);
        } catch (Exception exx) {
            log.info("异常:{}", exx);
            return "Purchase Failed:" + exx.getMessage();
        }
        return "SUCCESS";
    }
}
  • BusinessService.java
package com.lss.sample.operator;

import com.lss.sample.feign.AccountFeignClient;
import com.lss.sample.feign.OrderFeignClient;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class BusinessService {

    final String SUCCESS = "SUCCESS";

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Autowired
    AccountFeignClient accountFeignClient;

    @GlobalTransactional(rollbackFor = Exception.class)
    public void purchase(Integer type) {
        String xid = RootContext.getXID();
        log.info("business-xid:{}", xid);


        String accountResult = accountFeignClient.add();
        throw new RuntimeException("账户服务调用失败,事务回滚!");

        if (!SUCCESS.equals(accountResult)) {
            throw new RuntimeException("账户服务调用失败,事务回滚!");
        }else {
            log.info("账户服务调用成功...");
        }
//
        String orderResult = orderFeignClient.create(type);

        if (!SUCCESS.equals(orderResult)) {
            log.info("订单服务调用失败...");
            throw new RuntimeException("订单服务调用失败,事务回滚!");
        }else {
            log.info("订单服务调用成功...");
        }
    }
}
  • AccountFeignClient.java
package com.lss.sample.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "AccountFeignClient", url = "127.0.0.1:17002")
public interface AccountFeignClient {

    @GetMapping("api/add")
    String add();
}

  • OrderFeignClient.java
package com.lss.sample.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "OrderFeignClient", url = "127.0.0.1:17001")
public interface OrderFeignClient {

    @GetMapping("api/create")
    String create(@RequestParam(value = "type") Integer type);

}
  • 启动类 BusinessXAApplication.java
package com.lss.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class BusinessXAApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessXAApplication.class, args);
    }
}

5、account_xa项目
  • account_xa服务的bootstrap.yml
server:
  port: 17002

nacos:
  namespace: lss_test
  address: 123.57.26.81:8848

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/xa_account?useSSL=false&serverTimezone=UTC
    username: root
    password: 12345678
#    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: account_xa
  cloud:
    nacos:
      namespace: ${nacos.namespace}
      server-addr: ${nacos.address}
      config:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
#          file-extension: yml
#          shared-configs:
#            - data-id: common.yml
#              refresh: true
      discovery:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
        register-enabled: true

# seata 配置
seata:
  # 注册信息
  registry:
    type: nacos
    nacos:
      application: seata-server-lss   # 这个是 seata 服务端的应用名称
      server-addr: ${nacos.address}  # nacos 服务地址
      group : "SEATA_GROUP"           # nacos 分组
      namespace: ${nacos.namespace}           # nacos 命名空间
      username: "nacos"
      password: "nacos"
  # 配置信息
  config:
    type: nacos
    nacos:
      server-addr: 123.57.26.81:8848
      group: "SEATA_GROUP"
      namespace: ${nacos.namespace}
      username: "nacos"
      password: "nacos"
  application-id: ${spring.application.name}
  tx-service-group: seata-server_xa_test    # 自定义事物组 tc
  • AccountController.java
package com.lss.sample.operator;

import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api")
@Slf4j
public class AccountController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("add")
    public String add() {
        String xid = RootContext.getXID();
        log.info("account-xid:{}", xid);
        jdbcTemplate.update("update account set account = 10000 where id = 4 ");
        return "SUCCESS";
    }
}

  • DataSourceConfig.java
package com.lss.sample.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Bean("dataSourceProxy")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        // DataSourceProxy for AT mode
//         return new DataSourceProxy(druidDataSource);

        // DataSourceProxyXA for XA mode
        return new DataSourceProxyXA(druidDataSource);
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
        return new JdbcTemplate(dataSourceProxy);
    }
}
  • 启动类 AccountXAApplication
package com.lss.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableFeignClients
@EnableTransactionManagement
public class AccountXAApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountXAApplication.class, args);
    }
}
6、order_xa项目
  • bootstrap.yml
server:
  port: 17001

nacos:
  namespace: lss_test
  address: 123.57.26.81:8848

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/xa_order?useSSL=false&serverTimezone=UTC
    username: root
    password: 12345678
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: order_xa
  cloud:
    nacos:
      namespace: ${nacos.namespace}
      server-addr: ${nacos.address}
      config:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
#          file-extension: yml
#          shared-configs:
#            - data-id: common.yml
#              refresh: true
      discovery:
        enabled: true
        namespace: ${spring.cloud.nacos.namespace}
        register-enabled: true

# seata 配置
seata:
  # 注册信息
  registry:
    type: nacos
    nacos:
      application: seata-server-lss   # 这个是 seata 服务端的应用名称
      server-addr: ${nacos.address}  # nacos 服务地址
      group : "SEATA_GROUP"           # nacos 分组
      namespace: ${nacos.namespace}           # nacos 命名空间
      username: "nacos"
      password: "nacos"
  # 配置信息
  config:
    type: nacos
    nacos:
      server-addr: 123.57.26.81:8848
      group: "SEATA_GROUP"
      namespace: ${nacos.namespace}
      username: "nacos"
      password: "nacos"
  application-id: ${spring.application.name}
  tx-service-group: seata-server_xa_test    # 自定义事物组 tc
  • OrderController.java
package com.lss.sample.operator;

import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("api")
@RestController
@Slf4j
public class OrderController {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @GetMapping("create")
    public String create(@RequestParam(value = "type") Integer type) {

        String xid = RootContext.getXID();
        log.info("account-xid:{}", xid);
        try {
            deal(type);
        } catch (Exception e) {
            return "FAIL";
        }
        return "SUCCESS";
    }
    @Transactional
    public void deal(Integer type) {
        jdbcTemplate.update("update `order` set num = 100 where id = 1 ");
        if (type.equals(2)) {
            log.info("order 调用异常...");
            throw new RuntimeException("order 调用异常...");
        }

    }
}
  • DataSourceConfig
package com.lss.sample.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Bean("dataSourceProxy")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        // DataSourceProxy for AT mode
//         return new DataSourceProxy(druidDataSource);

        // DataSourceProxyXA for XA mode
        return new DataSourceProxyXA(druidDataSource);
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
        return new JdbcTemplate(dataSourceProxy);
    }
}
  • 启动类OrderXAApplication.java
package com.lss.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderXAApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderXAApplication.class, args);
    }
}

运行项目

1、business_xa、account_xa、order_xa依次运行后,在nacos上发现项目都已经注册上去了,但是项目会报一个错误

no available service 'null' found, please make sure registry config correct

报错原因在这个类里 io.seata.core.rpc.netty.NettyClientChannelManager
根据seata导入客户端相关配置后
添加一个配置

  • service.vgroupMapping.(自定义的事务组名称 tc)=default 就可以了
    本文的配置为: service.vgroupMapping.seata-server_xa_test = default
    nacos上如下图
    image.png
2、在数据库中添加sql脚本
  • xa_account 库
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;


DROP TABLE IF EXISTS `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=12 DEFAULT CHARSET=utf8;
  • xa_order 库
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `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=12 DEFAULT CHARSET=utf8;
3、至此,seata集成已经完成了,启动项目后,访问business_xa服务

curl http://localhost:17000/api/purchase?type=1
发现数据库数据被正常更新
重新手动更新数据后,再次访问
curl http://localhost:17000/api/purchase?type=2
发现数据库数据回滚

集成过程可能还会有其他的问题,本文已经尽可能写到了,希望对你学习seata有所帮助

你可能感兴趣的:(Seata AT分布式事务简单集成)