一. 微服务保护
1. 雪崩问题及解决方案
什么是雪崩问题?
微服务之间相互调用, 当其中一个服务发生故障, 导致调用该服务的其他服务的并发量提高, 导致请求一直阻塞, 最后会耗尽服务器资源, 从而也发生了故障, 随着时间的推移, 形成了级联失败, 最终导致整个调用链的服务都发生故障, 导致雪崩问题的发生
解决雪崩问题的方案有哪些?
- 超时处理
设置超时时间, 请求超过一定的时间没有响应就返回错误信息, 防止无休止等待
- 线程隔离(仓壁模式)
对该服务的每个业务 设置固定大小的线程数, 这样即使某个业务的服务调用发生阻塞, 不会影响到其他业务, 从而保障该服务正常运行不会挂掉
- 降级熔断(断路器模式)
由断路器统计业务执行的异常比例, 超出阈值会熔断该业务, 拦截访问该业务的一切请求
- 限流
限制业务访问的QPS, 避免服务因流量的突增而发生故障
注意:
- 限流 是对服务的保护,避免因瞬间高并发流量导致服务故障, 进而避免雪崩, 是一种预防措施
超时处理, 线程隔离, 降级熔断 是在部分服务发生故障时, 将故障控制在一定范围, 避免雪崩, 是一种补救措施
2. Sentinel的概念及其使用
Sentinel: 阿里开源的服务保护框架
Sentinel支持的雪崩解决方案:线程隔离(仓壁模式)
- 降级熔断
限流: 限制请求的数量
- 流控模式有哪些?
- 直接:对当前资源限流
- 关联:高优先级资源触发阈值,对低优先级资源限流。
- 链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
- 流控效果有哪些?
- 快速失败:QPS超过阈值时,拒绝新的请求
- warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机
- 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝
隔离: 全称”线程隔离”, 可以使用线程池隔离或者信号量隔离
- 线程隔离的两种手段和它们的特点是?
- 信号量隔离: 基于计数器模式,简单,开销小
- 线程池隔离: 基于线程池模式,有额外开销,但隔离控制更强
熔断: 当达到一定条件(响应时间超时、异常达到一定比例或一定数量),服务器熔断,不再对外提供服务.当请求过来不再正常处理请求,而是直接返回预先设置好的返回结果,相当于让服务器处于休息状态
- 状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
- 断路器熔断策略有三种:
- 慢调用
- 异常比例
- 异常数
降级: 当达到一定条件(响应时间超时、异常达到一定比例或一定数量),不再等待服务器正常处理请求,直接返回预先设置好的返回结果.
FeignClient整合Sentinel, 编写失败后的降级逻辑
授权: 校验请求来源是否合法
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
二. 分布式事务
1. 分布式事务概念和解决方案
什么是分布式事务?
在分布式系统中, 当一个服务调用多个服务, 或者当一个服务操作多个数据库时, 都会存在分布式事务
解决分布式事务的思路有哪些?
理论基础:
CAP定理、BASE理论
C可用性: 一个节点不可用不影响整个集群的使用 A一致性: 所有节点的数据必须一致 P分区容错: 一个节点与其他节点数据同步连接断掉, 形成独立分区
在P一定会出现的时候, A和C只能实现一个, 如何解决? 用BASE理论解决CAP
BA基本可用: 分布式系统出现故障时, 允许损失部分可用性, 即保证核心可用 S软状态: 在短时间内, 允许出现临时的不一致状态 E最终一致: 虽然无法保证强一致性, 但是在软状态结束后, 最终达到数据一致
解决思路:
- 合并, 把分布式事务中包含的所有本地事务, 看成一个全局事务的分支, 所有分支事务都执行成功才能提交全局事务
- 拆分, 使用MQ应用解耦, 把一个分布式事务拆分成多个本地事务, 通过消息进行数据传递, 只要保证消息的可靠性、幂等性, 就能保证数据最终一致, 从而解决分布式事务问题
幂等性: 在分布式环境下, 多次操作,结果是一致的。(多次操作数据库数据是一致的。)
解决方案:
- 两阶段提交
- 两阶段提交补偿(三阶段、事务补偿)
- MQ消息最终一致性
2. Seata概念及其使用
概念:
Seata是阿里开源的分布式事务组件
Seata事务管理的三个角色
- TC -事务协调者:事务协调。维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM -事务管理器:管理全局事务。定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM -资源管理器:管理分支事务。与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
-
XA模式: 强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
分支执行sql不提交 ==> 全局统一执行提交或回滚
一阶段:RM注册分支事务, 执行分支业务sql但不提交,报告执行状态到TC
二阶段:TC检测各分支事务执行状态.
a.都成功,通知所有RM提交事务
b.有失败,通知所有RM回滚事务
特点: 强一致, 完全隔离, 无代码侵入, 性能差, 适合对一致性隔离性有高要求的业务
AT模式(使用最多):最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
分支记录数据快照, 执行sql提交 ==> 全局若提交 删除快照, 若回滚 恢复快照
一阶段:RM注册分支事务, 记录undo-log(数据快照), 执行业务sql并提交, 报告事务状态到TC
二阶段:TC检测各分支事务执行状态
a.提交时, RM删除undo-log
b.回滚时,RM根据undo-log恢复数据到更新前
特点: 弱一致, 基于全局锁隔离, 无代码侵入, 性能好, 适合基于关系型数据库的大多数分布式事务场景
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致
应用场景: 对一致性要求没那么高, 对性能要求差不多就行, 使用最多的一种模式
TCC模式(使用次多):最终一致的分阶段事务模式,有业务侵入
扣款业务举例:可用金额表, 冻结金额表,现在可用金额100要扣款30的执行过程:
分支Try资源预留(添加冻结金额30, 扣减可用金额30)
a.全局若提交: 分支Confirm业务提交(删除冻结金额);
b.全局若回滚: 分支Cancel预留资源释放(删除冻结金额吗,恢复可用金额30)
一阶段:RM注册分支事务, Try资源预留, 执行业务sql并提交, 报告事务状态到TC
二阶段:TC检测各分支事务执行状态
a. 提交, RM Confirm业务执行和提交
b. 回滚, RM Cancel预留资源释放
特点: 弱一致, 基于资源预留隔离, 有代码侵入(编写3个接口try、Confirm和Cancel), 性能非常好, 适合对性能要求较高的事务, 和有非关系型数据库要参与的事务
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
应用场景: 对性能要求非常高, 有非关系型数据库参与的事务
什么是空回滚?如何解决?
在分布式事务中, 当某一个分支事务的try阶段阻塞, 导致全局事务超时而触发二阶段的cancel操作>,在未执行try之前执行cancel操作,这时候cancel不能做正常回滚, 就是空回滚。
解决:执行cancel操作时,应当先判断try是否已经执行,如果未执行,则应该空回滚(添加一条空记录到数据库中)
为什么要做空回滚?
在未执行try之前执行cancel操作,这时候cancel不能回滚, 而且不能报错, 如果报错,Seata会认为cancel出问题了,会重试,陷入死胡同里了。所以做一个空回滚。
什么是业务悬挂?如何解决?
对于已经空回滚的业务,之前阻塞的try又开始继续执行try,就永远不可能confirm或cancel,这就是业务悬挂
解决:执行try操作时, 应当先判断cancel是否已经执行, 如果执行过,拒绝业务(终止该方法return;)
为什么防止业务悬挂?
cancel做过空回滚,再执行try,全局事务已经结束了, 永远不可能再confirm或cancel了,导致数据不一致
注意:
- try正常执行的前提:cancel没有执行过
- cancel正常回滚的前提: 必须且仅执行过一次try
SAGA模式:长事务模式,有业务侵入
Saga也分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
特点: 最终一致, 无隔离, 有代码侵入(编写状态机和补偿机制), 性能非常好, 适合业务流程长流程多、参与者包含其他公司或遗留系统服务,无法提供TCC模式要求的三个接口
本地事务失效场景: 1)方法不是用public修饰 2)try catch 3)抛出异常不是RuntimeException类型 4)数据库底层引擎不支持事务 5)在一个类中A方法调用B方法,A上没有事务注解 6)在一个类中A方法调用B方法,A是有事务,事务传播特性不对
三. 分布式缓存
1. 如何解决redis数据丢失问题?
实现redis数据持久化, 开启RDB和AOF
RDB: 数据快照, 将内存中的数据持久化保存到磁盘中 (默认开启)
- 记录数据快照
- 数据恢复的速度快
- 只能全量同步
- 数据文件比较小, 并且可压缩
执行时机:
- 执行save命令时: 由主进程执行RDB, 其他命令都被阻塞
- 执行bgsave命令时: fork主进程得到一个子进程,共享内存资源, 主进程继续处理用户请求,由子进程执行RDB
- Redis停机时(默认)
- 触发RDB条件时
缺点:
- 两次RDB之间写入数据有丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时
注意: 配置redis时不能使用服务器内存太多, 要预留空间, bgsave执行RDB时, 会拷贝副本给主进程执行写操作, 所以会占用很多内存空间
AOF: 记录操作命令到AOF文件中,可以看做是命令日志文件 (手动开启)
- 记录操作命令
- 数据恢复的速度慢
- 既可以全量同步,也可以增量同步
- 数据文件大,但是也可以通过重写缩减大小
AOF文件重写: 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果
2. 如何提高redis并发能力?
搭建主从集群, 实现读写分离
三种集群模式:
- 主从:数据备份、读写分离(最少2台)
- 哨兵:数据备份、读写分离、故障恢复(最少5台,3台哨兵、1主1从)
- 分片:数据备份、读写分离、故障恢复、动态扩容(最少6台,3主3从)
主从和哨兵可以解决 高可用、高并发读的问题
分片集群可以解决 高可用、高并发读、海量数据存储、高并发写的问题
2.1 主从集群
master(写) ——-数据同步——-> slave(读)
主从数据同步原理是什么?
- 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
全量同步的详细流程: 阶段1: <—slave请求增量同步(携带repid和offset)
-->master判断replid, 不一致, 拒绝增量同步, 说明是第一次同步, 向slave发送自己的replid和offset
阶段2: —>master执行bgsave生成RDB, 发送RDB到slave 阶段3: —>master记录RDB期间的所有操作命令在repl_baklog, 并持续发送给slave(增量同步)
增量同步的详细流程: 阶段1: <—slave请求增量同步(携带repid和offset)
-->master判断replid, 一致, 同意增量同步, 向slave发送continue
阶段2: —>master去repl_baklog获取offset后的数据, 发送数据给salve
replid: 数据集的标记 offset:repl_baklog中的数据偏移量 repl_baklog文件: 一个固定大小的环形数组, 记录RDB之后的所有操作命令
什么时候执行全量同步?
- slave节点第一次连接master节点时
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步?
- slave节点断开又恢复,并且在repl_baklog中能找到offset时
2.2 哨兵集群
主从 + Sentinel哨兵
sentinel哨兵的作用是什么?
- 监控: 监控集群状态
- 自动故障恢复: master故障, 将选举一个slave提升为master
- 通知: 当集群发生故障转移时, 通知Redis客户端
Sentinel如何判断一个redis实例是否健康?
- 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线
- 如果大多数sentinel都认为实例主观下线,则判定服务下线
故障转移步骤有哪些?
- 首先选定一个slave作为新的master,执行slaveof no one
- 然后让所有节点都执行slaveof 新master
- 修改故障节点配置,添加slaveof 新master
笔记:搭建哨兵集群 和 redisTemple的相应的读写分离配置
2.3 分片集群
分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
散列插槽
Redis会把每一个master节点映射到0~16383共16384个插槽上。数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384取余
-
集群伸缩
使用 redis-cli —cluster命令,添加新节点到集群中,分配到新插槽
故障转移
自动故障转移:集群的一个master宕机, 自动提升一个slave为新的master
- 手动故障转移: 利用cluster failover命令可以手动让集群中的某个master宕机,提升一个slave为新的master,实现无感知的数据迁移
笔记: 搭建分片集群 和 RedisTemplate访问分片集群
3. 如何避免Redis中数据太多?
- 添加数据时设置有效时间
- 合理设置删除策略(缓存平时维护操作,剔除无用数据)
删除策略针对设置过期时间的数据。
- 定时:到了指定时间,执行删除过期数据操作 (特点:对CPU要求比较高)
- 惰性:第一次只是标记删除,第二次访问才会真正删除过期数据(特点:对内存要求比较高)极端情况,可能会导致Redis中都是过期数据
- 定期:把CPU一秒钟分成2块,750ms用于处理正常请求,250ms用于删除过期数据,既不会对CPU要求太高,也不会对内存造成太大压力
- 合理设置淘汰策略(缓存将满时维护操作,剔除数据)
淘汰策略针对所有数据,设置了过期时间的、没有设置过期时间的
- 过期数据:LRU LFU TTL Random
- 非过期数据:LRU LFU Random
- 所有数据:Eviction
四. 多级缓存
传统项目存在的问题:
- 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
- Redis缓存失效时,会对数据库产生冲击
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能
多级缓存处理过程
前端浏览器(浏览器客户端缓存)
—-CDN(内容分发网络,缓存)
—-反向代理nginx
—-Web服务器openresty(编写lua脚本: nginx本地缓存 —>redis缓存 —>tomcat)
—-tomcat服务器(JVM本地缓存Caffeine —> 数据库)
openresty:基于Nginx,并且集成了Lua脚本(并发支持10w) SpringCache整合Caffeine: 实现JVM本地缓存 Redis缓存预热: 在项目启动时将这些热点数据提前查询并保存到Redis中, 减轻数据库压力
缓存数据同步策略
保证数据库数据、缓存数据的一致性,这就是缓存与数据库的同步, 防止缓存数据与数据库数据存在较大差异
- 设置有效期:给缓存设置有限期, 到期自动删除, 再次查询时更新(时效性最差,解耦)
- 同步双写:在修改数据库的同时, 直接修改缓存(时效性最好,耦合最高)
- 异步通知(常用):修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
当增删改DB时,异步通知可以基于MQ或者Canal来实现:
1)[异步]更新openresty、redis、caffeine缓存数据(低耦合)
@EnableSync、@Sync
2)基于MQ发送消息openresty、redis、caffeine(解耦)
发送消息:RabbitTemplate、接收消息:@RabbitListener
3)(常用)基于canal通知(解耦)
Canal是基于mysql的主从同步来实现的。Canal把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,我们利用Canal客户端监听Canal通知消息,当收到变化的消息时,完成对缓存的更新
MySQL主从同步的原理
- 1)MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
- 2)MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
- 3)MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
笔记整理-多级缓存
五. mq高级篇
1.消息丢失如何解决(消息的可靠性)
- 生产者确认机制: confirm —>交换机消息回调 ack nack, return —>队列发生错误消息回调 ack
- 消息持久化 : 开启交换机 队列 消息持久化
- 开启消费者确认机制auto: manual手动ack, auto自动ack, none关闭ack
开启消息失败重试机制: 开启本地重试, 达到最大重试次数, 返回ack, 为避免消息丢失, 实现失败策略RepublishMessageRecoverer, 将失败消息发送到error交换机上
2.消息堆积如何解决
增加消费者数量, 提高并发量
- 增加队列容量, 使用惰性队列(消息基于磁盘存储, 不存放在内存中)
- 合并消息,减少消息数量
-
3.消息重复消费问题
防止重复消费: 设置消费历史表, 消费前判断一下是否被消费过
重复消费了也不会导致数据不一致: 使用幂等性消息, (eg: 指定id, 不使用自增长id)4.延迟队列实现方案?
存活时间TTL + 死信交换机DXL
- 延迟交换机插件delay-exchange