大厂要求
- 技术广度
- 项目经验
- 生产经验
- 技术深度
- 系统设计
Dubbo vs Spring Cloud
Eureka: 服务注册
Feign:服务调用
Ribbon:负载均衡
Zuul/Spring Cloud Gateway: 网关,灰度发布、统一限流(每秒 1W 个请求)、统一熔断、统一降级、统一缓存、统一授权认证
Hystrix:链路追踪
Stream:
Dubbo 底层运行原理
消费者
动态代理:Proxy
负载均衡: Cluster,负载均衡,故障转移
注册中心:Registry
通信协议:Protocol,http,rmi,dubbo
信息交换:Exchange,Request 和 Response
网络通信: Transport ,netty, mina
序列化:封装好的请求如何序列化成二进制数组,通过 netty /mina 发送出去
提供者
网络通信 : Transport 基于 Netty /Mina 实现的 Server
信息交换: Exchange, Response
通信协议:Protocol
动态代理:Proxy
Dubbo 底层网络通信
Dubbo 高可扩展性
- 核心组件全部接口化,组件和组件之间的调用,必须全部依托于接口,去动态找配置的实现类,如果没有配置就用他自己默认的
- 提供一种自己实现的组件的配置的方式,比如自己实现了某个组件,配置一下,运行的时候直接找你配置的组件,作为实现类 ,不用自己默认组件
设计 RPC 框架
核心就是Dubbo里的动态代理里面的代码逻辑,网络通信、代理机制、负载均衡
Spring Cloud 底层架构原理
- Eureka
作用:服务注册与发现、心跳与故障
二级缓存,优化并发冲突,读多写少
- Feign
对接口打了一个注解,针对这个注解标注的接口生成动态代理,然后针对Feign的动态代理去调用他的方法的时候,会在底层生成 http协议格式的请求
先使用 Ribbon 去本地的 Eureka 注册表的缓存里获取出来对方机器的列表 ,进行负载均衡,选择一台机器出来 ,然后使用HttpClient 框架组件对那台机器发送 HTTP请求
- Zuul
配置一下不同的请求和服务的对应关系,你请求到了网关,他直接查找匹配的服务,然后直接把请求转发给那个服务的某台机器,Ribbon 从 Eureka 本地的缓存列表里获取一台机器,负载均衡,把请求直接用 HTTP 通信框架发送到指定机器上去。
Dubbo VS Spring Cloud
- Dubbo ,RPC性能比 HTTP性能更好,并发能力更强。Dubbo一次请求 10ms,Spring Cloud 耗费 20ms
- Dubbox 以前定位是单纯的服务框架,Spring Cloud是全家桶概念、提供分布式配置中心、授权认证、服务调用链路追踪、资源隔离、熔断降级、请求 QPS 监控
契约测试、消息中间件封装、Zk封装
- Spring Cloud Alibaba, 技术相互融合
注册中心调研
- Eureka
- ZooKeeper
- Consul
- Nacos
Eureka 集群架构原理
peer-to-peer,部署一个集群,但是集群里每个机器的地方是对等的,各个服务可以向任何一个 Eureka实例服务注册和服务发现,集群里任何一个 Eureka 实例接收到写请求之后,会自动同步给其他所有的 Eureka 实例
ZooKeeper 集群架构原理
Leader + Follower 2 种角色,只有Leader 可以负责写(服务注册),他可以把数据同步给 Follower ,读的时候 Leader /Follower 都可以读
CAP
C 一致性,A 可用性,P 分区容错性
ZooKeeper 采用 CP,选举期间服务不可用
Eureka 采用 AP和最终一致性
服务注册发现时效性
- ZooKeeper 时效性更好,注册或者挂了,一般秒级就能感知到
- Eureka 默认配置非常糟糕,服务发现感知要几十秒,甚至分钟级别,上线一个新的服务实例,到其他人可以发现他,极端情况下要 1 分钟的时间,Ribbon 同时也需要间隔时间才会更新它自己的缓存。
服务故障,间隔 60 秒才去检查心跳,发现这个服务上一次心跳是 60 秒之前,隔 60 秒取检查心跳,超过 90 秒认为没有心跳,才会认为他死了,已经 2 分钟过去了,30 秒才会更新缓存,30 秒,其他服务才会来拉最新的注册表
容量
- zk 不适合大规模的服务实例,因为服务上下线的时候,需要瞬间推送数据通知到所有的其他服务实例,所以一旦服务规模太大,到了几千个服务实例的时候,会导致网络带宽会大量占用
- eureka也很难支撑大规模的服务实例,因为每个 eureka实例都要接受所有的请求,实例多了压力太大,扛不住,也很难到几千服务实例
注册中心高可用
eureka 集群部署
服务发现过慢
- eureka 里配置 ReadWrite 同步到 ReadOnly 缓存的间隔时间
eureka.server.responseCacheUpdateIntervalMs = 3000
- 服务里配置拉取注册中心配置的间隔时间
eureka.client.registryFetchIntervalSeconds = 3000
- 服务里配置心跳间隔的时间
eureka.client.leaseRenewalIntervalInSeconds: 30
- eureka里线程检查心跳的间隔时间
eureka.server.evictionIntervalTimerInMs = 6000
- eureka里配置心跳过期时间,这条记录就会从注册表中删掉,然后ReadWrite缓存就会淘汰掉
eureka.instance.leaseExpirationDurationInSeconds: 6
优化成12 秒内可以感受到节点上线、下线
- 关闭 eureka 的自我保护 (当突然大面积服务都没心跳了,eureka 会保护注册表不下线这些服务,源码层很多bug)
eureka.server.enableSelfPreservation: false
生产环境中的注册中心
- eureka 配置 8核 16G,16 核 32G,每台机器每秒钟的请求支撑几千绝对没问题,可以支撑上千个服务
- 时效性优化
网关
- 动态路由:新开发某个服务,动态把请求路径和服务的映射关系热加载到网关里去,服务增减机器,网关自动热感应
- 灰度发布:
- 鉴权认证
- 性能监控:每个API 接口的耗时、成功率、QPS
- 系统日志
- 数据缓存
- 限流熔断:控制 1 秒钟 1000 个请求
网关调研
- Kong
Nginx里面的一个基于 lua 写的模块,实现了网关的功能
- Zuul
Spring Cloud
- OpenResty
Nginx + lua
- 自研网关
Servlet + Netty来做网关
中小型公司:
SpringCloud体系用Zuul,如果是Dubbo,有的采用Kong;或者 nginx+负载均衡
大厂:
自研
Zuul(Servlet,Java)高并发能力不强,基于Tomcat部署把网关跑起来,Java语言开发,可以把控源码做二次开发
Nginx(Kong,Nginx+Lua)抗高并发能力很强,精通Nginx源码很难,很难从Nginx内核层面去做一些二次开发和源码定制
网关对服务动态路由
读取数据库(Redis ,ZooKeeper, 阿波罗)路由配置以及定时任务刷新
网关优化
每秒 10W 请求 ,一个 zuul 在 8核 16 上可以抗住 几千+ 请求,几十台 zuul 可以抗住 10W+
16 核 32G 的,一台抗住小几万,几台 zuul 就可以抗住 10W+
自研服务注册中心
eureka :peer to peer 每台机器都是高并发请求,有瓶颈
zookeeper: 上下线全量通知其他服务,网络带宽被打满,有瓶颈
分布式注册中心,分片存储服务注册表,横向扩容, 每天机器均摊高并发请求,各个服务按需主动拉取 ,避免反向通知网卡被打满,Master-Slave 高可用性,Master写到Slave后才算写成功,强一致性
网关灰度发布
eureka.instance.meta-data-map.version: new
RibbonFilterContextHolder
生产环境部署
注册中心
中小型公司 20~30 个服务,注册中心 2 ~3 台机器(4 核 8G),每秒抗上千请求
注册中心优化服务注册和发现配置
注册表多级缓存同步 1 秒,注册表拉取频率 1 秒
服务心跳 1 秒上报一次,服务故障发现 1 秒,发现 2 秒内服务没上报心跳,就故障
服务
每秒并发在1000 以内,每个服务部署 2 台机器,每台机器 4 核 8G,每台机器抗几百请求一点问题都没
大部分系统高峰期每秒几百请求,低峰期每秒几十请求,几个请求
网关系统
4 核 8G,一台机器抗每秒几百请求,部署 3~4 台
数据库
16 核 32G,物理机,高峰期每秒几千(三四千的时候,网络负载比较高,CPU 使用率比较高,I/O 负载比较高)请求问题不大,
QPS
metrics 机制,利用 AtomicLong 算出核心接口每分钟调用多少次(除以 60 就是高峰期每秒访问次数)以及每天被调用总次数
TP99 = 100ms , 99%的接口耗时在 100ms以内,1%的接口耗时在100ms以上
平均响应延时 = (每次调用的耗时 + 历史总耗时 )/ 当前请求的总次数
最大 QPS:
假设最大 QPS 为 800 ,当压测工具每秒发起 1000 个请求的时候,只有 800个可以同时被处理,200 个在排队被阻塞住
系统访问量增加 10 倍
- 网关多部署 10 倍机器
- 服务多加机器
- Eureka 换成 8 核 16G (抗住上千请求很轻松)
- 数据库换成32 核 128G (每秒抗住几千请求问题不大)
超时和重试
- 第一次请求的时候,会初始化 Ribbon 组件,需要耗费一定的时间 ,所以很容易导致超时。(改进:让每个服务启动的时候直接初始化 Ribbon 组件 )
ribbon.eager-load.enabled: true , zuul.ribbon.eager-load.enabled: true
- 关闭 hystrix (性能)
feign.hystrix.enabled:false
- 设置超时时间
connectTimeout:1000
ReadTimeout:1000
OKToRetryOnAllOperations:true
MaxAutoRetries:1
MaxAutoRetriesNextServer:1
防重幂等性
- 插入操作幂等性
数据库建立唯一索引
- 更新操作幂等性
在少数核心接口内,根据业务逻辑做幂等性,业务执行成功后,生成一个唯一的key(order_id_stock_deduct)到redis里,下一次进来的时候,如果发现重复key,则执行反向操作
分布式事务
XA、TCC、可靠消息最终一致性方案、最大努力通知方案、Sega
TCC
Try-Confirm-Cancel 每个人先走 try ,有人失败了就会让大家都走 Cancel, 如果Try都成功了,会让大家调用Confirm
库存接口拆分成 1. 扣减库存接口 2.回滚扣减库存接口 由分布式事务框架通知已经执行成功的服务的回滚接口,保证回滚掉。数据要么全部成功、要么全部失败。
技术选型
- TCC 框架有 ByteTCC ,Himly (个人),seata (阿里开源)
- 可靠消息最终一致性方案 基于ActiveMq封装可靠消息服务、基于RabbitMQ自己开发一个可靠消息服务,RocketMQ提供分布式事务支持,实现了可靠消息服务功能
TCC 核心架构
- TC TM RM
- TM 请求 TC 要开启一个新的分布式事务,TC 生成一个 XID
- XID 通过服务调用传递下去
- RM 注册本地事务作为一个此次分布式事务的一个分支事务到 TC 上
- TM 发现全部执行成功,则通知 TC 进行全局提交,此时 TC 通知每个分支事务,可以提交了
如果 TM 发现有错误(某个分支事务未执行成功,把异常一层一层抛到最外层),则通知 TC 进行回滚,TC 会通知所有分支事务进行回滚
TCC 高并发场景
- 每个服务都需要跟 TC 这个角色进行频繁的网络通信,带来开销,不引入分布式事务可能 100ms,引入了可能200ms
- 上报分支事务状态给 TC,其中seata-server 基于文件存储,会更耗费网络请求
- TC ,seata-server 也要支持扩容,部署多台机器 ,TC 背后的数据库要分库分表
RocketMQ对事务的支持
MQ:抗高并发 、削峰、解耦
自己实现 RocketMQ 对事务的支持
分布式锁
解决库存为-5 的问题
分布式锁解决库存问题
Redis 分布式锁
Redisson 框架
redisson.lock("product_1_stock")
product_1_stock:{
"87ff-dsfsfs-121212-sfsfsfs :1":1
}
生存时间:30s
watchdog,redisson 框架后台执行一段逻辑,每隔10s去检查一下这个锁是否还被当前客户端持有,如果是的话,重新刷新一下 key的生存时间为 30s
其他客户端尝试加锁,这个时候发现“product_1_stock” 这个 key已经存在了,里面显示被别的客户端加锁了,此时他就会陷入一个无限循环,阻塞住自己,不能干任何事情,必须在这里等待。
第一个客户端加锁成功,此时有 2 种情况,1)这个客户端操作完毕之后,主动释放锁(将 UUID 线程 ID 对应的值 -1 ,当加锁次数未 0 的时候,删除 key ,否则刷新 key的生存周期 30s) 2)如果这个客户端宕机了,那么和这个客户端的 redisson框架之前启动的后台 watchdog线程就没了,此时最多30s,这个key-value就消失了,自动释放了宕机客户端之前持有的锁
集群故障会导致锁失效?
会,除非是修改 redis和 redisson框架源码,二次开发,加锁必须是 master 和 slave 同时写成功了,才算是加锁成功。
ZooKeeper 分布式锁
curator,基于 zk 实现了一整套高级功能
ZooKeeper 羊群效应
如果几十个客户端同时争抢一个锁,此时会导致任何一个客户端释放锁的时候 ,zk要反向通知几十个客户端,几十个客户端又要发送请求到 zk 去尝试创建锁,几十个人要加锁,大家都是乱糟糟的,无序的。造成很多没必要的请求和网络开销。其实只需要下一个顺序节点收到通知去加锁
ZooKeeper 脑裂
因为ZooKeeper与客户端 A 之间网络发生问题,ZooKeeper 误认为客户端 A 挂掉了,此时删掉客户端 A 创建的临时节点,此时 A 还在运行,B 客户端监听到了 A 客户端的节点被删掉,以为获取到锁,加锁成功,此时分布式锁就失效了。
分布式系统,主控节点有一个 Master,此时因为网络故障,导致其他人以为这个 Master 不可用了,其他节点出现了别的Master,导致集群里有 2 个Master同时在运行。
解决方案:修改curator框架源码,加一些复杂协调机制
Redis VS ZooKeeper 分布式锁
从分布式系统协调语义而言,ZooKeeper 做分布式锁(选举、监听)更好一些,因为Redis 其实是缓存,但是Redis能抗高并发,高并发场景更好一些。
ZooKeeper 本身不适合部署大规模集群,他本身适用的场景就是部署三五台机器,不是承载高并发请求的,仅仅用作分布式系统协调的。
分布式锁高并发优化
Redis 单机抗住几万(1,2W)并发轻松
优化策略:分段加锁
比如:苹果库存有 10000 个,此时在数据库中创建 10 个库存字段,stock_01,stock_02..stock_10 ,每个库存字段里放 1000 个库存,此时这个库存的分布式锁,对应 10 个 key, product_1_stock_01, product_1_stock_02,product_1_stock_03 ,product_1_stock_10,请求过来之后,从 10个 key随机选择一个key,去加锁。每秒 1W 个请求过来,此时他们会对 10 个库存分段 key 加锁,每个 key就 1000 个请求,每台服务器也就 1000 个请求而已。
万一说每个库存分段仅仅剩余 10 个库存了,此时我下订单买 20 个苹果,合并扣减库存。对 product_1_stock_5 加锁了,此时库存只有 10 个,不够买 20 个苹果,可以尝试去 product_1_stock_1,再查询他的库存可能 有 30 个,此时下单就可以,锁定库存的时候,对 product_1_stock_5锁定 10 个库存,对product_1_stock_1锁定 10 个库存,一共锁定 20 个库存。
淘宝京东库存扣减方案
大公司一般有分布式 K-V 存储,tair,redis,mongodb,高并发,每秒几万几十万都没问题,甚至每秒百万。
实时库存数据放 K-V 存储里去,你在操作库存的时 候,直接扣减(不用先查库再扣减),如果你发现扣减之后是负数的话,此时就认为库存超卖了,回滚刚才的扣减,返回提示给用户,库存不足,不能下订单了。对 K-V 库存每次修改写到 MQ 里,异步同步到数据库,相当于异步双写,用分布式 K-V 抗高并发,做好一致性方案。
消息队列连环炮
- 项目里怎么样使用 MQ 的?
订单系统每次下一个新的订单的时候,会发送一条 MQ 消息,后台有个库存系统负责获取了消息然后更新库存。
- 为什么要使用消息队列?
- 消息队列有什么优点和缺点?
- kafka,activemq,rabbitmq,rocketmq 都有什么去呗?
- 如何保证消息队列高可用?
- 如何保证消息不被重复消费?
- 如何保证消息的可靠性传输?
- 如何保证消息的顺序性?
- 写一个消息队列架构设计?
面试官思路:
从做过的某一个点切入,然后层层展开深入考察,一个接一个问,直到把这个技术点刨根问底,问到最底层。
消息队列技术选型
解决的问题:
- 解耦
- 异步
- 削峰
不用 MQ 系统耦合场景
- A 系统产生了一个比较关键的数据,很多系统需要 A 系统将数据发过来,强耦合(B,C,D,E 系统可能参数不一样、一会需要一会不需要数据,A 系统要不断修改代码维护)
- A 系统还要考虑 B、C、D、E 系统是否挂了,是否访问超时?是否重试?
使用 MQ 系统解耦场景
- 维护这个代码,不需要考虑人家是否调用成功,失败超时
- 如果新系统需要数据,直接从 MQ 里消费即可,如果某个系统不需要这条数据就取消对 MQ 消息的消费即可。
总结:通过一个 MQ 的发布订阅消息模型(Pub/Sub), 系统 A 跟其他系统就彻底解耦了。
不用 MQ 同步高延迟请求场景
一般互联网类的企业,对用户的直接操作,一般要求每个请求都必须在 200ms以内,对用户几乎是无感知的。
使用 MQ 进行异步化之后的接口性能优化
提高高延时接口
没有用 MQ 时高峰期系统被打死的场景
高峰期每秒 5000 个请求,每秒对 MySQL 执行 5000 条 SQL(一般MySQL每秒 2000 个请求差不多了),如果MySQL被打死,然后整个系统就崩溃,用户就没办法使用系统了。但是高峰期过了之后,每秒钟可能就 50 个请求,对整个系统没有任何压力。
使用 MQ 进行削峰的场景
5000 个请求写入到 MQ 里面,系统 A 每秒钟最多只能处理 2000 个请求(MySQL 每秒钟最多处理 2000 个请求),系统 A 从 MQ 里慢慢拉取请求,每秒钟拉取 2000 个请求。MQ,每秒钟 5000 个请求进来,结果只有 2000 个请求出去,结果导致在高峰期(21小时),可能有几十万甚至几百万的请求积压在 MQ 中,这个是正常的,因为过了高峰期之后,每秒钟就 50 个请求,但是系统 A 还是会按照每秒 2000 个该请求的速度去处理。只要高峰期一过,系统 A 就会快速的将积压的消息给解决掉。(算一笔账,每秒积压在 MQ 里消息有 3000 条,一分钟就会积压 18W 条消息,一个小时就会积压 1000 万条消息。等高峰期一过,差不多需要 1 个多小时就可以把 1000W 条积压的消息给处理掉)
架构中引入 MQ 后存在的问题
- 系统可用性降低
MQ 可能挂掉,导致整个系统崩溃
- 系统复杂性变高
可能发重复消息,导致插入重复数据;消息丢了;消息顺序乱了; 系统 B,C,D 挂了,导致 MQ 消息积累,磁盘满了;
- 一致性问题
本来应该A,B,C,D 都执行成功了再返回,结果A,B,C 执行成功 D 失败
Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级(一秒1W~2W 左右请求) | 万级 | 十万级 | 十万级 |
时效性 | ms级 | 微秒级,这个是rabbitmq一大特点,延迟最低的 | ms级 | ms级以内 |
可用性 | 高,基于主从架构高可用性 | 高,基于主从架构高可用性 | 非常高,分布式架构 | 非常高,Kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低概率丢失数据 | 经过参数优化配置,可以做到 0 丢失 | 经过参数优化配置,消息可以做到 0 丢失 | |
优劣势总结 | 优点:非常成熟,功能强大,在业内大量的公司和项目都有应用。缺点:偶尔会有较低概率丢失消息,而现在社区以及国内应用越来越少,官方社区对ActiveMQ 5.x维护越来越少,而且确实主要是基于解耦和异步来用,较少在大规模吞吐的场景中使用 | 优点:erlang语言开发,性能极其好,延时很低,管理界面非常棒,社区活跃 缺点:RabbitMQ确实吞吐量会低一些(单机几万),这个是因为他的实现机制比较重。而且 erlang 开发,国内有几个实力做 erlang源码级别的研究和定制?缺乏掌控,依赖开源社区的维护和修复bug。而且 RabbitMQ集群动态扩展会很麻烦,其实主要是 erlang语言本身带来的问题,很难读源码,很难定制和掌控 | 优点:接口简单易用,阿里保障,日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都 OK,还可以支撑大规模的topic数量,支持复杂 MQ 业务场景,源码是 java,方便公司定制和掌控 缺点:社区活跃一般,接口不是按照标准的 JMS 规范走的,有些系统迁移需要修改大量代码,阿里出台的技术,有可能这个技术被抛弃。 | 优点:提供较少的核心功能,但是提高超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展,Kafka最好是支撑较少的topic数量来保证极高的吞吐量。缺点:有可能消息重复消费,会对数据准确性造成影响,大数据领域中以及日志采集,这点影响可以忽略,天然适合大数据实时计算以及日志收集 |
建议:中小型公司 RabbitMQ 大公司:RocketMQ 大数据实时计算:Kafka
消息队列高可用
RabbtitMQ 高可用
RabbitMQ有三种模式:单机模式 、普通集群模式、镜像集群模式
- 单机模式
demo级
- 普通集群模式(非高可用)
队列的元数据存在于多个实例中,但是消息不存在多个实例中,每次
多台机器上启动多个 rabbitmq 实例,每个机器启动一个。
优点:可以多个机器消费消息,可 以提高消费的吞吐量
缺点:可能会在 rabbitmq 内部产生大量的数据传输 ;可用性基本没保障,queue 所在机器宕机,就没办法消费了
没有高可用性可言
- 镜像集群模式(高可用,非分布式)
队列的元数据和消息都会存在于多个实例中,每次写消息到 queue的时候,都会自动把消息到多个实例的 queue 里进行消息同步。也就 是每个节点上都有这个 queue 的一个完整镜像(这个 queue的全部数据)。任何一个节点宕机了,其他节点还包含这个 queue的完整数据,其他 consumer 都可以到其他活着的节点上去消费数据都是 OK 的。缺点:不是分布式的,如果这个 queue的数据量很大,大到这个机器上的容量无法容纳 。
开启镜像集群模式方法: 管理控制台,Admin页面下,新增一个镜像集群模式的策略,指定的时候可以要求数据同步到所有节点,也可以要求同步到指定数量的节点,然后你再次创建 queue 的时候 ,应用这个策略,就 会自动将数据同步到其他的节点上去。
- Kafka 高可用架构
broker进程就是kafka在每台机器上启动的自己的一个进程。每台机器+机器上的broker进程,就可以认为是 kafka集群中的一个节点。
你创建一个 topic,这个topic可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition就存放一部分数据。
这就是天然的分布式消息队列,也就是说一个 topic的数据,是分散放在 多个机器上的,每个机器就放一部分数据。
分布式的真正含义是每个节点只放一部分数据,而不是完整数据(完整数据就是HA、集群机制)
Kafka 0.8版本之前是没有 HA 机制的,任何一个 broker 宕机了,那么就缺失一部分数据。
Kafka 0.8以后,提供了 HA 机制,就是 replica 副本机制。每个 partition的数据都会同步到其他机器上,形成自己的多个 replica 副本。然后所有 replica 会选举一个 leader。那么生产者、消费者都会和这个 leader 打交道,然后其他 replica 就是 follow。写的时候,leader 负责把数据同步到所有 follower上去,读的时候就直接读 leader 上的数据即可。如果某个 broker宕机了,刚好也是 partition的leader,那么此时会选举一个新的 leader出来,大家继续读写那个新的 leader即可,这个就 是所谓的高可用性。
leader和follower的同步机制:
写数据的时候,生产者就写 leader,然后 leader将数据落地写本地磁盘,接着其他 follower 自己主动从 leader来pull数据。一旦所有 follower同步好数据了,就会发送 ack给 leader,leader收到所有 follower的 ack之后,就会返回写成功的消息给生产者。
消费的时候,只会从 leader去读,但是只有一个消息已经被所有 follower都同步成功返回 ack的时候,这个消息才会被消费者读到。
消息队列重复数据
MQ 只能保证消息不丢,不能保证重复发送
Kafka 消费端可能出现的重复消费问题
每条消息都有一个 offset 代表 了这个消息的顺序的序号,按照数据进入 kafka的顺序,kafka会给每条数据分配一个 offset,代表了这个是数据的序号,消费者从 kafka去消费的时候,按照这个顺序去消费,消费者会去提交 offset,就是告诉 kafka已经消费到 offset=153这条数据了 ;zk里面就记录了消费者当前消费到了 offset =几的那条消息;假如此时消费者系统被重启,重启之后,消费者会找kafka,让kafka把上次我消费到的那个地方后面的数据继续给我传递过来。
重复消息原因:(主要发生在消费者重启后)
消费者不是说消费完一条数据就立马提交 offset的,而是定时定期提交一次 offset。消费者如果再准备提交 offset,但是还没提交 offset的时候,消费者进程重启了,那么此时已经消费过的消息的 offset并没有提交,kafka也就不知道你已经消费了 offset= 153那条数据,这个时候kafka会给你发offset=152,153,154的数据,此时 offset = 152,153的消息重复消费了
保证 MQ 重复消费幂等性
幂等:一个数据或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
思路:
- 拿数据要写库,首先检查下主键,如果有数据,则不插入,进行一次update
- 如果是写 redis,就没问题,反正每次都是 set ,天然幂等性
- 生产者发送消息的时候带上一个全局唯一的id,消费者拿到消息后,先根据这个id去 redis里查一下,之前有没消费过,没有消费过就处理,并且写入这个 id 到 redis,如果消费过了,则不处理。
- 基于数据库的唯一键
保证 MQ 消息不丢
MQ 传递非常核心的消息,比如:广告计费系统,用户点击一次广告,扣费一块钱,如果扣费的时候消息丢了,则会不断少钱,积少成多,对公司是一个很大的损失。
RabbitMQ可能存在的数据丢失问题
- 生产者写消息的过程中,消息都没有到 rabbitmq,在网络传输过程中就丢了。或者消息到了 rabbitmq,但是人家内部出错了没保存下来。
- RabbitMQ 接收到消息之后先暂存在主机的内存里,结果消费者还没来得及消费,RabbitMQ自己挂掉了,就导致暂存在内存里的数据给搞丢了。
- 消费者消费到了这个消费,但是还没来得及处理,自己就挂掉了,RabbitMQ 以为这个消费者已经处理完了。
问题 1解决方案:
事务机制:(一般不采用,同步的,生产者发送消息会同步阻塞卡住等待你是成功还是失败。会导致生产者发送消息的吞吐量降下来)
channel.txSelect
try {
//发送消息
} catch(Exception e){
channel.txRollback;
//再次重试发送这条消息
}
channel.txCommit;
confirm机制:(一般采用这种机制,异步的模式,不会阻塞,吞吐量会比较高)
- 先把 channel 设置成 confirm 模式
- 发送一个消息到 rabbitmq
- 发送完消息后就不用管了
- rabbitmq 如果接收到了这条消息,就会回调你生产者本地的一个接口,通知你说这条消息我已经收到了
- rabbitmq 如果在接收消息的时候报错了,就会回调你的接口,告诉你这个消息接收失败了,你可以再次重发。
public void ack(String messageId){
}
public void nack(String messageId){
//再次重发一次这个消息
}
问题 2 解决方案:
持久化到磁盘
- 创建queue的时候将其设置为持久化的,这样就可以保证 rabbitmq持久化queue的元数据,但是不会持久化queue里的数据
- 发送消息的时候将 deliveryMode 设置为 2,将消息设置为持久化的,此时 rabbitmq就会将消息持久化到磁盘上去。必须同时设置 2 个持久化才行。
- 持久化可以跟生产者那边的 confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack了 ,所以哪怕是在持久化到磁盘之前 ,rabbitmq挂了,数据丢了,生产者收不到 ack,你也可以自己重发。
缺点:可能会有一点点丢失数据的可能,消息刚好写到了 rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧, rabbitmq挂了,会导致内存里的一点点数据会丢失。
问题 3 解决方案:
原因:消费者打开了 autoAck机制(消费到一条消息,还在处理中,还没处理完,此时消费者自动 autoAck了,通知 rabbitmq说这条消息已经消费了,此时不巧,消费者系统宕机了,那条消息丢失了,还没处理完,而且 rabbitmq还以为这个消息已经处理掉了)
解决方案:关闭 autoAck,自己处理完了一条消息后,再发送 ack给 rabbitmq,如果此时还没处理完就宕机了,此时rabbitmq没收到你发的ack消息,然后 rabbitmq 就会将这条消息重新分配给其他的消费者去处理。
Kafka 可能存在的数据丢失问题
- 消费端弄丢数据
原因:消费者消费到那条消息后,自动提交了 offset,kafka以为你已经消费好了这条消息,结果消费者挂了,这条消息就丢了。
例子:消费者消费到数据后写到一个内存 queue里缓存下,消息自动提交 offset,重启了系统,结果会导致内存 queue 里还没来得及处理的数据丢失。
解决方法:kafka会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交,可以保证数据不会丢。但是此时确实还是会重复消费,比如刚好处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次 ,做好幂等即可。
- Kafka 丢掉消息
原因:kafka 某个 broker 宕机,然后重新选举 partition 的 leader时,此时其他的 follower 刚好还有一些数据没有同步,结果此时 leader挂了,然后选举某个 follower成 leader之后,就丢掉了之前leader里未同步的数据。
例子:kafka的leader机器宕机,将 follower 切换为 leader之后,发现数据丢了
解决方案:(保证 kafka broker端在 leader发生故障,或者leader切换时,数据不会丢)
- 给 topic设置 replication.factor ,这个值必须大于 1,保证每个 partition 必须至少有 2 个副本
- 在 kafka 服务端设置 min.insync.replicas 参数,这个值必须大于 1,这个是要求一个 leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保 leader挂了还有一个follower,保证至少一个 follower能和leader保持正常的数据同步。
- 在 producer 端设置 acks =all,这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。否则会生产者会一直重试,此时设置 retries = MAX(很大的重试的值),要求一旦写入失败,就卡在这里(避免消息丢失)
- kafka 生产者丢消息
按 2 的方案设置了 ack =all,一定不会丢。它会要求 leader 接收到消息,所有的 follower 都同步 到了消息之后,才认为本次写成功。如果没满足这个条件,生产者会无限次重试 。
消息队列顺序性
背景:mysql binlog 同步的系统,在mysql里增删改一条数据,对应出来了增删改 3 条binlog,接着这 3 条binlog发送到 MQ 里面,到消费出来依次执行,起码是要保证顺序的吧,不然顺序变成了 删除、修改、增加。日同步数据达到上亿,mysql->mysql,比如大数据 team,需要同步一个mysql库,来对公司的业务系统的数据做各种复杂的操作。
场景:
- rabbitmq,一个queue,多个consumer,这不明显乱了
- kafka,一个topic,一个partition,一个consumer,内部多线程,这不也乱了
RabbitMQ 消息顺序错乱
RabbitMQ 如何保证消息顺序性
需要保证顺序的数据放到同一个queue里
Kafka 消息顺序错乱
写入一个 partition中的数据一定是有顺序的。
生产者在写的时候,可以指定一个 key,比如订单id作为key,那么订单相关的数据,一定会被分发到一个 partition中区,此时这个 partition中的数据一定是有顺序的。Kafka 中一个 partition 只能被一个消费者消费。消费者从partition中取出数据的时候 ,一定是有顺序的。
Kafka 保证消息顺序性
如果消费者单线程消费+处理,如果处理比较耗时,处理一条消息是几十ms,一秒钟只能处理几十条数据,这个吞吐量太低了。肯定要用多线程去并发处理,压测消费者4 核 8G 单机,32 条线程,最高每秒可以处理上千条消息
消息队列延迟以及过期失效
消费端出了问题,不消费了或者消费极其慢。接着坑爹了,你的消息队列集群的磁盘都快写满了 ,都没人消费,怎么办?积压了几个小时,rabbitmq设置了消息过期时间后就没了,怎么办?
例如:每次消费之后都要写 mysql,结果mysql挂了,消费端 hang 不动了。
消费者本地依赖的一个东西挂了,导致消费者挂了。
长时间没处理消费,导致 mq 写满了。
场景:几千万条数据再 MQ 里积压了七八个小时
快速处理积压的消息
一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟是 18W 条,1000 多 W 条需要一个小时恢复。
步骤:
- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有的 consumer 都停掉
- 新建一个topic,partition是原来的 10 倍,临时建立好原先 10 倍或者 20 倍的 queue 数量
- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue
- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据
- 这种做法相当 于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常 10 倍速度
- 等快速消费完积压数据之后,恢复原先部署架构 ,重新用原先的 consumer机器消费消息
原来 3 个消费者需要 1 个小时可以搞定,现在 30 个临时消费者需要 10 分钟就可以搞定。
如果用的 rabbitmq,并且设置了过期时间,如果此消费在 queue里积压超过一定的时间会被 rabbitmq清理掉,数据直接搞丢。
这个时候开始写程序,将丢失的那批 数据查出来,然后重新灌入mq里面,把白天丢的数据补回来。
如果消息积压mq,长时间没被处理掉,导致mq快写完满了,你临时写一个程序,接入数据来消费,写到一个临时的mq里,再让其他消费者慢慢消费 或者消费一个丢弃一个,都不要了,快速消费掉所有的消息,然后晚上补数据。
如何设计消息队列中间件架构
- mq要支持可伸缩性,快速扩容。设计一个分布式的 MQ,broker->topic->partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够,给 topic 增加 partition ,然后做数据迁移,增加机器。
- mq数据落磁盘,避免进程挂了数据丢了,顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这个就是 kafka的思路。
- mq高可用性。多副本->leader & follower-> broker 挂了重新选举 leader 对外提供服务
- 支持数据 0 丢失。
分布式搜索引擎
es,
lucene底层的原理是倒排索引
分布式缓存
缓存好处:高性能 + 高并发
高性能(常用)
数据库查询耗费了800ms,其他用户对同一个数据再次查询 ,假设该数据在10分钟以内没有变化过,并且 10 分钟之内有 1000 个用户 都查询了同一数据,10 分钟之内,那 1000 个用户,每个人查询这个数据都感觉很慢 800ms
比如 :某个商品信息,在 一天之内都不会改变,但是这个商品每次查询一次都要耗费2s,一天之内被浏览 100W次
mysql 单机也就 2000qps,缓存单机轻松几万几十万qps,单机 承载并发量是 mysql 单机的几十倍。
高并发
在中午高峰期,有 100W 个用户访问系统 A,每秒有 4000 个请求去查询数据库,数据库承载每秒 4000 个请求会宕机,加上缓存后,可以 3000 个请求走缓存 ,1000 个请求走数据库。
缓存是走内存的,内存天然可以支撑4w/s的请求,数据库(基于磁盘)一般建议并发请求不要超过 2000/s
缓存不良后果
- 缓存与数据库双写不一致
- 缓存雪崩
- 缓存穿透
- 缓存并发竞争
Redis 线程模型
redis 单线程 ,memcached 多线程
redis 是单线程 nio 异步线程模型
Redis 和 Memcached 区别
- Redis 支持服务器端的数据操作:Redis比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络 IO 的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能支持更复杂的结构和操作,那么Redis会是不错的选择
- 集群模式:memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据,但是 Redis 目前是原生支持 cluster模式的
Redis 单线程模型
一个线程+一个队列
redis 基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler,这个文件事件处理器是单线程的,所以redis 是单线程的模型,采用 io多路复用机制同时监听多个 socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。
文件事件处理器包含:多个 socket,io多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器)
文件事件处理器是单线程的,通过 io 多路复用机制监听多个 socket,实现高性能和线程模型简单性
被监听的 socket 准备好执行 accept,read,write,close等操作的时候,会产生对应的文件事件,调用之前关联好的时间处理器处理
多个 socket并发操作,产生不同的文件事件,i/o多路复用会监听多个socket,将这些 socket放入一个队列中排队。时间分派器从队列中取出socket给对应事件处理器。
一个socket时间处理完后,时间分派器才能从队列中拿到下一个socket,给对应事件处理器来处理。
文件事件:
AE_READABLE 对应 socket变得可读(客户端对redis执行 write操作)
AE_WRITABLE 对应 socket 变得可写(客户端对 redis执行 read操作)
I/O 多路复用可以同时监听AE_REABLE和 AE_WRITABLE ,如果同时达到则优先处理 AE_REABLE 时间
文件事件处理器:
连接应答处理器 对应 客户端要连接 redis
命令请求处理器 对应 客户端写数据到 redis
命令回复处理器 对应 客户端从 redis 读数据
流程:
- redis 初始化时,会将连接应答处理器跟 AE_READABLE事件关联
- 客户端对 redis 发起连接,产生一个 AE_READABLE 事件
- 连接应答处理器处理客户端 AE_READABLE 事件,创建客户端对应的 socket,同时将这个 socket的 AE_READABLE 事件和命令请求处理器关联
- 客户端对 redis 发起读请求,会在 socket上产生一个 AE_READABLE 事件
- 绑定 AE_READABLE 事件的命令请求处理器会从 socket 中读取请求相关数据,执行对应操作,当执行完毕后,将 socket的 AE_WRITABLE 事件跟命令回复处理器关联
- 当客户端这边准备好读取响应时,会在 socket上产生一个AE_WRITABLE事件
- 绑定 AE_WRITABLE 事件的命令回复处理器将准备好的响应数据写入 socket,供客户端来读取
- 命令回复处理器写完后,删掉 socket的 AE_WRITABLE 事件和命令回复处理器的绑定关系
Redis 单线程模型效率高
一秒钟可以处理几万个请求
- 非阻塞 I/O 多路复用机制(不处理事件,只轮询请求压入队列)
- 纯内存操作(操作只有几微秒)
- 单线程反而 避免了多线程频繁上下文切换的问题
Redis 数据类型
- string
普通的 set,get kv缓存
- hash
类型 map结构,比如一个对象(没有嵌套对象)缓存到 redis里面,然后读写缓存的时候,可以直接操作hash的字段(比如把 age 改成 21,其他的不变)
key=150
value = {
"id":150,
"name":"zhangsan",
"age":20
}
- list
有序列表 ,元素可以重复
可以通过 list 存储一些列表型数据结构,类似粉丝列表,文章评论列表。
例如:微信大 V的粉丝,可以以 list 的格式放在 redis 里去缓存
key=某大 V value=[zhangsan,lisi,wangwu]
比如 lrange 可以从某个元素开始读取多少个元素,可以基于 list 实现分页查询功能,基于 redis实现高性能分页,类似微博下来不断分页东西。
可以搞个简单的消息队列,从 list头怼进去(lpush),list尾巴出来 (brpop)
- set
无序集合,自动去重
需要对一些数据快速全局去重,(当然也可以基于 HashSet,但是单机)
基于 set 玩差集、并集、交集的操作。比如:2 个人的粉丝列表整一个交集,看看 2 个人的共同好友是谁?
把 2 个大 V 的粉丝都放在 2 个 set中,对 2 个 set做交集(sinter)
- sorted set
排序的 set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序
排行榜:
- 将每个用户以及其对应的分数写入进去
zadd board score username
- zrevrange board 0 99 可以获取排名前 100 的用户
- zrank board username 可以看到用户在排行榜里的排名
例如:
zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lis
zadd board 62 zhaoliu
自动排序为:
96 lisi
85 zhangsan
72 wangwu
62 zhaoliu
获取排名前 3 的用户 : zrevrange board 0 3
96 lisi
85 zhangsan
72 wangwu
查看zhaoliu的排行 :zrank board zhaoliu 返回 4
Redis 过期策略
内存是宝贵的,磁盘是廉价的
给key设置过期时间后,redis对这批key是定期删除+惰性删除
定期删除:
redis 默认每隔 100ms随机抽取一些设置了过期时间的 key,检查其是否过期了,如果过期就删除。
注意:redis是每隔100ms随机抽取一些 key来检查和删除,而不是遍历所有的设置过期时间的key(否则CPU 负载会很高,消耗在检查过期 key 上)
惰性删除:
获取某个key的时候, redis 会检查一下,这个key如果设置了过期时间那么是否过期,如果过期了则删除。
如果定期删除漏掉了许多过期key,然后你也没及时去查,也没走惰性删除,如果大量过期的key堆积在内存里,导致 redis 内存块耗尽,则走内存淘汰机制。
内存淘汰策略:
- noeviction:当内存不足以容纳新写入数据时,新写入操作直接报错(没人用)
- allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用)
- allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,(没人用)
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(不合适)
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除(不合适)
LRU 算法:
package com.mousycoder.mycode;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @version 1.0
* @author: mousycoder
* @date: 2019/10/31 17:55
*/
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int CACHE_SIZE;
public LRUCache( int cacheSize) {
super((int)Math.ceil(cacheSize / 0.75) + 1 ,0.75f,true);
this.CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > CACHE_SIZE;
}
public static void main(String[] args) {
LRUCache<Integer,Integer> lruCache = new LRUCache<>(10);
for (int i = 0; i < 15; i++) {
lruCache.put(i,i);
}
Integer integer1 = lruCache.get(0);
for (Integer integer : lruCache.keySet()) {
System.out.println(integer);
}
}
}
Redis 高并发和高可用
缓存架构(多级缓存架构、热点缓存)
redis 高并发瓶颈在单机,读写分离,一般是支撑读高并发,写请求少,也就 一秒一两千,大量请求读,一秒钟二十万次。
主从架构
一主多从,主负责写,将数据同步复制到其他 slave节点,从节点负责读,所有读的请求全部走从节点。主要是解决读高并发。、
主从架构->读写分离->支撑10W+读QPS架构
Redis Replication
master->slave 复制,是异步的
核心机制:
- redis 采用异步方式复制数据到 slave 节点
- 一个 master node是可以配置多个 slave node的
- slave node也可以连接其他的 slave node
- slave node 做复制的时候,是不会 block master node的正常工作
- slave node 在做复制的时候,也不会 block对自己的查询操作,它会用旧的数据集来提供服务。但是复制完成时,需要删除旧数据集,加载新的数据集,这个时候就会暂停对外服务了。
- slave node 主要用来进行横向扩容,做读写分离,扩容 slave node 可以提高读的吞吐量
master持久化对主从架构的意义:
如果开启了主从架构,一定要开启 master node的持久化,不然 master宕机重启数据是空的,一经复制,slave的数据也丢了
主从复制原理:
第一次启动或者断开重连情况:
- 当启动一个 slave node的时候,它会发送一个 PSYNC 命令给 master node
- master 会触发一次 full resynchronization (如果不是第一次连接,master 只会复制给 slave 部分缺少的数据,从backlog里找)
- master会启动一个后台线程,开始生成一份 RDB 快照( bgsave,也可以直接在内存中创建),同时将从客户端收到的所有写命令缓存在内存中。RDB 文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后 master会将内存中缓存的写命令发送给 slave,slave也会同步这些数据(slave如果跟 master网络故障,断开连接,会自动重连,master如果发现有多个 slave 来重新连接,仅仅只会启动一个 RDB save 操作,用一份数据服务所有 slave node)
正常情况下:
master 来一条数据,就异步给 slave
Redis高可用性
全年 99.99%的时间,都是出于可用的状态,那么就可以称为高可用性
redis 高可用架构叫故障转移,failover,也可以叫做主备切换,切换的时间不可用,但是整体高可用。
sentinal node(哨兵)
Sentinal
作用:
- 集群监控,负责监控 redis master 和 slave进程是否正常
- 消息通知,如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移,如果 master 挂掉,会自动转移到 slave
- 配置中心,如果故障转移了,通知 client 客户端新的 master地址
两节点哨兵集群
quorum = 1 (代表哨兵最低个数可以尝试故障转移,选举执行的哨兵)
master 宕机,只有 S2 存活,因为 quorum =1 可以尝试故障转移,但是没达到 majority =2 (最低允许执行故障转移的哨兵存活数)的标准,无法执行故障转移
三节点哨兵集群(经典)
如果 M1 宕机了,S2,S3 认为 master宕机,选举一个执行故障转移,因为 3 个哨兵的 majority = 2,所以可以执行故障转移
Redis 主从 + 哨兵
丢数据:
- master内存中数据异步同步到 slave master 就挂掉了,丢掉了 master 内存中的数据
- 脑裂,某个 master 所在机器突然脱离了正常的网络,其他 slave机器不能连接,但是实际上 master还在运行,哨兵认为 master 宕机,选举 slave为master,此时集群里有 2 个 master, client还没来得及切换到新的master,还继续写在旧的 master上,数据丢了,此时旧的 master再次恢复,被被作为一个 slave 挂到新的 master 上,自己的数据被清空 (脑裂,大脑一分为 2,同时指挥同一个人)
解决方案:
- min-slaves-max-lag 10 (至少一个 slave同步的延迟不能超过 10s) 减少异步复制的数据丢失,发现slave复制数据和 ack延时过长,拒绝写入,减少同步数据损失。让client做降级写到本地磁盘里和限流,或者先暂存到消息队列,然后重新发回 master
- min-slaves-to-write 1 减少脑裂带来的数据丢失,最多损失 10 s数据,假设master 不能继续给 slave发送数据,并且 slave 10s没给自己的 ack消息,直接拒绝客户端写请求,同时 client做降写到本地磁盘、限流,或者先暂存到消息队列,然后重新发回 master
哨兵
sdown 主观宕机,哨兵觉得一个 master 宕机(ping 超过了 is-master-down-after-milliseconds毫秒数)
odown 客观宕机,quorum数量的哨兵都觉得 master宕机
哨兵互相感知通过 redis的 pub/sub系统,每隔 2 秒往同一个 channel里发消息(自己的 host,ip,runid),其他哨兵可以消费这个消息
以及同步交换master的监控信息。
哨兵确保其他slave修改master信息为新选举的master
当一个 master被认为 odown && marjority哨兵都同意,那么某个哨兵会执行主备切换,选举一个slave成为master(考虑 1. 跟master断开连接的时长 2. slave 优先级 3.复制 offset 4. runid)
选举算法:
- 如果slave跟master断开连接已经超过 down-after-milliseconds * 10 + master宕机时间,则放弃
- 按 slave 优先级排序 ,slave-priority 越小越靠前
- replica offset ,哪个slave复制越多的数据,越靠前
- runid 越小,越靠前
quorum 数量哨兵认为odown->选举一个哨兵切换->获得 majority哨兵的授权(quorum < majority 需要 majority个哨兵授权,quorum >= majority 需要 quorum 哨兵授权)
第一个选举出来的哨兵切换失败了,其他哨兵等待 failover-time之后,重新拿confiuration epoch做为新的version 切换,保证拿到最新配置,用于 configuration传播(通过 pu/sub消息机制,其他哨兵对比 version 新旧更新 master配置)
Redis 优化方案
高并发:主从架构
高容量:Redis集群,支持每秒几十万的读写并发
高可用:主从+哨兵
Redis 持久化
持久化的意义在于数据备份(到其他服务器)+故障恢复(遇到灾难,机房断电,电缆被切)
RDB 对 Redis 中的数据执行周期性的持久化
缓存雪崩
方案:
事前:保证 redis 集群高可用性 (主从+哨兵或 redis cluster),避免全盘崩溃
事中:本地 ehcache 缓存 + hystrix 限流(保护数据库) & 降级,避免 MySQL被打死
事后: redis持久化,快速恢复缓存数据,继续分流高并发请求
限制组件每秒就 2000 个请求通过限流组件进入数据库,剩余的 3000 个请求走降级,返回一些默认 的值,或者友情提示
好处 :
- 数据库绝对不会死,确保了每秒只会过去 2000 个请求
- 只要数据库不死,对于用户来说 2/5的请求可以被处理
- 系统没死,用户多点几次可能就刷出来了
缓存穿透
4000 个请求黑客攻击请求数据库里没有的数据
解决方案:把黑客查数据库中不存在的数据的值,写到缓存中,比如: set -999 UNKNOWN
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。