01-分布式事务案例demo准备工作

一、创建空工程

idea工具 new-project-Empty Project 创建一个空工程,命名seata-at,后面创建的项目都放在该目录下

二、数据库初始化工具

订单案例涉及四个数据库:
在这里插入图片描述
为了后续测试方便我们编写一个工具,用来重置所有数据库表,可以方便地把数据重置到初始状态。

新建springboot项目,命名为db-init,添加 JDBC API和 MySQL Driver 依赖:
在这里插入图片描述
application.yml 配置mysql连接

spring:
  datasource:
    url: jdbc:mysql:///?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

注意: 在连接地址中没有指定库名,是因为我们要先后连接四个数据库,后面执行的 sql 脚本文件中会执行 use 来进行数据库切换

把sql文件夹复制到resources目录下
在这里插入图片描述
sql文件夹下载地址 https://download.csdn.net/download/zhanlijuan2015/21476507

案例中的 sql 脚本来自不同的项目:

seata 库
seata 库是 Seata Server (TC,全局事务协调器)使用的数据库,建表语句来自这里:https://github.com/seata/seata/tree/develop/script/server/db

order、storage和account库中的 undo_log 表
undo_log 表是各分支事务用来记录回滚日志的表,建表语句来自这里:https://github.com/seata/seata/tree/develop/script/client/at/db

order、storage和account表
这个案例项目是 Seata 官方案例,我少做了一些改动。案例用的建表语句来自这里:https://github.com/seata/seata-samples/tree/master/springcloud-eureka-feign-mybatis-seata,在各项目的 resources 目录中的 sql 文件。

order库中的 segment 表
EasyIdGenerator 是一个非常简单易用的全局唯一id发号器,他支持数据库自增id方式和雪花算法,由于雪花算法需要用到zookeeper服务器,为了简便起见,我们使用数据库自增id的方式。segment 表就来自这个开源项目,项目地址:https://github.com/lookingatstarts/easyIdGenerator

主程序中添加代码,执行sql脚本
下面代码运行 sql 目录中的四个脚本程序,每次运行都会删除四个数据库再重新创建,并初始化数据

@SpringBootApplication
public class DbInitApplication {
    public static void main(String[] args) {
        SpringApplication.run(DbInitApplication.class, args);
    }
    @Autowired
    private DataSource dataSource;
    @PostConstruct
    public void init() throws SQLException {
        exec("sql/account.sql");
        exec("sql/storage.sql");
        exec("sql/order.sql");
        exec("sql/seata-server.sql");
    }

    private void exec(String sql) throws SQLException {
         /**
           *类加载器/classpath:sql/xxxx.sql. CPR可以得到类加载器的根路径,从根路径寻找 "sql/xxxx.sql" 文件
         */
        ClassPathResource classPathResource = new ClassPathResource(sql, DbInitApplication.class.getClassLoader());
        //处理文件中的中文字符编码的资源对象
        EncodedResource encodedResource = new EncodedResource(classPathResource, "UTF-8");
        //Spring 中提供了一个 jdbc 脚本执行器,执行sql脚本文件
        ScriptUtils.executeSqlScript(dataSource.getConnection(), encodedResource);
    }
}

三、eureka注册中心

新建springboot项目,命名eureka-server,选择 eureka server 依赖:
在这里插入图片描述
application.yml 配置

server:
  port: 8761
eureka:
  server:
    enable-self-preservation: false
  client:
    register-with-eureka: false
    fetch-registry: false

主程序添加 @EnableEurekaServer 注解

四、order-parent父项目

为了对 order、storage和account微服务项目依赖进行统一管理,这里创建一个 pom 类型的 maven 项目,作为父项目。
新建 Module,选择 Maven 项目,artifactId命名order-parent.
pom文件内容
看到 seata 依赖部分被注释掉了,后面添加 seata 事务时再启用。

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>order-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>order-parent</name>

    <properties>
        <mybatis-plus.version>3.3.2</mybatis-plus.version>
        <druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
        <seata.version>1.3.0</seata.version>
        <spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>
        <!--<dependency>-->
        <!--  <groupId>com.alibaba.cloud</groupId>-->
        <!--  <artifactId>spring-cloud-alibaba-seata</artifactId>-->
        <!--  <version>${spring-cloud-alibaba-seata.version}</version>-->
        <!--  <exclusions>-->
        <!--    <exclusion>-->
        <!--      <artifactId>seata-all</artifactId>-->
        <!--      <groupId>io.seata</groupId>-->
        <!--    </exclusion>-->
        <!--  </exclusions>-->
        <!--</dependency>-->
        <!--<dependency>-->
        <!--  <groupId>io.seata</groupId>-->
        <!--  <artifactId>seata-all</artifactId>-->
        <!--  <version>${seata.version}</version>-->
        <!--</dependency>-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </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>

</project>

1 account账户项目

此微服务项目中实现扣减账户金额的功能。
在order-parent项目下新建module,创建springboot项目,artifactId命名account
修改 pom.xml,设置继承父项目 order-parent

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>order-parent</artifactId>
        <groupId>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>account</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>account</name>
</project>

application.yml

spring:
  application:
    name: account
  datasource:
    url: jdbc:mysql:///seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

server:
  port: 8081

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.tedu.account.entity
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.account.mapper: debug

创建 Account 实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private Long id;
    private Long userId;
    private BigDecimal total;//总金额
    private BigDecimal used;//已使用
    private BigDecimal residue;//可用
    private BigDecimal frozen;//冻结
}

创建 AccountMapper 接口
这里继承 Mybatis-Plus 提供的通用 Mapper 父类,decrease() 方法实现扣减账户金额的功能

public interface AccountMapper extends BaseMapper<Account> {
    void decrease(Long userId, BigDecimal money);
}

Mapper配置
先在 resources 目录下新建文件夹 mapper,然后创建文件 AccountMapper.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="cn.tedu.account.mapper.AccountMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.account.entity.Account" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="total" property="total" jdbcType="DECIMAL" />
        <result column="used" property="used" jdbcType="DECIMAL" />
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
        <result column="frozen" property="frozen" jdbcType="DECIMAL"/>
    </resultMap>
    <update id="decrease">
        UPDATE account SET residue = residue - #{money},used = used + #{money} where user_id = #{userId};
    </update>
</mapper>

主程序
添加 Mybatis 扫描注解@MapperScan(“cn.tedu.account.mapper”)
AccountService接口及实现类

public interface AccountService {
    void decrease(Long userId, BigDecimal money);
}
@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountMapper.decrease(userId, money);
    }
}

添加 AccountController 类提供客户端访问接口

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    @GetMapping("decrease")
    public String decrease(Long userId, BigDecimal money){
        accountService.decrease(userId, money);
        return "扣减账户成功";
    }
}

2 storage库存项目

在order-parent项目下新建module,创建springboot项目,artifactId命名storage,该项目用来减少库存的功能
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>order-parent</artifactId>
        <groupId>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.tedu</groupId>
    <artifactId>storage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>storage</name>
</project>

application.yml 配置

spring:
  application:
    name: storage
  datasource:
    url: jdbc:mysql:///seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

server:
  port: 8082

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.tedu.storage.entity
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.storage.mapper: debug

在cn.tedu.storage.entity,创建 storage 实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;//总是
    private Integer used;//已售出的数量
    private Integer residue;//可用库存
    private Integer frozen;//冻结的库存
}

cn.tedu.storage.entity.Storage包下创建StorageMapper接口,继承 Mybatis-Plus 提供的通用 Mapper 父类

public interface StorageMapper extends BaseMapper<Storage> {
    void decrease(Long productId,Integer count);
}

Mapper配置
先在 resources 目录下新建文件夹 mapper,然后创建文件 StorageMapper.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="cn.tedu.storage.mapper.StorageMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.storage.entity.Storage" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="product_id" property="productId" jdbcType="BIGINT" />
        <result column="total" property="total" jdbcType="INTEGER" />
        <result column="used" property="used" jdbcType="INTEGER" />
        <result column="residue" property="residue" jdbcType="INTEGER" />
        <result column="frozen" property="frozen" jdbcType="INTEGER" />
    </resultMap>
    <update id="decrease">
        UPDATE storage SET used = used + #{count},residue = residue - #{count} WHERE product_id = #{productId}
    </update>
</mapper>

主程序添加 Mybatis 扫描注解 @MapperScan(“cn.tedu.storage.mapper”)

cn.tedu.storage.service添加 StorageService 接口和它的实现类,添加decrease() 方法实现减少商品库存功能。

public interface StorageService {
    void decrease(Long productId, Integer count);
}
@Service
public class StorageServiceImpl implements StorageService{
    @Autowired
    private StorageMapper storageMapper;

    @Override
    public void decrease(Long productId, Integer count){
        storageMapper.decrease(productId,count);
    }
}

在cn.tedu.storage.controller包下添加 StorageController 类提供客户端访问接口

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

    @GetMapping("/decrease")
    public String decrease(Long productId, Integer count){
        storageService.decrease(productId, count);
        return "减少商品库存成功";
    }
}

启动注册中心,storage服务已注册到注册中心
启动 storage 项目,访问 http://localhost:8082/decrease?productId=1&count=1 进行测试

3 order订单项目

在order-parent项目下新建module,创建springboot项目,artifactId命名order ,该项目用来保存订单,并调用 storage 和 account 减少库存和扣减金额
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>order-parent</artifactId>
        <groupId>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>order</name>
</project>

application.yml 配置

spring:
  application:
    name: order
  datasource:
    url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.tedu.order.entity
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.order.mapper: debug

cn.tedu.order.entity,创建 order 实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private Long id;
    private Long userId;//哪个用户
    private Long productId;//购买哪个商品
    private Integer count;//买多少件
    private BigDecimal money;//花多少钱
    private Integer status;//0-冻结状态  1-正常状态
}

在包cn.tedu.order.mapper下创建 OrderMapper接口

public interface OrderMapper extends BaseMapper<Order> {
    void create(Order order);//创建订单
}

在 resources 目录下新建文件夹 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="cn.tedu.order.mapper.OrderMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.order.entity.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 `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
        VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money},1);
    </insert>
</mapper>

主程序添加 Mybatis 扫描注解**@MapperScan(“cn.tedu.order.mapper”)**

在cn.tedu.order.service下创建 OrderService 接口和它的实现类

public interface OrderService {
    void create(Order order);
}
@Service
public class OrderServiceImpl implements OrderService{
    @Autowired
    private OrderMapper orderMapper;
    @Override
    public void create(Order order) {
        // TODO: 远程调用ID发号器获取订单id,这里暂时随机产生一个 orderId
        Long orderId = Math.abs(new Random().nextLong());
        order.setId(orderId);
        orderMapper.create(order);
        // TODO: 远程调用库存storage,减少库存
        // TODO: 远程调用账户account,扣减账户
    }
}

在cn.tedu.order.controller包下添加 OrderController 类提供客户端访问接口

@RestController
@Slf4j
public class OrderController {
    @Autowired
    OrderService orderService;

    @GetMapping("/create")
    public String create(Order order) {
        log.info("创建订单");
        orderService.create(order);
        return "创建订单成功";
    }
}

启动订单服务,访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试

五、全局唯一id发号器

分布式系统中,产生唯一流水号的服务系统俗称发号器。
本demo,使用 EasyIdGenerator
下载项目
访问 https://github.com/lookingatstarts/easyIdGenerator ,下载发号器项目
在这里插入图片描述
解压到 seata-at 工程目录下并命名为 easy-id-generator,然后将该项目导入成maven项目

pom.xml
发号器向 eureka 进行注册,以便其它服务发现它。
在pom.xml 中添加 Spring Cloud Eureka Client 依赖,并将springcloud版本更改为Hoxton.SR8

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

application.yml

server:
  port: 9090

easy-id-generator:
  snowflake: #雪花算法
    enable: false
    zk:
      connection-string: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
    load-worker-id-from-file-when-zk-down: true  # 当zk不可访问时,从本地文件中读取之前备份的workerId
  segment: #使用数据库来生成自增id
    enable: true
    db-list: "seata_order"  # ["db1","db2"]
    fetch-segment-retry-times: 3 # 从数据库获取号段失败重试次数
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: easy-id-generator

在 resources 目录下新建配置文件 seata_order.properties,配置 seata_order 数据库的连接信息

jdbcUrl=jdbc:mysql://localhost:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
driverClassName=com.mysql.cj.jdbc.Driver
dataSource.user=root
dataSource.password=1234
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048

启动服务,访问 http://localhost:9090/segment/ids/next_id?businessType=order_business测试

六、order订单添加Feign远程调用

启动类加注解@EnableFeignClients

添加Feign声明式客户端接口

调用发号器获得全局唯一id
在cn.tedu.order.feign包下创建发号器的客户端接口EasyIdClient

@FeignClient(name = "easy-id-generator")
public interface EasyIdClient {
    @GetMapping("/segment/ids/next_id")
    String nextId(@RequestParam("businessType") String businessType);
}

调用库存服务减少商品库存
在cn.tedu.order.feign包下创建库存服务的客户端接口StorageClient

@FeignClient(name = "storage")
public interface StorageClient {
    @GetMapping("/decrease")
    String decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

调用账户服务扣减用户金额
在cn.tedu.order.feign包下创建账户服务的客户端接口AccountClient

@FeignClient(name = "account")
public interface AccountClient {
    @GetMapping("/decrease")
    String decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

在业务代码中通过Feign客户端调用远程服务,OrderServiceImpl修改

@Service
public class OrderServiceImpl implements OrderService{
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private EasyIdClient easyIdClient;

    @Autowired
    private StorageClient storageClient;

    @Autowired
    private AccountClient accountClient;

    @Override
    public void create(Order order) {
        //Long orderId = Math.abs(new Random().nextLong());
        order.setId(Long.valueOf(easyIdClient.nextId("order_business")));
        orderMapper.create(order);
        // TODO: 远程调用库存storage,减少库存
        storageClient.decrease(order.getProductId(), order.getCount());
        // TODO: 远程调用账户account,扣减账户
        accountClient.decrease(order.getUserId(), order.getMoney());
    }
}

启动项目,访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试

将所有项目压缩 并命名为无事务版本,备后面用不同分布式事务方案使用
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值