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