一. 微服务保护

1. 雪崩问题及解决方案

什么是雪崩问题?

微服务之间相互调用, 当其中一个服务发生故障, 导致调用该服务的其他服务的并发量提高, 导致请求一直阻塞, 最后会耗尽服务器资源, 从而也发生了故障, 随着时间的推移, 形成了级联失败, 最终导致整个调用链的服务都发生故障, 导致雪崩问题的发生

解决雪崩问题的方案有哪些?

  1. 超时处理

设置超时时间, 请求超过一定的时间没有响应就返回错误信息, 防止无休止等待

  1. 线程隔离(仓壁模式)

对该服务的每个业务 设置固定大小的线程数, 这样即使某个业务的服务调用发生阻塞, 不会影响到其他业务, 从而保障该服务正常运行不会挂掉

  1. 降级熔断(断路器模式)

由断路器统计业务执行的异常比例, 超出阈值会熔断该业务, 拦截访问该业务的一切请求

  1. 限流

限制业务访问的QPS, 避免服务因流量的突增而发生故障
注意:

  • 限流 是对服务的保护,避免因瞬间高并发流量导致服务故障, 进而避免雪崩, 是一种预防措施
  • 超时处理, 线程隔离, 降级熔断 是在部分服务发生故障时, 将故障控制在一定范围, 避免雪崩, 是一种补救措施

    2. Sentinel的概念及其使用

    Sentinel: 阿里开源的服务保护框架
    Sentinel支持的雪崩解决方案:

  • 线程隔离(仓壁模式)

  • 降级熔断

限流: 限制请求的数量

  1. 流控模式有哪些?
  • 直接:对当前资源限流
  • 关联:高优先级资源触发阈值,对低优先级资源限流。
  • 链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
  1. 流控效果有哪些?
  • 快速失败:QPS超过阈值时,拒绝新的请求
  • warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机
  • 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝

隔离: 全称”线程隔离”, 可以使用线程池隔离或者信号量隔离

  1. 线程隔离的两种手段和它们的特点是?
  • 信号量隔离: 基于计数器模式,简单,开销小
  • 线程池隔离: 基于线程池模式,有额外开销,但隔离控制更强

熔断: 当达到一定条件(响应时间超时、异常达到一定比例或一定数量),服务器熔断,不再对外提供服务.当请求过来不再正常处理请求,而是直接返回预先设置好的返回结果,相当于让服务器处于休息状态

  1. 状态机包括三个状态:
  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态
  1. 断路器熔断策略有三种:
  • 慢调用
  • 异常比例
  • 异常数

降级: 当达到一定条件(响应时间超时、异常达到一定比例或一定数量),不再等待服务器正常处理请求,直接返回预先设置好的返回结果.
FeignClient整合Sentinel, 编写失败后的降级逻辑
授权: 校验请求来源是否合法
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

二. 分布式事务

1. 分布式事务概念和解决方案

什么是分布式事务?

在分布式系统中, 当一个服务调用多个服务, 或者当一个服务操作多个数据库时, 都会存在分布式事务

解决分布式事务的思路有哪些?

理论基础:
CAP定理、BASE理论

C可用性: 一个节点不可用不影响整个集群的使用 A一致性: 所有节点的数据必须一致 P分区容错: 一个节点与其他节点数据同步连接断掉, 形成独立分区

在P一定会出现的时候, A和C只能实现一个, 如何解决? 用BASE理论解决CAP

BA基本可用: 分布式系统出现故障时, 允许损失部分可用性, 即保证核心可用 S软状态: 在短时间内, 允许出现临时的不一致状态 E最终一致: 虽然无法保证强一致性, 但是在软状态结束后, 最终达到数据一致

解决思路:

  1. 合并, 把分布式事务中包含的所有本地事务, 看成一个全局事务的分支, 所有分支事务都执行成功才能提交全局事务
  2. 拆分, 使用MQ应用解耦, 把一个分布式事务拆分成多个本地事务, 通过消息进行数据传递, 只要保证消息的可靠性、幂等性, 就能保证数据最终一致, 从而解决分布式事务问题

    幂等性: 在分布式环境下, 多次操作,结果是一致的。(多次操作数据库数据是一致的。)

解决方案:

  1. 两阶段提交
  2. 两阶段提交补偿(三阶段、事务补偿)
  3. MQ消息最终一致性

2. Seata概念及其使用

概念:
Seata是阿里开源的分布式事务组件
Seata事务管理的三个角色

  • TC -事务协调者:事务协调。维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM -事务管理器:管理全局事务。定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM -资源管理器:管理分支事务。与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
  • image.png

    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并发能力?

搭建主从集群, 实现读写分离
三种集群模式:

  1. 主从:数据备份、读写分离(最少2台)
  2. 哨兵:数据备份、读写分离、故障恢复(最少5台,3台哨兵、1主1从)
  3. 分片:数据备份、读写分离、故障恢复、动态扩容(最少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)

    1. -->master判断replid, 不一致, 拒绝增量同步, 说明是第一次同步, slave发送自己的replidoffset

    阶段2: —>master执行bgsave生成RDB, 发送RDB到slave 阶段3: —>master记录RDB期间的所有操作命令在repl_baklog, 并持续发送给slave(增量同步)

    增量同步的详细流程: 阶段1: <—slave请求增量同步(携带repid和offset)

    1. -->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中数据太多?

  1. 添加数据时设置有效时间
  2. 合理设置删除策略(缓存平时维护操作,剔除无用数据)

删除策略针对设置过期时间的数据。

  • 定时:到了指定时间,执行删除过期数据操作 (特点:对CPU要求比较高)
  • 惰性:第一次只是标记删除,第二次访问才会真正删除过期数据(特点:对内存要求比较高)极端情况,可能会导致Redis中都是过期数据
  • 定期:把CPU一秒钟分成2块,750ms用于处理正常请求,250ms用于删除过期数据,既不会对CPU要求太高,也不会对内存造成太大压力
  1. 合理设置淘汰策略(缓存将满时维护操作,剔除数据)

淘汰策略针对所有数据,设置了过期时间的、没有设置过期时间的

  • 过期数据: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中, 减轻数据库压力

缓存数据同步策略

保证数据库数据、缓存数据的一致性,这就是缓存与数据库的同步, 防止缓存数据与数据库数据存在较大差异

  1. 设置有效期:给缓存设置有限期, 到期自动删除, 再次查询时更新(时效性最差,解耦)
  2. 同步双写:在修改数据库的同时, 直接修改缓存(时效性最好,耦合最高)
  3. 异步通知(常用):修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

当增删改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