作者: 陈健斌 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 以上特性,应用架构又可以根据实际场景灵活选择。
欢迎感兴趣的同学扫描下方二维码加入钉钉交流群,一起参与探讨交流。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。