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

视频链接: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)

再试一下

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
1、课程简介Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。       在本套课程中,我们将全面的讲解Spring Cloud技术栈, 从环境的部署到技术的应用,再到项目实战,让我们不仅是学习框架技术的使用,而且可以学习到使用Spring Cloud如何解决实际的问题。Spring Cloud各个组件相互配合,合作支持了一套完整的微服务架构。- 注册中心负责服务的注册与发现,很好将各服务连接起来- 断路器负责监控服务之间的调用情况,连续多次失败进行熔断保护。- API网关负责转发所有对外的请求和服务- 配置中心提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息- 链路追踪技术可以将所有的请求数据记录下来,方便我们进行后续分析- 各个组件又提供了功能完善的dashboard监控平台,可以方便的监控各组件的运行状况2、适应人群有一定的Java基础,并且要有一定的web开发基础。3、课程亮点       系统的学习Spring Cloud技术栈,由浅入深的讲解微服务技术。涵盖了基础知识,原理剖析,组件使用,源码分析,优劣分析,替换方案等,以案例的形式讲解微服务中的种种问题和解决方案l  微服务的基础知识n  软件架构的发展史n  微服务的核心知识(CAP,RPC等)l  注册中心n  Eureka搭建配置服务注册n  Eureka服务端高可用集群n  Eureka的原理和源码导读n  Eureka替换方案Consuln  Consul下载安装&服务注册&高可用l  服务发现与服务调用n  Ribbon负载均衡基本使用&源码分析n  Feign的使用与源码分析n  Hystrix熔断(雪崩效应,Hystrix使用与原理分析)n  Hystrix替换方案Sentinell  微服务网关n  Zuul网关使用&原理分析&源码分析n  Zuul 1.x 版本的不足与替换方案n  SpringCloud Gateway深入剖析l  链路追踪n  链路追踪的基础知识n  Sleuth的介绍与使用n  Sleuth与Zipkin的整合开发l  配置中心n  SpringClond Config与bus 开发配置中心n  开源配置中心Apollo4、主讲内容章节一:1.     微服务基础知识2.     SpringCloud概述3.     服务注册中心Eureka4.     Eureka的替换方案Consul章节二:1.     Ribbon实现客户端负载均衡2.     基于Feign的微服务调用3.     微服务熔断技术Hystrix4.     Hystrix的替换方案Sentinel章节三:1.     微服务网关Zuul的基本使用2.     Zuul1.x 版本的不足和替换方案3.     深入SpringCloud Gateway4.     链路追踪Sleuth与Zipkin章节四:1.     SpringCloud Config的使用2.     SpringCloud Config结合SpringCloud Bus完成动态配置更新3.     开源配置中心Apollo

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值