Redis 知识点

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景

Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。

除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等

Redis 数据类型以及使用场景#

Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

img

img

随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 五种数据类型的应用场景: String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。 List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。 Hash 类型:缓存对象、购物车等。 Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。 Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

  • BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;

  • HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;

  • GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;

  • Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

Redis 中String类型有哪些编码结构#

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M

img

内部实现

String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  • SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr

img

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int

如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstrembstr编码是专门用于保存短字符串的一种优化编码方式:

如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw

img

注意,embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的:

  • redis 2.+ 是 32 字节
  • redis 3.0-4.0 是 39 字节
  • redis 5.0 是 44 字节

可以看到embstrraw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObjectSDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObjectSDS。Redis这样做会有很多好处:

  • embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;
  • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。

但是 embstr 也有缺点的:

  • 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

Redis 管道#

Redis 管道(Pipeline)是一种批量执行 Redis 命令的技术,可以有效减少客户端与 Redis 服务器之间的网络延迟时间,提高 Redis 的性能。

使用 Redis 管道可以将多个 Redis 命令打包发送给 Redis 服务器,而不需要等待每个命令的响应结果,最终再一起接收所有命令的响应结果。这样可以减少客户端和服务端之间的通信次数和延迟,提高 Redis 的吞吐量。

在实际使用中,通过 Redis 管道可以大幅度提高 Redis 执行命令的速度,特别是在需要大量写入、读取数据的场景下,例如批量导入数据、统计数据等操作。

Redis 管道采用的是异步非阻塞模式,在管道内的 Redis 命令执行完成后,客户端会接收到所有命令的响应结果,无论是否出现错误或异常,都需要客户端自行处理。因此,使用 Redis 管道需要注意异常处理和数据完整性等问题。

Redis Stream流#

Redis Stream 是 Redis 5.0 引入的新数据类型,用于处理高吞吐量的消息流(Message Stream),支持在多个生产者和消费者之间快速传递消息,并且可以实现消息的持久化存储和事件驱动的处理。

Redis Stream 可以看作是一个有序、持久化的消息队列,每条消息都有唯一的 ID,可以按照消息的时间戳排序。在 Redis Stream 中,生产者将消息写入到指定的 Stream 中,而消费者则可以使用 Consumer Group 的方式从 Stream 中读取消息,支持多个消费者并发消费和负载均衡策略。

Redis Stream 提供了一系列的 API 方法,包括添加消息、删除消息、查看消息、消费消息等。其中最常用的方法是 XADD(添加消息)和 XREADGROUP(消费消息),例如:

# 添加一条消息到 Stream 中
XADD mystream * name John age 30

# 创建一个 Consumer Group 并从 Stream 中消费消息
XGROUP CREATE mystream mygroup $ MKSTREAM
XREADGROUP GROUP mygroup Alice BLOCK 0 STREAMS mystream >

Redis Stream 在实际应用中广泛用于消息队列、日志收集、事件驱动等场景,具有高性能、高可靠性和易于扩展等优点。

Redis 是单进程单线程还是多线程#

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。

但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
  • Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。

之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。

后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。

img

关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:

  • BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
  • BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
  • BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;

Redis 单线程模型#

Redis 6.0 版本之前的单线模式如下图:

img

图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:

  • 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
  • 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
  • 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。

初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:

  • 首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
  • 接着,调用 epoll_wait 函数等待事件的到来:
    • 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
    • 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
    • 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

Redis 采用单线程为什么还这么快?#

官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:

img

之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

Redis 6.0 之前为什么使用单线程#

CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。

除了上面的官方回答,选择单线程的原因也有下面的考虑。

虽然Redis只有一个主线程,但它利用了异步I/O和事件驱动的方式来处理并发请求。Redis将网络I/O、文件I/O等操作交给底层的操作系统处理,在这些操作完成之前,主线程可以继续执行其他任务。当异步操作完成时,操作系统会通知Redis主线程进行后续处理。

由于单线程的设计使得Redis不必担心锁的问题,同时避免了多线程并发时的锁冲突带来的开销,因此Redis的响应速度很快。此外,Redis将数据保存在内存中,并使用了多种技术(如压缩、对象共享等)来降低内存占用,进一步提高了性能。

使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗

Redis 6.0 之后为什么引入了多线程?

虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上

所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,*所以大家*不要误解 Redis 有多线程同时执行命令。

Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上

Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。

//读请求也使用io多线程
io-threads-do-reads yes 

同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项。

// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
io-threads 4 

关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。

因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程这里的线程数不包括主线程):

  • Redis-server : Redis的主线程,主要负责执行命令;
  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
  • io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。

Redis使用单线程模型,即所有的命令都在一个主线程中执行。这种设计是为了避免多线程的复杂性和线程上下文切换带来的开销。

Redis 是单线程的,如何提高多核 CPU 的利用率#

  1. 使用多个 Redis 实例:可以在同一台机器上启动多个 Redis 实例,并将不同的数据集分配到不同的实例中。这样每个实例都可以使用独立的 CPU 核心和内存,从而提高多核 CPU 的利用率。
  2. 集群部署:Redis 提供了集群模式,在多台机器上启动多个 Redis 节点,并通过复制和分片等技术将数据集分散到各个节点上。这样可以将负载均衡在多台机器上,并且每个节点都可以利用独立的 CPU 核心和内存,从而提高多核 CPU 的利用率。
  3. 使用 Lua 脚本:Redis 支持使用 Lua 脚本执行一些比较复杂的操作,例如批量删除或更新数据等。由于 Lua 脚本是在 Redis 服务器端执行的,因此可以充分利用服务器的 CPU 资源。
  4. 合理配置 Redis 参数:例如设置适当的并发连接数、调整最大内存限制、开启 LRU 或 LFU 等缓存淘汰策略,都可以让 Redis 更好地利用多核 CPU。

Redis性能调优#

  1. 合理配置Redis内存使用:通过设置最大内存限制和合理的过期策略,避免Redis内存占用过高。
  2. 使用持久化功能:将数据持久化到磁盘可以避免Redis重启时丢失数据,并提高Redis读取速度。可以根据实际情况选择RDB或AOF持久化方式。
  3. 配置合适的线程数和连接数:根据机器的CPU核心数量、内存大小等因素,合理配置Redis工作线程数和客户端连接数,以提高Redis的并发处理能力。
  4. 选择合适的数据结构:根据场景需求选择合适的Redis数据结构,如字符串、哈希表、有序集合等。
  5. 使用Redis集群:将数据分布在多个节点上可以提高Redis的整体性能,并增强Redis对故障的容错能力。
  6. 使用缓存穿透技术:采用类似Bloom Filter的技术,将查询不存在的数据缓存下来,可以避免频繁查询数据库导致Redis性能下降。
  7. 充分利用Redis Pipeline和批量操作:使用Pipeline和批量操作可以减少Redis与客户端之间的网络开销,从而提高Redis处理请求的性能。
  8. 监控和调优Redis性能:使用监控工具如Redis自带的redis-cli命令、redis-stat、Redis Desktop Manager等,及时监测Redis的状态,识别瓶颈并进行调整

Redis 持久化#

Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。

Redis 共有三种数据持久化的方式:

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点

Redis AOF和RDB#

  • RDB(Redis Database):默认开启,将 Redis 在内存中的数据周期性地写入磁盘保存为快照文件(一般以 dump.rdb 命名),可以设置自动触发或手动执行。当 Redis 重启时,会优先加载该快照文件用于数据恢复。

  • AOF(Append Only File):将 Redis 所有写命令追加到一个日志文件(aof 文件)中,以实现数据持久化。AOF 可以保证数据不丢失,但是会占用更多磁盘空间,并带来一定的性能损耗。可以根据需要选择是否开启 AOF 持久化。

Redis RDB 和 AOF 持久化方式各有优缺点:

  1. RDB 优点:
  • 数据库备份方便,快照文件为二进制格式,压缩率高,可以在 Redis 备份时进行复制,迁移等操作。
  • IO 消耗低,因为是快照形式保存数据,并在特定时间间隔内同步到硬盘上。
  1. RDB 缺点:
  • 可能会丢失一定数据,因为数据同步并不是实时的,若服务器意外宕机,最后一次备份和现在之间的数据将会丢失。
  • 在需要频繁备份的场景下,比如大量短期数据的应用,RDB 将消耗较多的 CPU 和 IO 资源,在备份过程中可能会影响服务性能。
  1. AOF 优点:
  • 可以做到更高的数据安全,数据写入 AOF 文件时通过 append only 的模式保证数据不丢失。
  • 可以选择不同的 fsync 策略来控制写入磁盘的时机,从而控制性能和安全的折中。
  • AOF 文件是文本文件,可以方便地进行人工恢复、修改和分析。
  1. AOF 缺点:
  • AOF 文件相对于 RDB 文件会占用更多的磁盘空间,同时也会带来更高的 IO 负载。
  • AOF 文件的重写过程(AOF Rewrite)可能会耗费大量的时间和 CPU 资源,造成 Redis 的停顿。
  • 使用 AOF 持久化的场景下需要尽量避免大量的写操作,否则可能会导致 AOF 文件过大、重写时间过长等问题。

Redis 的混合持久化#

RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。

AOF 优点是丢失数据少,但是数据恢复不快。

为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。

混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据

img

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

混合持久化优点:

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

混合持久化缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
  • 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

Redis 的淘汰策略#

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。

1、不进行数据淘汰的策略

noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。

2、进行数据淘汰的策略

针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值;
  • volatile-ttl:优先淘汰更早过期的键值。
  • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
  • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值;
  • allkeys-lru:淘汰整个键值中最久未使用的键值;
  • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值

缓存雪崩、缓存击穿、缓存穿透?#

缓存雪崩、缓存击穿和缓存穿透是缓存中的三种常见问题,具体定义如下:

  1. 缓存雪崩:指在某个时间段内,缓存集中失效导致大量请求直接访问数据库或后端服务,从而引起服务瘫痪的情况。
  2. 缓存击穿:指某个热点key在缓存过期后,恰好有大量并发请求访问该key,导致请求瞬间流过缓存直接访问后端服务,从而引起性能瓶颈或服务宕机的情况。
  3. 缓存穿透:指查询一个不存在的key,由于缓存中没有数据,每次请求都会直接访问后端服务,从而引起服务瘫痪或被攻击的情况。

针对这些问题,可以采取以下解决方案:

  1. 对于缓存雪崩,可以采用分布式锁、预热和多级缓存等方案来避免。
  2. 对于缓存击穿,可以采用热点数据永不过期、限流、加锁等方案来避免。
  3. 对于缓存穿透,可以采用布隆过滤器、缓存空对象等方案来解决。

对于缓存雪崩问题,我们可以采用两种方案解决。

  • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
  • 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

应对缓存击穿可以采取前面说到两种方案:

  • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

应对缓存穿透的方案,常见的方案有三种。

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

布隆过滤器#

布隆过滤器具有以下好处:

  1. 快速:布隆过滤器可以快速地判断一个元素是否可能存在于一个集合中,其时间复杂度为O(1)。
  2. 空间效率高:布隆过滤器仅需要一定的位数组和哈希函数即可实现高效的数据检索,因此占用的内存空间相比其他数据结构较小。
  3. 可扩展性:布隆过滤器可以通过增加位数组大小和哈希函数数量来提高准确性,从而满足不同的需求。
  4. 分布式支持:由于布隆过滤器只需要一个位数组和多个哈希函数即可实现,因此可以方便地在分布式系统中使用,并且不需要对所有节点都保存完整的数据集。
  5. 隐私保护:布隆过滤器只保存了经过哈希处理的值,并没有保存原始数据,因此可以一定程度上保护数据隐私。

Redis 布隆过滤器是一种基于内存的数据结构,用于快速检索一个元素是否存在于大型集合中。它使用哈希函数将每个元素映射到一个位数组中,并在查询时检查对应位数组的值来确定元素是否存在。

由于布隆过滤器是基于概率的数据结构,因此存在一定的误判率,即有可能误判某个元素存在于集合中,但实际上不存在。但是,这种误判率可以通过适当调整哈希函数数量和位数组大小来控制。

Redis 布隆过滤器通常用于缓存、排重和防止DDoS等场景,能够有效地提高系统性能和安全性。

以下是使用布隆过滤器防止缓存击穿的示例代码,假设有一个名为“cache”的缓存服务:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;

public class CacheService {
    private Jedis jedis;
    private BloomFilter<String> bloomFilter;
    
    public CacheService(String host, int port, int capacity, double errorRate) {
        this.jedis = new Jedis(host, port);
        this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(), capacity, errorRate);
    }
    
    public String getData(String key) {
        // 先从布隆过滤器中检查key是否可能存在于缓存中
        if (bloomFilter.mightContain(key)) {
            // 如果可能存在,则尝试从缓存中获取数据
            String data = jedis.get(key);
            if (data != null) {
                // 如果缓存中有数据,则直接返回结果
                return data;
            }
        }
        // 如果不存在或者缓存中没有数据,则从数据库中查询数据
        String data = queryDataFromDB(key);
        // 将查询结果写入缓存,并添加到布隆过滤器中
        jedis.setex(key, 600, data);
        bloomFilter.put(key);
        // 返回查询结果
        return data;
    }

    private String queryDataFromDB(String key) {
        // TODO: 实现从数据库中查询数据的逻辑
    }
}

在上述示例中,首先初始化了一个 Redis 客户端和一个布隆过滤器。然后在 getData 方法中,先使用布隆过滤器判断请求对应的 key 是否存在于缓存中。如果可能存在,则尝试从缓存中获取数据;否则直接返回无数据结果。如果缓存中没有数据,则从数据库中查询数据,并将查询结果写入缓存,并添加到布隆过滤器中。这样就可以实现使用布隆过滤器防止缓存击穿的功能。

需要注意的是,在使用 Google Guava 库实现布隆过滤器时,需要添加以下依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>

Redis 哨兵模式#

Redis 哨兵模式是 Redis 的高可用性解决方案之一,通过监控 Redis 主从节点状态并进行自动故障转移,实现 Redis 服务的自动化运维。

在 Redis 哨兵模式中,有以下三种角色:

  1. Redis 主节点:负责处理客户端请求,并将数据同步给从节点。当主节点宕机时,需要进行自动故障转移,选举出新的主节点。
  2. Redis 从节点:负责从主节点同步数据,并在主节点宕机时自动升级为新的主节点。
  3. Redis 哨兵节点:负责监控 Redis 主从节点的状态,并在发现主节点宕机时,自动选举新的主节点,并将整个集群的状态更新到哨兵配置文件中。

Redis 哨兵模式的原理如下:

  1. 每个 Redis 哨兵节点都会定期向整个集群中的 Redis 主从节点发送心跳包,用于检测节点的健康状态。
  2. 当哨兵节点发现某个 Redis 主节点不可用时,会对该节点进行标记,并开始对该节点进行故障诊断。
  3. 如果主节点确实已经宕机,则哨兵节点会根据预先设置的规则,选举一个从节点作为新主节点,并通知其他哨兵节点和 Redis 客户端更新集群配置信息。
  4. 新的主节点上线后,哨兵节点会通知其他从节点切换到新的主节点,并重新建立数据同步关系。

通过这种方式,Redis 哨兵模式可以实现自动故障转移和高可用性,使得 Redis 服务在主节点宕机或者网络故障等情况下依然能够正常工作。

Redis 事件机制#

Redis 事件机制是一种基于文件事件驱动的事件处理模型,主要用于实现 Redis 服务器的网络通信和命令处理等功能。其基本原理如下:

  1. Redis 服务器会监听一个 TCP 套接字,侦听客户端连接请求,并将连接请求添加到事件轮询器中。
  2. 事件轮询器使用 I/O 多路复用技术,等待事件的发生,例如客户端发送数据或者 Redis 内部有数据需要处理。
  3. 当有事件发生时,事件轮询器会通知 Redis 服务器,服务器会根据事件的类型选择相应的处理程序,例如读取客户端发送的数据、执行 Redis 命令、向客户端发送响应数据等。
  4. 处理程序会根据事件的类型进行相应的处理,并且可能会向事件轮询器添加新的事件,以便在将来的某个时间点处理。
  5. 整个过程是异步的,即处理程序不会阻塞事件轮询器,而是将任务委托给其他线程或者进程来完成。

通过事件机制,Redis 可以实现高效的网络通信和并发处理能力,从而为用户提供高性能、高可用的数据存储服务。

Redis 事务#

Redis事务是一组命令的集合,这些命令被视为一个单元来执行。在执行期间,其他客户端提交的命令不会被插入到该事务中。Redis事务使用MULTI、EXEC和DISCARD等命令控制事务的开始、提交和回滚操作。但是Redis的事务并不支持回滚,即使其中一条命令执行失败了,其他的命令也会继续执行,并且Redis事务也不保证原子性。

Redis事务相关命令和使用

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视

Redis 脑裂#

博客:https://xiaolincoding.com/redis/base/redis_interview.html#集群脑裂导致数据丢失怎么办

Redis 脑裂(Split-Brain)是指 Redis 集群出现网络分区或故障时,导致不同的节点在没有协调的情况下形成了多个独立的子集,每个子集都认为自己是有效的 Redis 主节点,并且独立地进行写入操作,这会导致数据的不一致性和丢失。

避免 Redis 脑裂可以采取以下措施:

  1. 使用 Redis Sentinel 进行监控和自动故障转移:Redis Sentinel 是一个用于管理 Redis 高可用性的工具,它能够监控 Redis 实例的状态,并在发生故障或网络分区时自动切换到备用节点,从而保证 Redis 集群的可用性和数据一致性。
  2. 使用 Redis Cluster 进行分布式存储:Redis Cluster 是 Redis 官方提供的分布式存储解决方案,它将数据分散存储在多个节点中,并使用 Gossip 协议进行节点间的通信和数据同步,可以避免数据不一致和脑裂等问题。
  3. 设计合理的网络架构和容错机制:例如使用双活架构、负载均衡、多可用区部署等手段来减少网络分区和单点故障的影响,以及使用数据备份和灾备恢复等技术来保护数据的安全和完整性。

需要注意的是,在实际应用中,无法完全避免 Redis 脑裂问题的发生,因此需要针对具体场景进行方案设计和测试,确保系统的高可用性和数据一致性。

Redis 的主从复制核心原理#

Redis的主从复制是一种异步复制机制,主要分为三个步骤:

  1. 主节点创建RDB文件或使用AOF方式记录操作命令,并将文件内容发送给从节点。
  2. 从节点接收到主节点发送的数据,并在本地进行相应的数据更新操作,以保证与主节点的数据一致性。
  3. 从节点向主节点发送SYNC命令,主节点接收到该命令后将再次发送最近的操作命令或RDB文件给从节点,并重复以上步骤。

在主从复制过程中,主节点持续记录执行的操作命令,并用于同步从节点。当从节点连上主节点时,通过发送PSYNC命令和传达前面执行的复制偏移量(offset)来获取主节点数据的同步数据流。如果从节点没有完成完整的复制流程,则使用主节点的RDB文件作为快照来恢复其数据状态。因此,在主从复制的初始阶段,可能会有时间窗口,在这个时间窗口内的写操作可能会丢失。

为了提高复制的效率和减少延迟,Redis采用了增量复制的方式,即只复制最新的数据,而不是所有的历史数据。从节点通过保存主节点的复制偏移量,定期向主节点发送PING命令来检查主节点是否存活并获取复制偏移量,确保数据的一致性。

总的来说,Redis主从复制的核心原理在于主节点将数据同步到从节点,实现数据的备份和负载均衡。主从复制可以提高Redis的可用性和性能,但需要注意的是,在复制过程中可能会存在数据同步延迟或写操作丢失等问题。

Redis 集群方案#

Redis集群是Redis官方推出的一种分布式解决方案,用于实现数据在多个节点之间的分片和复制,并提供高可用性和容错能力。以下是Redis集群的主要特点和实现机制:

  1. 分片:将数据分散存储在多个节点上,每个节点负责处理部分数据,并可动态扩展。
  2. 复制:每个节点都有多个副本,当主节点出现故障时,可以自动切换到备用节点,保证系统的高可用性。
  3. 节点间通讯:节点之间使用二进制协议进行通讯,并支持加密和压缩等方式。
  4. 集群管理:使用Gossip协议,在节点之间交换信息,更新整个集群的视图,支持增加、删除、重新分片等操作。
  5. 故障转移:当主节点故障时,通过Raft算法选举新的主节点。
  6. 客户端路由:客户端通过获得不同槽位对应的节点信息,以及节点信息中的主从关系,来对Redis集群进行读写操作。

Redis集群的实现需要至少6个节点,其中3个节点作为主节点,另外3个节点作为备份节点,每个主节点负责处理一部分数据,并将其复制到对应的备份节点上。这种方式可以提供更好的性能和容错能力,并避免了单点故障的风险。

Redis 集群方案什么情况下会导致整个集群不可用#

  1. 大量节点故障:如果大量的 Redis 节点同时宕机或者网络连接出现问题,就可能导致整个集群无法正常工作。
  2. 数据丢失:如果集群中某个节点发生数据丢失或者数据损坏,就可能导致整个集群的数据不一致或者无法访问。
  3. 故障转移失败:当主节点故障时,需要进行自动故障转移,将从节点提升为新的主节点。但如果故障转移过程中出现问题,例如选举出的新主节点无法正常工作,就可能导致整个集群无法正常工作。
  4. 网络拥堵:如果集群中节点之间的网络传输带宽受限或者网络拥堵,就可能导致整个集群的性能下降或者无法正常工作。

因此,在设计和部署 Redis 集群方案时,需要考虑这些潜在的风险,并采取相应的措施来保证集群的稳定性和高可用性,例如使用备份节点、多副本复制、监控告警等方式。

25.Redis 集群是否会写操作丢失#

在Redis集群中,当采用主从复制模式时,可能会出现写操作丢失的情况。这是因为Redis的主节点和从节点之间存在一定的数据同步延迟,导致从节点在接收到新的写操作之前,可能还没有完全同步主节点的最新状态。

如果在这种情况下,客户端向从节点发送写操作请求,那么该请求将被从节点拒绝,从而导致写操作丢失。为了解决这个问题,Redis引入了哨兵(Sentinel)机制和集群(Cluster)模式。

在哨兵机制中,将主节点和从节点统称为Redis实例,哨兵作为一个独立的进程监控Redis实例的状态,并在主节点发生故障时,自动将某个从节点提升为主节点。这种方式可以避免主从复制中的写操作丢失。

在Redis集群模式中,数据被分布在多个节点上,每个节点都保存部分数据,同时Redis使用复制、故障转移等技术来保证高可用性和数据一致性。虽然Redis集群也有可能存在写操作丢失的情况,但这种情况的概率很小,并且可以通过设置适当的副本数量、调整同步策略等手段来降低风险。

如何实现集群中的 session 共享#

  1. 使用 Redis:可以将 session 数据存储在 Redis 中,由于 Redis 的高性能和高可用性,可以很好地支持 session 数据的共享存储。多个应用服务器可以通过 Redis 客户端连接到同一个 Redis 集群,并使用相同的 Redis key 存储 session 数据。
  2. 使用 Memcached:与 Redis 类似,可以将 session 数据存储在 Memcached 中,多个应用服务器可以通过 Memcached 客户端连接到同一个 Memcached 集群,并使用相同的 key 存储 session 数据。
  3. 使用数据库:可以将 session 数据存储在数据库中,多个应用服务器都可以连接到同一个数据库,并使用相同的表结构存储 session 数据。需要注意的是,在高并发场景下,使用数据库存储 session 数据可能会对数据库造成较大的压力,需要合理设计表结构和索引以及优化 SQL 查询语句。

Redis 的大 key 如何处理方案#

参考:https://xiaolincoding.com/redis/base/redis_interview.html#redis-的大-key-如何处理

什么是 Redis 大 key?

大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。

一般而言,下面这两种情况被称为大 key:

  • String 类型的值大于 10 KB;
  • Hash、List、Set、ZSet 类型的元素的个数超过 5000个;

大 key 会造成什么问题?

大 key 会带来以下四种影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

Redis 缓存设计#

参考:https://xiaolincoding.com/redis/base/redis_interview.html#redis-缓存设计

如何用redis实现分布式锁#

博客:https://juejin.cn/post/6936956908007850014

方案一:SETNX + EXPIRE

方案二:SETNX + value值是(系统时间+过期时间)

方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)

方案四:SET的扩展命令(SET EX PX NX)

方案五:SET EX PX NX + 校验唯一随机值,再释放锁

方案六: 开源框架:Redisson

方案七:多机实现的分布式锁Redlock

实现分布式锁示例#

在 Java 中,可以使用 Redisson 等 Redis 客户端库来实现分布式锁的功能。以下是使用 Redisson 实现分布式锁的示例代码:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class Example {
    public static void main(String[] args) {
        // 创建 Redisson 客户端连接对象
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // 获取分布式锁对象,并加锁
        RLock lock = redisson.getLock("mylock");
        lock.lock();

        try {
            // 执行业务逻辑,例如修改共享资源
            System.out.println("execute business logic");
        } finally {
            // 释放锁
            lock.unlock();
        }

        // 关闭 Redisson 连接对象
        redisson.shutdown();
    }
}

这个例子演示了如何使用 Redisson 客户端库创建分布式锁并加锁、解锁,其中 getLock() 方法返回一个 RLock 对象,通过调用 lock() 方法获取锁资源,执行业务逻辑,最后通过调用 unlock() 方法释放锁资源。

需要注意的是,在实际应用中还需要考虑锁的超时和重试等问题,以及使用 Watch/Multi/Exec 事务机制保证数据一致性等技术细节。同时也需要在测试环节进行充分的验证和演练,以便于在出现问题时快速有效地应对和修复。

相关博客:

https://pdai.tech/md/db/nosql-redis/db-redis-overview.html,https://xiaolincoding.com/redis
https://xiaolincoding.com/redis/base/redis_interview.html#redis-的大-key-如何处理
https://mp.weixin.qq.com/s?__biz=MzkwODE5ODM0Ng==&mid=2247491521&idx=1&sn=dcefc00c23d0821990f62dc3749141bb&chksm=c0ccf764f7bb7e72defbc937a72e9d2ec766b8de1574def67a4650c80c8329694127b01405d4&scene=178&cur_album_id=2041709347461709827#rd

posted @   糯米๓  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示