1.分布式事务的产生

事务是通过jdbc Connection对象进行开启,及提交/回滚,同jvm 使用@Transaction注解时,只要业务method没有结束,spring是不会退回connection到连接池的,但不同jvm是不是spring上下文来管理connection的(即不同本地事务)。微服务划分因尽量的高内聚低耦合,减少分布式事务,但是随着系统逐渐变得庞大,分布式事务也情况也会增多,需要一种简易分布式事务解决方案

2.常见分布式事务解决方案

  • 本地事务:只是服务拆分,数据库未分库,系统较简单时且仅个别分布式事务场景,可以考虑牺牲系统耦合性,需要在不相关的领域模型写入其实没啥关系的sql语句,如为解决分布式事务可能有userMapper里可能有select  user  和select  order的语句。
  • 基于MQ最终一致性方案:该方案性能较好,一般情况都是准实时一致,除非MQ消费不过来才会有一定延时,对程序员要求也较高。该方案比较时候电商购物等长事务场景(支付及支付成功消息在一个本地事务里),如kafka消费者端只要没有成功消费消息即消息处理报错了kafka会只一直消费该消息,直到该消息被正常处理并commit。可能出现消费端一致报错,这需要监控报警即使发现并解决,如果长时间不能解决,数据会较长时间不能达到一致性。
  • TCC即Try、Confirm和Cancel,代码侵入太高,需要自己写补偿接口,如下单失败,还得在cancel代码片段写库存恢复接口,而且cancel里的数据库操作也可能会出错。
  • XA事务:CP模型,强一致性,性能也较差,一把是有关全局事务管理器如后面的seata-server。低并发,要求强一致性,接受一定的性能损失,需要一种简单的方式解决分布式事务就应该选择XA事务。

3.分布式事务中间件:seata ,LCN,阿里GTS,sharding sphere的xa等

LCN前两年发展还不错,今天一看官网已打不开,由于开源资金,阿里的seata的顺势而来,github已宣布停滞维护,LCN的使用还是很方便的,一个全局事务注解即可完成(参考)。GTS阿里没有开源,就不用想了,但是开源了一个seata,目前还是比较活跃的。github地址官方文档。seata 有个AT(自动事务)模式:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接

二阶段:commit:提交异步化,非常快速地完成 ; rollback:回滚通过一阶段的回滚日志进行反向补偿。

4.springcloud 2.2.1集成seata 1.4 官方文档

4.1 seata server端安装:下载https://github.com/seata/seata/releases,案例使用1.4.0版本测试,建议docker方式安装 。

注:202111测试docker安装的最新版本默认支持file模式的配置,注册中心不必使用registry.conf,file.conf

1.先默认启动
docker run --name seata-server \
        -p 8091:8091 \
        seataio/seata-server  
2.拷贝容器内/seata-server下的整个resources拷到宿主机进行修改, docker  cp seata-server:/seata-server/resources/  /etc/seata/resources
3.正式启动(进入docker exec -it seata-server /bin/sh)
docker run -d --name seata-server \
        -p 8091:8091 \
        -v /etc/seata/resources:/seata-server/resources  \
        seataio/seata-server
4.注意默认使用file模式,同#sh seata-server.sh -p 8091 -h 127.0.0.1 -m file,如果使用db储存事务消息可通过-e设置环境变量

或tar包解压安装,bin目录启动:./seata-server.sh  。

4.2 使用file方式配置seata-server

进入config目录修改配置: 首先是registry.conf ,注册中心与配置中心type 都是默认file,推荐使用nacos

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"
  file {
    name = "file.conf"
  }
}

 如果前面registry.conf指定使用file,则在file.conf配置seata的事务日志存储相关参数,如果使用nacos则可以在nacos上进行修改。(registry.conf,file.conf都是seata server的配置文件)1.4版本file.conf配置文件里只显示给出了store相关配置项,可能官方推荐使用nacos进行配置。

这里还是先测试一波file方式配置,追加了除store之外的其它配置项。必须修改的是vgroupMapping.payment="default"  ,vgroupMapping.study="default" ,这两个配置表示将payment与study两个服务映射到default“事务组。注意旧版本是vgroup_mapping ;如果使用store.mode=db则还需要配置DB连接参数并初始化化globleTable,branchTable,lockTable三张表(脚本位置https://github.com/seata/seata/tree/develop/script),用单独的库,非业务数据库。单机非高可用部署store.mode=file就狗了,如果要多节点高可用,必须使用db/redis,因为需要分布式锁支持

store {
  ## store mode: file、db、redis
  mode = "file"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://xxxxxx:3306/seata"
    user = "root"
    password = "xxxx"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    host = "127.0.0.1"
    port = "6379"
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }

}


transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.payment="default"
  vgroupMapping.study="default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}                        

4.2 springcloud alibaba 2.2.1微服务集成seata,注意该starter默认引入的seata包是1.1的,故这里单独引入1.4

业务库增加undo_log表,用于记录各个RM存储的branch_id(分支事务ID),xid(全局事务ID)及rollback_info(记录变更数据前后数据快照,是二阶段补偿的依据)

-- auto-generated definition
create table undo_log
(
    id            bigint auto_increment
        primary key,
    branch_id     bigint       not null,
    xid           varchar(100) not null,
    context       varchar(128) not null,
    rollback_info longblob     not null,
    log_status    int          not null,
    log_created   datetime     not null,
    log_modified  datetime     not null,
    constraint ux_undo_log
        unique (xid, branch_id)
)
    comment 'Seata分布式事务';

增加依赖

 <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.0</version>
        </dependency>

配置文件

spring:
  application:
    name: payment
  profiles:
    active: @package.environment@
  cloud:
    nacos:
      config:
        server-addr: @package.nacos.addr@         #注意不要http://,nacaos使用的是raft协议
        file-extension: yml                       #后缀
        namespace: @package.environment@          #配置命名空间,默认public
        shared-configs:
          - data-id: common.yml                #配置所有工程共享的配置,注意里面的配置默认不能动态刷新,需要时可配置自动刷新
            refresh: true
        password: @package.nacos.password@        #nacos 1.3.X才支持
        username: @package.nacos.username@
        context-path: /nacos
      discovery:
        password: @package.nacos.password@
        username: @package.nacos.username@
        server-addr: @package.nacos.addr@          #注意不要http://,nacaos使用的是raft协议
        namespace: @package.environment@           #服务命名空间,默认public



#分布式事务Seata配置
seata:
  tx-service-group: ${spring.application.name}
  enabled: true
  enable-auto-data-source-proxy: true
  service:
    vgroup-mapping:
      payment: default
    grouplist:
      default: 127.0.0.1:8091
  #config:
  #  type: springCloudConfig

启动类配置  新版已支持自动代理,不需要自己手动配置datasoure代理了

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@MapperScan("com.XXX.user.mapper")
=============================================================================
//这里去除了dataSource自动配置,需要在自定义配置,参考如下

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;


@Configuration
@EnableConfigurationProperties({MybatisPlusProperties.class})
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy,
        MybatisPlusProperties mybatisProperties) {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSourceProxy);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try {
            Resource[] mapperLocaltions = resolver
                .getResources(mybatisProperties.getMapperLocations()[0]);
            bean.setMapperLocations(mapperLocaltions);

            if (StringUtils.isNotBlank(mybatisProperties.getConfigLocation())) {
                Resource[] resources = resolver.getResources(mybatisProperties.getConfigLocation());
                bean.setConfigLocation(resources[0]);
            }
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

4.3启动及测试 

 A服务

 @GlobalTransactional
    @Override
    public ResultEntity test() {
        ResultEntity result = restTemplate.getForObject("http://study/test/test2", ResultEntity.class);
        int insert = orderMapper.insert(new ServiceOrder().setId("111").setOrderNo("aaaaa"));
        int a=1/0;
        return null;
    }


 B服务
    @Override
    @GlobalTransactional
    public void test2() {
        userMapper.updateRoleName();
    }

启动A B服务

0.正常测试,A先调B服务,B服务的本地事务提交,然后A抛出异常,B服务能基于undo_log日志表进行回滚,B服务日志可看到rollbacked相关日志

!.测试异常场景seata server宕机:AB服务均立即报错can not connect to 192.168.203.132:8091 cause:can not register RM,err:can not connect to services-server,请求接口也会返回该错误,seata server恢复,AB服务立即停止异常信息输出。seata-server宕机,只影响@GlobalTransactional注解方法

  @GlobalTransactional可能是基于aop实现的,不能连接server端即抛出异常

!!.测试异常场景B服务宕机:A服务抛出异常io.seata.common.exception.FrameworkException: No available service,A正常回滚,seata server 及A服务打印如rollbacked相关日志

!!!.断点打restTemplate调B服务处,检查该行修改已生效,此时再去手动修改A服务修改的那条记录,可发现是可以修改的没有锁定,断点打throw new CommonException("A服务异常")处,此时B修改已生效也可以修改,A抛出异常后服务方法走完,开始回滚事务,这里有个严重“BUG”,比如原始username =zs1 ,userMapper.updateUserName()修改为zs2,这时打个断点手动将zs2改为zs3,然后方法走完开始回滚发现不能正常回滚,死循环重试BUG????????这就是官方说的dirty data,  这时手动将zs3改回zs2,即可结束死循环并成功回滚zs1.为避免这种脏数据产生,应配合本地事务注解@Transaction ,全局事务开启,提交回滚可以监听的,默认实现是DefaultFailureHandlerImpl,需要自定义需要实现FailureHand接口,并自定义配置GlobalTransactionScanner 。 官方说明脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他)

 4.4 前面使用的是file,主流做法还是使用nacos做配置中心,注册中心

(注意:如果nacos开启了权限控制,要求nacos读取配置的seata-server,及微服务里nacos的java client都得配置username/password,否则报错403-unkown user)

 修改seata server主配置registry.conf

#注意这里配置里application="seata-server",默认,这个与后面的大坑有关
registry {
  type = "nacos"
  nacos {
    application = "seata-server"   
    serverAddr = "xxxx:8091"
    namespace = "public"
    group = "SEATA_GROUP"
    username = "xxxx"
    password = "xxxx"
  }

}
config {
  type = "nacos"
  nacos {
    serverAddr = "xxxx:8091"
    namespace = "public"
    group = "SEATA_GROUP"
    username = "xxxx"
    password = "xxxx"
  }

}

!!!新版1.4.2配置seata-server使用nacos作配置注册中心不必再使用registry.conf了,,直接docker映射出来的resources/application.yml配置nacos信息

 

配置好之后,启动seata,nacos

之前配置信息配置 是registry.file引入 file.conf里的配置,现改为nacos 故不再需要file.conf了,

使用nacos就是把原file.conf的配置全部移入nacos,但是手动移入麻烦,官方提供了一个较用以将配置写入nacos.

         先在seata目录创建config.txt(里面就是存的原file.conf的配置项,只是要改为k-v形式)配置如下,  再将https://github.com/seata/seata/tree/develop/script/config-center/nacos 搞下来, 执行 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -u nacos -w nacos -t test
(-t Tenant租户,就是namespace,其它参数说明详见nacos-config.sh),config.txt位置没放对,执行脚本会提示正确位。

      将配置批量写进nacos(其实就是post请求),也可以手动去nacos里改。这里面有几个参数由中画线改为了驼峰如dbType,driverClassName,版本不同可能报错。(注意去掉配置中的注释)

service.vgroupMapping.payment=default
service.vgroupMapping.study=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
store.mode=file #store.mode=xx是切换开关 ,store.mode=file就不必配置db等其它了,如果要配置多节点高可用则需要db分布式锁支持


store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
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
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson #这个在根据undolog回滚事务时Localedatetime反序列化出错可以考虑换这个
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

重启,然后访问nacos管理页面即可看到配置项,重启seata-server即可读取到配置信息。

Java client端不要作任何修改,测试回滚效果同file

4.5 其它

 1.client端其它可选配置

#Seata配置,配置seata-server地址等
seata:
  tx-service-group: ${spring.application.name}
  enabled: true                       #是否开启seata分布式事务
  enable-auto-data-source-proxy: true #是否开启数据源代理
  service:
      vgroup-mapping:
        user: default  #!!!!!!注意这里key不要写${spring.application.name}
      grouplist:
        default: 192.168.154.140:8091         #seata-server地址
      enable-degrade: false # 降级开关
      disable-global-transaction: false # 禁用全局事物(默认 false)

#seata client端配置,不配置用默认值也可以
  client:
      rm:
        report-retry-count: 5 # 一阶段结果上报TC充实次数(默认5)
        async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000)
        table-meta-check-enable: false # 自动刷新缓存中的表结构
        report-success-enable: true
        lock:
          retry-interval: 10 # 校验或占用全局锁重试间隔(默认10ms)
          retry-times: 30 # 校验或占用全局锁重试次数(默认30)
          retry-policy-branch-rollback-on-conflict: true
      tm:
        commit-retry-count: 3 # 一阶段全局提交上报 TC 重试次数(默认 1 次, 建议大于 1)
        rollback-retry-count: 3 # 一阶段全局回滚上报 TC 重试次数(默认 1 次, 建议大于 1)
      undo:
        data-validation: true # 二阶段回滚镜像校验(默认 true 开启)
        log-serialization: jackson # undo 序列化方式(默认 jackson)
        log-table: undo_log # 自定义undo表名(默认undo_log)
      log:
        exception-rate: 100 # 日志异常输出概率(默认 100)

2.优化配置nacos common.yml全局配置

#全局日志控制
logging.level:
  root: info  
  com.alibaba.nacos.naming.beat.sender: info  
  com.alibaba.nacos.client.naming.updater: info
  com.alibaba.nacos.client.config.impl.ClientWorker: error 

#全局分布式事务开关
seata:
  enabled: true                      
  enable-auto-data-source-proxy: true 
3.低版本的springcloud可以引入较低版本的seata,建议父工程锁定seata版本,或使用较高版本springcloud,避免踩到一些版本坑
   <!--父工程锁定seata版本1.4.0-->
   <dependencyManagement>
        <dependencies>     
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
                <version>${seat.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

     <!--子工程直接引入,也不必exclusive咯-->
     <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
     </dependency>

4.5 总结

TC (Transaction Coordinator) - 事务协调者(seata-server服务端)

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器(第一个@GlobleTransation方法)

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器(每个@GlobleTransation方法)

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

  • TM事务管理者开启全局事务
  • RM处理各自分支事务(异步的)
  • 分支事务完成后TC协调者汇总进行全局事务裁决(基于undo日志补偿)

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐