首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >SpringCloud Alibaba Seata处理分布式事务

SpringCloud Alibaba Seata处理分布式事务

作者头像
OY
发布2022-03-17 19:34:03
发布2022-03-17 19:34:03
5850
举报
文章被收录于专栏:OY_学习记录OY_学习记录

SpringCloud Alibaba Seata 处理分布式事务

一、分布式事务问题

① 分布式前

​ 单机单库没这个问题 从 1: 1 -> 1:N -> N: N

② 分布式之后

​ 单体应用被拆分成微服务应用, 原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。

​ 业务操作需要调用三个服务来完成。 此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。

③ 一句话

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

二、Seata 简介

① 是什么

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

​ 官网地址 http://seata.io/zh-cn/

② 能干嘛

​ 一个典型的分布式事务过程

  1. 分布式事务处理过程的-ID+三组件模型
  • Transaction ID XID 全局唯一的事务 ID
  • 3 组件概念 ==Transaction Coordinator(TC)== 事务协调器, 维护全局事务的运行状态, 负责协调并驱动全局事务的提交或回滚; ==Transaction Manager(TM)== 控制全局事务的边界, 负责开启一个全局事务, 并最终发起全局提交或全局回滚的决议; ==Resource Manager(RM)== 控制分支事务, 负责分支注册, 状态汇报, 并接收事务协调器的指令, 驱动分支(本地) 事务的提交和回滚;
  1. 处理过程
  1. 去哪下 发布说明: https://github.com/seata/seata/releases
  2. 怎么玩
  • Spring 本地@Transactional
  • 全局@GlobalTransactional

三、Seata-Server 安装

① 官网地址

代码语言:javascript
复制
http://seata.io/zh-cn/

② 下载版本

③ 修改配置文件

  • seata-server-0.9.0.zip 解压到指定目录并修改 conf 目 录下的 file.conf 配置文件
  1. 先备份原始 file.conf 文件 、

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

file.conf

  1. service 模块
代码语言:javascript
复制
vgroup_mapping.fsp_tx_group = "default"
  1. store 模块
代码语言:javascript
复制
mode = "db"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "你自己的密码"

④ mysql5.7 数据库新建库 seata

代码语言:javascript
复制
create database seata;

⑤ 在 seata 库里建表

建 表 db_store.sql 在 \seata-server-0.9.0\seata\conf 目 录 里 面 db_store.sq

代码语言:javascript
复制
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
  `xid` varchar(128)  not null,
  `transaction_id` bigint,
  `status` tinyint not null,
  `application_id` varchar(32),
  `transaction_service_group` varchar(32),
  `transaction_name` varchar(128),
  `timeout` int,
  `begin_time` bigint,
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`xid`),
  key `idx_gmt_modified_status` (`gmt_modified`, `status`),
  key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
  `branch_id` bigint not null,
  `xid` varchar(128) not null,
  `transaction_id` bigint ,
  `resource_group_id` varchar(32),
  `resource_id` varchar(256) ,
  `lock_key` varchar(128) ,
  `branch_type` varchar(8) ,
  `status` tinyint,
  `client_id` varchar(64),
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`branch_id`),
  key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

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

代码语言:javascript
复制
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }

==目的是==: 指明注册中心为 nacos, 及修改 nacos 连接信息

⑦ 先启动 Nacos 端口号 8848

⑧ 再启动 seata-server

  • seata-server.bat

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

① 以下演示都需要先启动 Nacos 后启动 Seata, 保证两个都 OK

② 分布式事务业务说明

==业务说明==

下订单–>扣库存–>减账户( 余额)

③ 创建业务数据库

  • seata_order: 存储订单的数据库
  • seata_storage:存储库存的数据库
  • seata_account: 存储账户信息的数据库

建表 SQL

代码语言:javascript
复制
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE  seata_account;

④ 按照上述 3 库分别建对应业务表

  • seata_order 库下建 t_order 表
代码语言:javascript
复制
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;
  • seata_storage 库下建 t_storage 表
代码语言:javascript
复制
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 t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
  • seata_account 库下建 t_account 表
代码语言:javascript
复制
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;

⑤ 按照上述 3 库分别建对应的回滚日志表

  • 订单-库存-账户 3 个库下都需要建各自的回滚日志表
  • \seata-server-0.9.0\seata\conf 目录下的 db_undo_log.sql

建表 SQL

代码语言:javascript
复制
drop table `undo_log`;
CREATE TABLE `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;

⑥ 最终效果

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

1. 业务需求
  • 下订单->减库存->扣余额->改(订单) 状态
2. 新建订单 Order-Module

建 Module : seata-order-service2001

POM

代码语言:javascript
复制
<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>

</project>

YML

代码语言:javascript
复制
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:
    # 当前数据源操作类型
    type: com.alibaba.druid.pool.DruidDataSource
    # mysql驱动类
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 6090
feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info

mybatis:
  mapper-locations: classpath:mapper/*.xml

file.conf

​ 拷贝 seata-server/conf 目录下的 file.conf 到项目 resources 里面

registry.conf

​ 拷贝 seata-server/conf 目录下的 registry.conf 到项目 resources 里面

domain

  • CommonResult
代码语言:javascript
复制
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
   private Integer code;
   private String message;
   private T date;

   public CommonResult(Integer code, String message){
       this(code, message,null);
   }
}
  • Order
代码语言:javascript
复制
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

    private Long userId;

    private Long productId;

    private Integer count;

    private BigDecimal money;

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

}

Dao 接口及实现

  • OrderDao
代码语言:javascript
复制
@Mapper
public interface OrderDao {

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

    // 修改订单状态,从零开始
    void update(@Param("userId")Long userId, @Param("status") Integer status);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 OrderMapper.xml
代码语言:javascript
复制
<?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.oy.springcloud.dao.OrderDao">

    <resultMap id="BaseResultMap" type="com.oy.springcloud.domain.Order">
        <id column="id" property="id" jdbcType="BIGINT"></id>
        <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>

   <!-- void create(Order order);-->
    <insert id="create">
        insert into t_order(id,user_id, product_id, count, money,status) values(null,#{userId},#{productId},#{count},#{money},0);
    </insert>
   <!-- void update(@Param("useId")Long userId, @Param("status") Integer status);-->
    <update id="update">
        update t_order set status = 1 where user_id=#{userId} and status = #{status};
    </update>

</mapper>

Service 接口及实现

  • OrderService
代码语言:javascript
复制
public interface OrderService {
    void create(Order order);
}
  • OrderServiceImpl
代码语言:javascript
复制
@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("----->订单微服务开始调用库存, 做扣减 Count");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("----->订单微服务开始调用库存, 做扣减 end");

        //扣减账户
        log.info("----->订单微服务开始调用账户, 做扣减 Money");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("----->订单微服务开始调用账户, 做扣减 end");
        //修改订单状态, 从零到 1 代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("----->修改订单状态结束");
        log.info("----->下订单结束了");
    }
}
  • StorageService
代码语言:javascript
复制
@FeignClient(value = "seata-storage-service")
public interface StorageService {

    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
  • AccountService
代码语言:javascript
复制
@FeignClient(value = "seata-account-service")
public interface AccountService {

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

Controller

代码语言:javascript
复制
@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult create(Order order)
    {
        orderService.create(order);
        return new CommonResult(200,"订单创建成功");
    }
}

Config 配置

  • MyBatisConfig
代码语言:javascript
复制
@Configuration
@MapperScan({"com.oy.springcloud.dao"})
public class MyBatisConfig {

}
  • DataSourceProxyConfig
代码语言:javascript
复制
package com.oy.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
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;

import javax.sql.DataSource;

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

}

主启动

代码语言:javascript
复制
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源自动创建的配置
public class SeataOrderMainApp2001 {
     public static void main(String[] args) {
           SpringApplication.run(SeataOrderMainApp2001.class, args);
      }
}
3.新建库存 Storage-Module

**seata-order-service2002 **

POM

代码语言:javascript
复制
<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>

YML

代码语言:javascript
复制
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:3306/seata_storage
    username: root
    password: 123456

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

file.conf

registry.conf

  • 拷贝 seata-server/conf 目录下的file.confregistry.conf 到项目 resources 里面

domain

  • CommonResult
代码语言:javascript
复制
@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);
    }
}
  • Storage
代码语言:javascript
复制
@Data
public class Storage {

    private Long id;

    // 产品id
    private Long productId;

    // 总库存
    private Integer total;

    // 已用库存
    private Integer used;

    // 剩余库存
    private Integer residue;
}

Dao 接口及实现

  • StorageDao
代码语言:javascript
复制
@Mapper
public interface StorageDao {

    /**
     * 扣减库存信息
     */
    void decrease(@Param("productId")Long productId, @Param("count") Integer count);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 StorageMapper.xml
代码语言:javascript
复制
<?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.oy.springcloud.dao.StorageDao">

    <resultMap id="BaseResultMap" type="com.oy.springcloud.domain.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"/>
    </resultMap>

    <!--void decrease(@Param("productId")Long productId, @Param("count") Integer count);-->
    <update id="decrease">
        UPDATE  t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId}
    </update>
</mapper>

Service 接口及实现

  • StorageService
代码语言:javascript
复制
public interface StorageService {

    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}
  • StorageServiceImpl
代码语言:javascript
复制
@Service
@Slf4j
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 中扣减库存结束");
    }
}

Controller

代码语言:javascript
复制
@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,"扣减库存成功! ");
    }
}

Config 配置

  • MyBatisConfig
代码语言:javascript
复制
@Configuration
@MapperScan({"com.oy.springcloud.dao"})
public class MyBatisConfig {
}
  • DataSourceProxyConfig
代码语言:javascript
复制
package com.oy.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
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;

import javax.sql.DataSource;

/**
 * @Author OY
 * @Date 2020/11/16
 */
@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();
    }

}

主启动

代码语言:javascript
复制
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002 {
     public static void main(String[] args) {
           SpringApplication.run(SeataStorageServiceApplication2002.class, args);
      }
}
4.新建账户 Account-Module

seata-account-service2003

POM

代码语言:javascript
复制
    <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>

</project>

YML

代码语言:javascript
复制
server:
  port: 2003

spring:
  application:
    name: seata-account-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:3306/seata_account
    username: root
    password: 6090

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

file.conf registry.conf

domain

  • CommonResult
代码语言:javascript
复制
@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);
    }
}
  • Account
代码语言:javascript
复制
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long id;
    /**
     * 用户 id
     */
    private Long userId;
    /**
     * 总额度
     */
    private BigDecimal total;
    /**
     * 已用额度
     */
    private BigDecimal used;
    /**
     * 剩余额度
     */
    private BigDecimal residue;
}

Dao 接口及实现

  • AccountDao
代码语言:javascript
复制
@Mapper
public interface AccountDao {
    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
  • resources 文 件 夹 下 新 建 mapper 文 件 夹 后 添 加 AccountMapper.xml
代码语言:javascript
复制
<?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.oy.springcloud.dao.AccountDao">
    <resultMap id="BaseResultMap"
               type="com.oy.springcloud.domain.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"/>
    </resultMap>
    <update id="decrease">
        UPDATE t_account SET residue = residue - #{money},used = used + #{money} WHERE user_id = #{userId};
    </update>
</mapper>

Service 接口及实现

  • AccountService
代码语言:javascript
复制
public interface AccountService {
    /**
     * 扣减账户余额
     */
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
  • AccountServiceImpl
代码语言:javascript
复制
public class AccountServiceImpl implements AccountService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Resource
    AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service 中扣减账户余额开始");
        try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e)
        { e.printStackTrace(); }
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service 中扣减账户余额结束");
    }
}

Controller

代码语言:javascript
复制
@RestController
public class AccountController {
    @Resource
    AccountService accountService;
    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId,
                                 @RequestParam("money") BigDecimal money){
        accountService.decrease(userId,money);return new CommonResult(200,"扣减账户余额成功! ");
    }
}

Config 配置

  • MyBatisConfig
代码语言:javascript
复制
@Configuration
@MapperScan({"com.oy.springcloud.dao.AccountDao"})
public class MyBatisConfig {

}
  • DataSourceProxyConfig
代码语言:javascript
复制
package com.oy.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
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;
import javax.sql.DataSource;


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

}

主启动

代码语言:javascript
复制
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003 {

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

注意:如果报错 A component required a bean of type 'com.oy.springcloud.dao.AccountDao' that could not be found.

请在启动类上加:

六、Test

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

我们只需要使用一个 @GlobalTransactional 注解在业务方法上;

② 数据库初始化情况

③ 正常下单

  • http://localhost:2001/order/create?userId=1&producId=1&count=10&money=100

数据库情况

④ 超时异常, 没加@GlobalTransactional

AccountServiceImpl 添加超时

  • 数据库情况 余额和库存都已经扣除,但是订单状态还是未支付,结果就是,这个订单仍要再次支付
  • 故障情况
  1. 当库存和账户余额扣减后, 订单状态并没有设置为已经完成,没有从零改为 1
  2. 而且由于 feign 的重试机制, 账户余额还有可能被多次扣减

⑤ 超时异常, 添加@GlobalTransactional

AccountServiceImpl 添加超时

OrderServiceImpl @GlobalTransactional

下单后数据库数据并没有任何改变, 记录都添加不进来

七、Seata 之原理简介

① Seata

​ 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案 Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架 2020 起初, 参加工作后用 1.0 以后的版本

② 再看 TC/TM/RM 三大组件

分布式事务的执行流程

  1. TM 开启分布式事务(TM 向 TC 注册全局事务记录)
  2. 换业务场景, 编排数据库, 服务等事务内资源(RM 报资源准备状态)
  3. TM 结束分布式事务, 事务一阶段结束(TM 通知 TC 滚分布式事务)
  4. TC 汇总事务信息, 决定分布式事务是提交还是回滚
  5. TC 通知所有 RM 提交/回滚资源, 事务二阶段结束

③ AT 模式如何做到对业务的无侵入

  • 一阶段加载
  • 二阶段提交
  • 二阶段回滚

④ 补充

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SpringCloud Alibaba Seata 处理分布式事务
    • 一、分布式事务问题
      • ① 分布式前
      • ② 分布式之后
      • ③ 一句话
    • 二、Seata 简介
      • ① 是什么
      • ② 能干嘛
    • 三、Seata-Server 安装
      • ① 官网地址
      • ② 下载版本
      • ③ 修改配置文件
      • ④ mysql5.7 数据库新建库 seata
      • ⑤ 在 seata 库里建表
      • ⑥ 修 改 seata-server-0.9.0\seata\conf 目 录 下 的 registry.conf 配置文件 、
      • ⑦ 先启动 Nacos 端口号 8848
      • ⑧ 再启动 seata-server
    • 四、订单/库存/账户业务数据库准备
      • ① 以下演示都需要先启动 Nacos 后启动 Seata, 保证两个都 OK
      • ② 分布式事务业务说明
      • ③ 创建业务数据库
      • ④ 按照上述 3 库分别建对应业务表
      • ⑤ 按照上述 3 库分别建对应的回滚日志表
      • ⑥ 最终效果
    • 五、订单/库存/账户业务微服务准备
    • 六、Test
      • ① 下订单->减库存->扣余额->改(订单) 状态
      • ② 数据库初始化情况
      • ③ 正常下单
      • ④ 超时异常, 没加@GlobalTransactional
      • ⑤ 超时异常, 添加@GlobalTransactional
    • 七、Seata 之原理简介
      • ① Seata
      • ② 再看 TC/TM/RM 三大组件
      • ③ AT 模式如何做到对业务的无侵入
      • ④ 补充
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档