Seata的四种模式

XA模式

Seata的四种模式_第1张图片
XA模式的优点:

  • 事务的 强一致性,满足ACID原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点:

  • 因为第一阶段需要锁定数据库资源,等待第二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

实现XA模式

1、修改applicaiton.yml(每个参与事务的微服务)文件,开启XA模式

seata:
 data-source-proxy-mode: XA

2、给发起全局事务的入口方法添加@GlobalTransactional注解,本例中是OrderServiceImpl中的create方法:

@GlobalTransactional

3、重启服务并测试

当库存不足时,会进行事务的回滚

我们的金额并没有减少,

AT模式

Seata的四种模式_第2张图片

AT模式与XA模式最大的区别:

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

AT模式的脏写

Seata的四种模式_第3张图片

解决办法:引入全局锁

Seata的四种模式_第4张图片

Seata的四种模式_第5张图片

AT模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好

  • 利用全局锁实现读写隔离

  • 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比XA模式要好很多

实现AT模式:

lock_table导入到TC服务相关联的数据库

DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table`  (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

Seata的四种模式_第6张图片

导入微服务相关联的数据库

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6jIDSEj-1662039964373)(D:\笔记\a\image-20220831212022129.png)]

2、修改配置文件为AT

data-source-proxy-mode: AT

3、重启微服务

TCC模式原理

Seata的四种模式_第7张图片

Seata的四种模式_第8张图片

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

案例:改造acccount-service服务,利用TCC实现分布式事务

  • 修改account-service,编写try、confirm、cancel逻辑

  • try业务:添加冻结金额,扣减可用金额

  • confirm业务:删除冻结金额

  • cancel业务:删除冻结金额,恢复可用金额

  • 保证confirm、cancel接口的幂等性

  • 允许空回滚(当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚):cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚

  • 拒绝业务悬挂(对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂):try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态,为此我们设计了一张表

/*
 Navicat Premium Data Transfer

 Source Server         : local
 Source Server Type    : MySQL
 Source Server Version : 50622
 Source Host           : localhost:3306
 Source Schema         : seata_demo

 Target Server Type    : MySQL
 Target Server Version : 50622
 File Encoding         : 65001

 Date: 23/06/2021 16:23:20
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account_freeze_tbl
-- ----------------------------
DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
  `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of account_freeze_tbl
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;


两个表对应的实体类

@Data
@TableName("account_tbl")
public class Account {
    @TableId
    private Long id;
    private String userId;
    private Integer money;
}
@Data
@TableName("account_freeze_tbl")
public class AccountFreeze {
    @TableId(type = IdType.INPUT)
    private String xid; //事务id
    private Stri	ng userId; //用户id
    private Integer freezeMoney; //冻结金额
    private Integer state;

    public static abstract class State {
        public final static int TRY = 0;
        public final static int CONFIRM = 1;
        public final static int CANCEL = 2;
    }
}

声明TCC的三个接口

@LocalTCC
public interface AccountTCCService {
    @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);

}

实现TCC接口

三个基本业务实现

@Service
public class AccountServiceImpl implements AccountService {
    //注入mapper
    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper freezeMapper;


    @Override
    public void deduct(String userId, int money) {
        //获取事务的id
        String xid = RootContext.getXID();
//1.扣减可用余额
        accountMapper.deduct(userId,money);
//2.记录冻结金额,和事务的状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);

        freeze.setXid(xid);
        freezeMapper.insert(freeze);

    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        //获取事务id
        String xid = ctx.getXid();
        //删除冻结金额
        int i = freezeMapper.deleteById(xid);

        //判断是否删除成功
        return i==1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        String xid = ctx.getXid();
        //0、查询冻结记录
        AccountFreeze accountFreeze = freezeMapper.selectById(xid);
        
        //1、恢复可用金额
        accountMapper.refund(accountFreeze.getUserId(),accountFreeze.getFreezeMoney());
        //2、将动态金额清零,状态修改为Cancel
accountFreeze.setFreezeMoney(0);
accountFreeze.setState(AccountFreeze.State.CANCEL);

        int i = freezeMapper.updateById(accountFreeze);
        return i==1;
    }
}

解决空回滚和业务悬挂,增加幂等判断

package cn.itcast.account.service.impl;

import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.service.AccountTccService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @ClassName AccountServiceImpl
 * @Description TODO
 * @Author ylh
 * @Date 2022/9/1 20:54
 * @Version 1.0
 */
@Service
public class AccountTccServiceImpl implements AccountTccService {
    //注入mapper
    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper freezeMapper;


    @Override
    public void deduct(String userId, int money) {

        //获取事务的id
        String xid = RootContext.getXID();

        //判断freeze中是否有冻结记录,如果有,一定是Cancel执行过,拒绝业务
        AccountFreeze oldFreeze=freezeMapper.selectById(xid);
        if (oldFreeze!=null){
            return ;
        }





//1.扣减可用余额
        accountMapper.deduct(userId,money);
//2.记录冻结金额,和事务的状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);

        freeze.setXid(xid);
        freezeMapper.insert(freeze);

    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        //获取事务id
        String xid = ctx.getXid();
        //删除冻结金额
        int i = freezeMapper.deleteById(xid);

        //判断是否删除成功
        return i==1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        String xid = ctx.getXid();
        //0、查询冻结记录
        AccountFreeze accountFreeze = freezeMapper.selectById(xid);

        //空回滚的判断,没有执行冻结
       if ( accountFreeze==null){
           //需要空回滚
           accountFreeze=new AccountFreeze();
           accountFreeze.setUserId(ctx.getActionContext("userId").toString());
           accountFreeze.setFreezeMoney(0);
           accountFreeze.setState(AccountFreeze.State.CANCEL);
           accountFreeze.setXid(xid);
           freezeMapper.insert(accountFreeze);
           return true;
       }

        //幂等判断--cancle超时了,又调用了一次cancel
        if (accountFreeze.getState()== AccountFreeze.State.CANCEL){
            //已经处理过了,无需继续处理
            return true;
        }

        //1、恢复可用金额
        accountMapper.refund(accountFreeze.getUserId(),accountFreeze.getFreezeMoney());
        //2、将动态金额清零,状态修改为Cancel
accountFreeze.setFreezeMoney(0);
accountFreeze.setState(AccountFreeze.State.CANCEL);

        int i = freezeMapper.updateById(accountFreeze);
        return i==1;
    }
}

修改web中service方法

@Autowired
private AccountTCCService accountService;

Saga模式

Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:

一阶段:直接提交本地事务

二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

Saga模式优点:

事务参与者可以基于事件驱动实现异步调用,吞吐高

一阶段直接提交事务,无锁,性能好

不用编写TCC中的三个阶段,实现简单

缺点:

软状态持续时间不确定,时效性差

没有锁,没有事务隔离,会有脏写

Seata的四种模式_第9张图片

你可能感兴趣的:(微服务,数据库,java,大数据)