SpingCloud 2020微服务教程【56】Seata模块搭建

本文介绍了如何使用SpringCloud和Seata搭建分布式事务管理的微服务架构。通过创建订单、库存和账户三个服务,模拟了下单流程中的扣库存、减余额和改订单状态操作。当订单服务调用库存和账户服务时,利用Seata进行全局事务管理,确保数据一致性。在遇到异常时,如模拟的账户服务超时,Seata能够自动回滚事务,避免数据问题。此外,还展示了各个微服务的模块创建、数据库表结构、配置文件以及关键代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

视频链接:2020最新版SpringCloud框架开发教程-周阳
文章源码:https://github.com/geyiwei-suzhou/cloud2020/

设计三个微服务:订单服务、库存服务、账户服务

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

下订单 --> 扣库存 --> 减余额 --> 改状态

该操作跨越三个数据库,有两次远程调用,是一个分布式问题

创建三个数据库、六张表
  • seata_order:存储订单的数据库,并创建t_order、undo_log表

  • seata_storage:存储库存的数据库,并创建t_storage、undo_log表

  • seata_account:存储账户信息的数据库,并创建t_account、undo_log表

    Drop Database if exists seata_order;
    create database seata_order;
    
    Drop Database if exists seata_storage;
    create database seata_storage;
    
    Drop Database if exists seata_account;
    create database seata_account;
    
    DROP TABLE IF EXISTS seata_order.t_order;
    CREATE TABLE seata_order.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;
    
    DROP TABLE IF EXISTS seata_storage.t_storage;
    CREATE TABLE seata_storage.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;
    
    DROP TABLE IF EXISTS seata_account.t_account;
    CREATE TABLE seata_account.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_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
    VALUES ('1', '1', '100', '0', '100');
    
    INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
    VALUES ('1', '1', '1000', '0', '1000');
    
    DROP TABLE IF EXISTS seata_order.undo_log;
    CREATE TABLE seata_order.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 = 1
      DEFAULT CHARSET = utf8;
    
    DROP TABLE IF EXISTS seata_storage.undo_log;
    CREATE TABLE seata_storage.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 = 1
      DEFAULT CHARSET = utf8;
    
    DROP TABLE IF EXISTS seata_account.undo_log;
    CREATE TABLE seata_account.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 = 1
      DEFAULT CHARSET = utf8;
    

创建三个模块:seata-order-service2001、seata-storage-service2002、seata-account-service2003

seata-order-service2001模块

1. 建module

New --> Module --> Maven[Module SDK:1.8.0_191] --> name[seata-order-service2001] --> Finish

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>cloud2020</artifactId>
    <groupId>com.antherd.springcloud</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>seata-order-service2001</artifactId>

  <dependencies>
    <!-- seata -->
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
      <exclusions>
        <exclusion>
          <groupId>io.seata</groupId>
          <artifactId>seata-all</artifactId>
        </exclusion>
        <exclusion>
          <groupId>io.seata</groupId>
          <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
        <exclusion>
          <artifactId>spring-boot-starter</artifactId>
          <groupId>org.springframework.boot</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>io.seata</groupId>
      <artifactId>seata-all</artifactId>
      <version>0.9.0</version>
      <exclusions>
        <exclusion>
          <artifactId>fastjson</artifactId>
          <groupId>com.alibaba</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- SpringCloud alibaba nacos -->
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- openfeign -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--引入公共api包-->
    <dependency>
      <groupId>com.antherd.springcloud</groupId>
      <artifactId>cloud-api-commons</artifactId>
      <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot 整合Web组件 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- actuator监控信息完善 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- mybatis -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
    <!--mysql-connector-java-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--jdbc-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</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:3306/seata_order?useSSL=false
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

然后把seata上一篇修改过的两个文件:conf/file.conf、conf/registry.conf 复制到resource目录下

4. 主启动

新建类:com.antherd.springcloud.alibaba.SeataOrderMainApp2001

package com.antherd.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderMainApp2001 {

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

5 写业务

新建类:com.antherd.springcloud.alibaba.config.DataSourceProxyConfig

package com.antherd.springcloud.alibaba.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

/**
 * 使用Seata对数据源进行代理
 */
@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();
  }
}

新建类:com.antherd.springcloud.alibaba.config.MybatisConfig

package com.antherd.springcloud.alibaba.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan({"com.antherd.springcloud.alibaba.dao"})
public class MybatisConfig {

}

新建类:com.antherd.springcloud.alibaba.domain.Order

package com.antherd.springcloud.alibaba.domain;

import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

  private Long id;

  private Long userId;

  private Long productId;

  private Integer count;

  private BigDecimal money;

  private Integer status; // 订单状态:0:创建中;1:已完结
}

新建类:com.antherd.springcloud.alibaba.dao.OrderDao

package com.antherd.springcloud.alibaba.dao;

import com.antherd.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderDao {

  // 1.新建订单
  void create(Order order);

  // 2.修改订单状态,从0改为1
  void update(@Param("userId") Long userId, @Param("status") Integer status);
}

新建类:com.antherd.springcloud.alibaba.service.OrderService

package com.antherd.springcloud.alibaba.service;

import com.antherd.springcloud.alibaba.domain.Order;

public interface OrderService {

  void create(Order order);
}

新建类:com.antherd.springcloud.alibaba.service.impl.OrderServiceImpl

package com.antherd.springcloud.alibaba.service.impl;

import com.antherd.springcloud.alibaba.dao.OrderDao;
import com.antherd.springcloud.alibaba.domain.Order;
import com.antherd.springcloud.alibaba.service.AccountService;
import com.antherd.springcloud.alibaba.service.OrderService;
import com.antherd.springcloud.alibaba.service.StorageService;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

  @Resource
  private OrderDao orderDao;

  @Resource
  private StorageService storageService;

  @Resource
  private AccountService accountService;

  /**
   * 下订单->减库存->减余额->改状态
   * @param order
   */
  @Override
  public void create(Order order) {
    log.info("***** 开始新建订单");
    orderDao.create(order);

    log.info("***** 订单微服务开始调用库存,做扣减Count");
    storageService.decrease(order.getProductId(), order.getCount());
    log.info("***** 订单微服务开始调用库存,做扣减end");

    log.info("***** 订单微服务开始调用账户,做扣减money");
    accountService.decrease(order.getUserId(), order.getMoney());
    log.info("***** 订单微服务开始调用账户,做扣减end");

    log.info("***** 修改订单状态开始");
    orderDao.update(order.getUserId(), 0);
    log.info("***** 修改订单状态结束");

    log.info("***** 下订单结束了,O(∩_∩)O哈哈~");
  }
}

新建类:com.antherd.springcloud.alibaba.service.AccountService

package com.antherd.springcloud.alibaba.service;

import com.antherd.springcloud.entities.CommonResult;
import java.math.BigDecimal;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-account-service")
public interface AccountService {

  @PostMapping(value = "/account/decrease")
  public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

新建类:com.antherd.springcloud.alibaba.service.StorageService

package com.antherd.springcloud.alibaba.service;

import com.antherd.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service")
public interface StorageService {

  @PostMapping(value = "/storage/decrease")
  public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

新建类:com.antherd.springcloud.alibaba.controller.OrderController

package com.antherd.springcloud.alibaba.controller;

import com.antherd.springcloud.alibaba.domain.Order;
import com.antherd.springcloud.alibaba.service.OrderService;
import com.antherd.springcloud.entities.CommonResult;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

  @Resource
  private OrderService orderService;

  @GetMapping(value = "/order/create")
  public CommonResult create(Order order) {

    orderService.create(order);
    return new CommonResult(200, "订单创建成功");
  }
}

在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.antherd.springcloud.alibaba.dao.OrderDao">
  <resultMap id="BaseResultMap" type="com.antherd.springcloud.alibaba.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>

seata-storage-service2002seata-account-service2003

因为篇幅有限,源码均上传至 GitHub:https://github.com/geyiwei-suzhou/cloud2020

seata-storage-service2002、seata-account-service2003 源码这里就不重复贴出来了

启动测试

启动:seata-order-service2001、seata-storage-service2002、seata-account-service2003 三个模块

访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 发现下单成功(订单生成、库存扣减、账户扣除、订单状态修改)

接下来,修改 seata-account-service2003模块中com.antherd.springcloud.alibaba.service.impl.AccountServiceImpl.decrease 方法:

@Override
public void decrease(Long userId, BigDecimal money) {

  log.info("***** account-service中扣减账户余额开始");
  // 模拟超时异常,全局事务回滚
  try {
    TimeUnit.SECONDS.sleep(20);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  accountDao.decrease(userId, money);
  log.info("***** storage-service中扣减账户余额结束");
}

访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 发现下单出错(Read timed out executing POST http://seata-account-service/account/decrease?userId=1&money=100),这是我们查看数据库,发现 订单生成、库存扣减、账户扣除,但是订单状态还是创建中,这就导致了严重的数据问题(钱被扣了,订单还是未支付状态)

com.antherd.springcloud.alibaba.service.impl.create 方法上添加

@GlobalTransactional(name = "antherd-create-order", rollbackFor = Exception.class)

再试一下

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值