seata使用,seata 三大模式

  seata使用,seata 三大模式

  背景在Seata的四种交易模式中,AT交易模式是阿里系统独创的一种交易模式,对业务没有入侵,也是Seata用户数量最多的一种交易模式,兼具易用性和高性能。

  目前,Seata社区正在大力推进其多语言版本建设,Go、PHP、JS、Python四个语言版本基本完成了TCC事务模式的实现。参考Seata v1.5.2中AT模式的实现和Seata官方文档,尝试从代码角度解释Seata AT事务模式的详细流程,旨在梳理Seata Java版本中AT模式的实现细节,并在后续的多语种版本开发中优先考虑AT事务模式的实现。

  1.什么是at模式?AT模式是具有两阶段提交的分布式事务模式。它使用本地撤销日志来描述数据修改前后的状态,并使用它来进行回滚。从性能上看,由于undo log的存在,AT模式可以在一阶段执行后立即释放锁和连接资源,其吞吐量高于XA模式。用户在使用AT模式时,只需要配置相应的数据源即可。交易提交和回滚的过程由Seata自动完成,对用户业务几乎没有入侵,使用方便。

  2.AT模式在谈到数据库与ACID和CAP的事务模式时,一般会先讨论与事务相关的ACID的特性,但在分布式场景下,也要考虑CAP的性质。

  2.1在数据库本地事务隔离级别at和ACID Read Committed或以上的基础上,Seata(AT模式)的默认全局隔离级别为Read Uncommitted。

  如果它应用于特定的场景,它必须要求全局读取已经被提交。目前,Seata由SELECT FOR UPDATE语句表示。

  SELECT FOR UPDATE语句的执行会查询全局锁,如果全局锁被其他事务持有,它会释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。在这个过程中,查询被bblocked,直到获得全局锁,也就是说,读取的相关数据在被返回之前已经被提交。

  出于整体性能的考虑,Seata目前的方案并不代理所有SELECT语句,只代理FOR UPDATE的SELECT语句。

  2.1.2写隔离在at模式下,at会拦截写操作的SQL。在提交本地事务之前,它将从TC获得全局锁。如果没有获得全局锁,它就不能写,以确保不会有写冲突:

  在提交第一阶段本地事务之前,需要确保首先获得全局锁;

  无法提交本地事务,因为无法获得全局锁;

  获取全局锁的尝试被限制在某个范围内。如果超出范围,将被放弃,本地事务将被回滚以释放本地锁。

  2.2一般来说,AT和CAPSeata的所有事务模式都需要保证CP,即一致性和分区容错,因为分布式事务的核心是保证数据一致性(包括弱一致性)。比如在一些交易场景中,多个系统涉及的金额发生变化,保证一致性可以避免系统的资金损失。

  不可避免的是,服务在分布式系统中是不可用的。例如,当Seata的TC不可用时,用户可能希望降级服务,以首先确保整个服务的可用性。这时候Seata就需要从CP系统变成保障AP的系统。

  比如有一个服务,为用户提供修改信息的功能。如果此时TC服务出现问题,为了不影响用户体验,我们希望服务仍然可用,但是所有SQL的执行会降级为本地事务而不是全局事务。

  默认情况下,AT模式保证CP,但为用户提供了在CP和AP模式之间切换的配置通道:

  配置文件的Tm.degradation-check参数,如果其值为真,则分支事务保证AP,否则保证CP;

  手动修改配置中心的service . disableglobaltransaction属性为真,然后关闭全局事务执行AP。

  3.AT数据源的代理在AT模式下,用户只需要配置AT的代理数据源,AT的所有过程都在代理数据源中完成,对用户没有任何感知。

  整个班级

  在AT的DataSource agent中,分别表示目标数据库的数据源、连接和语句。在执行目标SQL动作之前,完成RM资源注册、撤销日志生成、分支事务注册、分支事务提交/回滚等操作,但是这些操作并没有被用户感知。

  下面的序列图显示了在AT模式执行期间这些代理类的操作细节:

  4.AT模式流程以下是AT模式的整体流程。从这里可以看到分布式事务的每个关键动作的执行时间。我们将在后面讨论每个动作的细节:

  4.1第一阶段在AT模式的第一阶段,Seata会通过代理数据源拦截用户执行的业务SQL。如果用户没有打开一个事务,它将自动打开一个新的事务。如果业务SQL是写操作(添加、删除、更改)类型,那么它会解析业务SQL的语法,生成SELECT SQL语句,找出要修改的记录,保存为“镜像前”。然后执行业务SQL。执行完毕后,用同样的原理找出修改过的记录,保存为“后像”。至此,撤消日志记录完成。然后RM会向TC注册分支交易,TC端会增加一条新的锁记录。锁可以保证AT模式的读写隔离。RM提交撤销日志和业务SQL的本地事务,保证业务SQL和保留撤销日志记录的SQL的原子性。

  4.2两阶段提交在AT模式的两阶段提交中,TC端会删除该事务的锁,然后通知rm异步删除撤销日志记录。

  4.3两阶段回滚如果AT模式的第二阶段是回滚,那么RM侧需要根据第一阶段保存的撤销日志数据中的before image记录,通过反向SQL恢复第一阶段修改的业务数据。

  但是在恢复数据之前,有必要检查脏数据。因为在第一阶段提交到当前回滚期间,记录可能已被其他业务更改。验证的方法是将撤消日志的后像与当前数据库中的数据进行比较。如果数据一致,则不存在脏数据。不一致意味着存在脏数据。如果有脏数据,需要手动处理。

  5.关键代码模块以下是at模式整个流程的主要模块,从中可以了解到开发AT模式需要做哪些工作:

  5.1撤销日志数据格式撤销日志存在于表undo_log中,undo_log的表结构如下:

  Rollback_info存储修改前后业务数据的内容,数据表存储压缩格式。他的明文格式如下:

  {

   branchId:2828558179596595558,

  sqlUndoLogs:[

  {

  残像:{

  行:[

  {

  字段:[

  {

  keyType:PRIMARY_KEY ,

  姓名: id ,

   type:4,

  值:3

  },

  {

  keyType:NULL ,

  名称:计数,

   type:4,

  值:70

  }

  ]

  }

  ],

  表名: stock_tbl

  },

  图像前:{

  行:[

  {

  字段:[

  {

  keyType:PRIMARY_KEY ,

  姓名: id ,

   type:4,

  值:3

  },

  {

  keyType:NULL ,

  名称:计数,

   type:4,

  “值”:100

  }

  ]

  }

  ],

  表名: stock_tbl

  },

  sqlType:UPDATE ,

  表名: stock_tbl

  }

  ],

   xid : 192 . 168 . 51 . 102:8091:28285 . 179555565556

  } 5.2 UndologmanagerUndologmanager负责添加、删除和回滚撤消日志。不同的数据库有不同的实现(不同数据库的SQL语法会不同),通用的逻辑放在AbstractUndoLogManager的抽象类中。总体的类继承关系如下:

  插入和删除撤销日志的逻辑比较简单,直接操作数据表即可。以下是回滚撤消日志的逻辑:

  源代码分析如下:

  @覆盖

  public void undo(data source proxy data source proxy,String xid,long branchId)引发TransactionException {

  连接连接=空;b

  结果集rs=null

  prepared statement select PST=null;

  boolean originalAutoCommit=true

  for(;) {

  尝试{

  conn=data source proxy . getplainconnection();

  //整个撤消过程应该在本地事务中运行。

  //启动一个本地事务,确保删除撤销日志和恢复业务数据的SQL在一个事务中提交。

  if(originalautocomit=conn . getautocommit()){

  conn . set自动提交(false);

  }

  //查找撤消日志

  SELECT PST=conn . prepare语句(SELECT _ UNDO _ LOG _ SQL);

  selectPST.setLong(1,分支id);

  selectPST.setString(2,xid);

  //查出鳃类的所有撤消日志记录,用来恢复业务数据

  RS=选择PST。执行查询();

  布尔存在=假

  while (rs.next()) {

  存在=真

  //有可能是服务器反复发送回滚请求进行回滚

  //同一个分支事务到多个流程,

  //确保只处理正常状态的撤销日志.

  int状态=RS。getint(ClientTableColumnsName .撤消_日志_日志_状态);

  //如果状态=1,说明可以回滚;状态=1说明不能回滚

  如果(!canUndo(状态)){

  if (LOGGER.isInfoEnabled()) {

  LOGGER.info(xid {} branch {},ignore {} undo_log ,xid,branchId,state);

  }

  返回;

  }

  字符串上下文string=RS。getstring(ClientTableColumnsName .撤消_日志_上下文);

  映射字符串,字符串上下文=解析上下文(上下文字符串);

  byte[]roll back info=get roll back info(RS);

  字符串序列化程序=context==null?空:上下文。get(撤消日志常量.SERIALIZER _ KEY);

  //根据串行器获取序列化工具类

  撤消日志解析器parser=serializer==null?undologparserfactory。getinstance()

  :undologparserfactory。getinstance(序列化程序);

  //反序列化撤消日志,得到业务记录修改前后的明文

  分支撤消日志。解码(回滚信息);

  尝试{

  //将序列化程序名称放到本地

  setCurrentSerializer(解析器。getname());

  list SQLUndoLog sqlUndoLogs=分支撤消日志。getsqlundologs();

  if (sqlUndoLogs.size() 1) {

  收藏。反向(sqlUndoLogs);

  }

  for(SQLUndoLog SQLUndoLog:sqlUndoLogs){

  表元表元=表元缓存工厂。gettablemetacache(数据源代理。获取dbtype().getTableMeta(

  conn,sqlUndoLog.getTableName(),数据源代理。getresourceid());

  sqlundolog。可设置元(表格元);

  撤销执行器撤销执行器=撤销执行器工厂。getundo执行器(

  dataSourceProxy.getDbType()、sqlUndoLog);

  撤消执行程序。executeon(康涅狄格州);

  }

  }最后{

  //删除序列化程序名称

  removeCurrentSerializer();

  }

  }

  //如果撤消日志存在,说明分支事务已经完成第一阶段,

  //我们可以直接回滚并清除撤消日志

  //否则表示分支事务出现异常,

  //导致撤消日志不写入数据库。

  //比如业务处理超时,全局事务是发起方回滚。

  //为了保证数据的一致性,我们可以插入一个具有全球完成状态的撤消日志

  //阻止其他程序第一阶段的本地事务正确提交。

  //参见https://github.com/seata/seata/issues/489

  如果(存在){

  deleteUndoLog(xid,branchId,conn);

  conn . commit();

  if (LOGGER.isInfoEnabled()) {

  LOGGER.info(xid {} branch {},undo_log deleted with {} ,xid,branchId,

  状态全球完成。name());

  }

  }否则{

  //如果不存在撤消日志,可能是因为分支事务还未执行完成(比如,分支事务执行超时),TM发起了回滚全局事务的请求。

  //这个时候,往撤消日志表插入一条记录,可以使分支事务提交的时候失败(撤消日志)

  insertUndoLogWithGlobalFinished(xid,branchId,undologparserfactory。getinstance()、conn);

  conn . commit();

  if (LOGGER.isInfoEnabled()) {

  LOGGER.info(xid {} branch {},undo_log添加了{} ,xid,branchId,

  状态全球完成。name());

  }

  }

  返回;

  } catch(SQLIntegrityConstraintViolationException e){

  //可能的撤消日志已被其他进程插入数据库,正在重试回滚撤消日志

  if (LOGGER.isInfoEnabled()) {

  LOGGER.info(xid {} branch {},undo_log inserted,retry rollback ,xid,branch id);

  }

  } catch(可投掷e) {

  如果(conn!=null) {

  尝试{

  conn . roll back();

  } catch(SQLException roll backex){

  LOGGER.warn(撤消时无法关闭数据库编程资源. ,rollbackEx);

  }

  }

  抛出新的分支事务异常(BranchRollbackFailed _ retrieble,String。格式(分支会话回滚失败,稍后重试xid=%s branchId=%s %s ,xid,

  branchId,e.getMessage()),e);

  }最后{

  尝试{

  如果(rs!=null) {

  RS。close();

  }

  如果(选择PST!=null) {

  选择pst。close();

  }

  如果(conn!=null) {

  if(originalautocomit){

  conn . set自动提交(true);

  }

  conn . close();

  }

  } catch (SQLException closeEx) {

  LOGGER.warn(撤消时无法关闭数据库编程资源. ,close ex);

  }

  }

  }

  }备注:需要特别注意下,当回滚的时候,发现撤消日志不存在,需要往撤消日志表新加一条记录,避免因为空间在铥发出回滚请求后,又成功提交分支事务的场景。

  5.3压缩机压缩算法压缩机接口定义了压缩算法的规范,用来压缩文本,节省存储空间:

  公共接口压缩器{

  /**

  *将字节[]压缩为字节[]。

  * @param字节字节

  * @返回字节[]

  */

  byte[]compress(byte[]bytes);

  /**

  *将字节[]解压缩为字节[]。

  * @param字节字节

  * @返回字节[]

  */

  字节[]解压缩(byte[]bytes);

  }目前已经实现的压缩算法有如下这些:

  5.4撤销解析器序列化算法串行器接口定义了序列化算法的规范,用来序列化代码:

  公共接口UndoLogParser {

  /**

  *获取解析器的名称;

  *

  * @返回解析器的名称

  */

  string getName();

  /**

  *获取此解析器的默认上下文

  *

  * @如果撤消日志为空,则返回默认内容

  */

  byte[]获取默认内容();

  /**

  *将分支撤消日志编码到字节数组。

  *

  * @param branchUndoLog分支撤消日志

  * @返回字节数组

  */

  byte[]encode(分支撤销日志分支撤销日志);

  /**

  *将字节数组解码到分支撤消日志。

  *

  * @param字节字节数组

  * @返回分支撤消日志

  */

  BranchUndoLog解码(byte[]bytes);

  }目前已经实现的序列化算法有如下这些:

  5.5执行人执行器执行者是结构化查询语言执行的入口类,在在执行结构化查询语言前后,需要管理撤消日志的图像记录,主要是构建撤消日志,包括根据不同的业务SQL,来组装查询撤消日志的结构化查询语言语句;执行查询撤消日志的SQL,获取到镜像记录数据;执行插入撤消日志的逻辑(未提交事务)。

  公共接口执行器T {

  /**

  *执行t .

  *

  * @param args参数

  * @返回t

  * @ throws可扔可扔

  */

  t执行(对象.args)throws Throwable;

  }针对不同的业务SQL,有不同的执行者实现,主要是因为不同操作/不同数据库类型的业务SQL,生成撤消日志的结构化查询语言的逻辑不同,所以都分别重写了beforeImage()和余像()方法。整体的继承关系如下图所示:

  为了直观地看到不同类型的结构化查询语言生成的在图像结构化查询语言之前和在图像结构化查询语言之后,这里做个梳理。假如目标数据表的结构如下:

  创建表` t `(

  ` id int(64)NOT NULL AUTO _ INCREMENT

  ` name ` varchar(64)COLLATE utf8mb 4 _ unicode _ ci DEFAULT ,

  ` addr ` varchar(64)COLLATE utf8mb 4 _ unicode _ ci DEFAULT ,

  主键(` id `)

  )ENGINE=InnoDB DEFAULT CHARSET=utf8mb 4 COLLATE=utf8mb 4 _ unicode _ ci

  插入到` t` (`id `,` name `,` addr `)值中(1、《汤姆》、《北京》);

  插入到` t` (`id `,` name `,` addr `)值中(2,‘杰克’,‘南京’);

  注:图片建议在个人电脑端查看

  5.6异步工人是用来做异步执行的,用来做分支事务提交和撤消日志记录删除等操作。

  6、关于性能并不存在某一种完美的分布式事务机制可以适应所有场景,完美满足所有需求。无论(At)人名(越)乙模式、TCC模式还是冒险故事模式,本质上都是对辅助放大器(辅助放大器的缩写)规范在各种场景下安全性或者性能的不足的改进100 .西塔不同的事务模式是在一致性、可靠性、易用性、性能四个特性之间进行不同的取舍。

  近日,Seata社区发现有同行在没有详细分析at模式Java版代码的详细实现的情况下,只测试了一个早期Go版本的短链接的Seata,并对AT模式的性能及其数据安全性提出了质疑。在接受这个结论之前,有一定思辨能力的用户朋友们,要仔细检查自己的测试方法和测试对象,分清“李鬼”和“李悝jy”。

  其实这个早期Go版本只是指Seata v1.4.0,并没有严格实现Seata AT模式的所有功能。另一方面,即使是它推崇的Seata XA车型,也依赖于单DB的XA车型。目前最新版本的MySQL XA事务模式还存在很多bug,这个基础并没有预想的100%稳定。

  阿里和蚂蚁集团共同打造的Seata,是我们多年内部分布式事务工程实践和技术经验的结晶。开源后,经过150多家行业同行的生产环境验证。开元大道既长又宽。这条路上可以有机动车道、非机动车道、人行道。让我们一起来拓宽延伸道路,而不是站在人行道上宣传机动车道危险,慢行。

  7.总结Seata AT模式依赖于各个DB厂商不同版本的DB驱动(数据库驱动)。每个数据库发布新版本后,其SQL语义和使用模式可能会发生变化。近年来,Seata被用户广泛应用于各种商业场景。得益于开发者的努力,Seata AT mode保持了与其XA mode几乎一致的编程接口,适配几乎所有主流数据库,并覆盖了这些数据库的主要流行版本的驱动程序:真正将分布式系统的“复杂性”留在了框架层面,并给予用户易用性和高性能。

  当然,Seata Java版本的XA和AT模型还有待完善,更不用说其他多语言版本的实现了。欢迎有志于Seata及其多语种版本建设的同仁参与Seata的建设,共同努力将Seata打造成一个标准化的分布式交易平台。

  版权归作者所有:原创作品来自博主小二上九8,转载请联系作者取得转载授权,否则将追究法律责任。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: