springboot集成分布式事务框架:servicecomb-pack

源码地址:https://github.com/apache/servicecomb-pack

一、官网介绍:

概览

Pack中包含两个组件,即 alpha omega

  • alpha充当协调者的角色,主要负责对事务的事件进行持久化存储以及协调子事务的状态,使其得以最终与全局事务的状态保持一致。
  • omega是微服务中内嵌的一个agent,负责对网络请求进行拦截并向alpha上报事务事件,并在异常情况下根据alpha下发的指令执行相应的补偿操作。

Omega内部运行机制

omega是微服务中内嵌的一个agent。当服务收到请求时,omega会将其拦截并从中提取请求信息中的全局事务id作为其自身的全局事务id(即Saga事件id),并提取本地事务id作为其父事务id。在预处理阶段,alpha会记录事务开始的事件;在后处理阶段,alpha会记录事务结束的事件。因此,每个成功的子事务都有一一对应的开始及结束事件。

Omega负责追踪本地的事务运行情况,并将事务执行事件发送到Alpha端, Alpha作为事务协调器,会根据接收的事务事件在后台维护一套事务的状态信息,并根据预先定义的规则与Omega之间进行协调。

服务间通信流程

服务间通信的流程与Zipkin的类似。在服务生产方,omega会拦截请求中事务相关的id来提取事务的上下文。在服务消费方,omega会在请求中注入事务相关的id来传递事务的上下文。通过服务提供方和服务消费方的这种协作处理,子事务能连接起来形成一个完整的全局事务。

具体处理流程

成功场景

成功场景下,每个开始的事件都会有对应的结束事件。

异常场景

异常场景下,omega会向alpha上报中断事件,然后alpha会向该全局事务的其它已完成的子事务发送补偿指令,确保最终所有的子事务要么都成功,要么都回滚。

超时场景

超时场景下,已超时的事件会被alpha的定期扫描器检测出来,与此同时,该超时事务对应的全局事务也会被中断。

二、编译

1、官方只提供源码,需要自己编译打包。下载源码,地址见文章开头。在idea的terminal中进入到alpha目录,输入命令:mvn clean install -DskipTests=true -Pspring-cloud-eureka,spring-boot-2,这里使用了eureka注册中心。如项目中没使用eureka则直接输入命令:mvn clean install -DskipTests=true,默认打的spring-boot-2,如项目是低版本使用命令mvn clean install -DskipTests=true -Dspring-boot-1。

2、编译完成启动jar包命令:

java

-Dserver.port=8181

-Dalpha.server.port=8182

-Dspring.profiles.active=mysql

-Dspring.datasource.url=jdbc:mysql://192.168.18.112:3306/saga?useSSL=false

-Dspring.datasource.username=root

-Dspring.datasource.password=root

-Deureka.client.service-url.defaultZone=http://192.168.18.211:8900//eureka  -jar alpha-server-0.5.0-exec.jar

PS:启动alpha-server会自行创建数据库脚本。如不是root用户请自行手动创建.脚本如下:

CREATE TABLE IF NOT EXISTS TxEvent (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        type varchar(50) NOT NULL,
        compensationMethod varchar(512) NOT NULL,
        expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        payloads blob,
        retries int(11) NOT NULL DEFAULT '0',
        retryMethod varchar(512) DEFAULT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_events_index (surrogateId, globalTxId, localTxId, type, expiryTime),
        INDEX saga_global_tx_index (globalTxId)
        ) DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS Command (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        eventId bigint NOT NULL UNIQUE,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        compensationMethod varchar(512) NOT NULL,
        payloads blob,
        status varchar(12),
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        version bigint NOT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_commands_index (surrogateId, eventId, globalTxId, localTxId, status)
        ) DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS TxTimeout (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        eventId bigint NOT NULL UNIQUE,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        type varchar(50) NOT NULL,
        expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        status varchar(12),
        version bigint NOT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_timeouts_index (surrogateId, expiryTime, globalTxId, localTxId, status)
        ) DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS tcc_global_tx_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        txType varchar(12),
        status varchar(12),
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_global_tx_event_index (globalTxId, localTxId, parentTxId, txType)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS tcc_participate_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        confirmMethod varchar(512) NOT NULL,
        cancelMethod varchar(512) NOT NULL,
        status varchar(50) NOT NULL,
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_participate_event_index (globalTxId, localTxId, parentTxId)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS tcc_tx_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        methodInfo varchar(512) NOT NULL,
        txType varchar(12),
        status varchar(12),
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_tx_event_index (globalTxId, localTxId, parentTxId, txType)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

        CREATE TABLE IF NOT EXISTS master_lock (
        serviceName varchar(36) not NULL,
        expireTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lockedTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        instanceId  varchar(255) not NULL,
        PRIMARY KEY (serviceName)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

三、项目中引入omega依赖及配置:

1、pom文件中引入依赖包:

    <dependency>
      <groupId>org.apache.servicecomb.pack</groupId>
      <artifactId>omega-spring-starter</artifactId>
      <version>${pack.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.servicecomb.pack</groupId>
      <artifactId>omega-transport-resttemplate</artifactId>
      <version>${pack.version}</version>
   </dependency>

2、配置启动参数:

#启动omega
omega.enabled=true
#集群配置
alpha.cluster.address=192.168.18.211:8182,192.168.18.212:8182

3、代码中注解使用:

Saga 支持(TCC请参考后面给出的官网文档地址)

添加Saga的注解及相应的补偿方法 以一个转账应用为例:

a、在全局事务的起点添加 @SagaStart 的注解来让Omega创建一个新的全局事务。如果不标注这个事务起点的话,后续子事务在执行过程中会报找不到全局事务ID的错误。

import org.apache.servicecomb.pack.omega.context.annotations.SagaStart;

@SagaStart(timeout=10)
public boolean transferMoney(String from, String to, int amount) {
  transferOut(from, amount);
  transferIn(to, amount);
}

注意: 默认情况下,超时设置需要显式声明才生效。

b、在子事务处添加 @Compensable 的注解并指明其对应的补偿方法。

import javax.transaction.Transactional;
import org.apache.servicecomb.pack.omega.transaction.annotations.Compensable;

@Compensable(timeout=5, compensationMethod="cancel")
@Transactional
public boolean transferOut(String from, int amount) {
  repo.reduceBalanceByUsername(from, amount);
}
 
@Transactional
public boolean cancel(String from, int amount) {
  repo.addBalanceByUsername(from, amount);
}

注意: 实现的服务使用相当的参数,实现的服务和补偿必须满足幂等的条件,同时建议使用Spring @Transactional标注提供本地事务保证。

注意: 默认情况下,超时设置需要显式声明才生效。

注意:补偿方法的入参必须与原方法一致

注意: 若全局事务起点与子事务起点重合,需同时声明 @SagaStart 和 @Compensable 的注解。

更多详见官方文档:https://github.com/apache/servicecomb-pack/blob/master/docs/user_guide_zh.md

四、默认情况下必须写补偿方法,否则启动报错,如果不想写补偿方法需修改源码:

1、找到MethodCheckingCallback类,修改loadMethodContext方法(可直接复制覆盖),以下6、11、12、13行为新增代码:

protected void loadMethodContext(Method method, String ... candidates) {
  for (String each : candidates) {

    try {
      //修改源码 新增判断,允许在添加@Compensable时 忽略补偿方法
      if(!each.equals("")){
        Method signature = bean.getClass().getDeclaredMethod(each, method.getParameterTypes());
        String key = getTargetBean(bean).getClass().getDeclaredMethod(each, method.getParameterTypes()).toString();
        callbackContext.addCallbackContext(key, signature, bean);
        LOG.debug("Found callback method [{}] in {}", each, bean.getClass().getCanonicalName());
      }else{
        LOG.warn("建议添加补偿方法:No such " + callbackType + " method [" + each + "] found in " + bean.getClass().getCanonicalName());
      }
    } catch (Exception ex) {
      throw new OmegaException(
          "No such " + callbackType + " method [" + each + "] found in " + bean.getClass().getCanonicalName(), ex);
    }
  }
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值