中间件
2021-04-22 13:52:27 37 举报
AI智能生成
redis全解析、MQ全解析
作者其他创作
大纲/内容
MQ
RabbitMQ
基本概念
特点
RabbitMQ是一款开源的,使用Erlang语言编写的,基于AMQP协议的消息中间件
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、 安全
AMQP协议对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次
常用组件
Connection
连接,应用程序与Server的网络连接,TCP连接
Channel
信道,消息读写等操作在信道中进行
Message
消息,应用程序和服务器之间传送的数据
Virtual Host
虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue
Exchange
交换器,接收消息,按照路由规则将消息路由到一个或者多个队列
Binding
绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey
RoutingKey
路由键,生产者将消息发送给交换器时会发一个RoutingKey,用来指定路由规则
Queue
消息队列,用来保存消息,供消费者消费
Broker
标识消息队列服务器实体
交换器类型
Direct Exchange
完全匹配,消息中的路由键(routing key)和Binding中的binding key一致
Topic Exchange
模糊匹配,两个通配符:"#"和"*",#匹配0个或多个单词,*只匹配一个单词
Fanout Exchange
广播模式,不处理路由键,把所有发送到交换器的消息路由到所有绑定的队列中
Headers Exchange
忽略路由规则,根据消息中的headers属性来匹配,性能较低(不常用)
使用注意
系统的可用性降低
引入外部依赖越多,系统越容易挂掉MQ挂了,<br>也会导致整个系统不可用
系统的复杂性提高
如何保证消息没有重复消费?
幂等性
如何保证消息传递的顺序?
顺序性
如何保证消息不丢失?
可靠性
数据一致性的问题
A系统发送完消息直接返回成功,但是BCD系统之<br>中若有系统写库失败,则会产生数据不一致的问题
幂等性
使用全局唯一ID+指纹码(全局唯一ID:雪花算法生成的业务表的主键。指纹码:时间戳、UUID、订单号)
并发量不高的情况下可以在数据库维护一张消费记录表,并发量很高可将全局ID写入redis,利用其原子操作setnx
接收到消息后执行setnx,如果执行成功则表示没有处理过,可以消费,相反如果执行失败就表示该消息已经被消费了
顺序性
在 MQ 里面创建多个queue,使用hash算法将需要排序的数据有顺序的放入同一个queue,每个queue对应一个consumer
或者就一个queue,对应一个consumer,这个consumer内部用内存队列排队,然后分发给不同的worker来处理
可靠性
消息丢失场景
<b>生产者</b>发送消息到MQ
网络原因
代码/配置
<b>MQ</b>中存储的消息丢失
消息未完全持久化
<b>消费者</b>从MQ拉取消息
消费端接收到相关消息之后,消费端还没<br>来得及处理消息,消费端机器就宕机了
如何避免<br>消息丢失
生产者丢消息
<b>事务机制</b>(基于AMQP协议)
吞吐量下降(同步),不推荐
<b>confirm机制</b>(生产者确认机制)
异步回调,效率高
MQ丢消息
开启RabbitMQ<b>持久化</b>
创建queue时设置持久化
发送消息时设置持久化
使用<b>镜像集群</b><br>模式保证高可用
rabbitmq有很好的管理控制台,在后台新增一个镜像集群模式的策略<br>
指定同步节点的时候要求数据同步到<b>所有</b>节点(性能受极大影响)<br><b>镜像策略</b>:指定最多同步N台机器、只同步到符合指定名称的机器
再次创建queue的时候应用这个策略就会自动将数据同步到其他的节点上
消费者丢消息
关闭消费者的自动ack机制,采用手动ack形式
消费者处理完消息后手动ack通知MQ删除消息
常见问题
死信队列(DLX)
消息变成死信的原因
消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
TTL(time-to-live) 消息超时未消费
队列达到最大长度
死信队列的设置
提前设置好死信队列的 exchange 和 queue,然后进行绑定
在普通队列上加一个参数: argument.put("x-dead-letter-exchange", "dlx.exchange");
这样消息在过期或者队列达到最大长度时,消息就会直接路由到死信队列
延时队列怎么实现?
不推荐
使用定时任务
非常浪费服务器性能,不建议
使用Java自带的delayQueue
这种实现方式是数据保存在内存中,可能面临数据丢失的情况
无法支持分布式系统,不能做集群化处理且不易维护
推荐
使用rabbitmq的消息过期时间(TTL)和死信队列(DLX)来模拟出延时队列
还可以用RabbitMQ的插件 rabbitmq-delayed-message-exchange 插件来实现延时队列<br>达到可投递时间时并将其通过 x-delayed-type 类型标记的交换机类型投递至目标队列
重复排队怎么解决?
利用redis的原子递增来实现,使用用户名作为key,每次递增1,返回值大于1则抛异常
RocketMQ
特点
RocketMQ是由阿里研发的,<b>基于Java</b>,后来交给Apache孵化,是一款分布式、队列模型的消息中间件
支持事务消息、严格保证消息顺序、提供丰富的消息拉取模式、高效的订阅者水平扩展能力
实时的消息订阅机制、吞吐量仅次于Kafka,亿级的消息堆积能力与Kafka相当
角色
架构
Producer:消息的发送者<br>
Consumer:消息接收者<br>
Broker:暂存和传输消息<br>
NameServer:路由中心(管理Broker)<br> 类似注册中心<br>
NameServer挂了怎么办?
只要有一台NameServer存活就可以通信
NameServer全都挂了呢?
RocketMQ不可用,生产者发消息会失败
Topic:区分消息的种类; 一个发送者可以发送消息给一个或者多个Topic; 一个消息的接收者可以订阅一个或者多个Topic消息<br>
Message Queue:相当于是Topic的子分区;用于并行发送和接收消息
配置
单机配置
配置环境变量
vim /etc/profile
export ROCKETMQ_HOME=/usr/local/rocketmq/rocketmq-all-4.7.1-bin-release<br>export NAMESRV_ADDR=localhost:9876<br>export PATH=$ROCKETMQ_HOME/bin:$PATH
<b>source /etc/profile</b>
注意关闭防火墙,否则开放:9876|10911|10909 端口
修改 runserver.sh<br>修改 runbroker.sh<br>
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g"
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
修改 broker.conf
末尾添加 autoCreateTopicEnable=true 允许自动创建 topic 方便测试
启动
nohup bin/mqnamesrv &
不占用当前窗口启动,会在当前目录生成 nohup.out 日志文件
nohup bin/mqbroker -c conf/broker.conf &
启动时可指定配置文件
命令行快速验证
配置环境变量(已配置请忽略):export NAMESRV_ADDR=localhost:9876<br>
启动生产者发送消息:bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
启动消费者接收消息:bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
集群配置
双主双从异步复制
关键配置 2m-2s-master.properties
启动 nameServer
worker1<br>worker2<br>worker3
nohup bin/mqnamesrv &
启动 broker
worker2
nohup bin/mqbroker -c conf/2m-2s-async/broker-a.properties
nohup bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties
worker3
nohup bin/mqbroker -c conf/2m-2s-async/broker-b.properties
nohup bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties
查看集群状态
mqadmin clusterList -n worder1:9876
可以查看所有节点,因为存的数据一样
mqadmin -h:查看帮助文档
rocketmq控制台
源码地址:https://github.com/apache/rocketmq-externals
修改配置文件:src/main/resources/application.properties
打包运行:mvn clean package -Dmaven.test.skip=true
启动项目:nohup java -jar rocketmq-console.jar &
集群
特点
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步<br>
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的Brokerld来定义, Brokerld为0表示Master, 非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信 息到所有NameServer
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。 Producer完全无状态, 可集群部署
Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建 立长连接,且定时向Master、 Slave发送心跳。 Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定
模式
单Master模式
Broker重启或者宕机时,会导致整个服务不可用
多Master模式<br>2m-noslave
全是Master,没有Slave<br>
优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘<br>非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高
缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响
多主多从模式(异步)<br>2m-2s-async
主备消息同步采用<b>异步复制方式</b>,Master成功后立即响应,然后异步发送到从节点,主备有短暂消息延迟(毫秒级)<br>
优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,<br>而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样
缺点:Master宕机,磁盘损坏情况下会丢失少量消息
多主多从模式(同步)<br>2m-2s-sync
主备消息同步采用<b>同步双写方式</b>,只有主备都写成功,才向应用返回成功<br>
优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高
缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机
Dledger集群
支持高可用
工作流程
1. 启动NameServer,NameServer起来后监听端口,等待Broker、Producer. Consumer连上来,相当于一个路由控制中心<br>
2. Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,<br>NameServer集群中就有Topic跟Broker的映射关系<br>
3. 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic<br>
4. Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列<br>列表中选择一个队列, 然后与队列所在的Broker建立长连接从而向Broker发消息<br>
5. Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费
消息的生产/消费
生产消息
1. 创建消息生产者producer,并制定生产者组名<br>2. 指定Nameserver地址<br>3. 启动producer<br>4. 创建消息对象,指定主题Topic、Tag和消息体<br>5. 发送消息<br>6. 关闭生产者producer
消费消息
1. 创建消费者Consumer,制定消费者组名<br>2. 指定Nameserver地址<br>3. 订阅主题Topic和Tag<br>4. 设置回调函数,处理消息<br>5. 启动消费者consumer
消息类型
顺序消息
概念
消息有序指的是可以按照消息的发送顺序来消费(FIFO)
如何保证消息有序
全局有序:发送和消费参与的queue只有一个(没什么用)
分区有序:控制发送的顺序消息只依次发送到同一个queue中,消费时只从这个queue上依次拉取,则可保证顺序
分区有序的实现原理
生产者构建 <b>消息队列选择器 </b>new MessageQueueSelector(),通过订单号路由消息
消费者用 <b>单线程的监听器 </b>new MessageListenerOrderly()<b>,</b>消费队列中的有序消息
广播消息
<b>集群模式(CLUSTERING),</b>一条消息在同一个消费者组下,只会有一个消费者来消费(<b>默认模式</b>)
<b>广播模式(BROADCASTING),</b>不管消费者组的概念,一条消息过来就会推送给所有订阅了该topic的消费者
关键代码
consumer.setMessageModel(MessageModel.BROADCASTING); //设置消费者为广播模式
延时消息
Producer发送消息到Broker后,等待一段时间(可设置)再发送给消费者,可用作定时任务
使用限制
rocketmq定义了18个延迟级别:messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";<br>开源版本并不支持任意时间的延时,只能设置几个固定的延时等级,不过可以通过修改broker.conf来自定义符合自己预期的18个级别<br>
阿里商业版的RocketMQ在延时消息模块没有等级划分,<b>取而代之得是 setStartDeliverTime(long value) 方法,自定义开始时间</b>
关键代码
msg.setDelayTimeLevel(3); //取值范围 1~18
批量消息
Batch机制
把多条消息合成为一条批量消息,一次发过去
减少网络IO,能显著提高传递消息的性能
使用限制
批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息、事务消息
批量消息的单次发送总大小<b>不能超过4MB</b>,如果超过4M则需要对消息进行分割
关键代码
生产者批量发送消息
过滤消息
过滤有两种方式
Tag过滤
SQL过滤
enablePropertyFilter = true
使用限制
一个消息只能有一个标签,这对于复杂的场景可能不起作用
在这种情况下,可以使用<b>SQL表达式筛选消息</b>。SQL特性可以通过发送消息时的属性来进行计算
关键代码
生产者发送消息时可以通过msg.putUserProperty来设置消息的属性
消费者消费消息时可以用MessageSelector.bySql来使用sql筛选消息
事务消息
防止生产者丢消息
同步发送+多次尝试
事务消息机制<br> <b>推荐使用</b>
(1) 生产者发送 half 消息到 MQ(对消费者不可见)
(2) MQ服务端收到 half 消息后记录消息并回复生产者
(3) 生产者根据MQ响应结果执行本地事务,并发送本地事务的执行状态
(4) MQ服务器根据本地事务状态执行Commit或者Rollback
Commit操作提交half消息,使消费者可见
RollBack是进行回滚操作,删除half消息
(5) 对没有发送状态的事务消息,MQ服务端会发起“回查”(默认回查15次,如果仍然失败则丢弃消息)
(6) 生产者收到回查消息,检查对应的本地事务的状态,重新Commit或者Rollback
防止 Broker 丢消息
刷盘策略
默认为异步刷盘,修改为同步刷盘,存入磁盘后再返回写入成功
通过Broker配置文件里的 <b>flushDiskType </b>参数设置
ASYNC_FLUSH
SYNC_FLUSH
集群同步
默认为异步同步(master写成功就返回)修改为同步到slave再返回成功
通过Broker配置文件里的 <b>brokerRole </b>参数设置
ASYNC_MASTER
SYNC_MASTER
因此可以通过同步刷盘策略+同步双写策略+主从的方式解决丢失消息的可能
防止消费者丢消息
消费者收到消息后先执行本地事务,再修改offset,然后通知Broker,如果通知失败则重试
不要使用异步处理逻辑,如果收到消息后开启线程异步处理,就返回成功,很容易导致消息丢失
事务消息状态
提交状态
提交事务,它允许消费者消费此消息
回滚状态
回滚事务,它代表该消息将被删除,不允许被消费
中间状态
中间状态,它代表需要检查消息队列来确定状态
高频面试题
为什么需要消息队列?
业务不断扩张,使用消息队列进行异步处理,服务解耦,流量控制
<b>异步处理</b>:请求链路越来越长,响应越来越慢,异步可以<b>提高响应速度</b>,比如短信和积分可异步并行处理
<b>服务解耦</b>:防止下游服务的修改影响上游,屏蔽技术实现细节,比如Java对接python,还可以实现数据分发,比如订单
<b>流量削峰:</b>生产者和消费者速率不匹配,比如秒杀场景应对突发流量,请求进来后先放入消息队列,后端服务尽最大努力去消费
缺点
系统可用性降低
系统复杂度提高
数据一致性问题
MQ技术选型?
吞吐量:Kafka和RocketMQ可达10w级别,RabbitMQ和ActiveMQ可达1w级别
时效性:RabbitMQ的延迟在微秒级别,延迟最低,其他三者都是毫秒级别
可用性:Kafka和RocketMQ都是分布式架构,可用性非常高,其他两个采用主从架构
维护性:Kafka和RocketMQ社区活跃度很高,维护成本低,其他两个社区活跃度较低
Kafka
优点:吞吐量非常大,性能非常好,集群高可用
缺点:会丢消息(不适合订单系统),功能比较单一
使用场景:日志分析,大数据采集
RabbitMQ
优点:消息可靠性高,延迟低,功能较全面
缺点:吞吐量比较低,消息积累会严重影响性能,erlang语言不好定制
使用场景:小规模场景
RocketMQ
优点:高吞吐,高性能,高可用,功能非常全面
缺点:开源版功能不如商业版,官方文档和周边生态还不够成熟,客户端只支持Java
使用场景:几乎是全场景(后发优势,设计时借鉴前两者的优点)
消费消息是 push 还是 pull ?
push:MQ主动推送消息给消费者
优点:实时性高
缺点:如果消费者处理能力跟不上,会导致消息堆积,服务崩溃
pull:消费者主动拉取消息消费
优点:消费者可根据自己的消费能力进行消费(自己编写消费逻辑)
缺点:实时性较低,拉取消息的间隔不太好设置,间隔太短,对服务<br>器请求压力过大。间隔时间过长,那么必然会造成一部分数据的延迟
如何保证消息不丢失?
生产者往MQ发消息
Kafka:消息发送+回调
RocketMQ:事务消息机制<br>同步发送+多次尝试<br>
(1) 生产者发送 half 消息到 MQ(对消费者不可见)
(2) MQ服务端收到 half 消息后持久化消息并回复生产者
(3) 生产者根据MQ响应结果执行本地事务,并发送本地事务的执行状态(成功,失败,未知)
(4) MQ服务器根据本地事务状态执行commit或者rollback
commit操作提交half消息,使消费者可见
rollBack是进行回滚操作,删除half消息
(5) 对发送未知状态的事务消息,MQ服务端会发起“回查”(默认回查15次,如果仍然失败则丢弃消息)
(6) 生产者收到回查消息,检查对应的本地事务的状态,重新commit或者rollback
RabbitMQ:生产者confirm机制
异步回调
MQ主从同步
RocketMQ
默认为异步同步,即master写成功就返回,效率高但是可能丢消息,<b>改为同步到slave再返回</b>
Dledger集群-两阶段提交,各节点之间通过Master选举,选出主节点,超过半数节点同步成功再返回
RabbitMQ
镜像集群,可指定主动进行数据同步的节点,同步的节点越多,安全险越高,但是效率越低
Kafka
通常都是用在允许消息少量丢失的场景
MQ消息刷盘
RocketMQ:采用同步刷盘策略
异步刷盘效率更高,但有可能丢消息,同步刷盘安全性高,但是效率会降低
RabbitMQ:将队列配置成持久化队列
创建queue时设置持久化,发送消息时设置持久化,必须同时设置
新增Quorum类型的队列,会采用Raft协议来进行消息同步
消费者从MQ取消息
RocketMQ:使用默认的方式消费就行,不要采用异步方式
RabbitMQ:关闭消费者自动提交,改为本地事务成功再手动确认
Kafka:手动提交offset
如何保证消息幂等性?<br>防止消费者重复消费
由于网络的不可靠因素,生产者没有收到响应会重发消息,所以消息重复是不可避免的
利用幂等处理重复消息,比如修改操作引入版本号,新增操作使用业务ID做唯一键
如何保证消息有序性?
生产者和消费者同时控制,生产者将一组有序的消息发送到一个队列,每个队列只对应一个消费者
RocketMQ:生产者注册 new MessageQueueSelector(),消费者注册 new MessageListenerOrderly()
RabbitMQ:生产者将一组有序消息路由到一个队列,每个队列只对应一个消费者
Kafka:生产者通过定制 partition 分配规则,将消息分配到同一个 partition
如何保证消息的高效读写?
Kafka和RocketMQ都是通过 <b>零拷贝</b> 技术来优化文件读写
如何保证分布式事务的最终一致性?
生产者要保证100%的消息投递,可采用事务消息保证
消费者保证幂等性消费,采用失败重试保证最终一致性
如何处理消息堆积?
优化消息的消费逻辑
先定位消费慢的原因,检测并处理bug
批量处理的速度比一条一条消费要快
提高消费的并发度
优化完还是慢,则考虑水平扩容,增加队列和消费者数量
调大单个节点的线程数
如何设计一个MQ?
从整体到细节,从业务场景到技术实现
实现一个单机的队列数据结构,高效、可扩展
将单机队列扩展成为分布式队列(分布式集群管理)
基于 Topic 定制消息路由策略(发送者路由策略,消费者与队列对应关系,消费者路由策略)
实现高效的网络通信(Netty Http)
规划日志文件,实现文件高效读写(零拷贝,顺序写。服务重启后,快速还原运行现场)
定制高级功能,死信队列、延迟队列、事务消息等
Redis
基本数据类型<br>
<b>String</b>:基本<br>的<b>字符串</b>类型
可做简单的key-value缓存,实现计数器、分布式锁、session共享、分布式ID生成(自增)
redis底层是c,为什么<br>不用c字符串而用sds?
获取长度
c 字符串并不记录自身长度,想获取只能遍历
sds 直接获取 len 即可
内存分配
c 字符串每次长度变化都会对数组进行内存重新分配,比较耗时
对 sds 内容进行修改或者需要扩展时,sds 有空间预分配和惰性空间释放
缓冲区安全
c 字符串不记录自身长度,不会自动进行边界检查,所以会增加溢出的风险
sds 先检查空间是否满足修改所需的要求,如果不满足就先扩容再执行修改
二进制安全
c 字符串是以空字符(\0)结尾,所以字符串中不能包含空字符,只能保存文本数据
既能保存文本数据,也能保存二进制数据(通过长度判断结束,不受影响
<b>List</b>:<b>有序列<br>表</b>,异步解耦
List 是一个双向链表
可以通过 lpush/rpush 写入,rpop/lpop 读取
可以通过使用 brpop/blpop 来实现阻塞队列
可以通过 lrange key 0 -1 查看队列
-1 代表倒数第一个元素
-2 代表倒数第二个元素
应用
实现高性能分页,如微博、公众号消息流
实现栈或队列:例如到货通知、邮件发送,秒杀,保存待抢购的商品列表
底层实现
压缩列表(ziplist)
当列表对象同时满足以下两个条件时,列表对象使用ziplist进行存储
条件
列表对象保存的元素数量小于512个<br>
列表对象保存的所有字符串元素的长度都小于64个字节
它将所有的元素紧挨着一起存储,分配的是一块连续的内存
快速链表(quicklist)
由于普通链表指针比较浪费空间且会加重内存碎片化,所以优化为quicklist
特点
将多个ziplist使用双向指针串起来(链表+ziplist)
既满足了快速的插入删除性能,又不会出现太大的空间冗余
<b>Set</b>:<b>无序集<br>合,自动去重</b>
类似HashSet,内部的键值对是无序且唯一的,字典中所有的value都是一个值NULL
应用
利用交集查看共同粉丝列表
实现微信抽奖小程序
实现微博点赞、收藏、标签
Set底层实现
整数集合(intset)
使用intset存储必须满足下面两个条件,否则使用hashtable
条件
集合对象保存的所有元素都是整数值
集合对象保存的元素数量不超过512个
修改条件阈值
set-max-intset-entries
字典(hashtable)
<b>ZSet</b>:<b>有序集<br>合,自动去重</b>
写数据带分数,实现排行榜,定点提醒
底层实现
压缩列表(ziplist)
元素个数较少,或所占字节数较少时使用
条件
有序集合保存对元素数量小于128个<br>
有序集合保存的所有元素成员对长度都小于64字节
修改条件阈值
zset-max-ziplist-entries
zset-max-ziplist-value
跳跃表(skiplist)
不再符合上述两个条件时使用跳跃表结构
一个zset结构同时包含一个字典和一个跳跃表<br>
dict指针指向的是字典结构,zskiplist指针指向的是跳跃表结构
在链表的基础上增加了多级索引来提升查找效率,时间复杂度为O(logN)
通过 <b>object encoding</b> k1 查看底层实现结构
<b>Hash</b>:<b>无序字典</b>
类似HashMap,特别适合存储对象,可单独修改对象中的字段
可以快速定位,存储的信息需要被频繁修改可用hash存储,比如实现购物车
优缺点
优点
1) 同类数据归类整合储存,方便数据管理 <br>
2) 相比 string 操作消耗内存与 cpu 更小
3) 分字段存储,节省网络流量
缺点
1) 过期功能不能使用在 field 上,只能用在 key 上
2) Redis 集群架构下不适合大规模使用
Hash底层实现
压缩列表-ziplist
哈希对象同时满足以下两个条件时,列表对象使用ziplist进行存储
条件
哈希对象保存的键值对数量小于512个<br>
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节<br>
修改条件阈值
hash-max-ziplist-value
hash-max-ziplist-entries
字典-hashtable
为了高性能,不能堵塞服务,采用了渐进式 rehash 策略
特殊类型
Pipeline(管道)
管道就是打包多条无关命令批量执行,以减少多个命令分别执行消<br>耗的网络交互时间(TCP网络交互),可以显著提升Redis的性能
Geospatial
地理空间,可以录入地理坐标并计算距离
底层实现原理是ZSet
Hyperlogglog
基数统计的算法,根据并集的数量来计数
占用的内存固定,只需要12kb的内存,有0.81%错误率
Bitmaps
位图,本质是String,使用二进制记录,只有0和1两种状态
最大长度是512M,可以表示2^32个不同的位
可以用来统计用户信息、点赞、打卡,两个状态的都可以
海量数据统计
存储是否参过某次活动,是否已读谋篇文章,用户是否为会员,日活统计
Redis 事务
概念
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化
一句话:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
特点
没有隔离级别的概念
批量操作在事务提交前被放入缓存队列,并不会被实际执行
不保证原子性
Redis中单条命令是原子性执行的,但事务不保证原子性,且没有回滚
事务中任意命令执行失败,其余的命令仍会被执行
三阶段
开始事务、命令入队、执行事务
相关命令
watch key1 key2 ...:监视一或多个key,如果在事务执行前,被监视的key被其他命令改动,则事务执行失败
multi:标记一个事务块的开始
exec:执行所有事务块的命令(一旦执行exec后,不论成功与否,之前加的监控锁都会被取消掉)
discard:取消事务,放弃事务块中的所有命令
unwatch:取消watch对所有key的监控
常见问题
若在事务队列中存在「命令性错误」,则执行EXEC命令时,所有命令都不会执行
若在事务队列中存在「语法性错误」,则执行EXEC命令时,错误命令抛出异常,其他正确命令会被执行
内存淘汰策略
内存回收
<b>expire</b> key ttl:将 key 值的过期时间设置为 ttl <b>秒</b>
<b>pexpire</b> key ttl:将 key 值的过期时间设置为 ttl <b>毫秒</b>
<b>expireat</b> key timestamp:将 key 值的过期时间设置为指定的 timestamp <b>秒数</b>
<b>pexpireat</b> key timestamp:将 key 值的过期时间设置为指定的 timestamp <b>毫秒数</b>
<b>PS:不管使用哪一个命令,最终 Redis 底层都是使用 pexpireat 命令来实现的</b>
查看
<b>ttl </b>key 返回 key 剩余过期秒数
<b>pttl</b> key 返回 key 剩余过期的毫秒数
过期策略
<b>定时删除</b><br>
为每个key创建一个定时器,对内存友好,对CPU不友好<br>
<b>惰性删除</b>
用到的时候发现过期才删,可能存在大量过期key
<b>定期删除</b><br>
redis默认每隔100ms就随机抽取一些过期的key删除<br>
内存配置
maxmemory <bytes>(配置redis最大使用内存)
config set maxmemory 1GB(动态配置)
如果没有配置,32位OS最多占用3G,64位OS不限制
内存淘汰机制<br>
<b>使用命令动态配置:config set maxmemory-policy <策略></b>
8种淘汰策略
<b>allkeys-lru</b>:从数据集中挑选<b>最近最少使用</b>的数据淘汰(<b>推荐使用</b>)<br>
allkeys-random:从数据集中任意选择数据淘汰<br>
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰<br>
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰<br>
no-enviction:禁止驱逐数据,再写入会报错(默认不回收)<br>
volatile-lfu: 在设置了过期时间的键上,应用LFU策略
allkeys-lfu: 在所有键上,应用LFU策略
算法原理
LRU
redis4.0前,在redisobject对象中存在 lru 属性
记录对象最后一次被应用程序访问的时间(24位=3字节)
lru 属性在创建对象的时候写入,对象被访问到时也会进行更新
算法优化
随机抽取部分key,然后按照 lru 算法删除
逻辑优化:lru_clock
存在问题
LFU
redis4.0后,在redisobject对象中使用前16位记录 lru 属性,使用后8位记录访问频率
访问频次递增
使用基于概率的对数器counter来实现
访问频次递减
N 分钟内没有访问,counter 就要减 N
对比
LRU(Least Recently Used):最近最长时间未被使用,这个主要针对的是使用时间
LFU(Least Frequently Used):最近最少频率被使用,这个主要针对的是使用频率
Redis 持久化
RDB
原理
redis 会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化,<br>将数据写入到一个临时文件中,待持久化结束后替换上次持久化好的文件
相当于两个redis进程,这期间主进程<br>不参与持久化,保证了redis的高性能
这个持久化文件在哪里呢?
redis.conf 配置中默认有 dir ./ 参数,即 redis 启动时会检查当前目录是否有dump.rdb文件
注意:redis 在不同的目录启动,是有不同的数据空间的,所以我们通常把这个 dir 配置写死
触发
客户端执行 shutdown 命令时,如果没有开启 aof 会触发
配置文件中有快照配置,例如 save 900 1(15分钟内有1次修改)
执行 save 或 bgsave 命令
save 命令会阻塞主进程,一般不用
bgsave 会 fork 子进程异步持久化
执行 flushall 命令
清空内存中的数据,同时触发持久化,清空磁盘
特点
优点
恢复的时候比较快,适合大规模的数据恢复,冷备
缺点
如遇突然宕机,丢失的数据比较多
如果生成的快照文件比较大也会影响redis性能
AOF
原理
将所有的写命令追加到 AOF 缓冲区中,根据对应的写入策略向硬盘进行同步操作<br>
由主进程完成
随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的
fork 子进程来进行
这个持久化文件在哪呢?
同 RDB 目录(dir 配置)
AOF 为什么要把命令追加到缓冲区,而不直接追加到磁盘?
触发
需手动开启:appendonly yes
线上开启:CONFIG SET appendonly yes,避免丢失数据
开启后 redis 会保留一块内存供缓冲区使用,默认是 1M
aof 和 rdb 同时开启时,只保留 save 900 1 减少 fork 子进程的次数(优化点)
写入策略:appendsync everysec
everysec:每秒同步一次,效率高,可能会丢失1秒的数据【<b>默认也推荐使用</b>】
no:等到缓冲区满了才写入磁盘,次数少,效率高,不安全
追求效率
always:每次发生数据变更立即同步到磁盘,效率低,安全
追求安全
重写机制<br>bgrewriteaof<br>
默认配置
auto-aof-rewrite-min-size 64M
aof 文件大于该配置时重写
由于重写会 fork 子进程,为了减少重写次数,<br>这里建议配置 5GB 以上(优化点)
auto-aof-rewrite-percentage 100
指超过优化后大小的一倍时开始重写
重写后的文件为什么会变小?
进程内已经超时的数据不再写入文件,而且多条写命令可以合并为一条<br>
重写使用进程内数据直接生成,新的 AOF 文件只保留最终数据的写入命令
特点
优点
以append-only模式写入,没有磁盘寻址开销,写入性能高
相比于RDB,丢失的数据更少,不过建议与RDB同时开启
缺点
不适合冷备,恢复文件大,速度慢,恢复不稳定,容易bug
混合持久化<br>( rdb+aof )
配置:aof-use-rdb-preamble yes
5.0以后默认开启
优化重写机制
<b>重写后新的AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据</b>
特点
优点
由于绝大部分都是RDB格式,加载速度快
同时结合AOF,增量的数据得以保存,数据更少丢失
缺点
兼容性差,4.0之前不支持
可读性差
redis 启动后持久化文件的加载流程?
先判断是否开启了AOF,如果存在AOF文件,则直接加载AOF文件
如果找不到AOF文件,则直接启动,不会加载RDB文件
如果没有开启AOF,会去加载RDB文件,通过RDB来持久化数据
生产环境建议 aof 和 rdb 同时使用,rdb做容灾备份
主从复制
一主多从<br>读写分离
主负责写,写完同步到从节点,从负责读
可水平扩容,QPS再增加只需添加slave就ok了
主从复制产生短暂的数据延迟是允许的,保证最终一致性
心跳机制
主从节点彼此都有心跳检机制,各自模拟对方的客户端进行通信,<br>主节点的连接状态为 flags=M,从节点连接状态为 flags=S
主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活<br>性和连接状态。可通过 repl-ping-replica-period 10 控制发送频率
从节点在主线程中每隔一秒发送 offset 命令,给主节点上报自身当<br>前的复制偏移量。主节点根据 replconfa 命令判断从节点超时时间
数据同步
前提
master 和 slave 都会维护一个 offset 和 run id,slave 每秒都会上报自己的 offect 给 master<br>
master记录在backlog中,这样才能知道双方数据是否一致
slave发送 run id 和 offset 到 master,master 根据自身情况返回相应信息(增量/全量)复制
全量复制
触发时机
slave 结点第一次启动时
master 重启或者加载了之前的备份文件(run id 会变)
复制过程
1、slave 启动时会向 master 发送 SYNC 指令(请求同步)
2、master 收到后通过 bgsave 保存快照,同时缓存后续的写命令
3、slave 收到文件后先写入本地磁盘,然后再从本地磁盘加载到内存中<br>
4、最后 master 会将内存中缓存的写命令同步给 slave,slave 收到后再执行一遍
耗时原因
主节点 bgsave 时间
RDB 文件网络传输时间
从节点清空数据时间
可能伴随的 AOF 重写时间
增量复制
master 和 slave 都会维护一个 offset 和 run id,slave 每秒都会上报自己的 offect 给 master(backlog)<br>
master 根据 slave 发送的同步请求中的 offset,在 backlog 中查找到部分丢失的数据,发送给 slave
过期key处理
slave不会过期key,只会等待master过期通知
存在问题
数据一致性(同步延迟)
不具备自动容错和恢复
主从+哨兵
概念
哨兵是一个分布式系统,监控主从架构中的节点通过 <b>自动故障转移 </b>保证集群的高可用
哨兵也是一台redis服务器,只是不提供任何服务,推荐配置为单数
避免相同票
主要功能
监控
监控主节点和从节点是否正常运行
通知
检测到服务出现问题会通知其他哨兵
自动故障转移
当确认主节点宕机后,在从节点中选一个作为主节点,将<br>其他从节点连接到新的主节点上,通知客户端最新的地址
工作原理
1、发现master节点宕机
一台哨兵发现master宕机了,标记为sdown(主观下线),并通知其他哨兵
其他哨兵去查看,如果超过quorum数量的哨兵认为挂了就标记为odwon(客观下线)
2、选出一个哨兵去处理
每个哨兵作为参选者和投票者,向哨兵内网发送指令
指令中携带自己的竞选次数和runid,先收到谁的指令就投票给谁
3、哨兵从服务器列表中挑选master
先过滤掉不在线和响应慢的服务器
然后过滤掉与原master断开时间最久的
最后再比较优先级priority、偏移量offset、runid
4、新master诞生
哨兵向选举出的新master发送指令,断开与旧master的连接
把新master的ip地址同步到其他slave节点
redis cluster集群
为什么使用redis cluster
主从复制的缺点:master单点故障
主从+哨兵的缺点:节点数据冗余
redis cluster
动态扩容和缩容,保证数据不冗余,且吞吐量更大
自动数据分片,每个master节点存放一部分数据
提供内置的高可用支持,允许部分master节点宕机
数据分片方案
客户端
客户端使用一致性哈希等算法决定键应当分布到哪个节点
中间层
将客户端请求发送到代理上,由代理转发请求到正确的节点上
国内豌豆荚的Codis
国外Twiter的twemproxy
服务器
hash slot 算法
Redis Cluster
分布式寻址算法
hash算法
计算请求数据的hash值,并按照节点数量取模,再放入对应的master节<br>点中,如果某台master宕机了,由于master数量少了导致取模方式改变<br>
缺点:会造成大量缓存重建
一致性hash算法+虚拟节点
把请求数据的hash值对应在圆环的各个点上,然后顺时针寻找<br>离自己最近的master节点,如遇master宕机只会影响部分数据
基于上面的一致性hash算法,再在各个master节点之间创建均<br>匀分布的虚拟节点,如遇master宕机时就不会涌入同一个节点
优点:自动缓存迁移、自动负载均衡
hash slot算法
redis cluster有固定的16384个哈希槽,对每个key计算CRC16的值,<br>然后对16384取模计算卡槽位置(每个槽位可以存放多个key)
CRC16算法
为什么是16384
即使有任何一台机器宕机,其他master中的缓存是不受影响的,失效的节点重新分配就可以了
元数据维护
种类
集中式:将元数据(节点信息、故障)存储在某个节点上
优点:时效性好,同步快
缺点:更新压力和存储压力集中
gossip协议<br>
去中心化:每个节点都持有一份元数据
优点:缓解了元数据更新和存储的压力
缺点:元数据更新延迟,集群操作滞后
通信机制
Meet:集群中的节点会向新的节点发送邀请,加入现有集群
Ping:节点向集群中的其他节点发送ping消息传递自己的节点信息
Pong:收到ping消息的节点会回复pong,消息中同样携带节点信息
Fail:ping不通某节点会向集群中的其他节点广播该节点挂掉的消息
redis cluster基于gossip<br>协议的故障检测
节点间内部通信
采用gossip协议,每个节点都有一个专门用于节点间通信的<br>端口,就是自己提供服务的端口号+10000
集群中的每个节点都会定期地向集群中的其他节点发送PING<br>消息,以此交换各个节点状态信息,检测各个节点状态
高性能的主备切换
判断master宕机
PFAIL->超半数->FAIL
从节点过滤
过滤掉与master断开时间长的slave
master选举
slave发现自己的master的状态变为FAIL
将自己记录的选举轮次标记加1,并广播<br>通知给集群中其他节点
其他节点收到该信息,只有master响应,<br>判断请求者的合法性,并发送结果
尝试选举的slave收集master返回的结果,<br>收到超过半数master的统一后变成新Master
广播Pong消息通知其他集群节点
redis 高频面试题
单线程
redis是单线程吗?为什么?
这里的单线程是指Redis在处理网络请求的时候只有一个线程来处理
Redis为什么这么快?
1、基于内存实现,数据都存储在内存里,减少了一些不必要的 I/O 操作
2、redis采用IO多路复用模型,同时监听客户端连接,单线程在执行过程中不需要进行上下文切换,减少了耗时
epoll
3、高效的数据结构<br>
比如String底层的SDS、List的双端链表和压缩列表、Set的跳跃表等等
4、合理的数据编码
String:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码
List:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 quicklist 编码
Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码
Zset:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码
Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对
数据结构
redis为什么有16个库?
在单个库中不允许存在重复key
用来区分不同的业务逻辑
redis如何存放对象?
基于Json序列化存放
优点:阅读性强、可以跨语言
缺点:明文不安全
基于String的二进制存放
优点:比较安全
缺点:不支持跨语言、阅读性差
为什么zset使用跳跃表而不用红黑树?
跳跃表的时间复杂度和红黑树一样,而且实现起来更简单
在并发环境下红黑树在插入和删除时需要rebalance,性能不如跳跃表
什么样的数据适合放缓存?
高频被访问的数据<br>
数据的变化率不高<br>
非敏感数据
缓存问题
<b>缓存穿透</b>:大量请求一个数据库不存在的数据
简单方案
接口层增加校验
查询为空也暂时放入缓存,设置超时时间(不超过5分钟)
主流方案:布隆过滤器
将所有可能存在的数据hash到一个足够大的bitmap中
底层是很长的位数组
对同一个即将存入的数据 → 使用n个不同的hash算法<br>计算出n个哈希值,然后根据位数组长度取余后存入
影响误判的因素
Hash函数的个数
位数组的长度
缺点
难以维护(数据不能删除)、需要定时更新数据(重建)
种类
Google的Guava,存储在JVM
分布式,存储在Redis位图
<b>缓存击穿</b>:热点key扛高并发,缓存失效的瞬间
简单方案:不给缓存添加超时时间,热点数据不失效
主流方案:分布式锁
<b>缓存雪崩</b>:缓存服务器宕机或者缓存集中失效
给缓存加随机因子,分散失效时间
对redis缓存做高可用,集群部署,增加抗风险能力(前)
设置本地缓存ehcache+限流hystrix,避免数据库被干掉(中)
利用redis的持久化机制,重启redis快速恢复缓存数据(后)
数据一致性
缓存双写一致性?
先删缓存再更新数据库,期间有查询呢?
先更新数据库再删缓存,缓存删除失败呢?
最终解决方案
延迟双删:先删缓存再更新数据库,等几百毫秒<br>再删缓存 (延迟时间根据具体的业务耗时而定)
面试喜欢问<br>但是不常用
先更新数据库,然后删除缓存,最后记得设置过期时间<br>允许短时间的数据不一致,想完全一致只能牺牲性能<br>
【常用】
并发竞争?
分布式锁
set key value px milliseconds nx 或使用 jedis
注意点
value要具有唯一性,可用UUID.randomUUID().toString()方法生成
释放锁时使用lua脚本保证原子性,并验证value值,防止误解锁
存在的风险
如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁<br>的风险,导致出现多个客户端持有锁的情况
解决
redis官方提供了RedLock算法
Redission
大key
原生自带的bigkeys命令,找出来拆分或者删除
redis-cli --bigkeys命令,找出五大数据类型中最大的key<br>自定义扫描脚本,python居多,原理与bigkeys命令相似<br>也可以借助redis分析工具,分析RDB快照找出大key<br>注意redis是单线程处理任务,直接删除会阻塞<br>
如果知道大key的键,可使用scan命令扫描,然后慢慢删除<br>
redis4.0以后支持异步删除,unlink命令是非阻塞删除<br>使用另一个线程去处理,而不是redis主线程<br>
热点key
如何发现热点key?
凭借业务经验,进行预估哪些是热key
客户端收集,在操作redis前对数据进行统计
使用redis自带命令:redis-cli 时加上 –hotkeys
如何解决热点key?<br> <b>(隔离+分治)</b>
打散到多个节点
分散一台redis服务器上的压力
热点Key+随机数,分散缓存至其他节点
利用多级缓存
添加本地缓存缓解redis压力
读写分离扩容
增加从节点增加读能力
设置永不过期
防止缓存击穿
redis如何实现异步队列?
一般使用list结构作为队列,rpush生产消息,lpop消费消息<br>当lpop没有消息时,要sleep一会再重试(或者使用blpop)
缓存架构如何设计?
如果你的数据量不大(10G以内),单master就可以。redis持久化+备份方案+容灾方案+replication<br>(主从+读写分离)+sentinal(哨兵集群,3个节点,高可用性)
如果你的数据量很大(1T+),采用redis cluster。多master分布式存储数据,水平扩容,自动进行主备切换
0 条评论
下一页