分布式事务解决方案(一)Seata集成和使用

前言

什么是Seata?
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
官网 https://seata.io/zh-cn/
架构图
在这里插入图片描述

准备工作

准备业务场景

准备并启动nacos服务 nacos官网
在这里插入图片描述

为了实现分布式事务的场景需要先搭建好一些微服务并注册到nacos
在这里插入图片描述

订单服务 - 负责创建订单,减库存,扣余额

    @GetMapping("/createOrder")
    @GlobalTransactional
    public String createOrder(){

        log.info(Thread.currentThread().getName()+":下单");
        UserOrder entity = new UserOrder();
        entity.setMoney(100L);
        entity.setOrderNo(System.currentTimeMillis()+"");
        orderService.save(entity); //下订单
        
        String result = storeService.countDown(1L);//减库存
        log.info("减库存:"+result);

        String result1 = userService.spendMoney(1L);//扣钱
        log.info("扣钱:"+result1);

        return "ok";
    }

库存服务 - 扣减库存

@PostMapping("/countDown/{id}")
    public String countDown(@PathVariable Long id){
        log.info(Thread.currentThread().getName()+":减库存!");

        GoodsStore byId = goodsStoreService.getById(id);
        byId.setCount(byId.getCount()-10);
        if(byId.getCount()<0){
            throw new RuntimeException("库存不足");
        }
        boolean res = goodsStoreService.updateById(byId);
        if(res){
            log.info(Thread.currentThread().getName()+":减库存成功");
        }else{
            log.error(Thread.currentThread().getName()+":减库存失败");
        }
        return "ok";
    }

用户服务 - 扣减账户余额

    @GetMapping("/money/spend/{id}")
    public String spendMoney(@PathVariable Long id){
        log.info("配置文件:"+name);
        log.info(Thread.currentThread().getName()+":扣钱");
        QueryWrapper<SysUserMoney> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(SysUserMoney::getUserId,id);
        SysUserMoney one = userMoneyService.getOne(wrapper);
        one.setMoney(one.getMoney()-200);
        if(one.getMoney()<0){
            throw new RuntimeException("余额不足!");
        }

        boolean res = userMoneyService.updateById(one);
        if(res){
            log.info(Thread.currentThread().getName()+":更新余额成功");
        }else{
            log.error(Thread.currentThread().getName()+":更新余额失败");
        }
        return "ok";
    }

业务场景:订单服务在下订单的时候会分别调用 库存服务扣减库存,用户服务扣减余额。任意一个服务调用失败都将进行事务回滚。

搭建Seata的TC Server

下载:
https://seata.io/zh-cn/blog/download.html
点击下载
下载解压修改配置
1.修改seata/seata-server-1.4.2/conf/registry.conf

# 注册中心
registry {
  # 注册中心类型 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-tc-server"
    serverAddr = "192.168.31.46:8848"
    group = "DEFAULT_GROUP" #nacos中的服务分组, 跟订单,仓促,用户服务同一个组
    namespace = "3920cea4-c7cc-42e2-a818-3423ec169157" #NACOS 的命名空间ID
    cluster = "CD" #集群名称
    username = "nacos"
    password = "nacos"
  }
}
# 配置中心 同上
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "192.168.31.46:8848"
    namespace = "3920cea4-c7cc-42e2-a818-3423ec169157"
    group = "DEFAULT_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seata-tc-server.properties"
  }
}
添加Nacos配置

在这里插入图片描述

seata-tc-server.properties 内容如下

store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.31.60:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
建库建表

创建MySQL数据库: 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(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
启动TC 服务
[root@localhost bin]# pwd
/usr/local/seata/seata-server-1.4.2/bin
[root@localhost bin]# ll
总用量 12
-rwxr-xr-x. 1 502 games 3685 425 2021 seata-server.bat
-rwxr-xr-x. 1 502 games 4212 425 2021 seata-server.sh
[root@localhost bin]# nohup ./seata-server.sh >log.out 2>1 &

在这里插入图片描述
到这里 Seata-tc-server 启动完成了

集成Seata

集成到每个参与事务的服务

引入依赖:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <!-- 排除 1.3.0 的依赖 改为1.4.2的依赖包 -->
                <exclusion>
                    <artifactId>seata-spring-boot-starter</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

修改配置:
application.yaml
建议复制粘贴,手写容易打错字。

# seata 分布式事务配置
seata:
  application-id: ${spring.application.name}
  tx-service-group: my-group # 事务组,根据这个获取集群名称
  registry:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.server-addr}
      group: DEFAULT_GROUP
      namespace: "3920cea4-c7cc-42e2-a818-3423ec169157"
      username: nacos
      password: nacos
      application: seata-tc-server
  service:
    vgroup-mapping: # 事务组与TC服务 集群的映射关系
      my-group: CD
  data-source-proxy-mode: XA  # 默认是AT模式

启动我们的三个服务

2022-01-06 18:58:52.159  INFO 2552 --- [           main] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.31.56:8091
2022-01-06 18:58:52.162  INFO 2552 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : RM will register :jdbc:mysql://192.168.31.60:3306/storecenter
2022-01-06 18:58:52.176  INFO 2552 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:RMROLE,address:192.168.31.56:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://192.168.31.60:3306/storecenter', applicationId='store-service', transactionServiceGroup='my-group'} >
2022-01-06 18:58:53.181  INFO 2552 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0x8b1b6db5, L:/192.168.31.53:61915 - R:/192.168.31.56:8091]
2022-01-06 18:58:53.201  INFO 2552 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 171 ms, version:1.4.2,role:RMROLE,channel:[id: 0x8b1b6db5, L:/192.168.31.53:61915 - R:/192.168.31.56:8091]
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@362b384c

通过日志是能够看到RM 成功注册到了 seata-tc-server 服务器上了。

测试

通过 @GlobalTransactional 注解配置事务的入口
在这里插入图片描述

正常提交

先测试正常的情况, 余额充足。
初始数据:
在这里插入图片描述
浏览器发起请求
在这里插入图片描述
再次查看数据
在这里插入图片描述
整个过程是处于正常的情况
创建了订单, 减了库存10, 扣了余额200
后台日志

2022-01-06 20:58:36.396  INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.31.56:8091:2459182619839676463]
2022-01-06 20:58:36.397  INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController       : http-nio-8080-exec-2:下单
2022-01-06 20:58:37.036  INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController       : 减库存:ok
2022-01-06 20:58:37.403  INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController       : 扣钱:ok
2022-01-06 20:58:37.513  INFO 14052 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.31.56:8091:2459182619839676463,branchId=2459182619839676465,branchType=XA,resourceId=jdbc:mysql://192.168.31.60:3306/ordercenter,applicationData=null
2022-01-06 20:58:37.514  INFO 14052 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.31.56:8091:2459182619839676463 2459182619839676465 jdbc:mysql://192.168.31.60:3306/ordercenter null
2022-01-06 20:58:37.637  INFO 14052 --- [ch_RMROLE_1_3_8] i.s.rm.datasource.xa.ResourceManagerXA   : 192.168.31.56:8091:2459182619839676463-2459182619839676465 was committed.
2022-01-06 20:58:37.638  INFO 14052 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2022-01-06 20:58:38.672  INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : Suspending current transaction, xid = 192.168.31.56:8091:2459182619839676463
2022-01-06 20:58:38.674  INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.31.56:8091:2459182619839676463] commit status: Committed

异常回滚

当在次发起请求由于余额低于200 会出现扣减余额失败的情况
在这里插入图片描述
在这里插入图片描述
查看数据库的数据
在这里插入图片描述
订单未产生,库存未变更, 余额也未扣减。 说明事务已经回滚了。
查看日志
在这里插入图片描述

Seata 事务模式
XA

优点:
事务强一致性,满足ACID原则
常用数据库都支持,实现简单,没有代码侵入
缺点:
因为一阶段需要锁定数据库资源,等待第二阶段结束才释放,性能较差 牺牲了可用性, 保证一致性
依赖关系型数据库实现事务。

AT (默认)

优点
1阶段直接提交,记录undo_log 日志 01 事务提交之前的日志
2阶段提交,先记录日志02 进行对比,如果和提交之前的一致那么就删日志, 事务完成。
如果二阶段提交时发现和之前的日志不一致(不一致的原因,其他事务非Seata 管理的事务对数据进行了操作),则抛异常, 需要人工介入
2阶段回滚,先记录日志02 和日志01进行对比,如果和提交之前的一致那么就使用日志进行回滚在删除日志, 事务完成。
如果二阶段回滚时发现和之前的日志不一致(不一致的原因,其他事务非Seata 管理的事务对数据进行了操作),则抛异常, 需要人工介入
不需要等待
引入了全局锁 多个事务并发提交的时候需要获取表数据对应的全局锁,针对死锁的情况,如果超过时间未获取到全局锁则释放当前持有的锁,并回滚数据。
没有代码上的侵入,每个参与事务的服务需要单独建表。
缺点:
两阶段之间属于软状态,属于最终一致性。
框架的快照会影响数据库性能,但比XA模式要好很多

默认是AT 模式, 需要在参与事物的服务数据库创建表

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
TCC模式和 SAGA模式

这两种模式需要人工手写业务逻辑代码对事务回滚事件做补偿的业务逻辑。 这里就没有做实现了。 感兴趣的小伙伴百度一下。

AT 模式与XA模式的区别

XA 模式一阶段不提交事务 锁定资源 ,AT 模式一阶段直接提交不锁定资源
XA 模式依赖数据库回滚机制实现回滚,AT 模式利用数据快照实现数据回滚
XA 强一致,AT 最终一致

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Blazzer

[]~( ̄▽ ̄)~*,干一杯!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值