zl程序教程

您现在的位置是:首页 >  其他

当前栏目

SpringCloud Alibaba学习(十二):Seata处理分布式事务(三万字提供 介绍、搭建、实战、原理一条龙服务)(上)

2023-03-14 22:51:54 时间

一、前言:分布式事务问题


         

在学习mysql数据库时,我们肯定都学习过事务,也知道事务的概念,做过事务的实战;那么当我们的服务变成分布式微服务之后,事务会有怎样的问题呢?


 在分布式架构中,单体应用被拆分成微服务应用。以用户购买商品为例,原来的三个模块(订单、仓储、账户)被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。


77c1ed149d5a498485511c79063f96ca.png


此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

       

用一句话来概括:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题 。


因此,阿里巴巴为我们提供了seata来处理分布式事务问题。


二、Seata概述 



1、Seata是什么 


        Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 


2、官网地址 

       

http://seata.io/zh-cn/


3、Seata能做什么

       

我们需要了解一个典型的分布式事务过程 的 一ID + 三组件概念


(1)一ID

               

就是 全局唯一的事务ID


(2)三组件

               

Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;  


Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;


Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚


(3)处理过程


1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;


 2. XID 在微服务调用链路的上下文中传播;

 

3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;


 4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;


5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。


f49dcb8b96f247e2aaf8fde8a4b381e0.png


 4、下载地址(0.9.0版)

       

https://seata.io/zh-cn/blog/download.html


 5、如何起作用

     

玩Seata的方式非常简单 ,只需要在调用服务的地方加上全局@GlobalTransactional就可以了。


三、Seata-Server安装



1、下载并解压

       

前面已经给出下载地址,下载zip包。然后解压。


       

2、修改file.conf配置文件

       

找到解压后的目录并修改conf目录下的file.conf配置文件。


注意,配置类文件在修改之前都要先备份一下,避免改错。


我们主要修改的内容是:自定义事务组名称+事务日志存储模式为db+数据库连接信息


可以修改成任何名字  


b751eec659e7492c910707185054cf52.png7a6d6913678f464fa839bb8cc742677a.png


这里的数据库信息是写自己的,不要一味地按照我的来改。 

de412b7f1ee14eeeb0323007da3a7b86.png


3、新建seata数据库 

       

在mysql5.7数据库新建库seata ,并在seata-server-0.9.0seataconf目录里面找到建表所需的 db_store.sql 文件,导入或者复制到数据库中执行。

       

完成了之后是这样:

5ad8956be7c844529fcb454a23a874c6.png


4、修改registry.conf配置文件 

        

修改seata-server-0.9.0seataconf目录下的registry.conf配置文件

424eb3d4635f4a9d8d512069e5f5001f.png


下面我们以用户支付的业务逻辑为例,使用 订单/库存/账户 业务来进行Seata实战。


五、订单/库存/账户 业务数据库准备


       

1、业务说明

     

 这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。

     

 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

       

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。


操作步骤为:下订单--->扣库存--->减账户(余额)


2、创建三个数据库 


CREATE DATABASE seata_order;
 
CREATE DATABASE seata_storage;
 
CREATE DATABASE seata_account;


3、seata_order库下建t_order表


 
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;
 
SELECT * FROM t_order;
 


 4、seata_storage库下建t_storage表 


 
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');
 
SELECT * FROM t_storage;
 


5、seata_account库下建t_account 表 


 
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');
 
SELECT * FROM t_account;
 
 


6、在三个库下分别建各自的回滚日志表 

       

找到 seata-server-0.9.0seataconf目录下的db_undo_log.sql,分别在三个数据库下执行一次。

       

注意是分别执行一次,所以一共是执行三次。 

        

7、数据库最终效果 


674549e16e074c86937683c56b152864.png



六、订单/库存/账户 业务微服务准备



1、新建订单Order-Module 


(1)创建模块 

                

新建普通maven工程 seata-order-service2001 


(2)修改pom文件


<?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>cloud</artifactId>
        <groupId>com.shang.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
 
    <artifactId>seata-order-service2001</artifactId>
 
    <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>0.9.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>5.1.37</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>
 
 
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
 
</project>


(3)编写yml文件 


server:
  port: 2001
 
spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3308/seata_order
    username: root
    password: root
 
feign:
  hystrix:
    enabled: false
 
logging:
  level:
    io:
      seata: info
 
mybatis:
  mapperLocations: classpath:mapper/*.xml


(4)粘贴conf文件(1.0版本之后已经支持yml) 

               

将seata目录下的 file.conf 和 registry.conf 粘到resource目录下 


(5) 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);
    }
}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
    private Long id;
 
    private Long userId;
 
    private Long productId;
 
    private Integer count;
 
    private BigDecimal money;
 
    /**
     * 订单状态:0:创建中;1:已完结
     */
    private Integer status;
}

(6)dao包

@Mapper
public interface OrderDao {
 
    /**
     * 创建订单
     */
    void create(Order order);
 
    /**
     * 修改订单状态,从0改到1
     */
    void update(@Param("userId") Long userId, @Param("status") Integer status);
}


(7)resource/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.shang.cloud.dao.OrderDao">
 
    <resultMap id="BaseResultMap" type="com.shang.cloud.domain.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>
 
    <insert id="create">
        insert into t_order (id, user_id, product_id, count, money, status)
        values (null, #{userId}, ${productId}, ${count}, #{money},0)
    </insert>
 
    <update id="update">
        UPDATE `t_order`
        SET status = 1
        WHERE user_id = #{userId} AND status = #{status};
    </update>
</mapper>
 


(8)service包


@FeignClient(value = "seata-account-service")
public interface AccountService {
 
    /**
     * 扣减账户余额
     */
    //@RequestMapping(value = "/account/decrease", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
    @PostMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}


public interface OrderService {
 
    /**
     * 创建订单
     */
    void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService {
 
    /**
     * 扣减库存
     */
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

(9)service.impl包 


@Service
@Slf4j
public class OrderServiceImpl implements OrderService
{
    @Resource
    private OrderDao orderDao;
 
    @Resource
    private StorageService storageService;
 
    @Resource
    private AccountService accountService;
 
    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:
     * 下订单->减库存->减余额->改状态
     */
    @Override
//    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order) {
        log.info("------->下单开始");
        //本应用创建订单
        orderDao.create(order);
 
        //远程调用库存服务扣减库存
        log.info("------->order-service中扣减库存开始");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("------->order-service中扣减库存结束");
 
        //远程调用账户服务扣减余额
        log.info("------->order-service中扣减余额开始");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("------->order-service中扣减余额结束");
 
        //修改订单状态为已完成
        log.info("------->order-service中修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("------->order-service中修改订单状态结束");
 
        log.info("------->下单结束");
    }
}


(10)controller包 


@RestController
public class OrderController {
 
    @Autowired
    private OrderService orderService;
 
    /**
     * 创建订单
     */
    @GetMapping("/order/create")
    public CommonResult create(Order order) {
        orderService.create(order);
        return new CommonResult(200, "订单创建成功!");
    }
}


(11)config包 

@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 dataSource) {
        return new DataSourceProxy(dataSource);
    }
 
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
 
}


@Configuration
@MapperScan({"com.shang.cloud.dao"})
public class MyBatisConfig {
}

(12)编写主启动类 


@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class SeataOrderMainApp2001
{
 
    public static void main(String[] args)
    {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}


2、新建库存Storage-Module


(1)新建模块 

               

新建普通maven模块 seata-storage-service2002 


(2)修改pom文件 


<?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>cloud</artifactId>
        <groupId>com.shang.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
 
    <artifactId>seata-storage-service2002</artifactId>
 
    <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>0.9.0</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
 
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
 
</project>


(3)编写yml文件 


server:
  port: 2002
 
spring:
  application:
    name: seata-storage-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3308/seata_storage
    username: root
    password: root
 
logging:
  level:
    io:
      seata: info
 
mybatis:
  mapperLocations: classpath:mapper/*.xml


(4)粘贴conf文件(1.0版本后已经支持yml) 


将seata目录下的 file.conf 和 registry.conf 粘到resource目录下


(5) 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);
    }
}


package com.shang.cloud.domain;
 
import lombok.Data;
 
@Data
public class Storage {
 
    private Long id;
 
    /**
     * 产品id
     */
    private Long productId;
 
    /**
     * 总库存
     */
    private Integer total;
 
    /**
     * 已用库存
     */
    private Integer used;
 
    /**
     * 剩余库存
     */
    private Integer residue;
}
 


(6)dao包 


@Mapper
public interface StorageDao {
 
    /**
     * 扣减库存
     */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}


(7)service包 

public interface StorageService {
    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}


(8)service.impl包 


@Service
public class StorageServiceImpl implements StorageService {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
 
    @Resource
    private StorageDao storageDao;
 
    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        LOGGER.info("------->storage-service中扣减库存开始");
        storageDao.decrease(productId,count);
        LOGGER.info("------->storage-service中扣减库存结束");
    }
}


(9)controller包 

@RestController
public class StorageController {
 
    @Autowired
    private StorageService storageService;
 
    /**
     * 扣减库存
     */
    @RequestMapping("/storage/decrease")
    public CommonResult decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult(200,"扣减库存成功!");
    }
}


(10)config包 

同2001 


 (11)主启动类 


@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002 {
 
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageServiceApplication2002.class, args);
    }
 
}