目录:
- 简介
- 注册中心
- 微服务拆分原则AFK
- 分布式事务
参考/来源:
简介
微服务(microservice)是一种软件架构。
2014年,Docker 出现了,彻底改变了软件开发的面貌。它让程序运行在容器中,每个容器可以分别设定运行环境,并且只占用很少的系统资源。
可以用容器来实现”面向服务架构”,每个服务不再占用一台服务器,而是占用一个容器。这样就不需要多台服务器了,最简单的情况下,本机运行多个容器,只用一台服务器就实现了面向服务架构,这在以前是做不到的。这种实现方式就叫做微服务。
简单说,微服务就是采用容器技术的面向服务架构。它依然使用”服务”作为功能单元,但是变成了轻量级实现,不需要新增服务器,只需要新建容器(一个进程),所以才叫做”微服务”。
一个微服务就是一个独立的进程。 这个进程可以运行在本机,也可以运行在别的服务器,或者在云端(比如云服务和云函数 FaaS)。
注册中心
服务注册中心(Registry):用于保存 RPC Server 的注册信息,当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
这里主要介绍5种常用的注册中心,分别为Zookeeper、Eureka、Nacos、Consul和ETCD
Zookeeper
国内Dubbo场景下很多都是使用Zookeeper来完成了注册中心的功能。
随着Dubbo框架的不断开发优化,和各种注册中心组件的诞生,即使是RPC框架,现在的注册中心也逐步放弃了ZooKeeper。在常用的开发集群环境中,ZooKeeper依然起到十分重要的作用,Java体系中,大部分的集群环境都是依赖ZooKeeper管理服务的各个节点。
注册中心的实现
Zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(Ip+端口)去访问具体的服务提供者。如下图所示:
每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port} 。
在zookeeper中,进行服务注册,实际上就是在zookeeper中创建了一个znode节点,该节点存储了该服务的IP、端口、调用方式(协议、序列化方式)等。该节点承担着最重要的职责,它由服务提供者(发布服务时)创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正网络拓扑位置以及得知如何调用。
RPC调用流程
- 服务提供者启动时,会将其服务名称,ip地址注册到配置中心。
- 服务消费者在第一次调用服务时,会通过注册中心找到相应的服务的IP地址列表,并缓存到本地,以供后续使用。当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从IP列表中取一个服务提供者的服务器调用服务。
- 当服务提供者的某台服务器宕机或下线时,相应的ip会从服务提供者IP列表中移除。同时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
- 当某个服务的所有服务器都下线了,那么这个服务也就下线了。
- 同样,当服务提供者的某台服务器上线时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
- 服务提供方可以根据服务消费者的数量来作为服务下线的依据。
zookeeper提供了“心跳检测”功能:它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除。
比如100.100.0.237这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.100.0.238:16888。
Zookeeper的Watch机制其实就是一种推拉结合的模式:
- 服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),Zookeeper只会发送一个事件类型和节点信息给关注的客户端,而不会包括具体的变更内容,所以事件本身是轻量级的,这就是推的部分。
- 收到变更通知的客户端需要自己去拉变更的数据,这就是拉的部分。
Zookeeper不适合作为注册中心
作为一个分布式协同服务,ZooKeeper非常好,但是对于Service发现服务来说就不合适了,因为对于Service发现服务来说就算是返回了包含不实的信息的结果也比什么都不返回要好。所以当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。
但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
所以说,作为注册中心,可用性的要求要高于一致性!
在 CAP 模型中,Zookeeper整体遵循一致性(CP)原则,即在任何时候对 Zookeeper 的访问请求能得到一致的数据结果,但是当机器下线或者宕机时,不能保证服务可用性。
那为什么Zookeeper不使用最终一致性(AP)模型呢?因为这个依赖Zookeeper的核心算法是ZAB,所有设计都是为了强一致性。这个对于分布式协调系统,完全没没有毛病,但是你如果将Zookeeper为分布式协调服务所做的一致性保障,用在注册中心,或者说服务发现场景,这个其实就不合适。
Eureka
Eureka 特点
- 可用性(AP原则):Eureka 在设计时就紧遵AP原则,Eureka的集群中,只要有一台Eureka还在,就能保证注册服务可用,只不过查到的信息可能不是最新的(不保证强一致性)。
- 去中心化架构:Eureka Server 可以运行多个实例来构建集群,不同于 ZooKeeper 的选举 leader 的过程,Eureka Server 采用的是Peer to Peer 对等通信。这是一种去中心化的架构,无 master/slave 之分,每一个 Peer 都是对等的。节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。每个节点都可被视为其他节点的副本。
- 请求自动切换:在集群环境中如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点上,当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。
- 节点间操作复制:当节点开始接受客户端请求时,所有的操作都会在节点间进行复制操作,将请求复制到该 Eureka Server 当前所知的其它所有节点中。
- 自动注册&心跳:当一个新的 Eureka Server 节点启动后,会首先尝试从邻近节点获取所有注册列表信息,并完成初始化。Eureka Server 通过 getEurekaServiceUrls() 方法获取所有的节点,并且会通过心跳契约的方式定期更新。
- 自动下线:默认情况下,如果 Eureka Server 在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Eureka Server 将会注销该实例(默认为90秒, eureka.instance.lease-expiration-duration-in-seconds 进行自定义配置)。
- 保护模式:当 Eureka Server 节点在短时间内丢失过多的心跳时,那么这个节点就会进入自我保护模式。
除了上述特点,Eureka还有一种自我保护机制,如果在15分钟内超过 85% 的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
- Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务;
- Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
- 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。
Eureka工作流程
了解完 Eureka 核心概念,自我保护机制,以及集群内的工作原理后,我们来整体梳理一下 Eureka 的工作流程:
- Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息。
- Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务。
- Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常。
- 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例。
- 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端。
- 当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式。
- Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地。
- 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存。
- Eureka Client 获取到目标服务器信息,发起服务调用。
- Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除。
Eureka 为了保障注册中心的高可用性,容忍了数据的非强一致性,服务节点间的数据可能不一致, Client-Server 间的数据可能不一致。比较适合跨越多机房、对注册中心服务可用性要求较高的使用场景。
Nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
Nacos 主要特点:
服务发现和服务健康监测:
- Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。
- Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。
动态配置服务:
- 动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。
- 动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。
- 配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
- Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
动态 DNS 服务:
- 动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。
- Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表。
小节一下:
- Nacos是阿里开源的,支持基于 DNS 和基于 RPC 的服务发现。
- Nacos的注册中心支持CP也支持AP,对他来说只是一个命令的切换,随你玩,还支持各种注册中心迁移到Nacos,反正一句话,只要你想要的他就有。
- Nacos除了服务的注册发现之外,还支持动态配置服务,一句话概括就是Nacos = Spring Cloud注册中心 + Spring Cloud配置中心。
Consul
Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等)。
Consul 使用起来也较为简单,使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。
Consul 的调用过程
- 当 Producer 启动的时候,会向 Consul 发送一个 post 请求,告诉 Consul 自己的 IP 和 Port;
- Consul 接收到 Producer 的注册后,每隔 10s(默认)会向 Producer 发送一个健康检查的请求,检验 Producer 是否健康;
- 当 Consumer 发送 GET 方式请求 /api/address 到 Producer 时,会先从 Consul 中拿到一个存储服务 IP 和 Port 的临时表,从表中拿到 Producer 的 IP 和 Port 后再发送 GET 方式请求 /api/address;
- 该临时表每隔 10s 会更新,只包含有通过了健康检查的 Producer。
Consul 主要特征
- CP模型,使用 Raft 算法来保证强一致性,不保证可用性;
- 支持服务注册与发现、健康检查、KV Store功能。
- 支持多数据中心,可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟, 分片等情况等。
- 支持安全服务通信,Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。
- 支持 http 和 dns 协议接口;
- 官方提供 web 管理界面。
ETCD
etcd是一个Go言编写的分布式、高可用的一致性键值存储系统,用于提供可靠的分布式键值存储、配置共享和服务发现等功能。
ETCD 特点
- 易使用:基于HTTP+JSON的API让你用curl就可以轻松使用;
- 易部署:使用Go语言编写,跨平台,部署和维护简单;
- 强一致:使用Raft算法充分保证了分布式系统数据的强一致性;
- 高可用:具有容错能力,假设集群有n个节点,当有(n-1)/2节点发送故障,依然能提供服务;
- 持久化:数据更新后,会通过WAL格式数据持久化到磁盘,支持Snapshot快照;
- 快速:每个实例每秒支持一千次写操作,极限写性能可达10K QPS;
- 安全:可选SSL客户认证机制;
- ETCD 3.0:除了上述功能,还支持gRPC通信、watch机制。
ETCD 框架
etcd主要分为四个部分:
- HTTP Server:用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
- Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
- Raft:Raft强一致性算法(Paxos的简化版)的具体实现,是etcd的核心。
- WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。
通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。
选型
- 关于CP还是AP的选择:选择 AP,因为可用性高于一致性,所以更倾向 Eureka 和 Nacos;关于Eureka、Nacos如何选择,哪个让我做的事少,我就选择哪个,显然 Nacos 帮我们做了更多的事。
- 技术体系:Etcd 和 Consul 都是Go开发的,Eureka、Nacos、Zookeeper 和 Zookeeper 都是Java开发的,可能项目属于不同的技术栈,会偏向选择对应的技术体系。
- 高可用:这几款开源产品都已经考虑如何搭建高可用集群,有些差别而已;
- 产品的活跃度:这几款开源产品整体上都比较活跃。
5种对比
服务健康检查:Euraka 使用时需要显式配置健康检查支持;Zookeeper、Etcd 则在失去了和服务进程的连接情况下任务不健康,而 Consul 相对更为详细点,比如内存是否已使用了90%,文件系统的空间是不是快不足了。
多数据中心:Consul 和 Nacos 都支持,其他的产品则需要额外的开发工作来实现。
KV 存储服务:除了 Eureka,其他几款都能够对外支持 k-v 的存储服务,所以后面会讲到这几款产品追求高一致性的重要原因。而提供存储服务,也能够较好的转化为动态配置服务哦。
CAP 理论的取舍:
- Eureka 是典型的 AP,Nacos可以配置为 AP,作为分布式场景下的服务发现的产品较为合适,服务发现场景的可用性优先级较高,一致性并不是特别致命。
- 而Zookeeper、Etcd、Consul则是 CP 类型牺牲可用性,在服务发现场景并没太大优势;
Watch的支持:Zookeeper 支持服务器端推送变化,其它都通过长轮询的方式来实现变化的感知。
自身集群的监控:除了Zookeeper和Nacos,其它几款都默认支持 metrics,运维者可以搜集并报警这些度量信息达到监控目的。
Spring Cloud的集成:目前都有相对应的 boot starter,提供了集成能力。
微服务拆分原则AFK
需要对服务器进行集群,一变多,具体怎么扩充服务器?即AFK原则。
X轴拆分:水平复制,就是将单体系统多运行几个实例,做集群加负载均衡的模式,主主、主备、主从。
Y轴拆分:基于不同的业务拆分
Z轴拆分:基于数据拆分。
X轴拆分
一台机器可以看成另一台机器的镜像,基本具有全量数据,这种拆分模式就是AKF拆分模式之一:X轴拆分
为了解决单点故障,所以弄几台全量数据的机器做备份,例如主主、主备等,特点是任何两台包含的数据是差不多的,一台可以看成另一台的镜像。
Y轴拆分
这时候又有新的问题,例如一台服务器中,可能某些功能被频繁访问,涉及到的数据频繁读写,其他数据基本不怎么访问,这时候可以将这部分数据独立出来,也就是根据功能、业务继续拆分服务器,这种拆解就是AFK中的Y轴拆分
特点是Y轴纵向来看不同的Redis负责的功能是不同的,也就是所包含的数据也是不同的,另外仅仅扩展出一个Y轴上的业务服务器,又可能会存在单点问题,所以可以结合AFK的X轴拆分原则,继续对刚拆分的Y轴上的点进行X轴拆分。
Z轴拆分
在上面的AFK原则X-Y拆分之后,对服务器显示做了主从主备复制,然后做了业务拆分,不同的Redis负责不同的业务请求,这时候还会有一个新的问题,例如对于Y轴上一个Redis,它负责某一样业务,但是这天这个业务的数据访问巨大,贼大,那就只好对数据请求进行AFK的Z轴拆分,例如先分析下数据请求的情况,然后根据访问来源,分为北京的、上海的,这样不同的Redis虽然是负责不同的数据,但是负责的业务是一样的。AFK拆分图示:
分布式事务
在微服务架构下,数据库也会分库分表,此时依靠单数据库的ACID特性保证事务的手段失效。
一个事务可能涉及多个数据库或者多个表扩库执行,而网络具有不稳定性,也就是事务执行难度加大,分表分库后事务为了与传统事务做出区别,叫做分布式事务(跨分片事务)。
CAP和Base理论
相对于本地事务的ACID性质,分布式事务有也有对应的两个事务特性,按照保证事务一致性的方式,分为CAP(钢性事务)和Base理论(柔性事务)
具体描述可以见Zookeeper博客
分布式事务处理模型
DTP模型
DTP(Distributed Transaction Processing)是x/open组织提出来的分布式事务的模型.
一个DTP模型至少包含以下三个元素:
1. AP, 应用程序,用于定义事务开始和结束的边界. 说人话就是我们开启事务的代码所以的应用.
2. RM, 资源管理器. 理论上一切支持持久化的数据库资源都可以是一个资源管理器.
3. TM, 事务管理器, 负责对事务进行协调,监控. 并负责事务的提交和回滚.
XA规范
XA是x/open提出来的分布式事务的规范, 它是跟语言无关的.
XA规范定义了事务管理器TM(Transaction Manager)和资源管理器RM(Resource Manager)交互、应用程序的接口. 例如TM可以通过以下接口对RM进行管理
1. xa_open和xa_close, 用于跟RM建立连接
2. xa_star和xa_end, 开始和结束一个事务
3. xa_prepare, xa_commit和xa_rollback, 用于预提交, 提交和回滚一个事务
4. xa_recover 用于回滚一个预提交的事务
事务管理器TM就是事务的协调者,资源管理器RM可以认为就是一个数据库。
JTA规范
JTA规范是可以认为是XA规范java语言实现版的规范.
JTA定义了一系列分布式事务相关的接口:
1. javax.transaction.Status: 定义了事务的状态,例如prepare, commit rollback等等等
2. javax.transaction.Synchronization:同步
3. javax.transaction.Transaction:事务
4. javax.transaction.TransactionManager:事务管理器
5. javax.transaction.UserTransaction:用于声明一个分布式事务
6. javax.transaction.TransactionSynchronizationRegistry:事务同步注册
7. javax.transaction.xa.XAResource:定义RM提供给TM操作的接口
8. javax.transaction.xa.Xid:事务id
以上不同的接口由不同的角色(RM, RM等)来实现
分布式事务解决方案
二阶段提交(2PC)
两阶段提交的算法思路可以概括为:参与者将操作成功或失败的结果通知协调者,再由协调者根据所有参与者的反馈情况决定个参与者是否要提交操作还是中止操作。
1.1 第一阶段:请求阶段(投票阶段)
协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
事务参与者收到请求之后,本地执行事务,但不提交。
参与者将自己事务执行情况反馈给协调者,同时等待协调者的下一步通知。
1.2 第二阶段:提交阶段(执行阶段)
在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:
1、所有的参与者回复能够正常执行事务
①协调者向各个参与者发送commit通知,请求提交事务
②参与者收到事务提交通知之后,执行commit操作
③参与者向协调者返回事务commit结果信息。
2、一个或多个参与者回复事务执行失败
3、协调者等待超时
①协调者向各个参与者发送事务rollback通知,请求回滚事务
②参与者收到事务回滚通知之后,执行rollback操作
③参与者向协调者返回事务rollback结果信息
1.3 两阶段提交的缺点
同步阻塞:执行过程中,所有参与者的节点都是事务阻塞型的。当参与者占用公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
单点故障:由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其是在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
数据不一致:在第二阶段中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接收到commit请求的节点则无法提交事务。于是,整个分布式系统就出现了数据不一致的现象。
1.4 两阶段提交无法解决的问题
当协调者和参与者同时出现故障时,两阶段提交无法保证事务的完整性。如果调用者在发出commit消息之后宕机,而唯一接收到commit消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,因为没人知道事务是否已经被提交。
二阶段补偿型事务(TCC)
TCC是二阶段协议的一种,优化,只锁定了一部分的资源,其他的事务仍可以使用。TCC是一种比较成熟的分布式事务解决方案,可用于解决跨库操作的数据一致性问题,适用于公司内部对一致性、实时性要求较高的业务场景。其中Try、Confirm、Cancel 3个方法均由业务编码实现。其中Try操作为第一阶段,负责资源的检查和预留;Confirm操作为第二阶段,执行真正的业务操作;Cancel时执行取消(回滚)操作。
业务实现TCC服务之后,该TCC服务将作为分布式事务的其中一个资源,参与到整个分布式事务中;事务管理器分两阶段协调的TCC服务,第一阶段调用所有TCC服务的Try方法(和二阶段提交中的预提交不同,这里每一个RM来说,事务此时已经提交了),在第二阶段执行所有TCC服务的Confirm或者Cancel方法。
三阶段提交(3PC)
3PC 就是在 2PC 的基础上,为了解决 2PC 的某些缺点而设计的,3PC 分为三个阶段:CanCommit,PreCommit 和 doCommit。
CanCommit
流程如下图:
事务询问 协调者向所有参与者发送事务 canCommit 请求,请求中包含事务内容,询问是否可以执行事务提交操作,并开始等待响应。
反馈询问结果 参与者收到 canCommit 请求后,分析事务内容,判断自身是否可以执行事务,如果可以,那么就返回 Yes 响应,进入预备状态,否则返回 No 响应。
注意:此过程中并没有执行事务(对比 2PC 的 Prepare 阶段,参与者是执行了事务的)。
PreCommit
流程图如下:
PreCommit 阶段根据各参与者返回的 CanCommit 响应,决定下一步动作。如果收到了所有参与者的 Yes 响应,则执行事务预提交,否则(收到了至少一个 No 响应或一定时长内没有收到所有参与者的 Yes 响应,如 3PC 第一张图片中红色部分),执行事务中断。
事务预提交
- 发送 PreCommit 请求 协调者发送 PreCommit 请求,并进入 Prepared 阶段。
- 参与者处理 PreCommit 参与者收到 PreCommit 请求后,执行事务操作,并将 Undo 和 Redo 信息记录事务日志中。
- 反馈执行结果 如果参与者成功执行了事务并写入 Undo 和 Redo 信息,那么反馈 Ack 给协调者,并等待下一步指令。
事务中断
上图中,红色的 Abort 表示协调者发送的不是 PreCommit 请求,而是 Abort 请求。
- 发送事务中断请求 协调者向所有参与者发送 Abort 请求。
- 中断事务 参与者收到 Abort 请求后,会触发事务中断。此外,如果参与者在等待协调者指令超时,会自己触发事务中断,在 2PC 中,参与者会一直阻塞的等待协调者指令,所以 3PC 中解决了因为这种情况带来的阻塞。
doCommit
流程图如下:
协调者根据第二阶段的响应决定最终操作,如果协调者收到了所有参与者在 PreCommit 阶段的 Ack 响应,那么会进入执行事务提交阶段,否则执行事务中断。
事务提交
- 发送提交请求 协调者收到所有参与者在 PreCommit 阶段返回的 Ack 响应后,向所有参与者发送 doCommit 请求,并进入提交状态。
- 事务提交 参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
- 反馈结果 参与者完成事务提交之后,向协调者返回 Ack 响应。
- 完成事务 协调者收到所有参与者的 Ack 响应后,完成事务。
事务中断
发送事务中断请求 协调者向所有参与者发送 Abort 请求。
事务回滚 参与者收到 Abort 请求后,会使用第二阶段记录的 Undo 信息进行事务回滚,并在完成回滚后释放所有事务资源。
注意:因为第一阶段并没有任何参与者实际执行事务,所以在第二阶段(PreCommit 阶段)执行事务中断,是不需要事务回滚的,也就不需要下面的反馈结果,直接中断事务即可。
反馈回滚结果 参与者执行事务回滚后向协调者发送 Ack 响应。
中断事务 协调者接收到所有参与者反馈的 Ack 响应后,完成事务中断。
3PC 的改进和缺点
改进
- 降低了阻塞
- 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时,则自动 abort,降低了阻塞;
- 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞;
- 解决单点故障问题
- 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若协调者宕机,等待超时后自动 abort;
- 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若协调者宕机,等待超时后自动 commit 事务;
缺点
数据不一致问题仍然是存在的,比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致。
RocketMq的事务消息有点类似这里的3pc的思想,参与者broker在超时之后,会主动回查协调者那边的事务执行状态,然后根据执行状态判断应该执行abort还是commit操作
本地消息表(最终一致性)
本地消息表的方案最初是由ebay提出,核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败“,或”业务处理失败 + 事务消息发送成功“的棘手情况出现,保证2个系统事务的数据一致性。
处理流程
下面把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方。
为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:
- 步骤1 事务主动方处理本地事务。 事务主动发在本地事务中处理业务更新操作和写消息表操作。 上面例子中库存服务阶段再本地事务中完成扣减库存和写消息表(图中1、2)。
- 步骤2 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。 消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。 上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中3 - 5)。
- 步骤3 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中6 - 8)
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:
- 1、当步骤1处理出错,事务回滚,相当于什么都没发生。
- 2、当步骤2、步骤3处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。
- 3、如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
- 4、如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
方案总结
方案的优点如下:
- 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。
- 方案轻量,容易实现。
缺点如下:
- 与具体的业务场景绑定,耦合性强,不可公用。
- 消息数据与业务数据同库,占用业务系统资源。
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
MQ事务消息(最终一致性)
下面主要基于RocketMQ4.3之后的版本介绍MQ的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ的事务消息相对于普通MQ,相对于提供了2PC的提交接口,方案如下:
正常情况——事务主动方发消息 这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
- 图中1、发送方向 MQ服务端(MQ Server)发送half消息。
- 图中2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
- 图中3、发送方开始执行本地事务逻辑。
- 图中4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
- 图中5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况——事务主动方消息恢复 在断网或者应用重启等异常情况下,图中4提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
- 图中5、MQ Server 对该消息发起消息回查。
- 图中6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 图中7、发送方根据检查得到的本地事务的最终状态再次提交二次确认
- 图中8、MQ Server基于commit / rollback 对消息进行投递或者删除
有一些第三方的MQ是支持事务消息的,比如RocketMQ,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
Saga(最终一致性)
Saga事务源于1987年普林斯顿大学的Hecto和Kenneth发表的如何处理long lived transaction(长活事务)论文,Saga事务核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
处理流程
Saga事务基本协议如下:
- 每个Saga事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
- 每个Ti 都有对应的幂等补偿动作Ci,补偿动作用于撤销Ti造成的结果。
可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。
下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分 Saga的执行顺序有两种:
- 事务正常执行完成 T1, T2, T3, …, Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。
- 事务回滚 T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。
Saga定义了两种恢复策略:
- 向前恢复(forward recovery)
对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci。
- 向后恢复(backward recovery)
对应于上面提到的第二种执行顺序,其中j是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个Saga的执行结果撤销。
Saga事务常见的有两种不同的实现方式:
- 1、命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。
中央协调器(Orchestrator,简称OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
1、事务发起方的主业务逻辑请求OSO服务开启订单事务 2、OSO向库存服务请求扣减库存,库存服务回复处理结果。 3、OSO向订单服务请求创建订单,订单服务回复创建结果。 4、OSO向支付服务请求支付,支付服务回复处理结果。 5、主业务逻辑接收并处理OSO事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
- 2、事件编排 (Event Choreography0:没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者听到都意味着事务结束。
以电商订单的例子为例:
- 1、事务发起方的主业务逻辑发布开始订单事件
- 2、库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件
- 3、订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件
- 4、支付服务监听订单已创建事件,进行支付,并发布订单已支付事件
- 5、主业务逻辑监听订单已支付事件并处理。
事件/编排是实现Saga模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及2至4个步骤,则可能是非常合适的。
方案总结
命令协调设计的优点和缺点: 优点如下:
- 1、服务之间关系简单,避免服务之间的循环依赖关系,因为Saga协调器会调用Saga参与者,但参与者不会调用协调器
- 2、程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
- 3、易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试
缺点如下:
- 1、中央协调器容易处理逻辑容易过于复杂,导致难以维护。
- 2、存在协调器单点故障风险。
事件/编排设计的优点和缺点 优点如下:
- 1、避免中央协调器单点故障风险。
- 2、当涉及的步骤较少服务开发简单,容易实现。
缺点如下:
- 1、服务之间存在循环依赖的风险。
- 2、当涉及的步骤较多,服务间关系混乱,难以追踪调测。
值得补充的是,由于Saga模型中没有Prepare阶段,因此事务间不能保证隔离性,当多个Saga事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
方案比较
介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景。
- 2PC/3PC 依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
- TCC 适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
- 本地消息表/MQ事务 都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
- Saga事务 由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga事务较适用于补偿动作容易处理的场景。
分布式事务框架
在实际的应用中, 分布式事务出现的场景可以总结为两种. 还是以一个购买服务为例, 那么这两种分布式事务的场景可能是:
第一种, 同一个服务中对多个RM进行操作(数据库层面的一个分布式,服务并没有分布式)
第二种, 一个服务通过RPC调用多个服务, 间接操作了多个RM(服务分布式)
在微服务化大行其道的今天,按业务分库应该是大多公司搭建架构的一个基本准则了. 所以这样来看, 貌似是第二种场景更符合实际了.
当然第一种场景肯定也还是有存在的. 例如上面”本地消息表”的解决方案中, 就有需要再同一个服务中跟多个RM交互.
分布式事务开源框架其实市面上也挺多的,例如tcc-transactio等等。
关于分布式事务框架atomikos和seata的详细内容,可以见其单独的博客内容。
atomikos
atomikos是一个非常有名的分布式事务开源框架. 它有**JTA/XA**规范的实现, 也有TCC机制的实现方案, 前者是免费开源的, 后者是商业付费版的.
atomikos适用于场景1的分布式事务. 如果有场景1的分布式事务的话, 直接使用atomikos就可以了, 简单直接高效。但是话又说回来了, 实际场景的分布式事务更多的还是属于场景2的.。很明显简单的JTA事务是处理不了场景2的分布式事务的。场景2下的分布式事务, 可以考虑使用seata
seata
seata就是Fescar(TXC/GTC/FESCAR)和tcc-transaction整合后开源的一个分布式事务落地解决方案框架,实现了AT, TCC, SAGA三种模式, 大有一统江湖的意思.