Redis总篇(全)

180 阅读45分钟

1、什么是Redis,Redis有哪些特点?

Redis全称为:Remote Dictionary Server(远程数据服务),Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。

  • 特点1:丰富的数据类型

  • 传统SQL数据库处理二维关系数据;

  • MemCached数据库,键和值都是字符串;

  • 文档数据库(MongoDB)是由Json/Bson组成的文档。

  • 特点2:内存存储

  • 一种是硬盘数据库

  • 硬盘数据库是把值存储在硬盘上,在内存中就存储一下索引,当硬盘数据库想访问硬盘的值时,它先在内存里找到索引,然后再找值。问题在于,在读取和写入硬盘的时候,如果读写比较多的时候,它会把硬盘的IO功能堵死。

  • 一种是内存数据库

  • 内存存储是讲所有的数据都存储在内存里面,数据读取和写入速度非常快。

  • 特点3:持久化功能

  • 将数据存储在内存里面的数据保存到硬盘中,保证数据安全,方便进行数据备份和恢复(RDB和AOF)。

  • 特点4:速度快

  • 完全基于内存操作

  • C语言实现,优化过数据结构,基于几种基础的数据类型,redis做了大量优化,性能极高

  • 使用单线程,无上下文切换操作

  • 基于非阻塞的IO多路复用

2、Redis有哪些数据结构?

Redis是key-value数据库,key的类型只能是String,但是value的数据类型就比较丰富了,主要包括

五种基础数据结构:

  • string

  • hash

  • list

  • set

  • 是一种特殊的value为空的hash

  • 或者是整数数组 intset

  • zset

三种高级数据结构:

  • bitmap

  • bitmap并不是一种真实的数据结构,它本质上是String,只不过颗粒度变成了位,即bit。

  • GOE

  • GOE数据结构可以在redis种存储地理坐标,它本质上借助ZSET并使用GeoHash技术进行填充。将二维平面坐标转换为一维编码值,然后可以依靠

  • HyperLogLog

  • 计算唯一事物的概率数据结构

基于的是以下这些基本数据类型:

  • **字符串:**Redis是C语言开发的,但是底层存储不是使用的C语言的字符串类型,而是自己开发了一种数据类型动态字符串SDS进行存储,SDS保存了长度信息。三种编码格式(int-数字、row-长字符串、embstr-短字符串)

  • 这样的话可以以O(1)的复杂度获取字符串长度

  • 杜绝缓冲区溢出C字符串当进行字符串复制的时候,如果分配内存不够,就可能产生缓冲区溢出。当SDS需要修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动扩容到所需的大小,然后再执行实际的修改。

  • 减少修改字符串时带来的内存重分配次数。

  • **链表linkedlist:**Redis链表是一个双向无环链表结构,很多发布订阅、慢查询、监视器功能都是使用了链表来实现。

  • **字典hashtable:**用于保存键值对的数据结构。redis使用hash表作为底层实现,每个字典带两个hash表,供平时使用和rehash时使用。

  • 渐进式rehash:如果hash表中有很多元素的话,一次性将这些键全部rehash,可能会导致服务器再一段时间内停止服务。渐进式rehash会有一个rehashidx,把每一次rehash的操作分摊到每一个的访问中,避免集中rehash带来的庞大计算量。

  • 为h[1]分配空间,让字典同时拥有h[0]和h[1]两个哈希表

  • 将rehashidx设为0,开始rehash

  • 在rehash期间,每次对字典的CRUD中都会执行一个rehash

  • 随着字典操作的不断执行,最终会在某一时间段上将所有的键值对都rehash完成

  • 对比java中的hashmap,当数据量到达阈值的时候(0.75),就会发生rehash,将hash表长度变为原来的两倍,然后全部重新计算hash地址。

  • 跳表skiplist:跳表是有序集合的底层实现之一。

  • 因为有序列表查找需要O(N)的复杂度,间隔着的节点会有多个指针指向更后的节点,一般平均1.33,是的查找的时候复杂度和二分一样O(logn)。

  • skiplist和平衡树、哈希表比较

  • skiplist和平衡树是有序的,哈希表无序

  • 范围查找平衡树更复杂,插入和删除操作平衡树更复杂,可能导致子树的调整。

  • 内存占用上,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

  • 实现上来讲skiplist更容易实现。

  • 整数集合intset:用于保存整数值的集合抽象数据结构。

  • 压缩列表ziplist:

  • 是一种时间换空间的数据结构,类似链表,但是ziplist不维护前后节点的位置,指维护上一个节点的长度和当前节点的长度。每次通过长度计算

(1)String字符串

语法

SET KEY_NAME VALUE

使用场景:

  • 记录用户的访问次数,或者浏览商品的浏览次数。
  • 缓存信息读取(配置,用户信息)

(2)Hash哈希

语法

HSET KEY_NAME FIELD VALUE

Redis hash 是一个键值(key=>value)对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

(3)List列表

语法

//在 key 对应 list 的头部添加字符串元素LPUSH KEY_NAME VALUE1.. VALUEN//在 key 对应 list 的尾部添加字符串元素RPUSH KEY_NAME VALUE1..VALUEN//对应 list 中删除 count 个和 value 相同的元素LREM KEY_NAME COUNT VALUE//返回 key 对应 list 的长度LLEN KEY_NAME

Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)

(4)Set集合

语法

SADD KEY_NAME VALUE1...VALUEn

Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

(5)Sorted Set有序集合

语法

ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。

redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

3、一个字符串类型的值能存储最大容量是多少?

查询官方文档(redis.io/topics/data…

4、能说一下Redis每种数据结构的使用场景吗?

  • String

  • 字符串类型的使用场景:信息缓存、计数器、分布式锁等等。

  • Hash

  • 对象

  • List

  • 列表本质是一个有序的,元素可重复的队列。

  • 定时排行榜

  • Set

  • 收藏夹

  • Sorted Set

  • 有序集合的特点是有序,无重复值。

  • 实时排行榜

5、Redis如何做持久化的?能说一下RDB和AOF的实现原理吗?

RDB持久化

RDB(Redis Database)持久化是把当前内存数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发自动触发

(1)手动触发

手动触发对应save命令,会阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。

(2)自动触发

自动触发对应bgsave命令,Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。

在redis.conf配置文件中可以配置:

save <seconds> <changes>

表示xx秒内数据修改xx次时自动触发bgsave。如果想关闭自动触发,可以在save命令后面加一个空串,即:

save ""

还有其他常见可以触发bgsave,如:

  • 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。

  • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。

bgsave工作机制

(1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在,bgsave命令直接返回。

(2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒

(3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。

(4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。

(5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb_*相关选项。

-- RDB持久化完 --

AOF持久化

AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

AOF持久化工作机制

开启AOF功能需要配置:appendonly yes,默认不开启。

AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。

AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)。

(1)所有的写入命令会追加到aof_buf(缓冲区)中。

(2)AOF缓冲区根据对应的策略向硬盘做同步操作。

AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

(3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

(4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。

AOF重写(rewrite)机制

重写的目的:

  • 减小AOF文件占用空间;

  • 更小的AOF 文件可以更快地被Redis加载恢复。

AOF重写可以分为手动触发和自动触发:

  • 手动触发:直接调用bgrewriteaof命令。

  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

AOF文件重写后为什么会变小?

(1)旧的AOF文件含有无效的命令,如:del key1, hdel key2等。重写只保留最终数据的写入命令。

(2)多条命令可以合并,如lpush list a,lpush list b,lpush list c可以直接转化为lpush list a b c。

AOF文件数据恢复

数据恢复流程说明:

(1)AOF持久化开启且存在AOF文件时,优先加载AOF文件。

(2)AOF关闭或者AOF文件不存在时,加载RDB文件。

(3)加载AOF/RDB文件成功后,Redis启动成功。

(4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

-- AOF持久化完 --

RDB和AOF的优缺点

RDB优点

  • RDB 是一个非常紧凑的文件,它保存了某个时间点的数据集,非常适用于数据集的备份,比如你可以在每个小时保存一下过去24小时内的数据,保存一个月,可以根据需求恢复不同版本的数据。

  • RDB 是一个紧凑的单一文件,很方便传送到另一个远端数据中心,非常适用于灾难恢复。

  • RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 Redis 的性能。

  • 与AOF相比,在恢复大的数据集的时候,RDB 方式会更快一些。

AOF优点

  • AOF可以使用默认的每秒fsync,也可更改每次写的时候fsync。fsync是由后台线程进行处理的,一旦出现故障最多丢失一秒数据。

  • AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复。

  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写,来缩小AOF文件。

  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂,某些异常情况,可以进行手动修改,比如误删除命令。

RDB缺点

  • Redis 要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在 Redis 意外宕机,你可能会丢失几分钟的数据。

  • RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候, fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。

AOF缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

  • 数据恢复(load)时AOF比RDB慢,通常RDB 可以提供更有保证的最大延迟时间。

RDB和AOF简单对比总结

RDB优点:

  • RDB 是紧凑的二进制文件,比较适合备份,全量复制等场景

  • RDB 恢复数据远快于 AOF

RDB缺点:

  • RDB 无法实现实时或者秒级持久化;

  • 新老版本无法兼容 RDB 格式。

AOF优点:

  • 可以更好地保护数据不丢失;

  • appen-only 模式写入性能比较高;

  • 适合做灾难性的误删除紧急恢复。

AOF缺点:

  • 对于同一份文件,AOF 文件要比 RDB 快照大;

  • AOF 开启后,会对写的 QPS 有所影响,相对于 RDB 来说 写 QPS 要下降;

  • 数据库恢复比较慢, 不合适做冷备。

6、讲解一下Redis的线程模型?

redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

如果面试官继续追问为啥 redis 单线程模型也能效率这么高?

  • 纯内存操作

  • 核心是基于非阻塞的 IO 多路复用机制

  • 与之对应的是传统IO使用的的read函数是阻塞IO。

  • 导致服务端读取一个数据会阻塞在accept(接受链接)和read函数上,read函数会有两步”网卡拷贝到内核缓冲区“->”内核缓冲区拷贝到用户缓冲区“

  • 方案一:多线程处理,但是read还是阻塞的,而且服务器线程资源很容易耗光

  • 方案二:非阻塞read,如果数据没有到内核缓冲区返回 -1,循环调用read,但是会有很多浪费的系统调用

  • 方案三:使用select或poll(没由1024个文件限制)函数,把文件描述符的数组发给操作系统,由操作系统遍历。

  • 需要拷贝一份给内核,高并发情况下消耗资源(可优化为不复制)

  • select依然是通过遍历(可优化为异步事件通知)

  • select仅返回文件描述符个数,还是要遍历调用(可优化为直接返回文件描述符)

  • 最终优化版本操作系统提供epoll接口,是select和poll的增强版,解决了三个不足

  • 内核中保存了一份文件描述符集合,无需用户每次传入,只需告诉内核修改部分即可

  • 内核不在通过轮询的方式找到文件描述符,而是通过异步IO事件唤醒

  • 内核仅会将IO事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

  • 多路复用

  • 单线程反而避免了多线程的频繁上下文切换问题

  • IO多路复用其实指的就是内核一旦发现进程指定的一个或者多个IO准备读取,就通知该进程,epoll和select都可以提供IO复用的解决方案。

传统IO伪代码:

listenfd = socket();   // 打开一个网络通信端口bind(listenfd);        // 绑定listen(listenfd);      // 监听while(1) {  connfd = accept(listenfd);  // 阻塞建立连接  int n = read(connfd, buf);  // 阻塞读数据  doSomeThing(buf);  // 利用读到的数据做些什么  close(connfd);     // 关闭连接,循环等待下一个连接}

7、缓存雪崩、缓存穿透、缓存预热、缓存击穿、缓存降级的区别是什么?

在实际生产环境中有时会遇到缓存穿透、缓存击穿、缓存雪崩等异常场景,为了避免异常带来巨大损失,我们需要了解每种异常发生的原因以及解决方案,帮助提升系统可靠性和高可用。

(1)缓存穿透

什么是缓存穿透?

缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。

如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至击垮数据库系统。

缓存穿透常用的解决方案

(1)布隆过滤器(推荐)

布隆过滤器(Bloom Filter,简称BF)由Burton Howard Bloom在1970年提出,是一种空间效率高的概率型数据结构。

布隆过滤器专门用来检测集合中是否存在特定的元素。

如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率:

  • 采用线性表存储,查找时间复杂度为O(N)

  • 采用平衡二叉排序树(AVL、红黑树)存储,查找时间复杂度为O(logN)

  • 采用哈希表存储,考虑到哈希碰撞,整体时间复杂度也要O[log(n/m)]

当需要判断一个元素是否存在于海量数据集合中,不仅查找时间慢,还会占用大量存储空间。接下来看一下布隆过滤器如何解决这个问题。

布隆过滤器设计思想

布隆过滤器由一个长度为m比特的位数组(bit array)与k个哈希函数(hash function)组成的数据结构。位数组初始化均为0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。

当要向布隆过滤器中插入一个元素时,该元素经过k个哈希函数计算产生k个哈希值,以哈希值作为位数组中的下标,将所有k个对应的比特值由0置为1。

当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的k个比特值:如果有任意一个比特为0,表明该元素一定不在集合中;如果所有比特均为1,表明该集合有可能性在集合中。为什么不是一定在集合中呢?因为不同的元素计算的哈希值有可能一样,会出现哈希碰撞,导致一个不存在的元素有可能对应的比特位为1,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

总结一下:布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,可能在也可能不在集合中。

举个例子:下图是一个布隆过滤器,共有18个比特位,3个哈希函数。集合中三个元素x,y,z通过三个哈希函数散列到不同的比特位,并将比特位置为1。当查询元素w时,通过三个哈希函数计算,发现有一个比特位的值为0,可以肯定认为该元素不在集合中。

布隆过滤器优缺点

优点:

  • 节省空间:不需要存储数据本身,只需要存储数据对应hash比特位

  • 时间复杂度低:插入和查找的时间复杂度都为O(k),k为哈希函数的个数

缺点:

  • 存在假阳性:布隆过滤器判断存在,可能出现元素不在集合中;判断准确率取决于哈希函数的个数

  • 不能删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是造成假阳性的原因了

布隆过滤器适用场景

  • 爬虫系统url去重

  • 垃圾邮件过滤

  • 黑名单

(2)返回空对象

当缓存未命中,查询持久层也为空,可以将返回的空对象写到缓存中,这样下次请求该key时直接从缓存中查询返回空对象,请求不会落到持久层数据库。为了避免存储过多空对象,通常会给空对象设置一个过期时间。

这种方法会存在两个问题:

  • 如果有大量的key穿透,缓存空对象会占用宝贵的内存空间。

  • 空对象的key设置了过期时间,在这段时间可能会存在缓存和持久层数据不一致的场景。

(2)缓存击穿

什么是缓存击穿?

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

缓存击穿危害

数据库瞬时压力骤增,造成大量请求阻塞。

如何解决?

方案一:使用互斥锁(mutex key)

这种思路比较简单,就是让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可。

同一时间只有一个线程读数据库然后回写缓存,其他线程都处于阻塞状态。如果是高并发场景,大量线程阻塞势必会降低吞吐量。这种情况如何解决?大家可以在留言区讨论。

如果是分布式应用就需要使用分布式锁。

方案二:热点数据永不过期

永不过期实际包含两层意思:

  • 物理不过期,针对热点key不设置过期时间

  • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建

从实战看这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,对于不追求严格强一致性的系统是可以接受的。

(3)缓存雪崩

什么是缓存雪崩?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

缓存雪崩解决方案

常用的解决方案有:

  • 均匀过期

  • 加互斥锁

  • 缓存永不过期

  • 双层缓存策略

(1)均匀过期

设置不同的过期时间,让缓存失效的时间点尽量均匀。通常可以为有效期增加随机值或者统一规划有效期。

(2)加互斥锁

跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队。

(3)缓存永不过期

跟缓存击穿解决思路一致,缓存在物理上永远不过期,用一个异步的线程更新缓存。

(4)双层缓存策略

使用主备两层缓存:

主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值。

备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。

(4)缓存预热

什么是缓存预热?

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。

如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

缓存预热的操作方法

  • 数据量不大的时候,工程启动的时候进行加载缓存动作;

  • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;

  • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。

(5)缓存降级

缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。

在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。

降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

8、Redis的内存淘汰机制

Redis内存淘汰策略是指当缓存内存不足时,通过淘汰旧数据处理新加入数据选择的策略。

如何配置最大内存?

(1)通过配置文件配置

修改redis.conf配置文件

maxmemory 1024mb //设置Redis最大占用内存大小为1024M

注意:maxmemory默认配置为0,在64位操作系统下redis最大内存为操作系统剩余内存,在32位操作系统下redis最大内存为3GB。(2)通过动态命令配置

Redis支持运行时通过命令动态修改内存大小:

127.0.0.1:6379> config set maxmemory 200mb //设置Redis最大占用内存大小为200M127.0.0.1:6379> config get maxmemory //获取设置的Redis能使用的最大内存大小1) "maxmemory"2) "209715200"

淘汰策略的分类

Redis最大占用内存用完之后,如果继续添加数据,如何处理这种情况呢?实际上Redis官方已经定义了八种策略来处理这种情况:

  • 不淘汰

  • 随机淘汰

  • 最早过期淘汰

  • 最近最少使用淘汰 LRU

  • redis使用的是近似LRU算法:随机采样淘汰,范围越大约准确

  • redis新算法会维护一个候选池(大小为16)

  • 最少使用频率淘汰

  • redis 4.0 新增

noeviction

默认策略,对于写请求直接返回错误,不进行淘汰。

allkeys-lru

lru(less recently used), 最近最少使用。从所有的key中使用近似LRU算法进行淘汰。

volatile-lru

lru(less recently used), 最近最少使用。从设置了过期时间的key中使用近似LRU算法进行淘汰。

allkeys-random

从所有的key中随机淘汰。

volatile-random

从设置了过期时间的key中随机淘汰。

volatile-ttl

ttl(time to live),在设置了过期时间的key中根据key的过期时间进行淘汰,越早过期的越优先被淘汰。

allkeys-lfu

lfu(Least Frequently Used),最少使用频率。从所有的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。

volatile-lfu

lfu(Least Frequently Used),最少使用频率。从设置了过期时间的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。

注意:当使用volatile-lru、volatile-random、volatile-ttl这三种策略时,如果没有设置过期的key可以被淘汰,则和noeviction一样返回错误。

LRU算法

LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据。这个时候就可以使用LRU算法了。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

LRU在Redis中的实现

Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。近似LRU算法通过随机采样法淘汰数据,每次随机出5个(默认)key,从里面淘汰掉最近最少使用的key。

可以通过maxmemory-samples参数修改采样数量, 如:maxmemory-samples 10

maxmenory-samples配置的越大,淘汰的结果越接近于严格的LRU算法,但因此耗费的CPU也很高。

Redis为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。

Redis3.0对近似LRU的优化

Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。

当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。

LFU算法

LFU(Least Frequently Used),是Redis4.0新加的一种淘汰策略,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。

LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。

9、Redis有事务机制吗?

有事务机制。Redis事务生命周期:

  • 开启事务:使用MULTI开启一个事务

  • 命令入队列:每次操作的命令都会加入到一个队列中,但命令此时不会真正被执行

  • 提交事务:使用EXEC命令提交事务,开始顺序执行队列中的命令

10、Redis事务到底是不是原子性的?

先看关系型数据库ACID 中关于原子性的定义:

**原子性:**一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

官方文档对事务的定义:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。EXEC 命令负责触发并执行事务中的所有命令:如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。

官方认为Redis事务是一个原子操作,这是站在执行与否的角度考虑的。但是从ACID原子性定义来看,严格意义上讲Redis事务是非原子型的,因为在命令顺序执行过程中,一旦发生命令执行错误Redis是不会停止执行然后回滚数据。

11、Redis为什么不支持回滚(roll back)?

在事务运行期间虽然Redis命令可能会执行失败,但是Redis依然会执行事务内剩余的命令而不会执行回滚操作。如果你熟悉mysql关系型数据库事务,你会对此非常疑惑,Redis官方的理由如下:

只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。支持事务回滚能力会导致设计复杂,这与Redis的初衷相违背,Redis的设计目标是功能简化及确保更快的运行速度。

对于官方的这种理由有一个普遍的反对观点:程序有bug怎么办?但其实回归不能解决程序的bug,比如某位粗心的程序员计划更新键A,实际上最后更新了键B,回滚机制是没法解决这种人为错误的。正因为这种人为的错误不太可能进入生产系统,所以官方在设计Redis时选用更加简单和快速的方法,没有实现回滚的机制。

12、Redis事务相关的命令有哪几个?

(1)WATCH

可以为Redis事务提供 check-and-set (CAS)行为。被WATCH的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

(2)MULTI

用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行,而是被放到一个队列中,当 EXEC命令被调用时, 所有队列中的命令才会被执行。

(3)UNWATCH

取消 WATCH 命令对所有 key 的监视,一般用于DISCARD和EXEC命令之前。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

(4)DISCARD

当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空,并且客户端会从事务状态中退出。

(5)EXEC

负责触发并执行事务中的所有命令:

如果客户端成功开启事务后执行EXEC,那么事务中的所有命令都会被执行。

如果客户端在使用MULTI开启了事务后,却因为断线而没有成功执行EXEC,那么事务中的所有命令都不会被执行。需要特别注意的是:即使事务中有某条/某些命令执行失败了,事务队列中的其他命令仍然会继续执行,Redis不会停止执行事务中的命令,而不会像我们通常使用的关系型数据库一样进行回滚。

13、什么是Redis主从复制?

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

主从复制的作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

  • 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从复制实现原理

主从复制过程主要可以分为3个阶段:连接建立阶段、数据同步阶段、命令传播阶段。

  • 从服务器向主服务器发起SYNCPSYNC 命令

  • 主服务器执行 BGSAVE命令,生成RDB文件,并使用缓存区记录从现在开始的所有写命令

  • RDB文件生成完成后,主服务器会将其发送给从服务器

连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

步骤1:保存主节点信息

slaveof命令是异步的,在从节点上执行slaveof命令,从节点立即向客户端返回ok,从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。

步骤2:建立socket连接

从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。

从节点为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

主节点接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

步骤3:发送ping命令

从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。

从节点发送ping命令后,可能出现3种情况:

(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。

(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。

(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。

步骤4:身份验证

如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。

如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。

步骤5:发送从节点端口信息

身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制,后面再讲解这两种复制方式以及psync命令的执行过程,这里不再详述。

命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。

14、Sentinel(哨兵模式)的原理你能讲一下吗?

主从模式虽然能做到很好的数据备份,但是他并不是高可用,一旦宕机,只能人工去切换。redis哨兵就是为了解决主从模式的高可用方案。

哨兵模式的原理

哨兵模式通常由一组 Sentinel 节点和一组(或多组)主从复制节点组成。由哨兵去监视其所属从服务器,一旦发现住服务器宕机,就会选举其中一个服务器升级为主服务器。

心跳机制

(1)Sentinel与Redis Node

Sentinel 会定时向主节点和从节点发送 info 命令获取其拓扑结构和状态信息。

(2)Sentinel与Sentinel

基于 Redis 的订阅发布功能, 每个 Sentinel 节点会向主节点的 sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息 ,同时每个 Sentinel 节点也会订阅该频道, 来获取其他 Sentinel 节点的信息以及它们对主节点的判断。

通过以上两步所有的 Sentinel 节点以及它们与所有的 Redis 节点之间都已经彼此感知到,之后每个 Sentinel 节点会向主节点、从节点、以及其余 Sentinel 节点定时发送 ping 命令作为心跳检测, 来确认这些节点是否可达。

故障转移

每个 Sentinel 都会定时进行心跳检查,当发现主节点出现心跳检测超时的情况时,此时认为该主节点已经不可用,这种判定称为主观下线

之后该 Sentinel 节点会通过 sentinel ismaster-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断, 当 quorum(法定人数) 个 Sentinel 节点都认为该节点故障时,则执行客观下线,即认为该节点已经不可用。这也同时解释了为什么必须需要一组 Sentinel 节点,因为单个 Sentinel 节点很容易对故障状态做出误判。

这里 quorum 的值是我们在哨兵模式搭建时指定的,后文会有说明,通常为 Sentinel节点总数/2+1,即半数以上节点做出主观下线判断就可以执行客观下线。

因为故障转移的工作只需要一个 Sentinel 节点来完成,所以 Sentinel 节点之间会再做一次选举工作, 基于 Raft 算法选出一个 Sentinel 领导者来进行故障转移的工作。

被选举出的 Sentinel 领导者进行故障转移的具体步骤如下:

(1)在从节点列表中选出一个节点作为新的主节点

  • 过滤不健康或者不满足要求的节点;

  • 选择 slave-priority(优先级)最高的从节点, 如果存在则返回, 不存在则继续;

  • 选择复制偏移量最大的从节点 , 如果存在则返回, 不存在则继续;

  • 选择 runid 最小的从节点。

(2)Sentinel 领导者节点会对选出来的从节点执行 slaveof no one 命令让其成为主节点。

(3)Sentinel 领导者节点会向剩余的从节点发送命令,让他们从新的主节点上复制数据。

(4)Sentinel 领导者会将原来的主节点更新为从节点, 并对其进行监控, 当其恢复后命令它去复制新的主节点。

15、Cluster(集群)你能讲一下吗?

哨兵模式虽然实现了高可用,但是主节点还是只有一个,即写入操作都是在主节点中,这也成为了性能的瓶颈。集群将会通过分片的方式保存数据库中的键值对。

Redis的每个节点都可以分为主节点与对应从节点。主节点负责处理槽,从节点负责复制某个主节点,并在主节点下线时,代替下线的主节点。

如何实现故障转移

其实与哨兵模式类似,Redis的每个节点都会定期向其他节点发送Ping消息,以此来检测对方是否在线。当一个节点检测到另一个节点下线后,会将其设置为疑似下线。如果一个机器中,有半数以上的节点将某个主节点设为疑似下线,则该节点将会被标记为已下线状态,并开始执行故障转移。

  1. 通过raft算法从下线主节点的从节点中选出新的主节点

  2. 被选中的从节点执行 SLAVEOF no one 命令,成为新的主节点

  3. 新的主节点撤销掉已下线主节点的槽指派,并将这些槽指给自己

  4. 新的主节点向集群中广播自己由从节点变为主节点

  5. 新的主节点开始接受和负责自己处理槽的有关命令请求

分片

redis限于单机所支持的存储容量,分片机制允许数据拆分存放到不同的redis上。

  • 范围分片

  • 简单

  • 缺点:需要一个映射范围到示例的表格。

  • hash分片

  • 总槽数是(16384(2^14)),多个master平分槽位,具体落在哪个槽位上,会拿key进行CRC16算法,然后跟16384取模,得到的就是落在哪个槽位。

集群间的通信

**集中式:**例如springcloud,一旦数据变更,直接更新到集中式的存储中,其他节点读取的时候能立刻感知到,缺点就是所有的压力都集中在一个地方。

gossip:元数据更新比较分散,不是集中在一个地方,更新请求陆陆续续,缺点就是会有一定延时。

16、Memcache与Redis的区别都有哪些?

存储方式

Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。

Redis有部份存在硬盘上,这样能保证数据的持久性。

数据支持类型

Memcache对数据类型支持相对简单。

Redis有丰富的数据类型。

使用底层模型不同

它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。

Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

17、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表:keys pre*,这个时候面试官会追问该命令对线上业务有什么影响,直接看下一个问题。

18、如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

redis 的单线程的。keys 指令会导致线 程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时 候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间 会比直接用 keys 指令长。

19、如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象(因为redis是单线程的)。严重的话可能会导致服务器雪崩,所以我们一般在过期时间上加一个随机值,让过期时间尽量分散。

20、Redis常用的客户端有哪些?

Jedis:是老牌的Redis的Java实现客户端,提供了比较全面的Redis命令的支持。

Redisson:实现了分布式和可扩展的Java数据结构。

Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

Jedis:比较全面的提供了Redis的操作特性。

Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操 作单个Lettuce连接来完成各种操作。

21、Redis锁续期和锁重入问题

锁续期:

锁重入:

22、Redis缓存一致性问题

延时双删

  1. 先删除缓存
  2. 再更新数据库
  3. 异步线程延时几百毫秒进行删除,如果有主从同步则再加个几百毫秒

先更新数据库,再删缓存(不建议直接更新,多线程会有乱序问题,而且如果有计算还会浪费性能)

  1. 会有一段时间的延时
  2. 会有一种极端巧合情况,A读缓存的时候正好失效了,A去数据库读到旧值,B更新数据库,B删除缓存,A设置旧值到缓存(条件比较苛刻,再写库前失效,同时A要在B写操作之后设置旧值,也就是读要比写满才行

异步更新缓存

  1. 读取binlog日志后分析,利用消息队列,推送更新各redis 数据,类似mysql的主从复制

参考

I/O多路复用mp.weixin.qq.com/s/YdIdoZ\_y…

redis面试题mp.weixin.qq.com/s/GwjQalQ9Z…

数据库和缓存双写一致性