玩转Spring Cloud Alibaba之Seata「应用篇」

前言

距离上一次学习Spring Cloud全家桶已经是一年前,由于平时工作着CRUD,相关的知识点都忘却了,正好趁着这次复习,温固而知新。

简介

为了模拟分布式事务的实际应用场景,以三个微服务来作为示例

分别是订单服务、库存服务、账户服务

一、业务流程:

用户下单,会在订单微服务创建一个订单,然后通过feign远程调用库存服务接口来扣减下单的库存,再通过远程调用账户微服务来扣减用户余额

简单来说,下单->扣库存->扣余额->更新订单状态

整个下单流程涉及到3个不同的数据库的操作,2次远程调用,明显会产生分布式事务问题

二、准备工作

2.1创建seata数据库

建表

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `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`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

2.2创建业务数据库

  • seata_order:存储订单的数据库;

  • seata_storage:存储库存的数据库;

  • seata_account:存储账户信息的数据库。

建表

  • seata_order库中新建t_order

CREATE TABLE `t_order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `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:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SELECT * FROM t_order;
  • seata_storage库中新建t_storage

CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');

SELECT * FROM t_storage;
  • seata_account库中新建t_account

CREATE TABLE t_account(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
    `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
    `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
    `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=1 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个库分别建对应的回滚日志表

-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
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;

至此数据库准备工作完成

2.3安装并启动Seata服务

下载地址:http://seata.io/zh-cn/blog/download.html

我这里本地用的1.3.0版本

1、解压到本地目录

unzip seata-server-1.3.0.zip

2、修改配置文件

vim conf/file.conf
····
  #只修改db选项,其他的配置项省略
## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/seata"   #修改url
    user = "root"   #修改用户名
    password = "password"   #修改密码
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
····
vim conf/registy.conf
·····
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"   #type改为nacos

  nacos {
    application = "seata-server"
    serverAddr = "centos1:8848"    #修改nacos地址
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"    #nacos账号
    password = "nacos"    #nacos密码
  }
····

启动seata(注意这里要先启动nacos,否则会报错)

nohup sh seata-server.sh > ./seata.log 2>&1 &

在nacos服务列表看到seata-server实例,说明启动成功

玩转Spring Cloud Alibaba之Seata「应用篇」_第1张图片

三、搭建三个微服务

使用代码生成器生成通用的增删查改操作,这里不再赘述,如有需要自行clone源码

Gitee地址

首先来看看下单前数据

order表

玩转Spring Cloud Alibaba之Seata「应用篇」_第2张图片

account表

玩转Spring Cloud Alibaba之Seata「应用篇」_第3张图片

storage表

玩转Spring Cloud Alibaba之Seata「应用篇」_第4张图片

下单业务类OrderServiceImpl,伪代码如下

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改状态
     */
    @Override
    public void create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        orderDao.create(order);

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

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

        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("----->修改订单状态结束");

        log.info("----->恭喜你,下单成功!~");

    }
}

可以看到,业务代码基本与上描的流程一致

调用下单接口创建一个订单

localhost:2001/order/create?userId=1&productId=1&count=10&money=100

提示下单成功

玩转Spring Cloud Alibaba之Seata「应用篇」_第5张图片

来看数据

玩转Spring Cloud Alibaba之Seata「应用篇」_第6张图片
玩转Spring Cloud Alibaba之Seata「应用篇」_第7张图片
玩转Spring Cloud Alibaba之Seata「应用篇」_第8张图片

可以看到,订单已被成功创建,对应库存及余额也得到正确的扣减,但这只是最理想的情况,

此时来模拟出现一下异常情况,看看数据是否仍然符合我们的预期

在扣减账户余额处添加异常代码,如下

 public void decrease(Long userId, BigDecimal money) {
        int i = 10/0;
        LOGGER.info("------->account-service中扣减账户余额开始");
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }

再次创建订单

localhost:2001/order/create?userId=1&productId=1&count=10&money=100
玩转Spring Cloud Alibaba之Seata「应用篇」_第9张图片
玩转Spring Cloud Alibaba之Seata「应用篇」_第10张图片
玩转Spring Cloud Alibaba之Seata「应用篇」_第11张图片

由数据可以看出,订单创建成功库存也减了,钱却没有成功扣减,相当于商品免费送给客户···这可是大问题啊

四、解决方案

使用Seata全局事务注解 @GlobalTransactional

找到业务的入口:订单服务的OrderServiceImpl,用@GlobalTransactional标注create()方法

/**
     * @GlobalTransactional的属性说明
     * --name:随便命名,只要名字不冲突
     * --rollbackFor:发生什么异常的时候进行回滚,这里定义为Exception.class表示发生任何异常都进行回滚
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        orderDao.create(order);

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

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

        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("----->修改订单状态结束");

        log.info("----->gongxini xiadan chenggong ");

    }

再次创建订单,报错页面依然提示500,但此时查看数据库,发现三个表的数据照旧,没有任何变动,那么说明该事务已经被成功被回滚。

你可能感兴趣的:(个人学习,Spring,Cloud,java,spring,开发语言)