高级篇里的很多知识我都没有去实践过,只是知道相关的原理,主要原因是:
所以这部分记录更多是一个思路的梳理,可能无法作为各位全面掌握该部分知识的条目,望见谅
sentiel分为sentinel控制台和sentinel客户端,具体的规则过滤都由配置在微服务上的客户端完成,控制台只是进行可视化的规则配置,其逻辑图如下:
由于Sentinel客户端会将规则存储在微服务的运行内存中,因此一旦微服务重启,运行内存即被清空,也就意味着规则的丢失
该问题的解决方案可参照[1],有非常详细的描述:
因此pull方案适合于规则配置较长时间不变的情况,这时候直接使用pull方案简单快捷
上面我们对流程进行了梳理,同时也深入到了Sentinel使用中的持久化问题,并介绍了相关资料,而对于其推送的配置规则有哪些,即Sentinel实现了哪些功能,我们也将一一罗列:
Sentinel可以对访问微服务的流量进行控制,通过设置流控模式(根据谁的QPS,控制谁的访问),流控效果(按照什么原则控制访问)来实现(备注,这里的阈值默认为QPS计数)
与限流对扇入的保护不同,Sentinel还可以对资源的扇出进行隔离与熔断处理,以避免宕机的后端服务造成前端的雪崩
线程隔离即是限制扇出对象的最大线程数,限制的手段可以是通过为扇出对象建立独立的线程池,也可以是设置计数器
主动超时
调用线程,从而完成请求的立刻返回(抛出异常)主动超时
熔断降级即是在某个扇出对象的不可用调用(这里的不可用调用在Sentinel中分为两种,一种是慢调用,另一种是异常调用)达到一定比例或一定数量时,将对该扇出对象的断路器由closed转为open状态,熔断对该扇出对象的新调用,请求直接返回;降级一段时间后,断路器由open转为half-open状态,开放一次对扇出对象的访问,如果访问可以成功,关闭断路器,否则重新回到open状态,如下图所示:
为了避免内部服务器的地址泄露而存在的直接被外网访问的风险,Sentinel客户端可以通过授权规则来把关进入微服务的每一次访问,只有满足授权规则的才予以放行
流控应用名
设定白名单和黑名单流控应用
才能访问设定的资源流控应用名
通过实现RequestOriginParser
接口,实现类中要解析HTTP请求,根据解析内容返回对应流控应用名
,该实现类被装配为Bean由于自定义异常处理部分的内容不多,而且都是已有知识,所以这里就没有展开介绍该如何实现
事务遵循的是ACID的设计原理,用简洁的话来总结就是:一个事物中的所有分支要么同时完成,要么同时失败(这就是原子性
);原子性代表数据间可以保持一致性
,不会存在矛盾现象;事物需要取得数据锁,对同一资源的操作才不会同时发生(隔离性
);事务一旦提交则永久修改,不能回退(持久性
)
正如之前在spring的事务管理中所言,分布式系统中想要完成多个分支的事务管理就必须要声明一个全局的事务管理器来控制分支事务的,这也暗合了我们一直强调的一个观点:凡是难以解决的问题,不妨向上抽取,多加一层架构
然而,简单的抽取一层全局管理器还不行,事务管理中存在一个此消彼长的对弈,全局管理器必须要能够适应不同的对弈需求,这方面Seata是做得最好的。首先,我们想讲讲这个此消彼长的对弈是什么,然后再展开介绍Seata怎么来适应不同的对弈需求的
Partition——分区,代表几方独立运行的系统,这个系统可以是指独立运行的数据库,也可指代由于网络故障而分别提供服务的多个集群,总之,分区意味着系统间无法直接沟通
Avalibility——可用,代表客户端访问健康节点时,节点必须响应,而不是超时或拒绝
Consistency——一致,代表客户端访问分区的各个健康节点时,得到的回应必须一致
我们注意到,一致与可用正是一个此消彼长的对弈,为了更高的一致性,凡是无法沟通的节点在处理事务时,都必须等待进一步的确认才能够回应;反之,为了更高的可用性,必然意味着节点必须首先回应客户端,而不是等待进一步的确认
这正是CAP理论的基础,C与A之间是对立的,但BASE理论告诉我们,对立不意味着不可以折中:
因此我们有基于强一致性的CP方案,也有基于BASE理论的软一致性的AP方案:
不论是AP模式还是CP模式,都需要一个全局的事务管理器来进行子事务的注册,状态查询与判断,以及决定事物的回滚与提交,或者通过弥补措施恢复数据。Seata新增了一层事务协调者进行全局事务的注册与管理,不同的全局事务间被有效隔离,其还能够替Seata管理的不同全局事务间添加全局资源锁,避免数据脏写
发生的同时还可以比DB锁更细粒度
的占用资源。Seata由此形成事务协调者、事务管理器、资源管理器的三层架构。我们以XA为例,绘制了该架构
Seata在实践中,为了提供不同的一致性和可用性的平衡需求,分别给出了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。
修改业务
,新增和删除业务的全局锁在一阶段事务提交后即解除了,因此此时采用AP模式和TCC是一样的效果,还更加方便数字
资源,而对于字符串
,由于其并不能实现拆分与冻结,因此不适用于TCCTCC模式的逻辑是,不为TC管理的事务添加全局锁,而是增加一个freeze表记录冻结资源,在try阶段进行资源的预分配,而TC管理器根据所有分支的try阶段执行情况决定对分支事务的调用:
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模式,该事务要么就是长时间的持有全局锁,要么就是知道长时业务完成后,才真正的更新了数据库。这显然启迪着我们,需要一种支持异步操作
的新模式来进行事务管理难以拆分
成try和confirm两个阶段来分别执行。这要求我们,需要一种更为直接简洁
的新模式来进行事务管理这就是saga模式,它抛弃了TCC的confirm
阶段,而是令分支事务在第一阶段
就完成数据库的所有操作,同时记录第一阶段的相反操作,在需要进行第二阶段
时执行回滚。
但同时,saga的优化也带来了不可避免的数据脏写问题
例如银行账户的金额增减,假如初始为100,事务1中的分支1执行+100,成功,此时数据库记录为200,但事务1中分支2失败,失败前事务2中的分支1执行了-150,成功,此时数据库记录为50。由于事务1中分支2失败,事务1的分支1回滚,-100,发现减操作失败,无法回滚。且用户账户由于错误的记录,扣款超过账户额度却成功了
这是一个很严峻的问题,但采用saga模式这一问题确实无法回避的,因此一种广泛采用的解决思路是:
利用设计理念来减弱saga脏写的危害:例如银行账户问题中,我们可以在设计事务执行顺序时,将所有的减操作提前,加操作滞后,即使出现脏写问题,也只会是用户少钱而银行多钱,这种情况可以通过银行赔款来解决,相比案例中用户多钱银行少钱的情况,危害更小
前面的博客都没有系统的对Redis这门技术进行细致的讲解,默认大伙都对这门技术有基本了解,这里就跨越了原理直接来到了Redis的分布式技术上,如果对Redis有更深入兴趣的,可以参考我的另一篇博客:
Redis在单点部署时,面临着任何单点部署服务的共性问题,那就是:
而要解决单点部署的缺陷,主要的手段就是实现分布式部署,针对以上四个问题,Redis分别设计了各自的解决方案:
数据持久化又分为RDB和AOF,简单来说,RDB就是通过fork技术,利用子进程去读取内存数据并在磁盘中生成快照;而AOF则是每执行一条Redis命令,都记录在AOF文件中
以上两种思路各有优劣:
这时候我们就会思考,其实两者做个结合不是很棒吗,RDB继续定时存储,而AOF也进行相关的记录,如果我们可以同步RDB数据的版本与AOF记录的命令的位置,这不就意味着我们重启时可以先载入RDB数据,然后再从同步位置执行AOF命令吗?
虽然仍然没有解决RDB的内存占用问题,但也缓解了RDB的数据不完整,以及AOF载入慢的问题
因此,Redis在设计主从结构时,就结合了RDB和AOF来完成数据的持久化
slaveof [master IP][master Port]
向主节点发起从属请求;由于阶段二中发送的RDB包含主节点的replid和offset,因此即使从节点重启后,也可以在特定条件下以增量同步方式重新与主节点保持一致:
特定条件为: m a s t e r _ o f f s e t − s l a v e _ o f f s e t < m a x _ l e n g t h ( r e p l _ b a k l o g ) master\_offset-slave\_offset
否则主节点将拒绝从节点的增量同步请求,并执行全量同步
有了主从节点的结构以及其自动执行的数据持久化,我们就开始关心另一个问题了——分布式服务的高可用性。
在主从结构中有一个很大的问题,从节点宕机后立刻重启,系统可以无影响的恢复;但主节点如果宕机,整个Redis结构将无法写入,无法同步,失去了可用性。因此,Redis给出了哨兵机制来解决这个问题
哨兵的作用体现在三个方面:
slaveof no one
命令,让该节点成为masterslaveof [master IP][master Port]
命令,让这些slave成为新master的从节点,开始从新的master上同步数据正如前文所述,Redis主从结构和sentinel只是保证了高读取并发时的系统稳定性,而没有保证高写入并发下,以及海量存储需求下的系统稳定性
分片集群与散列插槽:遇事不决加一层,多个主从结构组成新的分片集群,主节点间通过均衡的分配散列插槽(也可以根据主节点处理能力有权重的分配),实现存储数据的分片以及对高写入并发的负载均衡
天然的哨兵机制与故障转移:由于多个主节点间互相进行心跳检测,因此天然的构成了哨兵机制,可自动完成故障转移(转移机制和哨兵类似,在发现某个master节点客观下线后,选举该分片下优先级最高的slave升为master节点,原master节点置为slave)。除自动转移外,也可以在对应的slave节点上执行cluster failover命令,通过下图所示的六个步骤将无感知的将slave升级为master
存储数据的位置分配算法:对存储数据的key进行keylen次crc16操作,返回值对16383(源码地址)取余,结果作为散列插槽值。该算法的好处是保证键均匀的分配到各个槽位上
多级缓存一般是指:
多级缓存在实现中需要注意以下几点:
Hash
处理请求的URI,结果对集群数取余后,决定要代理向的服务器,确保同一URI访问同一台服务器
RabbitMQ为实现消息的可靠性,主要做了三个方面的工作:
机制:生产者在发送消息后,需要监听MQ的回执,包括两种类型三种可能:
如果接收到nack或者publish return回执,则考虑通过重发数据,通知运维人员,记录错误日志等方式,维护生产者消息的可靠性
死信交换机,顾名思义,就是用来转发RabbitMQ因各种原因产生的死消息的交换机,这里的各种原因包括:
延时消息:
消息队列满额后,RabbitMQ会将一部分内存消息刷出到磁盘中,清理出空间,但该过程需要时间,这意味着如果发送消息速度高于消费消息速度:
为了处理这个问题,RabbitMQ提出一种惰性队列机制,消息不再存储在内存中,而是全部刷出到磁盘里,等消费者确定消费时,才从磁盘里刷回内存
MQ集群的实践在我的另一篇博客中有介绍,该博客主要是解决由于跨域而导致的跨主机搭建MQ集群的难题
MQ集群包括三类: