Spring-Cloud-Alibaba之Seata

vchar_fred發表於2021-05-06

微服務中不可避免的會發生服務間的呼叫,這就一定會涉及到事務相關的問題,在單體專案中我們可以直接很方便的實現事務回滾,但是在分散式系統中就不能像以前那麼做了,因為各個服務是獨立的一套系統; 而要實現跨服務的事務管理系統的複雜度必然會大大增加,因此我們應當儘可能的避免使用分散式事務;對於那種要求不是很嚴格的可以考慮忽略掉事務的問題,只對重要的資料才做分散式事務。下面我們使用spring-cloud-alibaba套件Seata來實現分散式事務的功能。

Seata簡介

Seata 是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務。在 Seata 開源之前,Seata 對應的內部版本在阿里經濟體內部一直扮演著分散式一致性中介軟體的角色,幫助經濟體平穩的度過歷年的雙11,對各BU業務進行了有力的支撐。

相關文件

功能特性如下

  • 微服務框架支援:目前已支援 Dubbo、Spring Cloud、Sofa-RPC、Motan 和 grpc 等RPC框架,其他框架持續整合中;
  • TCC模式:支援 TCC 模式並可與 AT 混用;
  • XA模式:支援已實現 XA 介面的資料庫的 XA 模式;
  • AT模式:提供無侵入自動補償的事務模式,目前已支援 MySQL、 Oracle 、PostgreSQL和 TiDB的AT模式,H2 開發中;
  • 高可用:支援基於資料庫儲存的叢集模式,水平擴充套件能力強;
  • SAGA模式:為長事務提供有效的解決方案;

Seata的使用

Seata支援本地檔案模式和遠端配置中心模式,下面我們分別介紹相關的使用方式。注意示例中使用的是spring-cloud-alibaba的套件;下面是程式碼示例:

使用file模式

新增maven依賴:

<!-- seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

spring-cloud-starter-alibaba-seata這個依賴中只依賴了spring-cloud-alibaba-seata,所以在專案中新增spring-cloud-starter-alibaba-seata和spring-cloud-alibaba-seata是一樣的

seata配置檔案

seata的配置引數官方文件 https://seata.io/zh-cn/docs/user/configurations.html

在application.yml裡面配置seata需要的資訊

spring:
  cloud:
    alibaba:
      seata:
        # 這裡定義seata服務分組名稱,必須和下面的seata.service.vgroup-mapping對應,否則將無法獲取seata服務端IP資訊
        tx-service-group: seata-dubbo-b-seata-service-group
seata:
  registry:
    type: file
  service:
    # seata服務端的地址和埠資訊,多個使用英文分號分隔
    grouplist:
      default: 192.168.56.101:8091
    vgroup-mapping:
      seata-dubbo-b-seata-service-group: default

上面的配置可以去看 io.seata.spring.boot.autoconfigure.properties.client.ServicePropertiesio.seata.discovery.registry.FileRegistryServiceImpl 這2個類你就明白了為啥這樣配置了。

建立資料庫表

在每一個業務庫裡面建立一個undo_log的表,這裡表裡面會記錄事務資訊,用於seata後面回滾資料使用。

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

開啟全域性事務

在需要開啟全域性事務的方法上新增 @GlobalTransactional 註解即可;只需要在起始的呼叫方法上加即可;注意對應異常情況想要回滾,直接丟擲異常即可,否則將無法觸發全域性事務的回滾。 程式碼示例如下:

服務A

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @DubboReference(interfaceClass = GoodsService.class, check = false)
    private GoodsService goodsService;
    /**
     * 預定
     */
    @GlobalTransactional
    @Override
    public String booking(Long goodsId, Integer num) throws SQLException {

        Order order = new Order();
        order.setOrderNo(String.valueOf(System.currentTimeMillis()));
        order.setUid(1L);
        order.setGoodsId(goodsId);
        order.setIntegral(num*50);

        int count = orderMapper.insert(order);
        if (count!=1){
            System.out.println("訂單建立失敗");
            return "訂單建立失敗";
        }
        boolean res = this.goodsService.deductInventory(goodsId, num);
        if(!res){
            throw new SQLException("庫存不足");
        }

        return order.getOrderNo();
    }
}

服務B

@DubboService
public class GoodsServiceImpl implements GoodsService {

    @Autowired
    private GoodsMapper goodsMapper;
    @DubboReference(interfaceClass = IntegralService.class, check = false)
    private IntegralService integralService;

    @Override
    public boolean deductInventory(Long id, int num) throws SQLException {
        Goods goods = goodsMapper.selectById(id);
        int count = goodsMapper.deductInventory(id, num);
        if (count!=1){
            throw new SQLException("庫存不足");
        }
        boolean res = this.integralService.deductIntegral(id, num*goods.getIntegral());
        System.out.println("積分扣除結果:"+res);
        if(!res){
            throw new SQLException("積分不足");
        }
        return true;
    }
}

服務C

@DubboService
public class IntegralServiceImpl implements IntegralService {
    @Autowired
    private MemberMapper memberMapper;
    
    @Override
    public boolean deductIntegral(Long id, int integral) {
        int count = memberMapper.deductIntegral(id, integral);
        return count==1;
    }
}

使用註冊中心nacos進行叢集

之前我們的seata是沒有叢集的,要叢集的話那麼就不能使用檔案模式了,這裡我們使用nacos來實現seata叢集間的通訊;注意這裡使用的是nacos-1.x,在實際測試中使用nacos-2.x的時候會偶發出現dubbo服務無法呼叫的問題。

修改application.yml的配置,將上面seata部分的配置改為如下所示:

seata:
  registry:
    type: nacos
    nacos:
      serverAddr: 192.168.56.1:8848
      application: seata-server
      group: SEATA_GROUP
  service:
    vgroup-mapping:
      # 這個必須和上面的匹配,同時最大長度為32;否則需要修改建立seata庫中的global_table表的transaction_service_group的長度限制
      seata-dubbo-b-seata-service-group: default

其他的無需改動;直接即可使用;服務啟動成功後,seata服務那邊也會列印相關資訊。最後不得不吐槽下加入分散式事務元件後系統的響應就變慢,因此不到萬不得已最好不用分散式事務,哪怕是通過後期手動處理。

相關文章