Seata分布式事务

Seata分布式事务

分布式事务介绍

什么是事务

       举一个简单的例子,日常最常见的场景为银行转账,此时有两个账户分别为A和B,此时账户A 需要向账 户B转账1000元,此时账户A余额减1000元(此操作我们用 Action 1 代表),账户B余额加1000元(此操作我们用Action 2代表);Action 1和Action 2两个操作一定存在先后顺序;那么在此操作过程中如果出现"异常问题",导致出现账户A余额已经减少1000元,但是账户B并未收到转账;为了解决这种因为"不确定性"因素导致的问题,引入了事务的概念。"事务"要求的是,在Action 1和Action 2操作时,如果出现了"异常"问题,要么这两个操作同时成功,要么这两个操作同时失败(事务的原子性)。事务就是通过它的 ACID(原子性、一致性、隔离性、持久性) 特性,保证一系列的操作在任何情况下都可以安全正确的执行

什么是分布式事务

        随着系统数据量的提升,微服务架构的发展,我们从传统的一个应用一个服务一个数据库的时代,发展到,一个应用对应多个服务(并且服务部署到了不同的服务器,这也就是所谓的SOA化,也就是业务服务化),每个服务都对应有自己的数据库,此时当多个服务之间存在事务型操作时,本地的事务无法满足,多个服务之间多个数据库之间的事务型操作;分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性

CAP理论

          CAP理论是分布式事务处理的理论基础:分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(PartitionTolerance)中满足两种,无法兼顾三种。

  • 一致性(Consistency):服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。
  • 可用性(Availability):服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。
  • 分区容忍性(Partition Tolerance):分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。

网络分区解释:分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。

Base理论

         BASE理论由eBay架构师Dan Pritchett提出,在2008年上被分表为论文,并且eBay给出了他们在实践中总结的基于BASE理论的一套新的分布式事务解决方案。
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求。即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
       针对数据库领域,BASE思想的主要实现是对业务数据进行拆分,让不同的数据分布在不同的机器上,以提升系统的可用性,当前主要有以下两种做法:按功能划分数据库分片(如开源的Mycat、Amoeba等)。
       由于拆分后会涉及分布式事务问题,所以eBay在该BASE论文中提到了如何用最终一致性的思路来实现高性能的分布式事务。

Base理论核心三要素介绍:

  • 基本可用
    基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
    比如:
    • 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
    • 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
  • 软状态
    软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
  • 最终一致性
    最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

Seate简介

       Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata整体架构图

image.png
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

在 Seata 中,分布式事务的执行流程

  • TM 开启分布式事务(TM 向 TC 注册全局事务记录)。
  • 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 )。
  • TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务)。
  • TC 汇总事务信息,决定分布式事务是提交还是回滚。
  • TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。

TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

Seata事务模式介绍

Seata 定义了全局事务的框架,主要分为以下几步:

  1. TM 向 TC请求 发起(Begin)、提交(Commit)、回滚(Rollback)等全局事务。
  2. TM把代表全局事务的XID绑定到分支事务上。(此步骤,其实就是将多个业务系统串联在一起,用一个统一的XID,进行事务关联)
  3. RM向TC注册,把分支事务关联到XID代表的全局事务中。
  4. RM把分支事务的执行结果上报给TC。
  5. TC发送分支提交(Branch Commit)或分支回滚(Branch Rollback)命令给RM。
Seata-AT模式

AT模式前提:

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

特点:

  1. 最终一致性。

  2. 性能较XA高。

  3. 只在第一阶段获取锁,在第一阶段进行提交后释放锁。

    一阶段中,Seata会拦截业务SQL ,首先解析SQL语义,找到要操作的业务数据,在数据被操作前,保存下来记录 undo log(记录事务开始前的状态,用于事务失败时的回滚操作),然后执行业务SQL 更新数据,更新之后再次保存数据 redo log(记录的是“在某个数据页上做了什么修改”),最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。
    相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的 undo Log ,通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。
    AT流程分为两阶段,主要逻辑全部在第一阶段,第二阶段主要做回滚或日志清理的工作
    image.png
    订单服务中TM向TC申请开启一个全局事务,TC会返回一个全局事务ID(XID),订单服务在执行本地事务之前,RM会先向TC注册一个分支事务, 订单服务依次生成undo log 执行本地事务,生成redo log 提交本地事务,向TC汇报,事务执行好了。
    订单服务发起远程调用,将事务ID传递给库存服务,库存服务在执行本地事务之前,先向TC注册分支事务,库存服务同样生成undo Log和redo Log,向TC汇报,事务状态成功。
    如果正常全局提交,TC通知RM一步清理掉本地undo和redo日志,如果存在一个服务执行失败,那么发起回滚请求。通过undo log进行回滚。
    在这里还会存在一个问题,因为每个事务从本地提交到通知回滚这段时间里面,可能这条数据已经被其他事务进行修改,如果直接用undo log进行回滚,可能会导致数据不一致的情况,
    这个时候 RM会用 redo log进行验证,对比数据是否一样,从而得知数据是否有别的事务进行修改过,undo log是用于被修改前的数据,可以用来回滚,redolog是用于被修改后的数据,用于回滚校验。
    如果数据没有被其他事务修改过,可以直接进行回滚,如果是脏数据,redolog校验后进行处理

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁

  • 拿不到 全局锁 ,不能提交本地事务。

  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

    如果此时有T1和T2两个全局事务,都是对同一张表a进行事务操作;如果此时T1先开始,那么T1会先开启本地事务,拿到本地锁然后更新数据,然后T1会拿到全局锁后释放本地锁;当T2开始操作时,会去拿T1释放的本地锁,执行更新操作后,去尝试获取全局锁,但是此时的全局锁被T1拿着,所以此时T2会等待重试获取全局锁。正常情况下,T1执行完第二阶段后会释放全局锁,然后T2拿到全局锁后在执行。
    但是如果此时T1需要在第二阶段进行回滚操作时,那么此时T1需要拿到本地锁,但是此时的本地锁在T2手中,那么会出现的情况就是,T2需要重试等待T1的全局锁,超时后,放弃等待获取全局锁,回滚本地事务释放本地锁,然后T1获取本地锁回滚事务。
    因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

Seata-TCC模式

      一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的

  • 一阶段 prepare (准备)行为
  • 二阶段 commit (提交)或 rollback (回滚)行为

image.png
TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel ),他们的具体含义如下

  1. Try:对业务资源的检查并预留(对应上图中的Prepare)。
  2. Confirm:对业务处理进行提交,即 commit 操作(对应上图中的commit),只要 Try 成功,那么该步骤一定成功。
  3. Cancel:对业务处理进行取消,即回滚操作(对应上图中的Rollback),该步骤回对 Try 预留的资源进行释放。

特点
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。
image.png

Seata-Saga模式

      Saga模式是SEATA提供的长事务(运行时间较长,调用链路较长的事务)解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务(执行处理时候出错了,给一个修复的机会)都由业务开发实现。
image.png
适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
  • 老系统,封闭的系统(无法修改,同时没有任何分布式事务引入)

优势:

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

缺点:

  • 不保证隔离性

     所有的子业务都不在直接参与整体事务的处理(只负责本地事务的处理),而是全部交由了最终调用端来负责实现,而在进行总业务逻辑处理时,在某一个子业务出现问题时,则自动补偿全面已经成功的其他参与者,这样一阶段的正向服务调用和二阶段的服务补偿处理全部由总业务开发实现。

Sage实现

      目前SEATA提供的Saga模式是基于状态机引擎来实现的,需要开发者手工的进行Saga业务流程绘制,并且将其转换为Json配置文件,而后在程序运行时,将依据子配置文件实现业务处理以及服务补偿处理

实现机制

注意: 异常发生时是否进行补偿也可由用户自定义决定

  1. 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
  2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  3. 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
  4. 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

具体使用方式,可参考文档
image.png

Seata-XA模式

XA模式前提

  • 支持XA 事务的数据库(部分关系数据库支持(Oracle、MySQL等))。
  • Java 应用,通过 JDBC 访问数据库。

特点

  • 强一致性
  • 数据库占用时间比较长,性能比较低

XA模式的执行流程

  • 第一阶段:准备阶段:协调者(TC)询问参与者™事务是否执行成功,参与者发回事务执行结果。
  • 第二阶段:提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。

image.png

存在的问题
同步阻塞:所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
单点问题 :协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
数据不一致 :在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
太过保守:任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

    总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘

总结

   总的来说在Seata的中AT模式基本可以满足百分之80的分布式事务的业务需求,AT模式实现的是最终一致性,所以可能存在中间状态,而XA模式实现的强一致性,所以效率较低一点,而Saga可以用来处理不同开发语言之间的分布式事务,所以关于分布式事务的四大模型,基本可以满足所有的业务场景,其中XA和AT没有业务侵入性,而Saga和TCC具有一定的业务侵入。

Seata中AT模式演示

安装Seata

此处安装的seata版本为1.5.0

  • 使用docker拉取镜像

    docker pull seataio/seata-server:1.5.0
    
  • 运行镜像(此处是为了将文件挂载出来先运行一个容器)

    docker run -d --name seata-server -p 8091:8091  (容器id)
    
  • 将容器中的内容cp出来

    docker cp seata-server(容器名):/seata-server/resources /home/seata(宿主机目录下的路径)

  • 强删容器

    docker rm -f seata-server
    

建表语句:
此处基于mysql创建seata需要的表结构
seata/script/server/db at develop · seata/seata

  • 异步任务调度表: distributed_lock(此表是1.5.x版本更新的内容,用于 seata-server 异步任务调度 )
  • 分支事务表名: branch_table
  • 全局事务表名: global_table
  • 全局锁表名: lock_table

image.png
修改配置文件:
application.example.yml:为参考文件
application.yml:实际需要修改的配置文件
image.png

server:
  port: 7091

spring:
  application:
    name: seata-server

# 日志配置
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  # 不外接日志,故如下配置可暂不考虑  
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash
# 新增加的console控制台,
# 可通过访问http://localhost:7091进行登录,账号如下seata/seata
console:
  user:
    username: seata
    password: seata

seata:
  #配置中心配置
   config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: ip:8848
      namespace: public
      group: DEFAULT_GROUP
      username: 
      password: 
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      # 该data-id需要在nacos中在进行配置(此处需要用yml)
      data-id: seataServer.yml
  #注册中心配置  
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: ip:8848
      group: DEFAULT_GROUP
      namespace: public
      cluster: default
      username: 
      password: 
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
  #数据库配置(因为此处配置nacos注册中和配置中心,可以不需要在此处修改)  
  store:
    # support: file 、 db 、 redis
    # mode: db
    
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

此处为seata,配置的nacos动态配置的db
image.png
踩坑记录,此处如果配置了配置中心,那么需要用yml,因为此处是读到seata中的application.yml中,如果使用动态配置的话,需要和seata中的保持一致;

配置参考

store:
  mode: db
  #-----db-----
  db:
    datasource: druid
    dbType: mysql
    # 需要根据mysql的版本调整driverClassName
    # mysql8及以上版本对应的driver:com.mysql.cj.jdbc.Driver
    # mysql8以下版本的driver:com.mysql.jdbc.Driver
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:3306/seata?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&rewriteBatchedStatements=true
    user: 
    password: 
    # 数据库最大连接数
    maxConn: 20
    # 获取连接时最大等待时间 默认5000,单位毫秒
    maxWait: 5000
    # 数据库初始连接数
    minConn: 1
    # 查询全局事务一次的最大条数 默认100
    queryLimit: 100
    # 异步任务调度表
    distributedLockTable: distributed_lock
    # 分支事务表名 默认branch_table
    branchTable: branch_table
    # 全局事务表名 默认global_table
    globalTable: global_table
    # 全局锁表名 默认lock_table
    lockTable: lock_table
   

server:
  # 二阶段提交重试超时时长 单位ms,s,m,h,d,对应毫秒,秒,分,小时,天,默认毫秒。默认值-1表示无限重试
  # 公式: timeout>=now-globalTransactionBeginTime,true表示超时则不再重试
  # 注: 达到超时时间后将不会做任何重试,有数据不一致风险,除非业务自行可校准数据,否者慎用
  maxCommitRetryTimeout: -1
  # 二阶段回滚重试超时时长
  maxRollbackRetryTimeout: -1
  recovery:
    #二阶段异步提交状态重试提交线程间隔时间 默认1000,单位毫秒
    asynCommittingRetryPeriod: 1000
    #二阶段提交未完成状态全局事务重试提交线程间隔时间 默认1000,单位毫秒
    committingRetryPeriod: 1000
    #二阶段回滚状态重试回滚线程间隔时间  默认1000,单位毫秒
    rollbackingRetryPeriod: 1000
    #超时状态检测重试线程间隔时间 默认1000,单位毫秒,检测出超时将全局事务置入回滚会话管理器
    timeoutRetryPeriod: 1000
  undo:
    #undo清理线程间隔时间 默认86400000,单位毫秒
    logDeletePeriod: 86400000
    #undo保留天数 默认7天,log_status=1(附录3)和未正常清理的undo
    logSaveDays: 7
#事务分组名=集群名称(当seata搭建了集群时可以添加指定的名称,但是未搭建集群时,设置为default)    
service:
  vgroupMapping:
    default_tx_group: default      
    
#client端配置(这部分配置我的理解是,可以配置到具体服务模块,没必要配置到此处)
client:
  #资源管理器配置
  rm:
    #异步提交缓存队列长度
    asyncCommitBufferLimit: 10000
    lock:
      #校验或占用全局锁重试间隔
      retryInterval: 10
      #分支事务与其它全局回滚事务冲突时锁策略 默认true,优先释放本地锁让回滚成功
      retryPolicyBranchRollbackOnConflict: true
      #校验或占用全局锁重试次数
      retryTimes: 30
    #一阶段结果上报TC重试次数  
    reportRetryCount: 5
    #是否上报一阶段成功
    reportSuccessEnable: false
    #是否开启saga分支注册
    #Saga模式中分支状态存储在状态机本地数据库中,
    #可通过状态机进行提交或回滚,为提高性能可考虑不用向TC注册Saga分支,
    #但需考虑状态机的可用性,默认false
    sagaBranchRegisterEnable: false
    #saga模式中数据序列化方式
    sagaJsonParser: fastjson
    #sql解析类型
    sqlParserType: druid
    #自动刷新缓存中的表结构
    tableMetaCheckEnable: true
    #定时刷新缓存中表结构间隔时间 (单位毫秒)
    tableMetaCheckerInterval: 60000
    tccActionInterceptorOrder: -2147482648
  #事务管理器  
  tm:
    #一阶段全局提交结果上报TC重试次数,默认1次,建议大于1
    commitRetryCount: 5
    #全局事务超时时间,默认60秒,TM检测到分支事务超时或TC检测到TM未做二阶段上报超时后,发起对分支事务的回滚
    defaultGlobalTransactionTimeout: 60000
    #降级开关,默认false。业务侧根据连续错误数自动降级不走seata事务
    degradeCheck: false
    #升降级达标阈值
    degradeCheckAllowTimes: 10
    #服务自检周期
    degradeCheckPeriod: 2000
    interceptorOrder: -2147482648
    #一阶段全局回滚结果上报TC重试次数
    rollbackRetryCount: 5
  undo:
    compress:
      #undo log压缩开关
      enable: true
      #undo log压缩阈值(默认值64k,压缩开关开启且undo log大小超过阈值时才进行压缩)
      threshold: 64k
      #undo log压缩算法(默认zip,可选NONE(不压缩)、GZIP、ZIP、SEVENZ、BZIP2、LZ4、DEFLATER、ZSTD)
      type: zip
    #二阶段回滚镜像校验  
    dataValidation: true
    #undo序列化方式
    logSerialization: jackson
    #自定义undo表名,默认undo_log
    logTable: undo_log
    #只生成被更新列的镜像
    onlyCareUpdateColumns: true            
    

重新启动seata(将我们改好的配置文件重新放入容器内部)
docker run -p 8091:8091 -p 7091:7091 --name seata-server --restart=always -e SEATA_IP=服务器外网ip -e SEATA_PORT=8091 -v /home/seata/resources(宿主机目录):/seata-server/resources(容器内目录) -d 6843114e8fbf

访问地址:ip:7091,注意此处需要打开服务器上的对应端口
初始密码为 seata seata
image.png
image.png

SpringCloud整合seata

这里模拟商品下单,来演示demo,创建两个数据库,分别对应product模块和order模块
image.png
此处的undo_log表为AT模式特有,其他的模式不需要创建这张表

--此处对应order模块
CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) DEFAULT NULL,
  `amount` double(11,2) DEFAULT NULL,
  `sum` int(11) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `replace_time` datetime DEFAULT NULL,
  `account_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


CREATE TABLE `undo_log` (
  `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,
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


-- 此处对应product模块
CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `price` double DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,
  `last_update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `product` VALUES ('1', '5', '100', '2020-09-18 21:31:13');
CREATE TABLE `undo_log` (
  `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,
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

order模块
<!-- nacos 作为服务注册中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 作为配置中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <exclusions>
    <exclusion>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<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>
spring:
   application:
      name: order-service
   main:
      allow-bean-definition-overriding: true
   cloud:
      nacos:
         discovery:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
         config:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
server:
   port: 8084

这部分内容也可以直接配置到nacos,config中

spring:
   datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://ip:3306/seata_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false
      driver-class-name: com.mysql.jdbc.Driver
      username: 
      password: 
      max-wait: 60000
      max-active: 100
      min-idle: 10
      initial-size: 10
mybatis-plus:
   mapper-locations: classpath:/mapper/*Mapper.xml
   typeAliasesPackage: icu.funkye.entity
   global-config:
      db-config:
         field-strategy: not-empty
         id-type: auto
         db-type: mysql
   configuration:
      map-underscore-to-camel-case: true
      cache-enabled: true
      auto-mapping-unknown-column-behavior: none
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#seata的主要配置      
seata:
   enabled: true
   application-id: orders-service
   #此处为事务没必要配置事务分组,因为我们的seata并未设置集群
   #tx-service-group: my_test_tx_group
   config:
      type: nacos
      nacos:
         namespace:
         serverAddr: 106.52.124.58:8848
         group: DEFAULT_GROUP
         username: "nacos"
         password: "nacos"
         data-id: seataServer.yml
   registry:
      type: nacos
      nacos:
         application: seata-server
         serverAddr: ip:8848
         group: DEFAULT_GROUP
         namespace:
         username: "nacos"
         password: "nacos"
   #data-source-proxy-mode: XA 此处不是xa模式,所以此处不需要配置
   client:
      undo:
         log-table: undo_log
         
         
server:
   tomcat:
      max-threads: 500

image.png
重点是controller部分内容

@RestController
    public class OrderController {
        private final static Logger logger = LoggerFactory.getLogger(OrderController.class);
        @Autowired
        IOrdersService ordersService;

        @RequestMapping("/save")
        //开启事务
         /**
     * lockRetryInternal:校验或占用全局锁重试间隔 默认10,单位毫秒
     * lockRetryTimes 校验或占用全局锁重试次数
     */
        @GlobalTransactional(lockRetryInternal = 10,lockRetryTimes = 30)
        public Boolean save(@RequestBody Orders orders) {
            //此处抛出空指针是为了模拟报错回滚
            throw new NullPointerException();
            //return ordersService.save(orders);
        }
    }

product模块
<!-- nacos 作为服务注册中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 作为配置中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <exclusions>
    <exclusion>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<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>
spring:
   application:
      name: product-service
   main:
      allow-bean-definition-overriding: true
   cloud:
      nacos:
         discovery:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
         config:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
server:
   port: 8083
spring:
   datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://ip:3306/seata_product?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&&useSSL=false
      driver-class-name: com.mysql.jdbc.Driver
      username: 
      password: 
      max-wait: 60000
      max-active: 100
      min-idle: 10
      initial-size: 10
mybatis-plus:
   mapper-locations: classpath:/mapper/*Mapper.xml
   typeAliasesPackage: icu.funkye.entity
   global-config:
      db-config:
         field-strategy: not-empty
         id-type: auto
         db-type: mysql
   configuration:
      map-underscore-to-camel-case: true
      cache-enabled: true
      auto-mapping-unknown-column-behavior: none
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
   enabled: true
   application-id: product-service
   #此处为事务没必要配置事务分组,因为我们的seata并未设置集群
   #tx-service-group: my_test_tx_group
   config:
      type: nacos
      nacos:
         namespace:
         serverAddr: ip:8848
         group: DEFAULT_GROUP
         username: "nacos"
         password: "nacos"
         data-id: seataServer.yml
   registry:
      type: nacos
      nacos:
         application: seata-server
         server-addr: ip:8848
         group: DEFAULT_GROUP
         namespace:
         username: "nacos"
         password: "nacos"
   client:
      undo:
         log-table: undo_log

image.png
主要核心部分

@RestController
public class ProductController {
        private final static Logger logger = LoggerFactory.getLogger(ProductController.class);
        @Autowired
        IProductService productService;

        @RequestMapping("/reduceStock")
        public Boolean reduceStock(@RequestParam(name = "id") Integer id, @RequestParam(name = "sum") Integer sum) {
            //throw new NullPointerException();
            return productService.reduceStock(id, sum);
        }
    }      
public interface IProductService extends IService<Product> {
    /**扣减库存*/
    Boolean reduceStock(Integer id, Integer amount);
}
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {

    @Override
     /**
     * lockRetryInternal:校验或占用全局锁重试间隔 默认10,单位毫秒
     * lockRetryTimes 校验或占用全局锁重试次数
     */
    @GlobalTransactional(lockRetryInternal = 5,lockRetryTimes = 200)
    @Transactional
    public Boolean reduceStock(Integer id, Integer sum) {
        return update(Wrappers.<Product>lambdaUpdate().eq(Product::getId, id).ge(Product::getStock, sum)
            .setSql("stock=stock-" + sum));
    }
}

client模块

此模块是用来调用order模块和product模块的部分,就是具体的消费者模块

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--feign-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- nacos 作为服务注册中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 作为配置中心 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<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>
spring:
   application:
      name: demo-client
   main:
      allow-bean-definition-overriding: true
   cloud:
      loadbalancer:
         retry:
            enabled: false
      nacos:
         discovery:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
         config:
            server-addr: ip:8848
            username: "nacos"
            password: "nacos"
server:
   port: 8081
ribbon:
   ConnectTimeout: 100000
   ReadTimeout: 1000000
   OkToRetryOnAllOperations: false
feign:
   sentinel:
      enabled: true
seata:
   enabled: true
   application-id: client
   #此处为事务没必要配置事务分组,因为我们的seata并未设置集群
   #tx-service-group: test
   config:
      type: nacos
      nacos:
         namespace:
         serverAddr: ip:8848
         group: DEFAULT_GROUP
         username: "nacos"
         password: "nacos"
         data-id: seataServer.yml
   registry:
      type: nacos
      nacos:
         application: seata-server
         serverAddr: ip:8848
         group: DEFAULT_GROUP
         namespace:
         username: "nacos"
         password: "nacos"
server:
   tomcat:
      max-threads: 500

image.png

@RestController
public class TestController {
        private final static Logger logger = LoggerFactory.getLogger(TestController.class);

        @Autowired
        private IOrderService orderService;

        @Autowired
        private IProductService productService;

        /**
         * 秒杀下单分布式事务测试提交
         */
        @GetMapping(value = "testCommit")
        @GlobalTransactional
        public Object testCommit(@RequestParam(name = "id",defaultValue = "1") Integer id,
                                 @RequestParam(name = "sum", defaultValue = "1") Integer sum) {
            System.out.println("开始全局事务,XID = " + RootContext.getXID());
            Boolean ok = productService.reduceStock(id, sum);
            if (ok) {
                LocalDateTime now = LocalDateTime.now();
                Orders orders = new Orders();
                orders.setCreateTime(now);
                orders.setProductId(id);
                orders.setReplaceTime(now);
                orders.setSum(sum);
                orderService.save(orders);
                if(ThreadLocalRandom.current().nextInt(20)==5){
                    throw new RuntimeException("mock fail");
                }
                return "ok";
            } else {
                return "fail";
            }
        }
    }
@FeignClient(value = "order-service")
public interface IOrderService {

    @RequestMapping(value = "/save")
    Boolean save(@RequestBody Orders orders);
}

@FeignClient(value = "product-service")
public interface IProductService {

    @RequestMapping(value = "/reduceStock")
    Boolean reduceStock(@RequestParam(name = "id") Integer id, @RequestParam(name = "sum") Integer sum);

}

演示图

1、三个服务分别启动后,可以通过命令去查看seata控制台输出的内容

docker logs -f seata-server(容器名) --tail 10

我们这里可以看到有对应的rm注册到seata上来了
image.png
2、在开始前对应表分别状态为

  • order表中没有数据

image.png

  • product表中

image.png

  • global_table表(此表是seata的全局事务表名

image.png
3、执行后

  • 此时global_table(全局事务表)表中会插入一条数据

image.png

  • product库中的undo_log表中会插入一天undolog日志

image.png

  • 此时因为是先执行product表中的更新语句这个时候会先更新product表

image.png

  • 此时因为在调用order模块是,我们模拟了报错,所以此处会回滚回来为100

image.png

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenlbang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值