Redis问答
1. 认识Redis
1.1 什么是Redis
Redis是一种基于内存的数据库,对数据的读写都在内存上操作,因此读写速度很快,常用于缓存、消息队列、分布式锁等的应用场景
Redis提供了很多数据类型来支撑各种场景,例如String、hash、list、set、zset、bitmapas、hyperloglog、GEO,对数据的操作都是原子性的,单线程过程,不存在锁的竞争
除此之外,Redis还支持事务、持久化、Lua脚本、集群方案、发布订阅模式等
1.2 Redis和Memcache有什么区别
Memcache也是基于内存的数据库,也可以用作缓存,和Redis一样都有过期策略,两者的性能都比较高
Redis优点:
- Redis支持的数据类型更多,比如String、list、set、zset、hash,Memcache只支持key-value类型的
- Redis支持持久化数据到磁盘,有利于数据恢复;Memcache没有持久化,重启或者宕机都会导致数据丢失
- Redis支持原生集群的模式,Memcache目前依赖客户端往集群中分片写入信息
- Redis还支持发布订阅模型、Lua脚本、事务等功能
1.3 为什么使用Redis作为MySQL的缓存
Redis属于高并发框架,具备高并发和高性能的特点
- 高性能:如果数据每次都是访问MySQL数据库,因为是从磁盘上读取数据,频繁的磁盘IO会导致程序非常慢,但是如果添加了Redis作为缓存,第一次访问时读取数据库然后存到缓存,后续就可以直接访问Redis,操作Redis是操作的内存,所以速度很快
- 高并发:单台Redis设备的QPS是MySQL的10倍,所以直接访问redis能承受的访问量是远大于MySQL的,所以会考虑部分数据缓存到Redis,并且Redis还有集群的模式
2. Redis的数据结构
1.1 Redis数据类型和使用场景
String、List、set、zset、hash
- String应用场景:于缓存对象、共享session、常规计数、分布式锁
- list应用场景:消息队列
- hash应用场景:缓存对象、购物车
- set:聚合计算场景,比如点赞、共同关注、抽奖活动
- zset:排序场景,比如排行榜
- BitMaps:二值状态统计的场景
- HyperLogLog:海量数据基数统计
- GEO:存储地理位置信息,例如滴滴叫车
1.2 Redis 数据类型内部实现
String:
String底层数据结构使用SDS(简单动态字符串),没有使用C语言字符串主要是
- SDS不仅能保存文本,还可以保存二进制数据(图片、音频、视频、压缩文件)
- SDS获取字符串长度的复杂度是O(1),因为SDS数据结构使用len属性记录了字符长度长度
- Redis的SDS API是安全的,不会出现缓冲区溢出,因为SDS在拼接字符串的时候都会检查空间是否足够,空间不够会自动扩容
List:
底层使用双向链表和压缩链表实现
当列表元素个数小于712,并且每个元素的大小不超过44字节时会使用压缩列表作为List类型的底层数据结构,否则使用双向链表
在Redis3.2以后list使用quickList代替压缩列表和双向列表
Set:
底层使用哈希表或者整数集合实现
当列表元素全是整数并且个数不超过712时使用的是整数集合作为Set的底层数据结构,否则使用哈希表
Zset
底层使用压缩列表或者跳表实现
当列表元素不超过128并且所又元素大小不超过44时使用压缩列表作为zset的数据结构,否则用跳表实现
Redis7.0以后压缩列表数据结构已经废弃,使用listPack数据结构实现
3. Redis线程模型
3.1 Redis是单线程的吗
Redis单线程指的是【接收客户端请求→解析请求→查询数据→返回客户端】这个操作是由一个主线程完成
但是Redis程序并不是单线程的,Redis程序在启动时还会启动后台线程
- Redis在2.4版本会启动两个后台线程分别处理关闭文件和AOF 刷盘任务
- Redis4.0以后新新增了后台线程用于释放Redis内存
Redis之所以使用另外的线程执行关闭文件、AOF刷盘、释放内存是因为这些操作是很费时间的,如果放在主线程里面会阻塞Redis,那么Redis的高性能就会受到影响
后台线程相当于一个任务队列,把耗时的工作都扔到任务队列里面,让消费者(BIO)不断地轮询这个队列,拿到任务执行方法,关闭文件、AOF刷盘、释放内存都有自己的任务队列
3.2 Redis使用单线程为什么还是那么快
Redis单机的QPS可以达到10w,性能是MySQL的10倍,主要原因有:
- 完全基于内存的操作
- 基于C语言的实现,优化过数据结构,使用的都是高性能的实现
- 单线程也避免了线程竞争带来的死锁和线程切换造成的时间和性能开销
- Redis使用非阻塞的IO多路复用机制
3.3 Redis4.0之前为什么使用单线程
- Redis的大部分操作都在内存中操作的,并且使用了高效的数据结构,因此Redis瓶颈可能是机器内存或者网络带宽,而不是CPU,既然CPU不是瓶颈自然就采用单线程的解决方案
- 使用单线程避免了线程的竞争,减少线程切换产生的时间和性能开销,也可以避免死锁
3.4 Redis4.0之后为什么又引入多线程
Redis使用多线程并非摒弃单线程,Redis还是使用单线程模型处理客户端请求,只是使用多线程处理数据的读写和协议解析,执行命令还是单线程的
这样做的目的是因为Redis的瓶颈在于网络IO并非CPU,使用多线程可以提高IO读写效率,从而提高Redis的性能
4. Redis过期删除和内存淘汰
Redis是基于内存操作的,内存资源有限,所以对于Redis来说内存资源非常宝贵,所以Redis有过期删除和内存淘汰机制
4.1 常见的删除策略
- 定时删除:创建key时设置过期时间,并创建一个定时器,定时器在键的过期时间快到时执行删除操作
- 定期删除:创建key时设置key的过期时间,Redis会在没隔100s抽取一些设置了过期时间的key进行检查
- 惰性删除:在获取key的时候对键进行过期检查,如果过期就直接删除并且不返回
4.1 Redis的过期删除策略有哪些
定期删除+惰性删除
定期删除
定期删除就是设置了键的过期时间,Redis会每隔100s抽取一些设置了过期时间的key检查是否过期,因为是抽取一部分所以仍然存在一部分键没有检查到,此时走惰性删除策略
惰性删除
在获取key的时候检查key是否过期,过期的话就直接删除并且不返回;仍然存在一部分key未被检查和访问,此时走内存淘汰机制
4.2 内存淘汰策略有哪些
- noevication:不淘汰任何数据,内存不足时新写入数据报错
- allkeys-lru:在键空间中移除最近最少使用的key
- volatile-lru:在设置了过期时间的key中移除最近最少使用的key
- allkeys-lfu:在键空间中,使用lfu算法淘汰键
- volatile-lfu:在设置了过期时间的键空间中,使用lfu算法淘汰键
- allkeys-random:在所有键空间中随机移除某个key
- volatile-random:在设置了过期时间的可以中随机移除某个key
- volatile-ttl:在设置了过期时间key中,越早过期的先淘汰
4.3 LRU算法和LFU算法有什么区别
LRU算法
表示最近很少使用,基于链表实现的,元素按照操作顺序从前往后,操作过的数据会被移到链表头部,所以需要进行内存淘汰时只需要删除链表尾部的数据
LFU算法
表示最近最少使用,与key的访问次数有关,其思想是根据key的访问频率进行淘汰,比较少访问的key先淘汰
4.4 Redis内存满了会发生什么
- 读性能下降
- 写操作失败
- 触发数据淘汰策略
- 内存严重不足时,Redis进程会崩溃:过多释放key,导致性能下降,影响QPS
解决方案
- 设置过期删除策略
- 配置内存淘汰策略
- 进行监控和警告机制
- 调整缓存的内存容量
4.7 RDB对过期键会如何处理
生成RDB文件
在生成RDB文件时,程序会检查键是否过期,过期的键不会被载入到RDB文件
载入RDB文件
- 如果是主服务器,在载入RDB文件时会检查键,过期的键会被忽略
- 如果是从服务器,会保存所有键(因为主服务在数据同步的时候会清空从服务器的数据,所以一般情况下从服务器载入了过期键也没关系)
4.4 AOF对过期键会如何处理
AOF文件写入
如果程序开启了AOF持久化模式,那么当有键被定期删除或者惰性删除时,程序会向AOF追加一条del命令
- 从数据删除key
- 追加一条del key的命令到AOF文件
- 向执行删除命令的客户端返回空
AOF文件重写
在执行AOF文件重写时会检查键是否过期,过期的键不会保存到重写后的AOF文件中
4.7 Redis主从模式中,对过期键如何处理
在主从复制模式下,从服务器的过期键删除动作由主服务器控制
- 从服务器在执行客户端发出的读命令时,即使发现键已经过期了,也不会进行删除,会正常返回
- 从服务器只有接收到主服务器的del命令才会删除键
- 主服务在检查到键已经过期以后,会对所有从服务器发送一条del命令,告知删除过期键
5.Redis的高并发和高可用
Redis的高并发主要依赖于主从架构,单主用来写数据,多从用于查数据;如果要在高并发的同时容纳海量数据,可以加上集群
Redis的高可用可以在主从架构的基础上加上哨兵模式
5.1 主从架构
单机支持的QPS一般用于支持读的高并发,所以做成主从模式,主节点用于写数据,并且同步到其他从服务器,从节点用来处理读请求,这样很容易实现了水平扩容,支撑高并发请求
5.2 哨兵模式
为了应对主服务器宕机,可以只用哨兵模式监控主从服务器,提供一定的容灾恢复
5.3 集群
哨兵模式归根结底还是基于主从模式增加slave节点来拓展读并发能力,但是不能拓展写和存储能力,所以引入集群模式,将数据分布在不同的服务器上,降低系统对单主节点的依赖,从而提高Redis的读写性能和海量数据存储
6. Redis持久化
6.1 Redis如何实现数据不丢失
Redis是基于内存操作的,为了保证数据不丢失,需要将数据持久化到磁盘,这样重启服务器时可以恢复数据,Redis的持久化方式有三种:RDB、AOF、混合持久化
- RDB快照(RedisDatabase):记录某一时刻内存数据,以二进制文件的方式写到磁盘
- AOF文件(Append Only File):记录所有的操作命令,以文本形式追加到文件
- 混合持久化:结合RDB和AOF的优点进行持久化
6.2 AOF日志如何实现的
采用写后日志的方式,先执行命令将数据写到内存,再记录操作命令,因为不对命令进行语法检查,所以只对执行成功的操作进行记录
优点
- 每隔1s执行一次,数据保存更加完整
- 以append-only的方式写入,没有寻址开销,不会影响正常的读写
- 有利于灾难性的紧急恢复例如flushAll命令
缺点
- 对于同一份数据,通常AOF文件会大于RDB文件
- 因为是每秒执行一次,支持的写QPS会比RDB支持的写QPS小
6.3 RDB快照如何实现的
数据快照对数据进行周期化持久化,有save和bgsave两种命令方式,save命令在主线程中执行,会造成阻塞,bgsave命令会创建子线程操作,避免了对主线程的阻塞,这也是Redis的默认配置
优点
- RDB数据快照一般是7分钟执行一次,丢失的数据比AOF多一点
- 会生成多个数据文件,适合做数据的冷备份
- RDB对于Redis的读写任务影响非常小,可以保持高性能,因为它可以fork一个子进程进行RDB的磁盘IO持久化
缺点
- 每隔7秒执行一次,丢失的数据会比AOF更多
- 子进程在执行数据快照时,如果文件特别大可能会暂停对客户端提供服务数毫秒甚至数秒
6.4 为什么要使用混合持久化
因为RDB和AOF各有优点和缺点,结合起来使用可以达到持久化的→_→
- 不要仅仅使用RDB备份,因为最大会丢失7秒的数据
- 也不仅仅使用AOF作为备份,因为AOF做冷备份没有RDB做冷备份来的回复速度快,而且RDB作为数据快照,可以避免AOF复杂的备份和恢复bug
- Redis支持开启两种备份,AOF可以保证数据不丢失,作为数据恢复的第一选择;RDB用作不同程度的冷备份,当AOF文件丢失或损坏不可用的时候,使用RDB快速恢复数据
7. Redis缓存设计
7.1 什么是热点Key?热Key问题如何解决?
热Key问题指的是突然有十几万的请求突然访问同一个Key,流量过于密集达到物理网卡的上线,导致这台机器宕机引发缓存雪崩
解决方案
- 数据分片,提前将key的数据打散到不同的服务器上,降低单个服务器的压力
- 缓存淘汰策略,防止缓存击穿
- 使用布隆过滤器,合并请求
- 限流和降级,对于热点Key进行限流和降级操作
- 使用二级缓存,提前加载热数据到内存,如果Redis宕机,走内存查询
7.2 如何避免缓存击穿、缓存穿透、缓存雪崩
缓存击穿
缓存击穿的概念就是单个key的并发量过高,当这个key过期所有请求同时进入db,与热点key不同的是,缓存击穿啊的点在于过期导致的请求进入db
解决方案:
- 加锁更新,例如请求A进来发现缓存没有,对这个key加锁,然后查询数据库,并存入缓存,再返回到用户,这样后续的请求就会查询缓存(互斥锁:缓存失效时不是立即查询数据库,而是利用setnx命令尝试加锁。因为sestnx命令只有key不存在时才会设置成功,如果设置成功就去请求数据库更新到缓存,否则就去重试获取缓存;这样可以保证大量的命令只有一个会请求到数据库,其余的请求redis)
- 将过期时间写入到value中,通过异步的方式刷新过期时间,防止击穿出现
缓存穿透
顾名思义就是直接透过缓存到达数据库,当查询不存在的key时,这个请求就会直接到db
解决方案
- 业务层加上参数判断,对于明显错误的参数进行拦截
- 缓存空值:当查询的数据在缓存和数据库都不存在时,缓存一个空值到redis,可以设置较短的过期时间,后续的请求就不会打到db
- 布隆过滤器:布隆过滤器是一种数据结构,利用极小的内存,可以判断大量数据"一定不存在或者可能存在";布隆过滤器的原理是在存入数据的时候通过散列函数将数据哈希到bitmap中,把他们置为1,如果一个数据不在bitmap中,那么这个数据也不会在数据集合中,所以当请求A进来时,布隆过滤器返回0,那么这个数据就是不存在的,就不再访问数据库
缓存雪崩
缓存大量失效的情况,例如Redis宕机或key同时失效造成的缓存不可用
解决方案:
- 设置不同的过期时间,避免缓存的集体失效
- Redis集群模式+哨兵模式保持Redis的高可用,节点宕机时切换节点
- 限流和降级,Redis宕机时避免过多的请求进入DB
- 二级缓存,数据提前加热到内存,Redis服务不可用时走内存查询
- 数据持久化,RDB和AOF恢复数据
7.3 说说常见的缓存更新策略
- 先更新缓存,再更新数据库
- 先更新数据库,再更新数据库
- 先删缓存,再更新数据库
- 先更新数据库,再删缓存
7.4 如何保证缓存和数据库数据的一致性
不管是先写缓存还是先写数据库,如果后一步在出现网络问题啊更新失败或者在高并发情况下,都有可能出现数据库不一致的情况,所以理论上不更新缓存而是删除缓存
-
先删缓存再写数据库:这种情况容易出现A写数据的操作还未完成,B读请求将旧数据读取再次存到缓存,此时写数据操作完成,就出现了数据不一致,所以使用双删,在写数据成功后再删一次缓存,但是要在写数据后隔一段时间再删,防止B已经读取到旧数据但是还未写到缓存导致第二次删缓存是失败的
-
先写数据再删缓存:与先删缓存的情况一致,容易出现最后一步删缓存失败的情况,可以开启重试机制,删缓存失败时重试三次,三次还是失败记录下来后续再处理
- 定时任务重试,属于异步重试,删除失败的记录到数据库,异步线程性能高,虽然实时性没那么高,但是可以保证数据落库不丢失
- 删除失败时发送到mq服务器,mq消费者拿到数据进行重试五次,还是失败就放到死信队列,rocketMQ支持重试机制和死信队列,也是异步执行的,并且实时性高
8. Redis实战
8.1 Redis如何实现延迟队列
-
利用 Redis 过期消息实现延时队列
该方法是基于 Redis key 的过期通知特性。当一个 key 过期时,如果配置了Redis 的 key 过期通知功能,Redis 会发布一个消息到特定的 Channel,应用程序可以订阅这个 Channel 来接收过期事件,进而触发相应的业务处理逻辑。
-
使用zset数据类型,zset有存储score和value,将任务时间作为score,然后另起一个线程,该线程会周期性地从zset中取出score最小(即最早要执行的)的任务
-
Redisson 提供的延迟队列(
Delayed Queue
)是它基于Redis 的发布/订阅机制
和zset
实现的一种高级数据结构,用于处理需要延迟执行的任务。
8.2 Redis的大Key如何处理
- 拆分:拆分成多个key-value形式的小key,value可以进行分片
- 压缩:使用合适的序列化框架和压缩算法
- 删除:大Key非热点数据可以删除缓存,直接使用数据库访问
8.3 Redis管道有什么用
- 提高数据处理速度:多个操作一次性完成,减少单个命令执行的内存开销
- 减少网络通信:多个命令一起打包发送到Redis,减少了网络IO
- 支持原子性操作和事务技术
8.4 Redis支持事务回滚吗
支持事务,通过MULTI和EXEC实现,但是不支持事务回滚,一旦执行EXEC,所有命令会按照顺序执行,如果事务中的某个命令执行失败,那么这个命令和事务中的其他命令都不会执行,但在发生错误之前已经执行的命令是不会发生回滚的
Redis支持高效,所以不会有事务回滚,因为支持事务回滚,Redis的简单高效会受到影响
8.5 如何使用Redis实现分布式锁
-
setnx+expire:抢占锁+锁过期时间:不是原子操作
-
setnx+value(当前时间+过期时间):处理发生异常时锁得不到释放的情况,并且可能被别的客户端请求覆盖释放
-
lua脚本:实现setnx和expire操作的原子性
-
SET的扩展命令(SET EX PX NX)
除了使用,使用Lua脚本,保证
SETNX + EXPIRE
两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]
),它也是原子性的!❝
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
-
SET EX PX NX + 校验唯一随机值,再删除:解决误删问题
-
使用现成的Redison框架:解决了「锁过期释放,业务没执行完」问题
-
多机实现的分布式锁Redlock+Redisson:应对多个Redis节点
性能和高效:使用4或5
安全性和可靠性:使用5或7
功能和易用性:使用6