目录


前言

放弃幻想,拒绝大饼,准备战斗!!!

所有内容均为自用整理收集,答案仅供参考,如有不同意见欢迎评论区私信交流。





Redis基础

项目有用到redis吗?你们项目为什么用redis?

根据实际项目来谈,比如用于(分布式)会话管理、(分布式)缓存、轻量级消息队列、计数器、限流、排序、消息代理、分布式锁、以及点赞排行榜等实时业务等。

为什么用:速度快,高性能、高并发、丰富的数据结构支持、分布式可扩展性高。



redis为什么这么快?

  1. 内存:redis的所有数据都在内存中,因此不需要访问磁盘,极大的降低了访问延迟;内存操作(读写)性能高,支持每秒百万级操作。
  2. 单线程:redis以单线程模式运行,避免了多线程上下文切换的开销问题和多线程竞争问题,提高了CPU利用效率。
  3. 高效的数据结构:redis利用了几个高效的底层数据结构来提高数据操作效率。
  4. 非阻塞I/O多路复用机制:Redis使用非阻塞I/O,这种机制使得redis可以处理多个连接而不阻塞其他操作,从而能快速处理请求,实现高并发和高吞吐量。

了解Redis的线程模型吗?

Reactor模型?

Redis基于Reactor模式开发网络事件处理器,redis将此称为文件事件处理器(file event handler),该处理器以单线程模式运行和处理事件。

文件事件处理器分为四个部分:

  1. 套接字
  2. I/O多路复用程序:用来监听多个套接字请求。
  3. 文件事件分派器:将套接字的事件交给不同的事件处理器。
  4. 具体的事件处理器:实现不同的网络通信需求,比如应答客户端连接、接收客户端命令请求、返回执行结果等操作。

一个文件事件实际上就是套接字操作的抽象,当一个套接字准备好执行连接应答(accept)、写入、读取、关闭操作时就会产生一个文件事件。redis服务器通常会产生多个套接字(多个客户端多个连接访问),所以文件事件有可能会并发的出现,这时I/O多路复用程序会将所有事件的套接字放在一个队列里,然后文件事件分派器根据该队列有序、同步的接收套接字,由于文件事件处理是以单线程运行,所有只有在上一个套接字生产的事件执行完毕后,才会执行下一个套接字事件。



Redis优缺点?

优点:高性能、高并发

  1. 高性能:redis将数据存储在内存中,因此读写速度非常快。
  2. 高并发:redis基于Reactor 模式开发的网络事件处理器(文件事件处理器)是单线程模式运行的,所有操作都是原子性的,天然就支持高并发。
  3. 数据结构丰富:redis支持各种抽象数据结构,如字符串,列表,映射,集合,排序集合,HyperLogLogs,位图,流和空间索引。
  4. 支持持久化:redis支持将数据持久化到磁盘,提供可靠的数据保证。
  5. 分布式支持:redis支持主从复制和哨兵机制,可以实现数据备份、读写分离和自动故障转移,提高系统的可用性和可靠性。

缺点(内存消耗、数据一致性问题、阻塞):

  1. 内存消耗较大:由于redis将数据存储在内存中,因此对于大规模数据的存储需求,需要考虑服务器的内存容量和成本。
  2. CPU操作瓶颈:单线程模型可以避免上下文切换,但是也无法利用现代多核处理器的性能,对于CPU密集型的操作或服务器存在CPU密集型服务时可能存在性能瓶颈(一般很少见,redis的瓶颈主要在内存和网络),这种情况下就不适合处理大量并发请求。
  3. 线程阻塞:当单线程执行的命令阻塞时(慢查询、大key操作、网络问题等),将严重影响redis的性能。
  4. 数据一致性:当redis作为缓存使用时,就一定存在缓存和数据库数据不一致的问题。此外redis的主从复制存在一定的延迟。
  5. 无法处理复杂查询:redis不支持复杂查询操作,对于需要进行复杂数据分析和统计的场景不太适用。
  6. 数据容量受限:由于redis将数据存储在内存中,所以数据的容量受到内存大小的限制,无法存储超过内存容量的数据。



redis如何实现持久化?

为什么要持久化?redis是内存数据库,它的数据都存储在内存中,如果不把数据保存到磁盘里,那么服务器进程一旦退出,服务器的数据就丢失了。

通过两种方式:

  1. RDB(Persistence DB):RDB类似快照,它只能持久化某一时刻的服务器数据。RDB持久化后,文件将被保存至同文件夹下的dump.rdb文件中,且以压缩的二进制文件表示,服务器重启都会加载该文件以加载数据。
  2. AOF(Append Only File):AOF可以支持实时的数据持久化。AOF持久化功能通过保存redis服务器所执行的写命令来记录数据库状态,默认存储在appendonly.aof中,AOF文件中存储的都是redis相关命令。



RDB持久化过程?

1. 持久化部分

Redis有俩个命令进行RDB持久化:

  • SAVE:将会阻塞redis进程,此时服务器不能处理任何命令请求,直到RDB文件创建完毕。
  • BGSAVE:派生子进程(创建子进程执行fork函数时服务器也将会阻塞,不过用时会很短),由子进程创建RDB,服务器进程继续

bgsave命令持久化过程如下:

  1. fork:创建子线程
  2. rdbSave:子线程创建rdb文件(save命令相当于是父进程直接执行这个函数)
  3. signal_parent:子线程创建完成后向父进程发送信号

RDB持久化可以手动执行,也可以根据服务器配置选项定期执行,在配置文件中设置save属性,会让服务器每隔一段时间自行执行一次bgsave命令。可以配置多个save选项,任一选项条件满足时都被会执行。

# save <second> <修改次数>
save 900 1
save 300 10
save 60 10000
# bgsave保存出错时,停止继续写入
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
# The filename where to dump the DB
dbfilename dump.rdb

2. 载入数据
服务器启动时启动的,检测到RDB文件就会自动载入RDB中的数据。



AOF持久化过程?AOF持久化会出现阻塞吗?为什么?

1. 持久化部分
AOF持久化功能的实现可以分为命令追加、文件写入和同步。

  1. 命令追加:当服务器完成一个写命令之后,并不会直接将这些命令写入AOF文件,而是以Redis协议格式将被执行的写命令追加到服务器缓冲区(aof_bug)的末尾。
  2. 文件写入与同步:根据设置的数据同步策略(always、everysec、no),当满足同步条件或缓冲区满时,redis会将AOF缓冲区中的数据一次性写入磁盘的AOF文件。

文件写入的配置如下:

# appendfsync always
# appendfsync no
appendfsync everysec

写入类型的区别:

  • always:每次执行写命令都将缓冲区的内容写入AOF文件。效率最慢,也最安全,故障停机只会丢失一个事件循环中的数据命令。
  • everysec:如果上次同步AOF文件时间距离现在已经超过了1秒,就进行文件同步。该同步由一个线程单独负责效率够快,且故障停机也只会丢失一秒钟的命令数据。
  • no:由操作系统决定何时进行AOF文件的同步。一般来说,os可能会等到缓冲区的空间被填满、或者超过了指定时限之后,才真正地将缓冲区的数据写入到磁盘里面。fsync和fdatasync两个同步函数,可以强制让os立即将缓冲区中的数据写入到磁盘里面。总体写入AOF文件的速度最快,但是单次同步时长是三种模式中最长的。

2. 载入数据
AOF文件只包含了重建数据库状态所需的所有命令,因此服务器只需要重新执行一遍AOF文件中保存的写命令即可恢复服务器状态。大致步骤如下:

  • 创建一个不带网络连接的伪客户端,用来模拟客户端发送写命令。
  • 从AOF文件中执行并分析出一条写命令
  • 使用伪客户端执行被读出的写命令。
  • 重复读取和写入,直到AOF文件被处理完毕。

3. AOF持久化会出现阻塞
Redis每次执行写命令都将缓冲区的内容写入AOF文件,此时线程可能会因为持久化操作(磁盘IO)而阻塞。



你知道AOF重写吗?为什么要进行重写?简单说下AOF重写流程?

由于AO是F通过保存被执行的写命令来记录数据库状态,随时间流失,AOF文件中的内容会越来越多,体积越来越大,且会包含很多无用数据,比如新增键后又删除改键,AOF总是会保存这两个操作,实际上该键被删除已经不需要记录了,这种些命令显然没有存在的必要。
因此,为了解决AOF文件体积膨胀的问题,redis提供了AOF文件重写(Rewrite)功能。

重写命令:bgrewriteaof,redis会创建一个子进程以处理该命令。

通过创建一个新的AOF文件替代现有的AOF文件。新的AOF文件不会包含任何浪费空间的冗余命令。

重写过程:

  1. 子进程开始从数据库遍历每一个数据库中的所有键。父进程此时设置一个AOF重写缓冲区,记录子进程重写期间的所有写命令。
  2. 读取一个未过期的键和值,然后用一条命令去记录键值对,即将键值对倒推为写命令。如果该键对应的值的元素超过64个时,那么该键的命令将会被分割为多条键的写命令。最后保存至AOF文件。
  3. 键如果带有过期时间,且未过期,构造过期命令。保存至AOF文件。
  4. 循环遍历,直到读取完所有键值对。
  5. 子进程结束,并通知父进程,此时父进程会将该缓冲区的内容写入新的AOF文件,保证AOF保存的数据库状态和服务器一致。
  6. 对新的AOF文件进行改名,原子地覆盖现有的AOF文件。



RDB和AOF有什么区别?分别在什么场景下使用RDB和AOF?

RDB持久化与AOF持久化的区别:

  1. 实现原理:RDB是保存所有的键值对数据,而AOF则是保存服务器执行的写命令;
  2. 实现方式:RDB、AOF都可以实现手动和自动执行;
  3. 服务器执行:如果AOF功能处于打开状态,那么服务器总是会读取AOF的文件来恢复数据库状态,只有当AOF关闭时,才会读取RDB文件。
  4. 读取性能:AOF文件保存的是服务器写命令,容易形成很多不必要的冗余数据,读取文件时,服务器需要重写执行这些命令,速度较慢;RDB文件仅仅保存了数据库需要的所有状态,恢复较快;
  5. 写入性能:都是自动保存时,RDB需要额外的定时线程维护,且需要判断自动保存的条件,性能一般;而AOF支持服务器每次执行写命令都可以将缓存区的数据保存至AOF文件中,单次性能快。
  6. 安全性:AOF的更新频率更快,安全性更高,但是总体写入性能较差。

根据上述的区别,选择哪种方式主要取决于业务需求和数据安全性要求。

RDB持久化适用于以下场景:

  • 数据备份:由于RDB文件是紧凑的二进制文件,适合数据备份。
  • 主从复制:在Redis的主从复制场景中,主节点通常会将RDB文件发送给从节点,以便从节点能够快速加载数据并保持与主节点的同步。
  • 对数据一致性要求不高:如果能接受在Redis崩溃时丢失最近一段时间的数据,那么RDB是一个不错的选择。因为RDB持久化只保证某个时间点的数据一致性,而无法保证数据的实时性。

AOF持久化适用于以下场景:

  • 对数据实时性要求高 + 对数据安全性要求高:AOF持久化通过记录所有对数据库的写操作来保证数据的实时性。即使Redis崩溃,也可以通过执行AOF文件中的写操作来恢复大部分数据(取决于你的AOF文件写入配置)。



RDB,AOF都开启时,服务器是如何加载数据的,数据以谁为准?

数据载入工作是在服务器启动时启动的,检测到RDB文件或AOF文件存在时自动执行的,redis没有主动命令用以载入。

由于AOF文件的更新频率更高更准确,如果RDB和AOF持久化同时处于打开状态,那么服务器将优先使用AOF文件来还原数据库状态,只有AOF关闭,才会通过RDB方式恢复服务器。



Redis什么情况下会出现阻塞,怎么解决?

那条熟悉说那条,不必全部都记下,不知道的词不要乱答。

  1. 慢查询:
    由于redis是单线程的,大量的慢查询可能会阻塞服务器。当慢查询发生时,其他命令可能会因为命令执行排队机制而导致级联阻塞。
    通过查询redis的慢查询日志slowlog get命令逐一进行监控和优化,确保系统的稳定性和性能。
    slowlog-log-slower-than time:记录大于 time 的慢日志记录
    slowlog-max-len:最大记录条数(先进先出的队列)

在这里插入图片描述

慢日志输出:

8 # 序号
1702536559 # 产生时间戳
18817 # 查询耗时 微妙 1000 * 1000 = 1s
HSCAN # 执行命令以及参数,相当于执行:HSCAN  user 0 COUNT 10000
user
0
COUNT
10000
ip:port # 客户端IP信息
  1. 阻塞命令:
  • 使用KEYS命令(大key问题)遍历大量键可能导致Redis阻塞。使用SCAN命令来遍历键;
  • 手动执行RDB持久化命令:save,阻塞redis服务器进程,直到RDB文件创建完毕,在此期间,服务器不能处理任何命令。使用bgsave代替,bgsave将派生出子进程,由该进程创建RDB文件,服务器则不受其影响。
  1. 内存交换:
    当内存空间不足时,操作系统可能会使用内存交换(swap)来释放内存,这会导致Redis性能下降甚至阻塞。为了避免这种情况,应该确保Redis服务器有足够的内存,并监控内存使用情况。

  2. 持久化阻塞:
    当AOF持久化功能处于打开且AOF配置文件写入为appendfsync always时,Redis每次执行写命令都将缓冲区的内容写入AOF文件,此时主线程可能会因为持久化操作而阻塞(将数据写到磁盘的IO操作)。
    解决这个问题的方法是控制Redis最大内存,使用高效支持fork操作的虚拟机,或者优化持久化策略。

  3. 网络问题:
    网络闪断、Redis连接拒绝或连接溢出等网络问题也可能导致Redis阻塞。
    解决方案是检查网络连接稳定性,调整Redis连接配置,以及优化网络带宽和延迟。

  4. 高并发写入:
    在高并发写入的情况下,多个客户端同时请求写入操作可能导致阻塞。可以使用Redis的事务功能将多个写入操作打包成一个原子性的操作序列,以减少阻塞的可能性。



知道哪些redis命令?平时用的最多的命令是什么?

参考:三分钟学会Redis 客户端常用命令



如何处理redis过期键?你知道哪些删除策略?分别有什么优缺点?

Redis服务器实际使用的是:惰性删除和定期删除两种策略来处理redis过期键。

1. 定时删除:在设置键的过期时间时,创建一个定时器timer,定时器时间到了就删除过期键。
优缺点:

  • 对内存友好,一旦有过期键便会很快删除
  • 对CPU压力最大,过期键过多时(或同一时刻大面积的键失效),会占用很大一部分的CPU时间在删除键上,致使服务器的响应时间增加、吞吐量下降。

2. 惰性删除:程序在取出键时,才对键进行过期检测,并选择是否删除。
优缺点:

  • 对CPU压力最小,这种策略不会操作不访问的无关过期键
  • 对内存压力大,当大量键过期时,如果不对键进行访问,那么所有过期键都不会被删除,造成内存垃圾堆积。

3. 定期删除:每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减小对CPU的影响。
优缺点:

  • 有效减少了过期键带来的内存浪费
  • 有效减少了删除操作对CPU时间的影响
  • 但是很难控制和确定定期删除的执行时长和频率。执行时长过长或频率频繁就容易形成定时删除的弊端,反之则会形成惰性删除的弊端。



redis最大支持多少客户端连接?连接数满了怎么办?

默认情况下,redis最大支持10000个客户端连接,可以通过配置文件中的maxclients 进行自定义。如果Redis服务器不能配置进程文件限制来允许指定的限制,允许的最大客户端数量被设置为: maxclients - 32(Redis需要保留部分供内部使用)。

maxclients 10000

一旦达到限制,Redis将拒绝所有新连接,并发送错误“max number of clients reached”。

连接数满有两种情况:

  1. 创建连接的服务多。一般这种情况就需要配置集群,增加机器来降低redis单点压力。
  2. 不当的创建连接。可能存在某个服务频繁创建连接,且未能及时或正确的销毁连接。这种情况需要对服务进行优化(一般redis框架都会有连接池控制连接数,可以考虑是不是这个连接池不是单例造成的)。



redis有内存限制吗?内存大小如何设置?内存被占满如何处理?

redis内存直接受限于机器内存,同时redis内存也不会超过maxmemory指定字节数的内存。默认情况下maxmemory默认值为0,在32位操作系统中,最大内存不超过3G,而64位则没有理论上的限制。

通过修改maxmemory来调整redis的最大可用内存:

maxmemory <bytes>

当达到内存限制时,redis就会根据maxmemory-policy选择的内存淘汰机制(eviction policy)尝试删除键。

maxmemory-policy noeviction



说说内存淘汰机制?有哪些策略?

当redis内存超过maxmemory的限制时,就会根据maxmemory-policy选择的内存淘汰机制(eviction policy)尝试删除键。

maxmemory-policy默认值为noeviction。策略内容如下:

  • noeviction:在达到内存限制并且客户端尝试执行可能导致使用更多内存的命令时返回错误(一般出现在写入命令时,但删除和一些其他操作时也有可能引发异常)。
  • allkeys-lru:尝试在键空间中先删除最近最少使用的(LRU)键
  • volatile-lru:在设置了过期时间的键空间中,尝试在键空间中删除最近最少使用(LRU)的键
  • allkeys-random:随机删除键
  • volatile-random:在设置了过期时间的键空间中随机删除键
  • volatile-ttl:在设置了过期时间的键空间中,尝试首先逐出具有较短生存时间(TTL)的密钥(即更早过期的键)

当redis中没有设置任何过期键或删除过期键仍没有多余内存时,volatile-lru、volatile-random以及volatile-ttl的行为跟noeviction相似。





redis数据结构

Redis由C语言编写。

知道redis字符串实现方式吗?

Redis没有直接使用C语言的传统字符串表示,而是自己实现了SDS(simple dynamic string)抽象类型,并将其作为Redis的默认字符串表示方式。

每个sds.h/sdshdr表示一个SDS值,SDS的定义如下:

struct sdshdr {
  int len; // 记录buf数组中已使用的字节数量,等于SDS所保存字符串的长度
  
  int free; // 记录buf数组中未使用的字节数量
  
  char buf[]; // 字节数组,用于保存字符串
}

SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里。该空字符的添加操作由SDS函数自动完成。这一操作的好处是,可以直接复用C语言的一些字符串操作。

可以简单介绍一下SDS结构,以及修改字符串的实现方式(空间预分配、惰性空间释放,后面有介绍)



sds和c区别?

缓冲区溢出:增长字符串时,程序需要通过内存分配来来确定是否需要对底层数组的空间进行扩充,以便存放增长后的字符串的值。如果忘了这一步,那么保存字符串的数组就会发生越界行为,占用未分配给它的内存区域,从而导致其他数据被意外篡改,这就是缓冲区溢出。


内存泄漏:缩短字符串时,同样需要重新分配内存释放掉不需要的那部分空间。如果忘了这一步,那么剩下那一部分空间将会一直处于未使用状态且无法分配给其他程序来使用,这就是内存泄漏。

区别如下:

  1. sds的len属性记录了sds的长度,而c则必须遍历字符串来获取。
  2. C不记录长度,由此可能带来一个问题:缓冲区溢出(buffer overflow)。
  3. sds内部实现空间预分配和惰性空间释放,减少了修改字符串带来的内存重分配次数。而c每次修改字符串长度都会执行内存重新分配。
  4. sds实现了二进制安全:c字符串的字符必须符合某种编码,并且除了字符串的末尾之外不能包含空字符,否则会被误认为是字符串的结尾。这些限制使得C字符串只能保存文本信息,而无法保存图片、音视频等二进制数据。sds改进数据结构后则不受此限制。
  5. sds可以执行部分c字符串函数,c则能执行所有。



解释一下什么是空间预分配?什么是惰性空间释放?

空间预分配:

  • 描述:空间预分配操作用于优化SDS字符串的增长操作。
  • 实现:当修改后的SDS长度小于1MB时,那么程序将分配和len相同大小的未使用空间。这时len的值和free的值相同。
    SDS的长度大于等于1MB时,那么会分配1MB的未使用空间。
  • 作用:Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

惰性空间释放:

  • 描述:用于优化SDS字符串缩短操作。
  • 实现:缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
  • 作用:SDS避免了缩短字符串时所需的内存重分配操作。并为将来可能出现的增长操作提供了优化;不过有可能造成内存空间的浪费。



redis字典结构是什么样的?

C语言并没有内置字典。因此字典由Redis本身实现。

结构大致如下:

  • redis字典内部维护了俩个哈希表,一些特定类型函数和rehash索引,当rehashindex为-1(没有进行rehash)时使用ht[0]保存的字典,rehash正在进行时使用ht[1]。
  • 哈希表中,存储了一个哈希数组,key通过哈希函数计算后会放在合适的数组下标处,并用哈希节点保存改key-value值。
  • 每个哈希节点包括key、value对象,以及用于解决哈希冲突的下一个哈希节点的指针。

dict.h/dict表示一个字典。

typedef struct dict {
  // 特定类型函数
  dictType *type;
  // 私有数据
  void *privdata; 
  // 哈希表
  dictht ht[2]; 
  // rehash索引,没有进行rehash时,值为-1
  int rehashindex;
} dict;

dict.h/dictht表示一个哈希表。

typedef struct dictht {
  // 哈希表,这里相当于Java中的 Object [] table;
  dictEntry **table;
  // 大小,指数组
  unsigned long size; 
  // 哈希表大小掩码,总是用于计算索引值,总是等于size - 1
  unsigned long sizemask; 
  
  // 哈希表已有节点的数量
  unsigned long used; 
} dictht;

而每个哈希节点使用dictEntry表示。

typedef struct dictEntry{
  void *key;

  // 值 可以是一个对象指针,或uint64_t整数或int64_t整数
  union {
    void *val;
    uint64_t u64;
    int64_t s64;
  } v; 

  // 指向下一个哈希节点,用于解决键冲突(链地址法)
  struct dictEntry *next;
} dictEntry;

hash冲突怎么办?

Redis使用了链地址法解决,在产生冲突的下标地址的链表中,使用头插法插入哈希节点。



介绍一下rehash?

rehash:扩容时,重新计算键的哈希值和索引值下标,以减低哈希冲突。收缩时,以节省内存空间。

Redis也有一个负载因子用于控制哈希表的数组大小。当负载因子超过承受限制或远低于预期时,就会进行rehash的操作,进行扩展或缩小哈希表。

负载因子(load factor)计算方式:load_factor = ht[0].used / ht[0].size

**扩容时:**一般情况下,负载因子大于等于1,就进行扩展;在执行BGSAVE或BGREERITEAOF命令时,负载因子大于等于5才开始扩展。扩展后的size大小:size大于等于ht[0].used * 2的第一个2^n数。
**收缩时:**负载因子小于0.1,就开始收缩。收缩后的size大小:size大于等于ht[0].used的第一个2^n数。

rehash实现流程:

  1. 为ht[1]分配空间,通过上面的扩展收缩size计算方法得出分配空间。
  2. 将ht[0]的所有键值对重新rehash到ht[1]上。
  3. 完成上面的操作后,将释放ht[0]的内存,ht[0] = ht[1],ht[1] = 新的空白哈希表。

事实上,Redis服务器处于对哈希表中的数据量的考虑,它的rehash不是一次性的、集中式的完成,而是分多次、渐进式的完成,这一过程也叫渐进式rehash。渐进式rehash流程如下:

  1. 为ht[1]分配空间,字典此时同时拥有ht[0]和ht[1]俩个哈希表。
  2. 将字典中的rehashindex = 0,表示rehash开始工作.
  3. 在rehash期间,在字典进行增删改查操作的同时,还会将ht[0]在rehashindex索引位置上的所有键值对rehash到ht[1]中,rehashindex++;此时删、查、改等操作会同时在俩个哈希表上进行,比如查找会现在ht[0],没有时在ht[1]。在rehash期间,新添加的键值对均会添加到ht[1]中,而ht[0]则不会进行任何添加操作,保证ht[0]的键值对数量只减不增。
  4. 当rehashindex == ht[0].size时,ht[0]中的所有键值对都被rehash到ht[1]中,rehashindex = -1,表示rehash操作已完成,将释放ht[0]的内存,ht[0] = ht[1],ht[1] = 新的空白哈希表。



redis有序集合怎么实现的?

知道跳跃表吗?如何实现的?

普通的集合(set)由借助哈希表存储数据,且value永远为null,类似于HashSet;而有序集合类似Redis集合,不同的是它的value存储的不再是null,而是记录权重的分数score(double)。

为了达到快速的顺序访问,它内部还维护了一个压缩列表(ziplist)或跳跃表(skiplist)。元素数量小时使用压缩列表实现以节省空间,当、元素数据变大超过阈值时则转为跳跃表,以此降低插入、删除和查找操作的复杂度(O(log N))。

压缩列表和跳跃表都是用来维护排序信息,元素为哈希表的key数据,排序依据是key对应的score。





redis多机与分布式

用过主从复制吗?怎么配置?介绍下主从复制过程?

Redis主从复制是一种将数据从主节点(master/leader)复制到从节点(slave/follower)的异步机制。这种复制是单向的,只能从主节点到从节点。主节点主要负责写操作,而从节点则主要负责读操作,以此实现读写分离和负载均衡。

在主从复制的配置中,一般遵循“配从不配主”的原则,即所有的配置操作都在从节点进行,不需要对主节点进行操作。每个Redis服务器默认都是主节点,一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

配置主从,我们只需要在从服务器配置中添加如下配置,此时从服务器会主动向主服务器发起请求:

slaveof <masterip> <masterport>

主从复制流程(2.8前):

  1. 同步(复制):
  • 当一个从节点启动后,它会向主节点发送一个SYNC命令,请求数据同步。
  • 主节点接收到SYNC命令后,会启动一个后台进程(bgsave)来保存当前数据库的状态到一个快照文件(RDB持久化)。同时,主节点会使用一个缓冲区记录所有在此期间接收到的写命令。
  • 快照完成后,主节点会将快照文件发送给从节点,从节点接收到快照文件后,会先将其写入本地磁盘,然后再加载到内存中。
  • 主节点将缓冲区的所有写命令发送给从节点,从节点执行写命令,更新自己的数据库状态。
  1. 增量同步(命令传播):
  • 在完成初始化同步后,主节点会将其后续接收到的所有写命令发送给从节点,实现实时的数据同步。
  • 只要从节点与主节点的连接断开并重新连接,就会触发一次全量同步(即重新执行上述的初始化同步过程)。

主从复制流程(2.8后):

  1. 初始化复制:
  • 当从节点启动时,它会发送一个PSYNC命令给主节点,请求数据同步。
  • 如果是第一次连接主节点,从节点会触发全量同步(full synchronization)。主节点会生成一份RDB快照文件,同时使用缓冲区记录所有在此期间接收到的写命令。
  • 快照完成后,主节点会将快照文件发送给从节点,从节点接收到快照文件后,会先将其写入本地磁盘,然后再加载到内存中。
  • 主节点将缓冲区的所有写命令发送给从节点,从节点执行写命令,更新自己的数据库状态。
  1. 增量同步:
  • 在全量同步完成后,主节点会进入增量同步阶段。主节点会将其后续接收到的所有写命令实时发送给从节点,确保主从节点之间的数据保持同步。
  • 如果从节点与主节点的连接断开并重新连接,主节点会根据从节点发送的PSYNC命令中携带的复制偏移量(replication offset)来决定是进行全量同步还是部分同步。如果偏移量有效,主节点会仅发送从节点缺失的部分数据,实现更高效的同步。



如何保证主从复制的高可用性(主从复制中主节点挂掉怎么办)?介绍一下Sentinel?

可以使用Sentinel哨兵系统。

Redis Sentinel系统是一个用于实现Redis高可用性(High Availability)的解决方案。它由一个或多个Sentinel实例组成,用于监视任意多个主服务器以及这些主服务器属下的所有从服务器。

当被监视的主服务器进入下线状态时,Sentinel系统会自动进行故障转移操作。具体来说,它会将下线主服务器属下的某个从服务器升级为新的主服务器,并由新的主服务器代替已下线的主服务器继续处理命令请求。这一过程中,Sentinel系统会向其他从服务器发送新的复制指令,让它们成为新的主服务器的从服务器。当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。

Sentinel系统通过接收主服务器或从服务器发来的频道信息来发现新的Sentinel实例,并建立订阅连接。已经相互知道的Sentinel实例则使用命令连接进行通信。在默认情况下,Sentinel会以每秒一次的频率向所有连接的实例(包括主服务器、从服务器和其他Sentinel)发送心跳检测信息,以确保它们的状态正常。



你们公司使用redis集群部署吗?说说你对集群的理解?介绍下扩容过程?节点通信机制?

Redis集群(Redis Cluster)是Redis提供的分布式数据库解决方案,它由多个节点组成,数据也被分片(sharding)到不同的节点上,每个节点负责存储和处理一部分数据。这使得整个集群可以存储比单个Redis实例更多的数据,并提供了更高的读写性能。同时,集群中的每个节点都是独立的Redis服务器,它们之间通过特定的通信协议进行协作,确保数据的一致性和可靠性。

Redis集群的每个节点都支持主从复制和故障自动转移功能。

扩容机制(增加新节点到集群):

  1. 准备新节点:安装并启动redis新节点,此时它还没有与其他节点进行通信,也没有加入到集群环境中。
  2. 加入集群:将新的redis节点添加到现有的Redis集群中。redis-cli命令行工具cluster meet ip port命令来实现。cluster meet命令会和指定节点进行握手,握手成功后就会将ip port所在节点添加到执行命令的节点的集群中。
  3. 迁移槽和数据:当新节点成功添加到集群中后,需要进行数据迁移。这通常涉及到将部分槽(slot)和数据从旧节点迁移到新节点上。迁移过程需要确保数据的完整性和一致性,通常涉及到一些复杂的操作,如重新分配槽位、调整主从复制关系等。

通信机制:
Redis集群节点之间的通信机制主要依赖于Gossip协议。



redis集群最大支持多少节点?为什么是16384?

redis集群通过分片来保存数据库中的键值对,槽(slot)则是redis集群中用于数据分片的单位,集群的整个数据库被划分为16384个槽,每个键通过哈希计算被均匀的分散在这16384个槽中的一个。每个节点可以拥有16384个槽中的0个或最多16384个槽,也就是redis集群最多支持16384个节点。

redis计算键所在的槽时,使用的CRC16算法,它所产生的16位输出中的14位会被用到,因此,Redis集群的哈希槽数量被设定为 2^14 = 16384个。





redis综合实战问题

你平时是如何设计键的命名?

Redis的数据以键值(Key-Value)为基础,一般key都是string类型,value有如下类型:字符串,列表,哈希,集合,有序集合,HyperLogLogs,位图,流和空间索引。

  • 严格限制key大小:首先键允许的最大大小为512 MB,必须严格控制键的大小,确保程序正确执行。

  • key使用的字节不宜过长:键太长时,在内存负担加重,并且redis服务器为了查找这个键,可能需要更加高昂的代价来计算哈希值。

  • key也不宜设置太短:类似“ u1000flw”这种键毫无意义,键的可读性也应该尽可能的考虑(user:1000:followers),以便运维和慢查询优化。

  • key使用同一种设置类型:使用形如“ object-type:id”的格式,不仅在可读性方面更佳,也能有效的降低键重复时造成的值覆盖。点或破折号通常用于多字段的连接,例如“ comment🔢reply.to”或“ comment🔢reply-to”中。



你用过redis的哪些数据类型,用来做什么?

字符串

  • 会话缓存,如session、token
  • 分布式锁
  • 详情数据或摘要数据,一般是不会频繁修改的文本内容。

集合(适合一些需要去重、快速判断是否存在的场景,集合可以很方便的实现交集、并集和差集运算):

  • 存储在线用户
  • 存储用户参与的活动ID(项目ID)
  • 存储用户好友关系

有序集合(适用于排行榜、时间顺序消费等场景):

  • 得分榜单
  • 文章热度排名
  • 根据请求时间的限流

哈希(适合存储对象类型数据):

  • 用户属性、用户菜单权限
  • 通用区划
  • 绝大部分Java对象都是使用哈希

位图(Bitmaps,适合于空间敏感的场景):

  • 签到统计
  • 统计用户活跃天数
  • 用户是否参与某项活动



遇到过redis缓存穿透问题吗?什么场景下产生的?如何解决的?

缓存穿透:缓存中不存在,且数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。

没遇到过……

解决方案:

  • 缓存空值:即使数据库查询结果为空,也将一个特殊标记(如“NULL”或“NOT_FOUND”)作为值缓存起来,并设置一个合理的过期时间,以便在短期内拦截相同的无效请求。
  • 布隆过滤器:部署一个布隆过滤器,所有查询请求先经过过滤器验证,只有可能存在的数据才会进一步查询,这样可以过滤掉大部分肯定不存在的数据请求。



遇到过雪崩问题吗?什么场景下产生的?如何解决的?

大规模key失效,导致大量请求打到数据库,使得数据库瞬间承受了远超常规水平的并发请求量,进而可能导致数据库响应变慢、服务不可用,最终对整个系统造成严重的性能瓶颈甚至崩溃。

没遇到过……

可能产生的场景:

  • 大量缓存同时过期:例如,若系统中的多个关键热数据的缓存设置了相同或相近的过期时间,一旦这些缓存同时过期,会导致大量请求直接打到数据库。
  • 缓存实例故障:Redis服务器本身发生故障,如宕机或网络断开,导致整个缓存集群失去作用,所有请求无法命中缓存而转向数据库。
  • 批量操作失误:由于运维或者其他原因,执行了错误的大规模删除缓存操作,使大量缓存数据瞬间失效。

解决方案:

  • 分散过期时间:对不同缓存数据设置随机的过期时间,避免同一时间点大量缓存同时失效。
  • 二级缓存策略:采用主从、分层或多级缓存架构,即使一级缓存失效,还有其他层次的缓存可以抵挡一部分冲击。
  • 熔断机制:引入类似Hystrix这样的熔断器组件,当请求量超过数据库能承受的阈值时,立即停止服务调用,避免压垮数据库。
  • 限流/降级:使用令牌桶或漏桶算法等实现流量控制,当请求过多时进行限流,优先确保核心服务的稳定。
  • 后台更新缓存:在缓存失效时,不是立即从数据库加载数据并返回给客户端,而是异步地去更新缓存,减少数据库压力。
  • 冗余设计:提升缓存集群的可用性和容错能力,即便部分节点失效,也能通过其他节点继续提供服务。



遇到过击穿问题吗?什么场景下产生的?如何解决的?

某个热点key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增,甚至可能导致数据库崩溃的情况。

没遇到过……

可能产生的场景:

  • 缓存过期:假设某个热点Key设置了固定的过期时间,当过期时间到达时,这个Key的缓存失效,而在接下来的一段时间内,有大量的并发请求恰好都访问到了这个失效的Key,所有的请求都会绕过缓存直接查询数据库。
  • 缓存被大量并发删除或更新:如果因为某种原因(如误操作、代码bug等),导致大量并发请求删除或更新了同一个热点Key的缓存,也会造成短时间内数据库压力激增。

解决方案:

  • 缓存续期(缓存永不过期):对于热点Key,可以采取主动续期的方式,即在每次访问热点Key时都刷新其过期时间,使其始终保持有效状态,避免因过期导致的击穿问题。
  • 互斥锁(Mutex Lock):在访问热点Key时,先尝试加分布式锁,成功加锁后才去查询数据库并更新缓存,其他请求则等待解锁后再进行。这样可以确保同一时刻只有一个请求穿透到数据库。
  • Cache Aside Pattern:采用缓存旁路模式,当缓存失效时,只让一个线程重建缓存,其他线程等待重建完成之后再从缓存中获取数据,避免所有线程都去查询数据库。
  • 延时双删策略:在删除缓存的同时,设置一个短暂的延迟时间,在延迟期间再次删除缓存。这样可以尽量覆盖到缓存真正失效和新缓存重建完成之间的空窗期,减少数据库压力。



使用redis分布式锁,如何合理设置过期时间?一个业务执行时间比较长,锁过期了怎么办?怎么保证释放锁的一个原子性?

需要考虑如下几个因素:

  • 任务执行时间:确保过期时间大于预期的最长执行时间,以免任务还在执行过程中锁就被自动释放,导致并发问题
  • 锁自动续期:如果使用了支持锁自动续期的Redis客户端库(如Redisson),在持有锁的线程还在执行任务期间,可以定期自动延长锁的有效期,这样可以减小因锁过期导致的并发问题。
  • 锁竞争激烈程度:如果锁的竞争非常激烈,过期时间不宜设置得太短,否则可能会频繁触发锁的竞争,消耗更多资源。反之,如果锁的竞争不大,可以适当缩短过期时间,更快地回收锁资源。
  • 死锁检测与处理:设定一个合理的最大等待时间,超过这个时间还没有释放的锁可以被认为是持有锁的客户端出现问题,可以通过监控和相应的逻辑来处理此类死锁。
  • 网络延迟和异常恢复:考虑到网络不稳定等因素,过期时间还应该预留一部分用于处理网络延迟或客户端异常恢复的情况。过期时间太短可能导致客户端未能及时释放锁或重新获取锁。
  • 锁释放的可靠性:使用lua脚本来保证解锁操作的原子性,同时结合watch命令或事务处理,以最大程度地确保锁在业务逻辑完成后能够正确释放,降低对过期时间依赖的程度。

一个业务执行时间比较长,锁过期了怎么办?
使用了支持锁自动续期的Redis客户端库(如Redisson),在持有锁的线程还在执行任务期间,可以定期自动延长锁的有效期

怎么保证释放锁的一个原子性?
使用lua脚本来保证解锁操作的原子性



Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐