16. 分布式事務(wù)seata的四種模式

Seata四種分布式事務(wù)解決方案:

  1. XA模式:強(qiáng)一致性分階段事務(wù)模式,犧牲了一定的可用性,無業(yè)務(wù)侵入
  2. TCC模式:最終一致的分階段事務(wù)模式,有業(yè)務(wù)侵入
  3. AT模式:最終一致的分階段事務(wù)模式,無業(yè)務(wù)侵入,也是Seata的默認(rèn)模式
  4. SAGA模式:長事務(wù)模式,有業(yè)務(wù)侵入

XA模式原理

image-20210724174424070.png

seata的XA模式做了一些調(diào)整,但大體相似:

  • RM一階段的工作:
    1.注冊分支事務(wù)到TC
    2.執(zhí)行分支業(yè)務(wù)sql但不提交
    3.報告執(zhí)行狀態(tài)到TC

  • TC二階段的工作:

    • TC檢測各分支事務(wù)執(zhí)行狀態(tài)
      1.如果都成功,通知所有RM提交事務(wù)
      2.如果有失敗,通知所有RM回滾事務(wù)
  • RM二階段的工作:

    • 接收TC指令,提交或回滾事務(wù)

XA模式的優(yōu)點(diǎn)是什么?
1.事務(wù)的強(qiáng)一致性,滿足ACID原則。
2.常用數(shù)據(jù)庫都支持,實(shí)現(xiàn)簡單,并且沒有代碼侵入
XA模式的缺點(diǎn)是什么?
1.因?yàn)橐浑A段需要鎖定數(shù)據(jù)庫資源,等待二階段結(jié)束才釋放,性能較差
2.依賴關(guān)系型數(shù)據(jù)庫實(shí)現(xiàn)事務(wù)

實(shí)現(xiàn)XA模式

  1. 修改application.yml文件(每個參與事務(wù)的微服務(wù)),開啟XA模式:
seata:
  data-source-proxy-mode: XA
  1. 給發(fā)起全局事務(wù)的入口方法添加@GlobalTransactional注解
  @Override
  @GlobalTransactional
  public Long create(Order order) {
    // 創(chuàng)建訂單
    orderMapper.insert(order);
    try {
      // 扣用戶余額
      accountClient.deduct(order.getUserId(), order.getMoney());
      // 扣庫存
      storageClient.deduct(order.getCommodityCode(), order.getCount());

    } catch (FeignException e) {
      log.error("下單失敗,原因:{}", e.contentUTF8(), e);
      throw new RuntimeException(e.contentUTF8(), e);
    }
    return order.getId();
  }

AT模式原理

AT模式同樣是分階段提交的事務(wù)模型,不過缺彌補(bǔ)了XA模型中資源鎖定周期過長的缺陷


image-20210724175327511.png
  • 階段一RM的工作:
    1.注冊分支事務(wù)
    2.記錄undo-log(數(shù)據(jù)快照)
    3.執(zhí)行業(yè)務(wù)sql并提交
    4.報告事務(wù)狀態(tài)
  • 階段二提交時RM的工作:
    1.刪除undo-log即可
  • 階段二回滾時RM的工作:
    1.根據(jù)undo-log恢復(fù)數(shù)據(jù)到更新前

簡述AT模式與XA模式最大的區(qū)別是什么?
1.XA模式一階段不提交事務(wù),鎖定資源;AT模式一階段直接提交,不鎖定資源。
2.XA模式依賴數(shù)據(jù)庫機(jī)制實(shí)現(xiàn)回滾;AT模式利用數(shù)據(jù)快照實(shí)現(xiàn)數(shù)據(jù)回滾。
3.XA模式強(qiáng)一致;AT模式最終一致

AT模式的優(yōu)點(diǎn):
1.一階段完成直接提交事務(wù),釋放數(shù)據(jù)庫資源,性能比較好
2.利用全局鎖實(shí)現(xiàn)讀寫隔離
3.沒有代碼侵入,框架自動完成回滾和提交
AT模式的缺點(diǎn):
1.兩階段之間屬于軟狀態(tài),屬于最終一致
2.框架的快照功能會影響性能,但比XA模式要好很多

實(shí)現(xiàn)AT模式

  1. 創(chuàng)建lock_table(全局鎖)表和undo_log(快照)表,lock_table是給TC服務(wù)用的創(chuàng)建在TC數(shù)據(jù)庫表中
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;

undo_log是給RM微服務(wù)數(shù)據(jù)庫表用的,在創(chuàng)建的RM數(shù)據(jù)庫中執(zhí)行

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;
  1. 修改application.yml文件,將事務(wù)模式修改為AT模式即可:
seata:
  data-source-proxy-mode: AT
  1. 給發(fā)起全局事務(wù)的入口方法添加@GlobalTransactional注解

TCC模式原理

TCC模式與AT模式非常相似,每階段都是獨(dú)立事務(wù),不同的是TCC通過人工編碼來實(shí)現(xiàn)數(shù)據(jù)恢復(fù)。需要實(shí)現(xiàn)三個方法:
1.Try:資源的檢測和預(yù)留;
2.Confirm:完成資源操作業(yè)務(wù);要求 Try 成功 Confirm 一定要能成功。
3.Cancel:預(yù)留資源釋放,可以理解為try的反向操作。


image-20210724182937713.png

TCC模式的每個階段是做什么的?
1.Try:資源檢查和預(yù)留
2.Confirm:業(yè)務(wù)執(zhí)行和提交
3.Cancel:預(yù)留資源的釋放
TCC的優(yōu)點(diǎn)是什么?
1.一階段完成直接提交事務(wù),釋放數(shù)據(jù)庫資源,性能好
2.相比AT模型,無需生成快照,無需使用全局鎖,性能最強(qiáng)
2.不依賴數(shù)據(jù)庫事務(wù),而是依賴補(bǔ)償操作,可以用于非事務(wù)型數(shù)據(jù)庫
TCC的缺點(diǎn)是什么?
1.有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
2.軟狀態(tài),事務(wù)是最終一致
3.需要考慮Confirm和Cancel的失敗情況,做好冪等處理

實(shí)現(xiàn)TCC模式

  • 修改account-service,編寫try、confirm、cancel邏輯
  • try業(yè)務(wù):添加凍結(jié)金額,扣減可用金額
  • confirm業(yè)務(wù):刪除凍結(jié)金額
  • cancel業(yè)務(wù):刪除凍結(jié)金額,恢復(fù)可用金額
  • 保證confirm、cancel接口的冪等性
  • 允許空回滾
  • 拒絕業(yè)務(wù)懸掛

為了實(shí)現(xiàn)空回滾、防止業(yè)務(wù)懸掛,以及冪等性要求。我們必須在數(shù)據(jù)庫記錄凍結(jié)金額的同時,記錄當(dāng)前事務(wù)id和執(zhí)行狀態(tài),為此我們設(shè)計了一張表:

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 '事務(wù)狀態(tài),0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
  1. 實(shí)體類
package cn.itcast.account.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @author ylf
 */
@Data
@TableName("account_freeze_tbl")
public class AccountFreeze {
    @TableId(type = IdType.INPUT)
    private String xid;
    private String userId;
    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;
    }
}

  1. Mapper
package cn.itcast.account.mapper;

import cn.itcast.account.entity.AccountFreeze;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author ylf
 */
@Mapper
public interface AccountFreezeMapper extends BaseMapper<AccountFreeze> {}

  1. 創(chuàng)建service表,編寫try、confirm、cancel邏輯、
package cn.itcast.account.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author ylf
 * @version 1.0
 */
@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);
}

  1. 實(shí)現(xiàn)
package cn.itcast.account.service.impl;

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
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;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author ylf
 * @version 1.0
 */
@Service
public class AccountTccServiceImpl implements AccountTccService {
  @Autowired AccountMapper accountMapper;
  @Autowired AccountFreezeMapper freezeMapper;

  @Override
  @Transactional
  public void deduct(String userId, int money) {
    // 0.獲取事務(wù)id
    final String xid = RootContext.getXID();
    // 1.判斷freeze中是否有凍結(jié)業(yè)務(wù),如果有,一定是Cancel,我要拒絕業(yè)務(wù)
    final AccountFreeze oldFreeze = freezeMapper.selectById(xid);
    if (oldFreeze != null) {
      return;
    }
    // 2.扣減可用金額
    accountMapper.deduct(userId, money);
    // 3.記錄凍結(jié)金額,事務(wù)狀態(tài)
    AccountFreeze accountFreeze = new AccountFreeze();
    accountFreeze.setUserId(userId);
    accountFreeze.setFreezeMoney(money);
    accountFreeze.setState(AccountFreeze.State.TRY);
    accountFreeze.setXid(xid);
    freezeMapper.insert(accountFreeze);
  }

  @Override
  public boolean confirm(BusinessActionContext ctx) {
    // 1.獲取事務(wù)id
    final String xid = ctx.getXid();
    // 2.根據(jù)事務(wù)id刪除記錄
    final int count = freezeMapper.deleteById(xid);
    return count == 1;
  }

  @Override
  public boolean cancel(BusinessActionContext ctx) {
    // 0.查詢凍結(jié)記錄
    AccountFreeze accountFreeze = freezeMapper.selectById(ctx.getXid());
    final String userId = ctx.getActionContext("userId").toString();
    // 1.空回滾判斷,判斷accountFreeze是否為null,如果為null,就需要空回滾
    if (accountFreeze == null) {
      accountFreeze = new AccountFreeze();
      accountFreeze.setUserId(userId);
      accountFreeze.setFreezeMoney(0);
      accountFreeze.setState(AccountFreeze.State.CANCEL);
      accountFreeze.setXid(ctx.getXid());
      freezeMapper.insert(accountFreeze);
    }
    // 2.判斷冪等
    if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
      return true;
    }
    // 3.恢復(fù)可用金額
    accountMapper.refund(accountFreeze.getUserId(), accountFreeze.getFreezeMoney());
    // 4.將凍結(jié)金額清理,改變狀態(tài)為cancel
    accountFreeze.setFreezeMoney(0);
    accountFreeze.setState(AccountFreeze.State.CANCEL);
    final int count = freezeMapper.updateById(accountFreeze);
    return count == 1;
  }
}

  1. 測試
package cn.itcast.account.web;

import cn.itcast.account.service.AccountTccService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ylf
 */
@RestController
@RequestMapping("account")
public class AccountController {

  @Autowired private AccountTccService accountService;

  @PutMapping("/{userId}/{money}")
  public ResponseEntity<Void> deduct(
      @PathVariable("userId") String userId, @PathVariable("money") Integer money) {
    accountService.deduct(userId, money);
    return ResponseEntity.noContent().build();
  }
}

Saga模式

Saga模式是SEATA提供的長事務(wù)解決方案。也分為兩個階段:
1.一階段:直接提交本地事務(wù)
2.二階段:成功則什么都不做;失敗則通過編寫補(bǔ)償業(yè)務(wù)來回滾
Saga模式優(yōu)點(diǎn):
1.事務(wù)參與者可以基于事件驅(qū)動實(shí)現(xiàn)異步調(diào)用,吞吐高
2.一階段直接提交事務(wù),無鎖,性能好
3.不用編寫TCC中的三個階段,實(shí)現(xiàn)簡單
缺點(diǎn):
1.軟狀態(tài)持續(xù)時間不確定,時效性差
2.沒有鎖,沒有事務(wù)隔離,會有臟寫

image-20210724185021819.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容