共计 6088 个字符,预计需要花费 16 分钟才能阅读完成。
作者:陈健斌 github id:a364176773
Seata 是一款开源的分布式事务解决方案,star 高达 23000+,社区活跃度极高,致力于在微服务架构下提供高性能和简略易用的分布式事务服务,本文将分析 Seata 1.6.x 版本的外围个性,让用户对 Seata 有更深刻的意识。
Seata 1.6x 带来了什么?
- AT 模式语法及个性加强
首先在 1.6 上,咱们针对外围独创的 AT 模式进一步的进行了加强,反对了更多的罕用语法个性,如 mysql 下的 update join,oracle 及 pgsql 中的多主键。
- 申请响应通信模型优化
而在整体的通信架构上,进一步优化了网络通信模型,将 batch 及 pipeline 个性使用到极致,进一步晋升 Seata Server 到 Client 的整体吞吐量。
- 全局锁反对策略管制
全局锁方面减少了更加贴近业务应用场景的乐观 / 乐观获取锁的配置项,依据不同的业务场景抉择不同的锁策略,以上三点将会在后续的 AT 模式加强篇章中具体介绍。
- 反对 JDK17 & Spring Boot 3.0
在技术摸索及先进性上,咱们早早的在 1.5.x 上便反对了 jdk17,而在 1.6.1 中社区更进一步,率先反对了 spring boot3 并且是齐全兼容 springboot2 的模式,而非独自分支模式反对,这两个重大 feature 将使业务同学选型底层内核版本时更加从容自在。
- 反对多注册核心服务裸露
服务裸露发现模型上,咱们减少了对多个注册核心同时裸露 Seata-Server 的 feature,这将在后续篇章中与大家进一步分享。
- ……
除此之外,Seata 1.6.x 有更多的 optimize 和 bugfix,这里不再一一开展介绍,欢送大家尝鲜 Seata 最新 release 版本,有任何问题欢送在 github 中沟通及探讨。
AT 模式加强
AT 模式语法及个性反对
AT 模式原理回顾
首先咱们先来回顾下 AT 模式的流程和原理,以便更容易了解相干的 feature,能够看到如图所示,利用启动时,Seata 会主动把用户的 DataSource 代理,还对 JDBC 操作相熟的用户其实对 DataSource 还是比拟相熟的,拿到了 DataSource,就等于把握了数据源连贯,也就能在背地做些“小动作”,此时该对用户也是无感知无 入侵。
之后业务有申请进来,执行业务 sql 时,Seata 会解析用户的 sql,提取出表元数据,生成前镜像,再通过执行业务 sql,保留执行 sql 后的后镜像(至于后镜像的用户之后会说到),再生成全局锁,再注册分支是携带到 Seata-Server 也就是 TC 端。
update join 原理分析
首先,咱们依据回顾的 AT 原理能够通晓,当一个 dml 语句执行的时候,其语句会被解析后会生成对应的前镜像查问语句及后镜像查问语句,以 update join 为例,update join 会连贯多张表,且批改的行可能是多张表都波及,所以咱们第一部要提取 join 的表数量,失去 join 的数量,为波及到批改表行数据的表,进行构建 select 语句查问行在改变之前的数据,如下:
update table1 inner join table2 on table1.name = table2.name set table1.money = money+100 where table2.name=“张三”
能够发现波及了 2 张表,1 个表内的多条数据变更,所以咱们要对 name= 张三的 table1 的表进行前镜像生成。
Select table1.pk,table1.money from table1 inner join table2 on table1.name = table2.name where table2.name=“张三”for update
如上,咱们将 join 的语句和 where 的条件进行了提取,如果还牵涉到另一个表如 table2,那么也会生成一条 table 查问 name= 张三的语句进行查问。
通过以上的前镜像 sql 后,咱们便能构建前镜像的信息,而后镜像的话就会更加简略,以前镜像 sql 为例,那么后镜像只需将前镜像查出来的主键作为条件,也防止了再次对非索引列进行检索,晋升了效率,sql 如下:
Select table1.pk,table1.money from table1 inner join table2 on table1.name =
table2. name where table1.pk=xxx
Support Multi-PK 原理
接下来咱们聊聊如何对多主键进行了反对。
反对多主键无非最终目标就是为了拿到主键,而主键对于 seata 而言是不可或缺的,他是定位一条数据被批改的惟一索引,如果这条数据没有主键,那么也就代表后续二阶段回滚可能无奈定位精确数据,而无奈回滚。所以带着这个目标咱们来剖析一下上图对自增主键和非自增多主键的反对。
INSERT INTO TABLE_NAME (pk1,pk2,column3,...columnN)
VALUES (value1,value2,value3,...valueN);
首先第一种 sql 会在插入列中间接指定多个主键的值,所以 seata 能够轻松的能够通过插入列的 value 读取到多个主键,因为 seata 会获取表的元数据,所以像主键到底是哪个字段就都一清二楚了。
INSERT INTO TABLE_NAME (pk1,column2,...columnN)
VALUES (value1,value2,...valueN);
至于第二种,seata 缓存着表的元数据,也就发现从插入数据中只能失去一个 pk 的值,所以这个时候就要调 driver 的对立 api,getGeneratedkeys,由驱动层面将主键的值返回给 seata,因为两种状况下都能获取到主键,所以对后续 AT 模式的二阶段回滚也就不成问题了。
LockStrategyMode 原理
当初咱们介绍一个新的概念,在 seata1.6 中咱们引入了 lockStrategyMode 概念,目前有 OPTIMISTIC 和 PESSIMISTIC 两种模式,别离对应乐观和乐观。首先在以往的 Seata 获取全局锁的流程如下。
当注册分支时,会将前镜像 / 插入数据时获取到的主键值作为全局锁带到 tc,而此时就会通过这个全局锁信息去查询数据库,当这个锁在数据库没有记录时会去插入这个锁,看似非常简单的流程,却存在着肯定问题,那就是第二步画了虚线的中央,可能心细的同学会有疑难,为什么要查问锁有没有记录呢,数据库有惟一索引,redis 提供 setnx,间接无脑尝试插入即可呗。
- 这个查问是为了保障事务的全副流程的信息通明和故障排查时的便捷性,比方我有个锁争抢不到了,作为业务的同学必定得去排查这个锁抢不到到底是被谁拿走了呢?比方一个商品在被大量秒杀的时候,商家正在补货,因为有大量的并发在秒杀同一个商品,很有可能商家补货的申请会失败,这个时候开发同学必定要去看这个锁被哪个事务持有,再通过持有这个锁的 xid 去查看业务中哪个接口波及了这个锁的争取,一查查到是扣库存的接口,那如果你是业务同学,是不是就能够通过锁竞争周期的配置去调优,将减少库存的接口锁竞争周期调长减少成功率呢?
- 其二呢,如 A 服务调 B 服务,更新了 B 服务对应数据库中 id 为 1 的数据,B 响应胜利给 A,A 进行了一段业务解决,再次发了一个申请批改 id 为 1 的数据,这种场景咱们叫做锁重入,而这个状况在 xa 事务中是无奈反对的,因为 xa 的事务应用的 connection 在一阶段进行了 prepare 后,就无奈再应用这个 connection 或者这个 xa 事务去批改波及的数据,如果有这种场景,那么在应用 xa 模式肯定会变成一个死锁,而在 AT 模式上,Seata 应用了先查问锁持有者,再插入锁的形式,很轻松的就能够在同一个 xid 中,屡次对一个数据进行批改,因为这个全局事务只有持有这个锁,那么无论是多少次的批改,最终回滚肯定是会依照批改数据程序回滚结束。
然而基于以上的 2 点,很多同学应该会发现,90% 以上的业务场景中不存在锁重入,而锁重入又会额定造成磁盘和网络 io 开销,导致利用一旦应用了 seata at 模式,吞吐量进一步降落,所以在 1.6 上咱们推出了乐观和乐观的锁策略,乐观状况下咱们认为锁肯定是被其它人持有,所以逻辑是先查后插,而乐观下,对分支事务而言锁就肯定是能够被持有,所以缩小了查问的一次开销,晋升了整体的吞吐。但因为第一点事务链路异样排查,追踪等问题,Seata 只会在第一次竞争锁时放弃查问锁,当重试获取全局锁 >1 时便会主动转为乐观模式,这样既能在特定场景有肯定晋升吞吐量的劣势,又能在排查问题上不造成额定的排查老本。
申请响应模型
- 合并申请并行处理(Pipeline+Parallel)
- 批量响应(batch response)
1.5 之前的申请响应模型
首先咱们来回顾下 1.5 之前的网络模式,如图所示 T1,T2,T3 是 3 个线程,同时并发在同一个 client 中时,T1 T2 T3 各自都会将各自的 rpcmessage 放入本地的 batch 队列中,并 futureget 期待服务端的响应,而此时 rm 侧会有一个批量 merge 线程,将同一毫秒并发内的申请合并发到 tc,而当 tc 侧承受到申请后,将会按程序将 t1 t2 t3 的音讯进行解决,再一并下发。
咱们根据上述的信息其实能够发现 1.5 之前的网络通信模型是存在肯定问题的。
- 队头阻塞,批量达到 tc 端的申请会被串行解决,导致效率都不如单个申请(单个独立申请的线程池外围线程数为 50),假如两个申请,1 个申请竞争 10000 个全局锁,而另一个申请至竞争一个全局锁,如果两者被合并,那么对后者而言将是极其不偏心和拖慢效率的。
- 申请须要有序解决。
- 响应须要期待其它申请处理完毕。
而在发现这些问题后,社区做了及时的调整和重构,咱们先来看下每一条 request/response 长什么样。
批量音讯构造
能够看到每一条音讯都领有 id,messagetype,codec,compressor,headmap,body,而合并申请的话会将同一种序列化类型和压缩形式的音讯合并在一起(另外创立一个 mergerequest,将其 body 里放入其它申请的 msg),这样在 tc 侧解压缩和反序列化就会非常容易。
申请并行处理
对于队头阻塞问题,这就像 http1.1 推出的 pipeline 一样,批量产生到 server 端后要一并响应,解决时长较高的申请就会影响其它申请,所以基本上这个 http1.1 的 pipeline 都是无用武之地的。
而在 seata1.5 后,将多个合并的申请达到 server 侧时会通过 CompletableFuture 来提交多个 request 的解决到 forkjoinpool 中,这样既防止了额定的线程池开销(seata 的业务线程池外),且在线程数上也是以 cpu 核数为准,当所有申请都处理完毕后,再将多个 response 一并响应回 client。
批量响应
其次在第二,第三点中的问题,社区实现了乱序批量响应的性能,无需有序的期待申请执行结束,只有某个申请的处理完毕后,就能够与其它同时实现解决的申请 (留神,不肯定是 t1,t2,t3,可能是其它的线程) 一并返回响应内容,具体示意可看下图。
当 t1,t2,t3 的申请被合并至 tc 时,此时 tc 将并行处理 1,2,3 的申请,而其中 1,2,3 申请的 response 会与客户端一 致,期待最多 1ms 的工夫,这个 response 是被交到一个 batch 线程中异步期待和响应,如果 1ms 到时还没有一起可返回 client 的响应,便会间接响应回 client。
以上图所示,在 1,2,3 的 response 期待 1ms 时,假如有另外的 t4 线程的申请达到 tc 并也实现了解决,此时 1,2,3 线程的 response 正在期待 0.5ms,便会被 t4 线程的 response 唤醒,此时 batch 线程因为投递了新的 response 被激活后,便将此刻所有雷同压缩形式和序列化的 response 合并为一个 response 批量响应,此解决无程序关系,1,2,3 能够任意一个申请解决实现后便丢到 batch 线程队列中即可,而 client 收到响应时会从 rpcmsg 中的 id 匹配 response 中的 id,找到对应的 client 的 future 对象进行 setresult,此时 client future.get 的线程便会失去响应,再持续进行 client 侧相应的解决。
Seata 服务发现模型
如图所示,Seata 的 client 和 server 的服务发现模型基本上与传统的 RPC 框架所应用的模型统一,TC 裸露服务 地址至注册核心,TM 和 RM 从注册核心发现 TC 地址,而后进行一个连贯,然而在这种模型中会存在非凡的问题,咱们接下来看。
现存服务发现模型缺点
咱们以上图为例,假如某公司在微服务框架中抉择了 spring-cloud+dubbo,或存在 dubbo 迁徙到 spring-cloud 或相同的状况下,可能就会存在 dubbo 的服务应用 http 调用 springcloud 的服务,相互调用,而咱们晓得 springcloud 的服务大多数会应用 eureka 或 nacos,dubbo 大多会应用 zookeeper 和 nacos。
那么如果两者注册核心不是同一个,那么在应用上就会呈现,dubbo 这块的利用通过 zookeeper 发现 seata- server,spring-cloud 通过 eureka 发现 seata-server,因为 seata-server 不存在同时对多个注册核心进行服务裸露,所以用户很可能为了两边的事务能串起来,便会搭建 2 个集群,而且这两个集群仅仅是将事务串起来,但并不能在二阶段下发的时候顺利下发,因为其中一个 client 节点只与对应注册核心的 tc 阶段通信,那么就会导致下发失败,二阶段的执行滞后。所以让咱们来总结梳理下存在的问题。
- 保护多套集群,人力及资源老本略高。
- 二阶段下发存在滞后,导致 AT 以外模式的事务被动降级成最终一致性。
而最新的服务发现模型变能解决以上问题。
改良后的服务注册模型
在 1.6 上咱们反对了多注册核心的服务裸露,这就轻松的将上述问题毁灭,从 Seata-Server 侧可一次性配置多个注册核心,并配置好多个注册核心相干配置后,启动 Seata-server 便会注册到对应的注册核心中,这就使相干企业及用户在面对微服务架构选型,迁徙,混部混用等场景上不再因为 Seata-server 不反对多个注册核心裸露所掣肘,升高用户对多个集群的保护老本。
总结
Update join&Multi pk 反对就是将 Seata 独有的 AT 模式个性进一步的扩大,将更多的 sql 个性交融在 Seata 分布式事务中,乐观乐观的全局双策略兼顾了多种业务场景,晋升性能。
全新设计的网络通信模型使申请不在梗塞,将批量和多线程正当利用,极大的利用现代化高性能多外围的服务器资源晋升 Seata-Server 吞吐量。
多注册核心服务注册发现助力企业降本增效,赋能业务多种微服务架构选型,而业务在基于 Seata 以上个性,利用架构又能够依据理论场景灵便抉择。
欢送感兴趣的同学扫描下方二维码退出钉钉交换群,一起参加探讨交换。