笔记:Redis面试金典
Redis属于单线程还是多线程?不同的版本有什么区别
在redis4.0之前是单线程,之后是多线程
为什么采用单线程(4.0之前)
单线程模型开发基于维护更简单
基于多路复用使得单线程模型的redis也能并发处理
对于redis系统来说,主要的性能瓶颈是内存和网络带宽而非CPU
Redis为什么这么快
基于内存操作,性能高
优秀的数据结构设计,比如SDS字符串类型,在存储的数据为int类型的时候SDS使用int类型存储,在存储的数据为字符串类型是存储类型为embstr,当字符串长度大于44字节时使用raw存储;在有序集合中使用了skiplist,查询时间复杂度为log(n)
多路复用机制,redis同时监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免I/O阻塞
避免上下文切换,单线程模型避免了不必要的上下文切换和多线程竞争。节省了多线程切换带来的时间和性能上的消耗
I/O多路复用
套接字的读写方法默认是阻塞的(例如当调用读取操作read方法时,缓冲区没有任何数据,那么这个线程就会阻塞卡在这里,知道缓冲区有数据或者是连接被关闭时,read方法才可以返回,线程才可以继续处理其他业务)
而对于非阻塞I/O尽管性能上得到了提升,但是在执行读取数据操作时,有可能只读取了一部分数据,同时写入数据也是可能出现这种情况,当缓冲区满了之后我们的数据还没有写完,剩余的数据何时写与何时读就成了一个问题。
基于以上问题,提出了I/O多路复用(一种简单的实现方式就是基于select实现)
IO多路复用(事件驱动模型)是指内核一旦发现程序指定的一个或者多个IO条件准备读取,它就通知该程序
(select(遍历注册信息进行通知,O(n),在文件描述符特别多的时候性能低),epoll(直接查询注册信息,O(1))
Redis单线程的缺点
单线程的机制导致 Redis 的 QPS(Query Per Second,每秒查询率)很难得到有效的提高;官方数据是10W QPS,如果业务中需要100W QPS那么单线程模型的
Redis多线程(4.0版本)
在redis4.0版本中虽然引入了多线程,但是该版本中的多线程只是用于大数据量的异步删除,对于非删除操作的意义不是很大
Redis多线程(6.0版本)
引入多线程的原因:redis虽然使用了I/O多路复用,并且是基于非阻塞I/O进行的,但是I/O的读和写本身是阻塞的(比如当socket中有数据时,redis会通过调用先将数据从内核态空间拷贝到用户态空间,再交给redis调用,而拷贝过程就是阻塞的)
引入多线程的目的:分摊redis同步读写I/O的压力,充分利用多核CPU资源,并且可以有效提升redis的QPS。
实现思路:主要是将主线程的IO读写任务拆分给一组独立的线程去执行,这样多个socket的读写可以并行化了,但redis的命令依旧是由主线程串行执行的。
redis6.0中默认是禁用多线程的,可以通过修改redis的配置文件redis.conf使io-threads-do-reads=true
来开启多线程,但是要正确激活多线程功能还需要修改配置io-threads=需要开启的线程数(官方建议线程数一定要小于机器核数)
redis有哪些数据类型
字符串、字典、列表、集合以及有序集合
字符串:基于SDS结构实现,在存储类型为int以int对象接收,在存储类型为string时以embstr接收,当接收的字符串大小超过44字节时以raw对象接收
字典:基于hashMap实现(发生冲突时使用链表解决冲突(拉链法)),详情可参考数据结构hashmap篇
列表:基于链式结构存储的有序结构(基于双链表实现的双端队列),元素的插入和删除操作时间复杂度为O(1),查询时间复杂度为O(n)
集合:无序并唯一的键值集合,在存储的键值均为int类型时使用整数集合存储(基于有序数组实现),在存储其他类型时使用字典存储(key为集合元素,value为null)
有序集合:有序,键值唯一的元素。有序集合相比于集合类型多了一个排序属性score(分值)。基于跳表存储。
skiplist
redis高级数据结构以及用途
GEO(地理位置类型):用于存储和查询地理位置。如附近的人、查询附近的商家等功能
Stream(流类型):是redis5.0新增的数据类型。提供可靠性,可提供具备订阅发布功能(基于分组实现)的消息队列
HyperLogLog(统计类型,简称HLL),高性能基数(去重)统计功能,存在极低误差率。
如何实现查询附近的人功能
使用GEO。
GEO是基于有序集合实现的
如何实现限流功能
滑动时间窗口
在一段时间(从当前时间往前取一定时间,如1分钟或者1小时)内最大处理一定数量的请求
漏桶
漏桶算法类似于生活中的漏斗,无论请求有多少,它都以均匀的速度慢慢流出。
当进入漏斗的速率大于流出漏斗的速率,漏斗就会慢慢变满(满时新来的请求就会被丢弃)
漏桶算法实现:
可以声明一个队列(漏斗)来保存请求,当队列容量满了就丢弃新来的请求
声明一个线程定期从该队列中取出一定数量的任务执行
令牌桶
令牌桶算法中有一个程序以某种恒定速度生成令牌,并存入令牌中,而每个请求需要线获取到令牌才能执行(若没有获取到令牌可以选择等待或者放弃)
redis分布式限流方案
redis4.0提供的Redis-Cell模块基于漏斗算法实现,并且提供了原子的限流指令
Redis-Cell 实现限流的方法也很简单,只需要使用一条指令 cl.throttle 即可,使用示例如下:
其中 15 为漏斗的容量,30 / 60s 为漏斗的速率。
Redis内存用完会怎么样
在redis中配置了最大使用内存,redis中内存用完通常是redis超过了其设置的最大内存,该值可以通过修改配置文件进行修改(如果设置为0,则表示没有内存大小限制,知道耗尽机器中所有内存为止,在64位操作系统下默认为0,在32位操作系统下默认值为3GB)
当内存用完之后(且最大内存配置不为0)就会触发redis内存淘汰策略
内存淘汰策略
redis4.0版本之后一共有8种:
noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-random:随机淘汰任意键值;
volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值;
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
allkeys-lfu:淘汰整个键值中最少使用的键值。
内存淘汰算法
LRU算法(最近最少使用的)
基于链表实现,最新操作的元素会被移动到表头,当内存需要淘汰时,移除链表尾部的元素
redis的近LRU算法
为了更好的节约内存,redis额外增加一个字段记录元素的最后一次时间,在内存淘汰时,会使用随机采样的方式来淘汰数据(默认取5个值,淘汰最久没有使用的那个元素)
LFU算法(最不常用的)
淘汰使用频次最小的元素; 基于双向链表与频次map实现,在内存淘汰时,移除最小频次链表中的最后一个元素节点
如何处理已过期的数据
在redis中维护了一个过期字典,会将所有已经设置了过期时间的键值对全部存储到此字典中
在获取键值时会先判断是否在过期字典中(是否设置过期时间),如果在则判断过期时间是否小于当前时间,如果不小于当前时间则说明已经过期了则删除键值并返回nil(默认的是惰性删除和定期删除)
过期数据删除策略
定时删除
在设置键值过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。优点:保证内存可以被尽快的释放;缺点:在redis高负载或大量过期键需要同时处理时,会造成redis服务卡顿,影响主业务执行
惰性删除
不主动删除过期键,每次从数据库获取键值时判断是否过期,如果过期则删除键值,并返回null。优点:因为每次访问时,才会判断过期键,所以此策略只会使用很少的系统资源;缺点:系统占用空间删除不及时,导致空间利用率低,造成了一定的空间浪费
定期删除
每隔一段时间检查一次数据库,随机删除一些过期键。redis默认每秒进行10次过期扫描,此配置可通过 redis的配置文件redis.conf配置字段hz
,redis每次扫描并不是通过遍历过期字典中的所有键,而是采用随机抽取判断并删除过期键。优点:通过限制删除操作的时长和频率,来减少删除操作对redis主业务的影响,同时也能删除一步分过期的数据减少了过期键对空间的无效占用;缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的资源少。
redis如何实现分布式锁
分布式锁可以通过incr
或set nx
实现
在操作某个资源时,第一次获取到锁的用户A执行incr lock
使得lock
的值变为1,此时(用户A未释放锁)如果用户B要操作该资源执行incr lock
使得lock
的值变为2,lock值不为1说明获取锁失败。当用户A事务完成使用del lock
释放锁
使用set key value nx
实现分布式锁,当使用命令创建键值成功(返回"OK")时说明加锁成功,否则加锁失败(返回"nil")
分布式死锁问题
当一个程序创建了分布式锁之后,因为某些特殊的原因导致程序意外退出,那么这个锁将永远不会被释放,就造成了死锁问题
一个简单的解决方法是给锁设置过期时间set key value nx ex 过期时间
。但是在极端的情况下这种方式还是存在问题:误删锁
误删锁问题
假设锁的最大超时时间是 30s,应用 1 执行了 35s,然而应用 2 在 30s,锁被自动释放之后,用重新获取并设置了锁,然后在 35s 时,应用 1 执行完之后,就会把应用 2 创建的锁给删除掉,如下图所示:
我们可以在锁的value值中写入一个唯一标识来区分锁的所属,这样来解决锁被误删的情况。
如何保证redis消息队列中的数据不丢失
使用stream实现消息队列,stream支持消息持久化,支持消息消费确认。
redis消息队列优缺点分析
List 优点:
消息可以被持久化,借助 Redis 本身的持久化 (AOF、RDB 或者是混合持久化),可以有效的保存数据;
消费者可以积压消息,不会因为客户端的消息过多而被强行断开。
List 缺点:
消息不能被重复消费,一个消息消费完就会被删除;
没有主题订阅的功能。
ZSet 优点:
支持消息持久化;
相比于 List 查询更方便,ZSet 可以利用 score 属性很方便的完成检索,而 List 则需要遍历整个元素才能检索到某个值。
ZSet 缺点:
ZSet 不能存储相同元素的值,也就是如果有消息是重复的,那么只能插入一条信息在有序集合中;
ZSet 是根据 score 值排序的,不能像 List 一样,按照插入顺序来排序;
ZSet 没有向 List 的 brpop 那样的阻塞弹出的功能。
Pub/Sub 优点:
提供了普通消息订阅模式和主题订阅模式。
Pub/Sub 缺点:
无法持久化保存消息,如果 Redis 服务器宕机或重启,那么所有的消息将会丢失;
发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
Stream 优点:
支持消息持久化;
支持消息消费确认。
Stream 缺点:
实现代码比较繁琐;
没有提供主题订阅的功能。
Redis如何实现延时队列
在redis中延迟消息队列(延时队列)一般是基于ZSet(有序集合)实现的。它的核心思想是在程序中开启一个循环的延迟任务检测器,用于检测以及执行延迟任务
基于ZSet实现延迟队列的方法有两种:
zrangebyscore 查询所有任务,此实现方式是一次性查询出所有的延迟任务,然后再进行执行
判断最早的任务,此实现方式是每次查询最早的一条任务,再与当前时间进行判断,如果任务执行时间大于当前时间则表示应该立即执行延迟任务
布隆过滤器的应用:在海量数据中查询一个值是否存在
使用布隆过期检索一个元素是否在一个集合中(有一定误差率,可以设置理想误差率,但是要保证误差率越低需要的空间也就越大)。
优点:消耗空间少,查询速度快
缺点:存在一定误差,不能删除元素
常见使用场景:
垃圾邮件过滤;
爬虫里的 URL 去重;
判断一个值在亿级数据中是否存在
常用redis优化手段
最有效的提供redis性能的方案就是在没有必要开启持久化的情况下,关闭持久化功能,这样每次对redis的操作就无需进行IO磁盘写入操作了,因此性能会提升很多;
缩短键值对的存储长度;
使用耗时长的命令,禁用慢查询;
更多见:[笔记:Redis核心原理与实战分析(下)]Redis性能优化方案
最后更新于
这有帮助吗?