分布式事务——Seata概念及使用

分布式事务——Seata

1 Seata概念及术语

Seata是由Alibaba开源的一个用于解决分布式事务问题的框架

Seata官网地址
Seata是由1+3组成:
1:Transaction ID【XID】:全局唯一的事务ID
3:三个组件,TC(事务协调器)、TM(事务管理器)、RM(资源管理器)

1.1 TC、TM、RM关系

在这里插入图片描述

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。

班主任老师向授课老师传达上课请求,并且开启一个直播,直播链接就是XID

  1. XID在微服务调用链路的上下文传播

老师和同学在互相传播直播链接(XID)

  1. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖

同学们加入直播,受到直播老师的管辖

  1. TM向TC发起针对XID的全局提交或回滚决议

班主任老师,发现上课的人已经来齐了,给直播老师说"可以开始上课"

  1. TC调度XID下管辖的全部分支事务完成提交或回滚请求

直播过程中,不认真听讲,玩手机的,直接踢出会议

1.2 本地启动Seata

①下载SeataServer:seata-server-0.9.0.zip

此处我使用0.9.0版本做示范,其他版本大同小异
GitHub下载地址:https://github.com/seata/seata/releases

②下载完成之后,将默认配置文件备份,然后修改配置文件
在这里插入图片描述
修改file.conf:

  1. 将service的tx_group修改为自己的,名字随便起
    在这里插入图片描述

下面是我的配置

service {
  #vgroup->rgroup
  vgroup_mapping.zi_tx_group = "default" # 修改为自己的
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

  1. 将file.conf的store模块的模式修改为db
    在这里插入图片描述
  2. 将file.conf模块的db修改为自己的
    在这里插入图片描述
    因为我使用的是mysql8,所以driver-class-name添加了cj,并且在url上设置了时区
  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC"
    user = "root"
    password = "123456"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
  1. 备份包registry.conf之后,修改文件内容,我们将注册中心改为nacos
    在这里插入图片描述
    ③修改lib目录下,将mysql驱动改为8.0以上版本的【jar包可以自己通过maven下载】

因为我是mysql8,所以采用8的驱动,如果大家不是的话,可以不用修改,并且前面file.conf里面数据库配置的驱动类也不用修改

在这里插入图片描述
添加之后,删除原来默认的5的jar包

④建立seata数据库,执行SQL文件
在这里插入图片描述

-- 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`)
);

⑤启动本地nacos【如果本地没有下载的话,自行下载启动即可】
⑥双击bin目录下的seata-server.bat
在这里插入图片描述

表明启动成功

2 在微服务中应用Seata

项目说明:

此处演示Seata使用,我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务

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

该操作跨越三个数据库,有两次远程调用(RPC),因此会涉及到分布式事务问题

业务需求:下订单 - 减库存 - 扣余额 - 改(订单)状态

2.1 项目准备

①建库建表语句【项目中使用到的】
seata_order库及表:

CREATE DATABASE seata_order;
USE seata_order;
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 CHARSET=utf8;

seata_storage库及表:

CREATE DATABASE seata_storage;
USE seata_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=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);

seata_account库及表:

CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(
    id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    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=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);

②为所有数据库创建undo_log表,因为会涉及到事务回滚,方便记录

seata、seata_account、seata_order、seata_storage分别创建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.2 order-service模块创建

2.3 order-account模块创建

2.4 order-storage模块创建

这几个模块的代码这里我就不贴出来了,我将代码提交到了gitee上
Gitee代码地址

注意:下载下来的是我手动添加了异常的代码,如果想要演示正常状态,去掉AccountServiceImpl中的
"TimeUnit.SECONDS.sleep(20);"即可	

如果在测试过程中遇到什么问题了,欢迎提问

2.5 测试(正常情况)

①数据库状态

  • seata_account中的t_account表:
    在这里插入图片描述

用户的user_id为1,用户总金额为1000,剩余金额为1000

  • seata_order中的t_order表:
    在这里插入图片描述

订单表中初始没有任何数据

  • seata_storage中的t_storage表:
    在这里插入图片描述

产品的product_id为1,产品总数量为100,剩余库存为100

②发起请求【此时没有设置异常】
在浏览器中输入:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

表明创建订单,用户id为1,产品id为1,订单中产品数量为10,花了100元

浏览器结果:
在这里插入图片描述
库存表(t_storage):
在这里插入图片描述
订单表(t_order):
在这里插入图片描述
账户表(t_account):
在这里插入图片描述

2.6 测试(模拟异常与使用Seata)

①模拟异常

模拟Feign超时异常,在AccountServiceImpl中添加超时异常:

@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Autowired
    private AccountDao accountDao;

    /**
     * 扣减账户余额
     * @param userId
     * @param money
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        logger.info("---->account-service扣减账户余额");
        //模拟超时异常,全局事务回滚
        //暂停几秒钟线程
        try{
            TimeUnit.SECONDS.sleep(20);
            accountDao.decrease(userId, money);
            logger.info("----->account-service扣减账户余额结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

再用浏览器发送一次请求:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
可以看到服务器报错:
在这里插入图片描述
此时数据库状态:

可以看到下面的订单表,订单状态为0,表明订单未完成
在这里插入图片描述
查看库存表与账户表:
在这里插入图片描述
在这里插入图片描述

此时发现库存和账户都更改了,也就是库存减了,钱扣了,但是订单却没有完成,这显然是不合理的
注意:并且由于feign的重试机制,库存和账户可能都会扣减多次

②使用Seata中的@GlobalTransactional注解解决

@GlobalTransactional只需要在我们的业务方法上添加即可,这样就可以保证全局事务一致

  1. 我们在seata-order-service模块中的orderServiceIml的create方法添加Seata注解
    //创建订单:调用库存服务扣减库存 - 调用账户服务扣减账户余额 - 修改订单状态
    @GlobalTransactional
    @Override
    public void create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        orderDao.createOrder(order);
        //2 扣减库存
        log.info("----->调用storageService,扣减count");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->扣减count结束");
        //3 扣减账户
        log.info("----->调用storageService,扣减money");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->扣减money结束");

        //4 修改订单状态,从0-1,代表订单完成
        log.info("----->修改订单状态");
        orderDao.updateOrder(order.getUserId(), 0);
        log.info("----->修改订单状态结束");

    }
  1. 重启服务,重新测试
  • 查看服务器,依然报超时异常,此时查看数据库
  • 可以发现订单表中没有新增订单,依然是前两次的数据,并且库存表与账户表也没有扣减,表明在发生异常的时候,事务进行了回滚
    在这里插入图片描述

3 Seata原理

3.1 测试效果

默认Seata使用的是AT模式
其他模式:TCC、Saga、XA模式
官网地址

我们之前在seata数据库中生成了以下几张表:
在这里插入图片描述
并且在另外三个数据库中seata_order、seata_account、seata_storage中生成了对应的undo_log
那么,这些表是干什么用的呢?
①我们首先将AccountServiceImpl中的睡眠代码注释掉,然后在AccountServiceImpl中打一个断点,所有服务debug启动,在浏览器中输入:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
然后,让断点停止在那个位置,此时访问数据库
在这里插入图片描述

②查看数据库状态

  1. seata数据库:
  • branch_table:
    在这里插入图片描述

xid:全局事务id
resource_id:对应另外三个业务数据库
branch_type:AT

  • global_table:
    在这里插入图片描述
  • lock_table:
    在这里插入图片描述
  1. seata_account:
  • undo_log:
    在这里插入图片描述
  1. seata_order:
  • undo_log:
    在这里插入图片描述
  1. seata_storage:
  • undo_log:
    在这里插入图片描述

3.2 解释

AT模式对业务无入侵:

  • 一阶段加载
  • 二阶段提交
  • 三阶段回滚
保存原快照before image - 执行业务SQL - 保存新快照 after image
"Spring的AOP"

过程:业务SQL - 解析SQL语义 - 提取表元数据 - 【保存原快照before image - 执行业务SQL - 保存新快照 after image - 生成行锁】 - 提交业务SQL、undo/redo log、行锁
【1】前置SQL:before image

前置SQL:select age from t where id = 1; //结果:age=22

前置SQL查询出来之前的age为22
【2】业务SQL

业务SQL:update t set age = 28 where id = 1;

业务SQL修改年龄为28
【3】后置SQL:after image

后置SQL:select age form t where id = 1; //结果:age = 28

【4】假如此时业务出现了异常,需要回滚,则反向update,将age从28改为之前的22

所有的操作都记录在一张表中

注意:如果此时反向update的时候发现age不是28了(脏写),则会转为人工处理【类比CAS,乐观锁】

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值