写在前面

高级篇里的很多知识我都没有去实践过,只是知道相关的原理,主要原因是:

  1. 应用场景在多数情况下难以碰见(高并发)
  2. 对于运行的要求过高(如果是单节点进行模拟,那么就必须有很大的运行内存)

所以这部分记录更多是一个思路的梳理,可能无法作为各位全面掌握该部分知识的条目,望见谅

7. 微服务的保护技术:Sentinel

sentiel分为sentinel控制台和sentinel客户端,具体的规则过滤都由配置在微服务上的客户端完成,控制台只是进行可视化的规则配置,其逻辑图如下:

JAVA如何获取微服务状态 java微服务实战_开发语言


由于Sentinel客户端会将规则存储在微服务的运行内存中,因此一旦微服务重启,运行内存即被清空,也就意味着规则的丢失

该问题的解决方案可参照[1],有非常详细的描述:

  • 比较简单的解决方案是pull方案,也就是Sentinel客户端将配置规则通过其提供的API缓存到本地文档中,然后每当控制台发起规则更新的命令时,客户端按照先更新内存,再更新本地文档的时序进行。由于是集群部署微服务,因此还会有其他的Sentinel客户端部署,它们将定时轮询本地文档的规则并同步至自身内存
  • 优点:实现简单,不需要修改源码与额外的中间件
  • 缺点:定时轮询时间点不同带来的各个微服务间配置更新的不同步

JAVA如何获取微服务状态 java微服务实战_java_02

因此pull方案适合于规则配置较长时间不变的情况,这时候直接使用pull方案简单快捷

  • 比较难的是push方案,控制台将规则推送到nacos或其他远程配置中心。Sentinel客户端链接nacos,获取规则配置,当nacos中的配置规则发生变化,nacos会将变化配置push至各个客户端,客户端就更新本地缓存,从而让本地缓存总是和nacos一致
  • 优点:同步不再依赖于定时轮询,而是一有变化就通过push告知,时效性高
  • 缺点:需要完全改动控制台,使其不再将配置推送给客户端而是上传nacos

上面我们对流程进行了梳理,同时也深入到了Sentinel使用中的持久化问题,并介绍了相关资料,而对于其推送的配置规则有哪些,即Sentinel实现了哪些功能,我们也将一一罗列:

限流控制

Sentinel可以对访问微服务的流量进行控制,通过设置流控模式(根据谁的QPS,控制谁的访问),流控效果(按照什么原则控制访问)来实现(备注,这里的阈值默认为QPS计数)

  • 流控模式: 流控模式说清楚了监控的QPS对象,又根据该对象对谁进行限流
  • 直接模式: 统计当前URI资源的请求,超过QPS阈值即对当前资源的访问实行流控
    例如: 查询订单业务,当对该业务的请求达到阈值,对访问实行流控
  • 关联模式: 配置当前URI资源的关联资源,统计关联资源的请求,超过QPS阈值即对当前资源的访问实行流控
    例如: 对查询订单业务建立写入订单业务关联,当写入订单业务的请求达到阈值,为了保证写入顺序,对查询订单业务的访问实行流控
  • 链路模式: 当前资源同时作为多个链路的后端节点,此时可以通过指定前端节点的资源名,限制对应该资源的前端节点的访问
    例如: 查询订单业务和写入订单业务都以查询商品业务作为后端节点,此时可在查询商品业务配置对查询订单的链路模式限流,保障写入订单业务的顺序查询
  • 热点参数限流: 这个并不在流控模式的配置里,但其做的事是和流控模式一样的,所以也把它归纳在流控模式中。
    热点参数限流可以对当前资源某个参数进行配置,该参数值相同的访问的QPS不能超过阈值,否则触发访问限制
  • 流控效果: 流控效果说清楚了发生限流需求时,应该怎么对待未被处理的请求
  • 快速失败: 在检测到QPS达到阈值后,新的请求会被立即拒绝并抛FlowException异常。是默认的处理方式
  • warm up: 在检测到QPS达到阈值后,同样拒绝新请求并抛FlowException异常,但阈值是随时间变化的
    变化规律: 由参数max_threshold和coldFactor相关的函数确定的,在时间从0至coldFactor过程中,阈值也不断变大并最终达到max_threshold
  • 排队等待: 在检测到QPS达到阈值后,不直接拒绝新请求,而是将请求以队列存储并按照指定的间隔放出,若队列中有请求的预期等待时间(队长*指定间隔)超过最大时长,则拒绝新请求并抛FlowException异常

与限流对扇入的保护不同,Sentinel还可以对资源的扇出进行隔离与熔断处理,以避免宕机的后端服务造成前端的雪崩

线程隔离

线程隔离即是限制扇出对象的最大线程数,限制的手段可以是通过为扇出对象建立独立的线程池,也可以是设置计数器

  • 线程池隔离:即通过为扇出对象建立独立的线程池,来确保不会有单个扇出对象占用所有线程资源,也保证了各个扇出对象的独立
  • 优点:由于请求与调用可以不是同一个线程,因此线程池隔离可以支持异步调用,由于请求和调用不是一个线程,所以请求体可以主动超时调用线程,从而完成请求的立刻返回(抛出异常)
  • 缺点:线程的切换将占用cpu资源,拖慢运行速度
  • 适用场景:请求的并发量大,并且调用服务时间长,扇出低的场景(异步)
  • 信号量隔离:即通过为扇出对象建立计数器,来确保不会有单个扇出对象占用超过阈值的线程资源
  • 优点:由于请求与调用是同一个线程,不需要进行线程的切换而获得了更快的调度速度
  • 缺点:请求和调用必须同步完成,不支持异步调用,也不可以对调用进行单独的主动超时
  • 使用场景:请求的并发量大,并且调用服务处理迅速,高频高额扇出的场景(高效)
熔断降级

熔断降级即是在某个扇出对象的不可用调用(这里的不可用调用在Sentinel中分为两种,一种是慢调用,另一种是异常调用)达到一定比例或一定数量时,将对该扇出对象的断路器由closed转为open状态,熔断对该扇出对象的新调用,请求直接返回;降级一段时间后,断路器由open转为half-open状态,开放一次对扇出对象的访问,如果访问可以成功,关闭断路器,否则重新回到open状态,如下图所示:

JAVA如何获取微服务状态 java微服务实战_java_03

授权规则

为了避免内部服务器的地址泄露而存在的直接被外网访问的风险,Sentinel客户端可以通过授权规则来把关进入微服务的每一次访问,只有满足授权规则的才予以放行

  • 授权规则根据流控应用名设定白名单和黑名单
  • 只有白名单内的流控应用才能访问设定的资源
  • 流控应用名通过实现RequestOriginParser接口,实现类中要解析HTTP请求,根据解析内容返回对应流控应用名,该实现类被装配为Bean

由于自定义异常处理部分的内容不多,而且都是已有知识,所以这里就没有展开介绍该如何实现

8. 微服务的分布式事务:Seata

事务遵循的是ACID的设计原理,用简洁的话来总结就是:一个事物中的所有分支要么同时完成,要么同时失败(这就是原子性);原子性代表数据间可以保持一致性,不会存在矛盾现象;事物需要取得数据锁,对同一资源的操作才不会同时发生(隔离性);事务一旦提交则永久修改,不能回退(持久性

正如之前在spring的事务管理中所言,分布式系统中想要完成多个分支的事务管理就必须要声明一个全局的事务管理器来控制分支事务的,这也暗合了我们一直强调的一个观点:凡是难以解决的问题,不妨向上抽取,多加一层架构

然而,简单的抽取一层全局管理器还不行,事务管理中存在一个此消彼长的对弈,全局管理器必须要能够适应不同的对弈需求,这方面Seata是做得最好的。首先,我们想讲讲这个此消彼长的对弈是什么,然后再展开介绍Seata怎么来适应不同的对弈需求的

CAP理论

Partition——分区,代表几方独立运行的系统,这个系统可以是指独立运行的数据库,也可指代由于网络故障而分别提供服务的多个集群,总之,分区意味着系统间无法直接沟通

Avalibility——可用,代表客户端访问健康节点时,节点必须响应,而不是超时或拒绝

Consistency——一致,代表客户端访问分区的各个健康节点时,得到的回应必须一致

我们注意到,一致与可用正是一个此消彼长的对弈,为了更高的一致性,凡是无法沟通的节点在处理事务时,都必须等待进一步的确认才能够回应;反之,为了更高的可用性,必然意味着节点必须首先回应客户端,而不是等待进一步的确认

这正是CAP理论的基础,C与A之间是对立的,但BASE理论告诉我们,对立不意味着不可以折中:

  • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用
  • Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致

因此我们有基于强一致性的CP方案,也有基于BASE理论的软一致性的AP方案:

  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态
Seata的实践

不论是AP模式还是CP模式,都需要一个全局的事务管理器来进行子事务的注册,状态查询与判断,以及决定事物的回滚与提交,或者通过弥补措施恢复数据。Seata新增了一层事务协调者进行全局事务的注册与管理,不同的全局事务间被有效隔离,其还能够替Seata管理的不同全局事务间添加全局资源锁,避免数据脏写发生的同时还可以比DB锁更细粒度的占用资源。Seata由此形成事务协调者、事务管理器、资源管理器的三层架构。我们以XA为例,绘制了该架构

JAVA如何获取微服务状态 java微服务实战_微服务_04

Seata在实践中,为了提供不同的一致性和可用性的平衡需求,分别给出了XA方式和AP方式,两者的区别有:

  • XA方式由TC来统一控制事务的提交和回滚,而AP方式由本地资源管理器直接控制事务的提交和回滚。因此XA实现的是强一致性,而AP满足的是软一致性
  • XA方式依托于事务型数据库提供的回滚机制来回退修改,而AP方式则依赖于提前备份的资源回退修改。

但事实上,AP也会将占用资源的全局锁持有到最终一致性达成前,也就是我们常说的二阶段完成后,因此,单纯以TC管理的事务而论,AP与XA相比其占用资源的可用性并没有得到提升(这是必须的,否则脏写将无法避免)

AP的可用性提升主要体现在:AP提供的全局锁由TC进行统一的互斥管理,通过xid-tableName-itemId锁定,只在TC管理的事务内生效。因此,AP会在本地资源管理器执行完事务后直接释放DB锁,允许不被TC管理的其他数据库操作来获取DB锁,而不需要像XA一样等到统一控制完成后才释放DB锁,由此提高了可用性

值得强调的是,由于对于没有经过TM管理的数据库操作,TC是无法进行全局锁的,因此AP释放DB锁后,有极低的概率会被没有经过TM管理的数据库操作破坏一致性,这个时候Seata会记录异常,由人工解决

AP仍然有提升的空间,如果可以想办法在第一阶段就释放全局锁,对于TC管理的事务而言,也将达到最优效果,这就是第三种模式——TCC。

  • TCC适用于修改业务,新增和删除业务的全局锁在一阶段事务提交后即解除了,因此此时采用AP模式和TCC是一样的效果,还更加方便
  • TCC适用于可拆分资源,例如资金或库存这类数字资源,而对于字符串,由于其并不能实现拆分与冻结,因此不适用于TCC

TCC模式的逻辑是,不为TC管理的事务添加全局锁,而是增加一个freeze表记录冻结资源,在try阶段进行资源的预分配,而TC管理器根据所有分支的try阶段执行情况决定对分支事务的调用:

  • 如果全局事务执行成功,TC管理器对每个分支事务调用confirm阶段,将预分配变为真实的更新,同时删除freeze表记录,如果该步执行失败,重复尝试直至成功(所以要保证幂等性)
  • 如果全局事务执行失败,TC管理器对每个分支事务调用cancel阶段,执行数据的回退(通过freeze表记录执行相反操作,因为不是直接覆盖,所以不会出现数据脏写)如果该步执行失败,重复尝试直至成功(所以要保证幂等性)
try:
	if needResource > totalResource - freezeResource: // 避免资源不够冻结
		throw exception
	if xid exist in freezeTable: // 避免非空悬挂
		return;
	do: 
		add: xid-resourceId-needResource-state=0 to freezeTable;
confirm:
	do:
		update: needResource to resourceTable; // 将真实更新放在confirm阶段,避免数据脏写
		delete: xid-resourceId from freezeTable;
cancel:
	if xid not exist in freezeTable: // 空回滚
		add: xid-resourceId-needResource=0-state=2 to freezeTable;
		return;
	do:
		update: needResource=0-state=2 to freezeTable;
		update: needResource to resourceTable;

然而,试想这样一个场景:

  • 当事务中某个业务需要执行的时间长达几天甚至几周,按照AT或者TCC模式,该事务要么就是长时间的持有全局锁,要么就是知道长时业务完成后,才真正的更新了数据库。这显然启迪着我们,需要一种支持异步操作的新模式来进行事务管理
  • 当事务中某个业务来自于已封装好的API或者是来自于其他公司的服务,难以拆分成try和confirm两个阶段来分别执行。这要求我们,需要一种更为直接简洁的新模式来进行事务管理

这就是saga模式,它抛弃了TCC的confirm阶段,而是令分支事务在第一阶段就完成数据库的所有操作,同时记录第一阶段的相反操作,在需要进行第二阶段时执行回滚。

  • saga是异步的,同一事务的不同业务间遵循异步调用原则,前端节点不必像TCC中一样,等待后端完成后再更新数据库
  • saga是简洁的,没有confirm阶段,意味着可以直接移植其他公司提供的服务,再编写一个相反逻辑的服务作为第二阶段回滚的依据即可

但同时,saga的优化也带来了不可避免的数据脏写问题

例如银行账户的金额增减,假如初始为100,事务1中的分支1执行+100,成功,此时数据库记录为200,但事务1中分支2失败,失败前事务2中的分支1执行了-150,成功,此时数据库记录为50。由于事务1中分支2失败,事务1的分支1回滚,-100,发现减操作失败,无法回滚。且用户账户由于错误的记录,扣款超过账户额度却成功了

这是一个很严峻的问题,但采用saga模式这一问题确实无法回避的,因此一种广泛采用的解决思路是:

利用设计理念来减弱saga脏写的危害:例如银行账户问题中,我们可以在设计事务执行顺序时,将所有的减操作提前,加操作滞后,即使出现脏写问题,也只会是用户少钱而银行多钱,这种情况可以通过银行赔款来解决,相比案例中用户多钱银行少钱的情况,危害更小

9. 微服务的分布式缓存:Redis

前面的博客都没有系统的对Redis这门技术进行细致的讲解,默认大伙都对这门技术有基本了解,这里就跨越了原理直接来到了Redis的分布式技术上,如果对Redis有更深入兴趣的,可以参考我的另一篇博客:

Redis在单点部署时,面临着任何单点部署服务的共性问题,那就是:

  • 服务器宕机时的内存数据丢失
  • 高并发场景下的服务器宕机
  • 海量缓存数据下的存储容量不足
  • 服务器宕机后的服务停止

而要解决单点部署的缺陷,主要的手段就是实现分布式部署,针对以上四个问题,Redis分别设计了各自的解决方案:

  • 服务器宕机时的内存数据丢失 —— Redis数据的持久化
  • 高并发读场景下的服务器宕机 —— Redis主从结构(主节点写,从节点读,读写分离,提高并发能力)
  • 海量缓存数据下的存储容量不足,高并发写场景下的服务器宕机—— Redis分片集群
  • 服务器宕机后的服务停止—— Redis哨兵
数据持久化

数据持久化又分为RDB和AOF,简单来说,RDB就是通过fork技术,利用子进程去读取内存数据并在磁盘中生成快照;而AOF则是每执行一条Redis命令,都记录在AOF文件中

以上两种思路各有优劣:

  • RDB读取快,但存储慢,而且子进程会定时占用大量cpu和内存消耗,同时,由于fork对主进程写入的保护,子进程存储的数据与主进程是version.k-1和version.k的区别;
  • AOF写入快,写入时不占用过多的cpu和内存,而且数据是完整的,但读取时由于需要重新执行AOF记录的所有命令,速度慢

这时候我们就会思考,其实两者做个结合不是很棒吗,RDB继续定时存储,而AOF也进行相关的记录,如果我们可以同步RDB数据的版本与AOF记录的命令的位置,这不就意味着我们重启时可以先载入RDB数据,然后再从同步位置执行AOF命令吗?

虽然仍然没有解决RDB的内存占用问题,但也缓解了RDB的数据不完整,以及AOF载入慢的问题

因此,Redis在设计主从结构时,就结合了RDB和AOF来完成数据的持久化

主从结构
  • 阶段一:建立连接,请求数据:
  • 在从节点通过slaveof [master IP][master Port]向主节点发起从属请求;
  • 成功建立连接后,从节点会发起增量同步请求,该http请求会携带从节点的replid(每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID)和offset(默认是0),主节点建立后也有一个自己的replid,当主节点发现两个replid不同时,会拒绝增量同步,向从节点发送自身的replid和offset(从节点由此获得主节点的数据版本信息)
  • 阶段二:全量同步:
  • 主节点立即fork出子进程存储RDB(该RDB中还包含着主节点的replid和offset)并发送给从节点,从节点清空内存并依照接收的RDB写入缓存信息
  • 阶段三:增量同步:
  • 主节点将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给从节点,从节点执行接收到的命令,保持与主节点之间的同步

由于阶段二中发送的RDB包含主节点的replid和offset,因此即使从节点重启后,也可以在特定条件下以增量同步方式重新与主节点保持一致:

特定条件为:JAVA如何获取微服务状态 java微服务实战_开发语言_05

否则主节点将拒绝从节点的增量同步请求,并执行全量同步

有了主从节点的结构以及其自动执行的数据持久化,我们就开始关心另一个问题了——分布式服务的高可用性。

在主从结构中有一个很大的问题,从节点宕机后立刻重启,系统可以无影响的恢复;但主节点如果宕机,整个Redis结构将无法写入,无法同步,失去了可用性。因此,Redis给出了哨兵机制来解决这个问题

哨兵机制

哨兵的作用体现在三个方面:

  • 监控:通过配置哨兵集群,利用心跳检测和主客观下线机制,监控master和slave的状态
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
  • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
  • sentinel给所有其它slave发送slaveof [master IP][master Port]命令,让这些slave成为新master的从节点,开始从新的master上同步数据
  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
  • 通知:sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将新的master和slave地址推送给Redis的客户端
Redis分片集群

正如前文所述,Redis主从结构和sentinel只是保证了高读取并发时的系统稳定性,而没有保证高写入并发下,以及海量存储需求下的系统稳定性

  • 分片集群与散列插槽:遇事不决加一层,多个主从结构组成新的分片集群,主节点间通过均衡的分配散列插槽(也可以根据主节点处理能力有权重的分配),实现存储数据的分片以及对高写入并发的负载均衡
  • 天然的哨兵机制与故障转移:由于多个主节点间互相进行心跳检测,因此天然的构成了哨兵机制,可自动完成故障转移(转移机制和哨兵类似,在发现某个master节点客观下线后,选举该分片下优先级最高的slave升为master节点,原master节点置为slave)。除自动转移外,也可以在对应的slave节点上执行cluster failover命令,通过下图所示的六个步骤将无感知的将slave升级为master
  • JAVA如何获取微服务状态 java微服务实战_开发语言_06

  • 存储数据的位置分配算法:对存储数据的key进行keylen次crc16操作,返回值对16383(源码地址)取余,结果作为散列插槽值。该算法的好处是保证键均匀的分配到各个槽位上

10. 分布式缓存进阶:多级缓存机制

多级缓存一般是指:

  • 第一级:Nginx级
  • 缓存方式:Nginx服务器内存缓存
  • 更新方式:通过超时机制实现缓存数据更新
  • 实现方式:OpenResty工具
  • 第二级:Redis级
  • 缓存方式:Redis服务器内存缓存
  • 更新方式:
  • 对于同步准确率要求一般的数据,采用异步通知的方式更新
  • 可选择MQ方式或者是Canal方式
  • 对于同步准确率要求高的数据,采用事务管理的方式更新
  • 实现方式:Redis
  • 第三级:Tomcat级
  • 缓存方式:JVM进程本地内存缓存
  • 更新方式:同Redis一致
  • 实现方式:Caffine

多级缓存在实现中需要注意以下几点:

  • 由于是集群部署,而缓存信息在每台主机的本地,因此在做集群的负载均衡时,应通过Hash处理请求的URI,结果对集群数取余后,决定要代理向的服务器,确保同一URI访问同一台服务器
  • 有关于nginx的一般Hash和一致Hash,可参考[1],有很多头头道道
  • 由于OpenResty不方便实现异步通知或同步更新,只能采用超时更新,因此在Nginx服务器上缓存的数据应尽量是那种长时间无变化的稳定数据

多级缓存结构图:

JAVA如何获取微服务状态 java微服务实战_客户端_07

11. RabbitMQ进阶内容

RabbitMQ消息可靠性

RabbitMQ为实现消息的可靠性,主要做了三个方面的工作:

  • 生产者确认
  • 机制:生产者在发送消息后,需要监听MQ的回执,包括两种类型三种可能:
  • publish confirm:
  • publish confirm & ack:消息到达了MQ并且发送到队列
  • publish confirm & nack:消息没有到达MQ
  • publish return & ack:消息到达了MQ但是没有发送到队列
  • 如果接收到nack或者publish return回执,则考虑通过重发数据,通知运维人员,记录错误日志等方式,维护生产者消息的可靠性
  • MQ持久化
  • 机制:MQ将交换机,队列以及消息存储在磁盘中,实现数据的持久化
  • 消费者确认
  • 机制:MQ在将消息推送给消费者后,需要等待消费者的回执,消费者回执有多种可能:
  • 如果消费者消费成功,MQ得到ack回执,删除消息
  • 如果消费者第一次消费不成功,消费者会调用spring的retry机制重试数次,如果达到上限仍然不成功,消费者可选择:
  • 返回reject回执,MQ得到reject回执,拒绝并删除消息
  • 返回nack回执,MQ得到nack回执,消息重新入队并再次发给消费者
  • 失败消息发送到MQ的error message queue(自己创建)留存
死信交换机与延时消息

死信交换机,顾名思义,就是用来转发RabbitMQ因各种原因产生的死消息的交换机,这里的各种原因包括:

  • 消费者返回reject回执,或返回nack回执但队列不允许重新入队时
  • 消息等待消费时间超过设定时间(可以由发送者设定超时时间,也可以设定队列超时时间,两者中较短的生效)
  • 消息队列满额,在等待刷出过程中到来的新消息无法投递

延时消息:

  • 可以利用死信原理,通过为消息队列绑定死信交换机,为发送的消息绑定超时时间,消费者监听死信队列,即可以在超时时间后读取到消息,实现消息的延时
  • 除此之外,DelayExchange插件提供了一种新的交换机,可以在交换机处将消息主动延时,超时后再发送到对应队列,避免了多设计一条交换机和队列,使用更为方便
惰性队列

消息队列满额后,RabbitMQ会将一部分内存消息刷出到磁盘中,清理出空间,但该过程需要时间,这意味着如果发送消息速度高于消费消息速度:

  • RabbitMQ的处理能力将呈现锯齿状,不够平滑
  • 如果在刷出过程中到来新消息,该消息将成为死信,无法被消息队列接收

为了处理这个问题,RabbitMQ提出一种惰性队列机制,消息不再存储在内存中,而是全部刷出到磁盘里,等消费者确定消费时,才从磁盘里刷回内存

MQ集群知识

MQ集群的实践在我的另一篇博客中有介绍,该博客主要是解决由于跨域而导致的跨主机搭建MQ集群的难题

MQ集群包括三类:

  • 普通集群:集群间只共享交换机和队列信息,其他队列仅知道某个消息在哪个队列并可以负责转发,但并不能处理,一旦该队列宕机,消息将无法获取
  • 镜像集群:集群间互相进行消息的备份,由得到该消息的队列为主,其他队列为镜像,一切对该消息的访问都必须转发到主节点处理
  • 仲裁队列:仲裁队列只是针对特定的队列而言,其是镜像队列的扩展,在镜像队列的基础上,通过Raft协议保证强一致性(从节点操作日志必须与主节点一致,不一致将覆写)