springboot整合seata

前言

在上一篇中,我们简单聊了聊分布式事务的问题和seata的基本介绍,在使用seata实现分布式事务的解决方案中,提供了常用的3种模式,AT模式,TCC模式和saga模式,并且说明了AT模式下的使用原理,下面对AT模式下,使用springboot与seata整合解决分布式事务的问题,做简单的介绍

环境准备

1、官网下载seata-server-1.0.0.zip

在这里插入图片描述
上文谈AT模式时候提到了一个TC,即事务协调者,seata-server即一个后台服务,启动之后管理并协调全局的事务,它的作用原理很简单,我们可以类比zookeeper,当一个集群启动之后,zookeeper是怎么管理其他节点的呢?就是不断的与各个节点发送和接受心跳包,那么seata-server理解也是如此

下载之后解压出来,如果使用默认的配置,直接接入bin目录,在windows环境下,直接双击.bat文件即可启动

但本文的环境构建基于springcloud,因此各个微服务之间的交互打算通过eureka注册中心进行RPC的调用,因此需要在conf目录下简单配置一下
springboot整合seata_第1张图片
重点需要关注的上图中的两个文件,如果要使用eureka作为服务的注册中心,需要修改的地方如下,即在registry的配置文件中做如下修改,默认的type是file类型
springboot整合seata_第2张图片
这个不难理解,就是说当seata-server启动的时候,这个服务要以什么形式注册到哪里去,seata提供了多种方式,比如redis,zk,nacos等,可以根据自己的实际情况做选择,nacos也是一个不错的选择,属于阿里开源的分布式配置中心

springboot整合seata_第3张图片
而file.conf配置文件要关注的上图中的几行,简单解释就是这个创建的全局事务协调器管理的各个本地事务,对他们进行分组,这一seata-server才能通过这个全局事务协调各个分支事务,更加详细的配置解释官网都有说明,可以参考学习

2、业务场景

本文用到的业务场景即模拟一个下单的场景,项目结构如下:
springboot整合seata_第4张图片

eureka-server:服务注册中心
business:业务服务,即完成TM的功能,在该服务中进行全局的事务控制
order:下单服务
points:积分服务
storage:库存服务

springboot整合seata_第5张图片

3、数据库准备

各个微服务创建自己的数据库,同时为了TM管理全局的事务,需要在每个数据库创建一个undo_log表,官网提供了执行sql
springboot整合seata_第6张图片
springboot整合seata_第7张图片

项目搭建

创建一个聚合maven项目,包括5个模块的子工程,如上图所示,

1、eureka-server 工程

服务注册中心,这个没什么要说的,主要是在配置文件yml的配置

server:
  port: 8761
eureka:
  server:
    enable-self-preservation: false
  instance:
    appname: provider-service
    hostname: localhost
  client:
    service-url:
      defaultZone:
        http://localhost:8761/eureka/
    register-with-eureka: false
    fetch-registry: false
spring:
  main:
    allow-bean-definition-overriding: true

pom依赖

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.11.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR4</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

启动类

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerStarter {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerStarter.class, args);
    }

}

启动eureka-server,再启动上面已经配置好的seata-server,然后浏览器访问:http://localhost:8761/
springboot整合seata_第8张图片

springboot整合seata_第9张图片
可以看到eureka启动成功,同时seata-server也注册到了eureka中

2、order、storage、points 工程

这三个微服务各自负责处理自己的业务逻辑,整合过程中流程基本上一样,下面列举其中一个进行说明,以storage工程为例

springboot整合seata_第10张图片

2.1 添加pom依赖

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>

    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </pluginRepository>
    </pluginRepositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency> 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <version>2.1.0.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.0.0</version>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2.2 配置yml

server:
  port: 8003
spring:
  application:
    name: storage-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://IP:3306/storage?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: user
    password: user

将file.conf文件以及registry文件拷贝至resources目录

2.3 配置数据源代码

说明:seata在AT模式下解决分布式事务的具体逻辑体现在对数据源的代理上,即对DataSource产生代理变成DataSourceProxy,进行全局事务的管理和协调,因此在整合时,需通过配置类的方式进行配置

/**
 * 数据源代理
 */
@Configuration
public class DataSourceConfiguration {

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

    @Primary
    @Bean("dataSource") //6.1 创建DataSourceProxy
    public DataSourceProxy dataSourceProxy(DataSource druidDataSource){
        return new DataSourceProxy(druidDataSource);
    }

    @Bean //6.2 将原有的DataSource对象替换为DataSourceProxy
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/mapper/*.xml"));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

到这里,基本的配置工作已经接近尾声,下面只需要添加本工程自己的业务逻辑即可,由于需要对外提供服务,本工程上库存模块,提供一个对外接口,即扣减库存的接口

controller层

@RestController
public class StorageController {
    @Resource
    private StorageService storageService;

    @GetMapping("/decrease")
    public Storage decrease(@RequestParam(required = false)String goodsCode,
                            @RequestParam(required = false)Integer quantity){
        return storageService.decrease(goodsCode, quantity);
    }
}

service业务实现层

@Service
public class StorageService {
    @Resource
    private StorageDAO storageDAO;

    /**
     * 减少库存
     * @param goodsCode 商品编码
     * @param quantity 减少数量
     * @return 库存对象
     */
    public Storage decrease(String goodsCode, Integer quantity) {
        Storage storage = storageDAO.findByGoodsCode(goodsCode);
        if (storage.getQuantity() >= quantity) {
            storage.setQuantity(storage.getQuantity() - quantity);
        }else{
            throw new RuntimeException(goodsCode + "库存不足,目前剩余库存:" + storage.getQuantity() );
        }
        storageDAO.update(storage);
        return storage;
    }
}

接口:

@Repository
@Mapper
public interface StorageDAO {
    public Storage findByGoodsCode(String goodsCode);
    public void update(Storage storage);
}

启动类:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@MapperScan(basePackages="com.congge.storage.dao")
public class StorageApplication {

    public static void main(String[] args) {
        SpringApplication.run(StorageApplication.class, args);
    }

}

按照上述同样的流程将points以及order工程配置完毕即可

3、business 工程

该工程提供对外访问的入口,即创建订单的操作,同时也是TM即事务管理器的地方,在本工程中,需要通过rpc的方式调用order、points、storage等接口,为使用方便,这里使用springcloud中的open-feign进行调用,

3.1 pom依赖

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

其他依赖可参考上述的

3.2 yml配置

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
mybatis:
  mapperLocations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

server:
  port: 8000
spring:
  application:
    name: bussiness-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://IP:3306/business?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: user
    password: user
logging:
  level:
    io:
      seata: debug

3.3 DataSource配置【同上】

3.4 对外提供调用接口

controller层

@RestController
public class BussinessController {
    @Resource
    private BussinessService bussinessService;
    @GetMapping("/test1")
    public Order test1(){
        return bussinessService.sale("coke",10,"zhangsan",3,30f);
    }
    @GetMapping("/test2")
    public Order test2(){
        return bussinessService.sale("coke",10000,"zhangsan",3000,30000f);
    }
}

service层 ,要注意的上@GlobalTransactional注解,通过这个注解来控制全局事务

@Service
public class BussinessService {

    @Autowired
    private PointsServiceClient pointsServiceClient;
    @Autowired
    private StorageServiceClient storageServiceClient;
    @Autowired
    private OrderServiceClient orderServiceClient;


    /**
     * 商品销售
     * @param goodsCode 商品编码
     * @param quantity 销售数量
     * @param username 用户名
     * @param points 增加积分
     * @param amount 订单金额
     * @return 订单对象
     */
    @GlobalTransactional(name = "fsp-sale" , timeoutMills = 20000 , rollbackFor = Exception.class)
    //@Transactional
    public Order sale(String goodsCode , Integer quantity ,String username ,Integer points, Float amount ){
        pointsServiceClient.increase(username, points);
        storageServiceClient.decrease(goodsCode, quantity);
        Order order = orderServiceClient.create(goodsCode, quantity, username, points, amount);
        try {
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return order;
    }
}

feign客户端接口层

@FeignClient("points-service")
public interface PointsServiceClient {

    @GetMapping("/increase")
    public Points increase(@RequestParam(value = "username",required = false) String username,
                           @RequestParam(value = "quantity",required = false) Integer quantity);

}
@FeignClient("storage-service")
public interface StorageServiceClient {
    @GetMapping("/decrease")
    public Storage decrease(
            @RequestParam(value="goodsCode",required = false) String goodsCode,
            @RequestParam(value = "quantity",required = false) Integer quantity);
}
@FeignClient("order-service")
public interface OrderServiceClient {
    
    @GetMapping("/create")
    public Order create(@RequestParam(value="goodsCode",required = false) String goodsCode ,
                        @RequestParam(value = "quantity",required = false) Integer quantity ,
                        @RequestParam(value = "username",required = false) String username ,
                        @RequestParam(value = "points",required = false) Integer points,
                        @RequestParam(value = "amount",required = false) Float amount );
}

启动类:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class BussinessApplication {

    public static void main(String[] args) {
        SpringApplication.run(BussinessApplication.class, args);
    }

}

到这里演示工程的搭建以及配置都全部搞定了,下面将项目全部启动,启动过程中,注意这行日志,反应的就是各个服务和seata-server进行着心跳包的交换
springboot整合seata_第11张图片

首先在数据库的storage表初始化一条数据
springboot整合seata_第12张图片

假设当前商品数量上5000个,如果购买10个,则可以购买成功,同时积分也会增加,订单增加一条数据

分别调用接口:http://localhost:8000/test1 【正常的接口】

分别调用接口:http://localhost:8000/test2 【数据异常的接口】

然后再去观察数据库数据,在调用接口2的时候,在分布式环境下,扣减库存的操作会失败,加上了@GlobalTransactional注解之后,可以发现数据库的3张表没有产生中间数据,即通过seata-server操作的undo_log表完成了数据的回滚,从而解决了这个问题,具体的效果可以在本地调用接口后,去数据库观察undo_log表的数据
在这里插入图片描述

以上便是使用seata在AT模式下整合的全部步骤,细节尚未考考虑周全之处敬请谅解,最后感谢观看!

你可能感兴趣的:(seata,springboot)