分布式事务:seata

Seata介绍

Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。
它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。
此外,通常分支事务本身就是一个关系数据库的本地事务。

 

Seata主要由三个重要组件组成:

  • TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。

  • TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务。

  • RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务。

Seata的执行流程如下:

  1. A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID

  2. A服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖

  3. A服务执行分支事务,向数据库做操作

  4. A服务开始远程调用B服务,此时XID会在微服务的调用链上传播

  5. B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖

  6. B服务执行分支事务,向数据库做操作

  7. 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚

  8. TC协调其管辖之下的所有分支事务, 决定是否回滚

Seata实现2PC与传统2PC的差别:

  1. 架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。

  2. 两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率

写隔离

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

  • 拿不到 全局锁 ,不能提交本地事务,拿到全局锁,提交本地事务并插入undo_log记录。

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

  示例

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 完成阶段 全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的 完成阶段 全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离  

  • 在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。

在 全局事务 提交之前,本地事务 会先提交,这时候查询数据,对于本地库是 Read Committed,对于全局来说是 Read Uncommitted。

  • 如果要求全局的读已提交,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

服务端

一.准备工作

下载Seata服务端包:https://github.com/seata/seata/releases;本例使用seata-1.4.0。

下载nacos服务端包:https://github.com/alibaba/nacos/releases;本例使用nacos-1.4.0。

(Window环境需要python环境,下载安装配置Python环境:https://www.python.org)。

二.修改配置

Seata解压,找到对应的conf文件夹

本例使用db模式,因此可以删除file.conf文件;

打开文件README-zh.md,可以找到 server所需SQL和部署脚本地址 (https://github.com/seata/seata/tree/develop/script/server);

config.txt 和 nacos-config.py 拷贝到conf 文件夹下;

新建数据库 seata , 将 SQL脚本导入执行,会生成三张表:global_table、branch_table、lock_table;

修改 registry.conf 配置文件:本例使用nacos,所以只保留 nacos 配置即可,需注意 namespace。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = "1344cd38-7919-4482-9c6f-11d73ab865b2"
    cluster = "default"
    username = ""
    password = ""
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "1344cd38-7919-4482-9c6f-11d73ab865b2"
    group = "DEFAULT_GROUP"
    username = ""
    password = ""
  }
}

修改 config.txt 配置文件:

...忽略
service.vgroupMapping.my_test_tx_group=default #seata客户端事务组名称
service.default.grouplist=127.0.0.1:8099
...忽略
store.mode=db #修改成db模式

store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://localhost:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
...忽略

修改 nacos-config.py 配置文件:

#!/usr/bin/env python3
#  -*- coding: UTF-8 -*-

import http.client
import sys

if len(sys.argv) <= 2:
    print ('python nacos-config.py nacosAddr')
    exit()

headers = {
    'content-type': "application/x-www-form-urlencoded"
}

# 将“../config.txt”需要改为“./config.txt”,因为 nacos-config.py和 config.txt在同目录 hasError
= False for line in open('./config.txt'): pair = line.split('=') if len(pair) < 2: continue print (line), url_prefix = sys.argv[1] conn = http.client.HTTPConnection(url_prefix) if len(sys.argv) == 3: namespace=sys.argv[2] url_postfix = '/nacos/v1/cs/configs?dataId={0}&group=SEATA_GROUP&content={1}&tenant={2}'.format(str(pair[0]),str(line[line.index('=')+1:]).strip(),namespace) else: url_postfix = '/nacos/v1/cs/configs?dataId={}&group=SEATA_GROUP&content={}'.format(str(pair[0]),str(line[line.index('=')+1:])).strip() conn.request("POST", url_postfix, headers=headers) res = conn.getresponse() data = res.read() if data.decode("utf-8") != "true": hasError = True if hasError: print ("init nacos config fail.") else: print ("init nacos config finished, please start seata-server.")

将seata配置推送到nacos:

切换到 Seata 的conf目录,执行命令: python nacos-config.py nacos服务地址 命名空间

例:python nacos-config.py 127.0.0.1:8848 1344cd38-7919-4482-9c6f-11d73ab865b2

三.启动seata服务

切换到 Seata 的 bin 目录,执行命令:sh seata-server.sh

完成。

客户端

一.引入包

compile 'io.seata:seata-spring-boot-starter:1.4.0'

二.yml配置

seata:
  enabled: true
  data-source-proxy-mode: AT
  application-id: account-seata-example
  tx-service-group: my_test_tx_group # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
  enable-auto-data-source-proxy: true
  use-jdk-proxy: false
  client:
    rm:
      report-success-enable: true
      table-meta-check-enable: false # 自动刷新缓存中的表结构(默认false)
      report-retry-count: 5 # 一阶段结果上报TC重试次数(默认5)
      async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000)
      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)
      degrade-check: false
      degrade-check-period: 2000
      degrade-check-allow-times: 10
    undo:
      data-validation: true # 二阶段回滚镜像校验(默认true开启)
      log-serialization: jackson # undo序列化方式(默认jackson)
      log-table: undo_log  # 自定义undo表名(默认undo_log)
    log:
      exceptionRate: 100 # 日志异常输出概率(默认100)

  service:
    vgroupMapping:
      my_test_tx_group: default  # TC 集群(必须与seata-server保持一致)
    enable-degrade: false # 降级开关
    disable-global-transaction: false # 禁用全局事务(默认false)
    grouplist:
      default: 127.0.0.1:8091
  transport:
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      server-executor-thread-prefix: NettyServerBizHandler
      share-boss-worker: false
      client-selector-thread-prefix: NettyClientSelector
      client-selector-thread-size: 1
      client-worker-thread-prefix: NettyClientWorkerThread
    type: TCP
    server: NIO
    heartbeat: true
    serialization: seata
    compressor: none
    enable-client-batch-send-request: true # 客户端事务消息请求是否批量合并发送(默认true)
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace:
      group: DEFAULT_GROUP
      cluster: default
  config:
#    file:  # 使用 nacos 后就不能打开,否则报错
#      name: file.conf
#    type: nacos  # 不能打开,否则报错,有可能是新版已经没有该配置
    nacos:
      namespace:
      server-addr: localhost:8848
      group: DEFAULT_GROUP

三.业务层使用 

@GlobalTransactional(rollbackFor = Exception.class)

 

posted @ 2020-12-25 17:57  柒月丶  阅读(733)  评论(0)    收藏  举报