Redis 数据类型和数据结构

常见的数据类型

  • String类型

    • 底层数据结构:SDS
  • List类型

    • 底层数据结构:双向链表或者压缩列表
      • 当列表元素小于512个,列表的每个元素的值都小于64字节,Redis会使用压缩列表
      • 否则Redis会使用双向链表
      • 在Redis 3.2之后,List的底层数据结构由quickList实现
  • Hash类型

    • 底层数据结构:压缩列表或者哈希表实现
      • 如果哈希类型元素小于512个且所有值小于64字节,Redis会使用压缩列表作为Hash类型的底层数据结构
      • 否则使用哈希表
      • 在Redis 7.0之后,由listpack数据结构实现
  • Set类型

    • 底层数据结构:哈希表或者整数集合
      • 如果集合中的元素都是整数且个数小于512,Redis会使用整数集合作为Set类型的底层数据结构
      • 否则,使用哈希表作为Set类型的底层数据结构
  • ZSet类型

    • 底层数据结构:压缩列表或者跳表
      • 如果有序集合的元素个数小于128个,并且每个元素的值小于64字节,Redis会使用压缩列表作为ZSet类型的底层数据结构
      • 否则使用跳表
      • 在Redis 7.0中,压缩列表的数据结构已经废弃,使用listpack数据结构实现

Redis数据结构(8种)

  • SDS

    • 结构设计:
      • Len:记录字符串长度
      • Alloc:记录分配的空间长度
      • Flags:记录SDS的类型
      • Buf[]:存放数据的字节数组
    • 相比于原本C的字符串的好处:
      • 可以以O(1)的时间复杂度获取字符串的长度,直接返回Len
      • 二进制安全,可以存储\0
      • 不会发生缓冲区溢出,会自动扩容
        • 扩容机制:
          • 如果所需的SDS长度小于1MB,扩容策略是翻倍
          • 如果所需的SDS长度超过1MB,扩容策略是所需SDS长度+1MB
        • 好处:
          • 可以有效减少内存的分配次数
      • 节省内存空间
        • 使用不同的SDS类型:数据结构中的LenAlloc变量的数据类型不同,可以灵活地保存不同大小的字符串,从而节省内存空间
        • 使用编译优化:要求编译器在编译过程中取消优化对齐,按照实际占用的字节数进行对齐
  • 双向链表

    • 结构设计:
      • head 头节点
        • 节点设计:
          • prev 前置节点
          • next 后置节点
          • value 节点值
      • tail 尾节点
      • dup 节点负责函数
      • free 节点释放函数
      • match 节点比较函数
      • len 节点数量
    • 优点:
      • 获取前置节点和后置节点的时间复杂度为O(1),前后置节点都可以指向NULL,所以无环
      • 获取头节点和尾节点的时间复杂度是O(1)
      • 获取节点数量的时间复杂度是O(1)
      • 可以保持不同类型的值
    • 缺点:
      • 每个节点的内存并不连续,所以无法很好地利用CPU缓存
      • 每个节点要存储额外的信息,内存开销大
  • 压缩列表

    • 设计思想:一种内存紧凑型的数据结构,占用一块连续的内存空间,便于利用CPU缓存,根据不同长度的数据,进行相应的编码
    • 缺陷:
      • 不能保存过多的元素,否则查询效率会降低
      • 新增或者修改某个元素的时候,压缩列表占用的内存空间需要重新分配,可能引发连锁更新的问题
    • 结构设计:
      • 表头:
        • zlbytes:记录整个压缩列表占用内存字节数
        • zltail : 记录尾部节点距离起始地址多少节点,也就是列表尾的偏移量
        • zllen:记录压缩列表包含的节点数量
      • 表尾:
        • zlend:记录压缩列表的结束点,固定值0xFF
      • 节点:
        • Prevlen:记录前一个节点的长度,便于实现从后向前遍历
          • 如果前一个节点的长度小于254字节,需要1字节的空间
          • 如果前一个节点的长度大于等于254字节,需要5字节的空间
        • encoding:记录当前节点实际数据的类型(字符串和整数)和长度
          • 如果节点数据为整数,那么encoding需要1字节的空间
          • 如果是字符串,根据字符串的大小,使用1字节/2字节/5字节空间进行编码
          • 前两个bit表示数据的类型,后续表示数据的实际长度
        • data:记录当前节点的实际数据
    • 优点:
      • 查询第一个节点,最后一个节点,节点数的实际复杂度是O(1),查找其他元素的时间复杂度是O(N),因此压缩列表不适合保存过多的元素
    • 缺陷:
      • 连锁更新:
        • 更新一个节点导致Prevlen发生变化,进而导致后续所有节点的Prevlen发生变化
  • 哈希表

    • 结构设计:
      • dictEntry table:哈希表数组
        • key 键值对中的键
        • v 键值对中的值
        • next:指向下一个节点,形成链表
      • size:哈希表大小
      • sizemask:哈希表大小的掩码,用于计算索引值
      • used:哈希表已有的节点数量
    • 哈希冲突的解决:
      • 使用链式哈希,被分配到同一个哈希桶的多个节点,使用单向链表连接起来
      • Rehash:如果一个桶的数据过多,就需要Rehash
        • 步骤:
          • 给哈希表2分配空间,一般比哈希表1大一倍
          • 将哈希表1的数据迁移到哈希表2中
          • 迁移完成后,将哈希表2设置为哈希表1,新创建一个哈希表2,为下次rehash做准备
        • 渐进式Rehash:
          • 在Rehash期间,每次对哈希表进行新增、删除、查找、更新操作时,除了执行对应的操作之外,还会按照顺序将哈希表1上索引位置所有的key-value迁移到哈希表2上
        • Rehash的触发条件:
          • 负载因子:
            • 计算:哈希表已保存节点数量 / 哈希表的大小
          • 当负载因子大于等于1,并且Redis没有执行bgsave或者bgrewriteaof,也就是没有执行RDB快照或者进行AOF重写的时候,就会进行Rehash操作
          • 当负载因子大于5的时候,无论有没有执行RDB快照或者AOF重写,都会强制进行Rehash操作
  • 整数集合

    • 结构设计:
      • encoding:编码方式
      • length:元素数量
      • contents:保存元素的数组,元素的类型取决于encoding的值
    • 整数集合的升级操作:
      • 当新元素的类型比现在所有元素的类型都要长时,整数集合要先按照新元素的类型扩展contents数组的大小,再将新元素加入到整数集合
      • 好处:节省内存资源
  • 跳表

    • 结构设计:
      • headertail:跳表的头尾节点
        • ele 对象的元素值
        • score 元素的权重值
        • backward 后向指针
        • level[]
          • 保存每一层的前向指针和跨度
      • length:跳表的长度
      • level:跳表的最大层数
    • 查询过程:
      • 从头节点的最高点开始,逐一遍历每一层
        • 如果当前节点的权重小于要查找的权重,跳表就会访问该层的下一个节点
        • 如果当前节点的权重等于要查找的权重,并且当前节点的SDS类型数据小于要查找的数据,跳表就会访问该层的下一个节点
        • 如果以上条件不满足,或者下一个节点为空,跳表会使用当前遍历节点level数组里的下一层指针,沿着下一层指针继续查找,相当于跳到了下一层继续查找
    • 跳表相邻两层的节点数量最理想的比例是2:1,查找复杂度可以降低到O(logN)
    • 跳表在创建节点的时候,会生成范围(0 - 1)的一个随机数,如果这个随机数小于0.25,层数就增加一层,然后继续生成下一个随机数,直到结果大于0.25
    • 层高的限制是64,在创建头节点的时候,就会直接创建64层高的头节点
    • 为什么要使用跳表而不使用平衡树:
      • 从内存占用来说,跳表每个节点只有1.33个指针,调整参数后,跳表比平衡树更灵活一些
      • 在做范围查找的时候,跳表比平衡树操作要简单
      • 从算法实现难度上来说,跳表比平衡树要简单
  • Quicklist

    • Quicklist取代了List的双向链表或者压缩列表的底层数据结构
    • 本质上是 双向链表 + 压缩列表
    • 结构设计:
      • headtailquicklist链表头
        • prevnext:前置,后置节点
        • zl,节点对应的压缩列表
        • 压缩列表的字节大小
        • 压缩列表的元素个数
      • count 压缩列表元素总数
      • len quicklist节点总数
  • Listpack

    • 目的:用于替换压缩列表,解决连锁更新的问题
    • 结构设计:
      • 头部:
        • listpack总字节数
        • listpack元素数量
      • 节点:
        • encoding:元素的编码类型,会对不同长度的整数和字符串进行编码
        • data,数据存放
        • lenencoding + data的总长度
      • 尾部:
        • 结尾标识
    • listpack没有压缩列表中记录前一个节点长度的字段了,listpack只记录了当前节点的长度,当我们向listpack加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题
    • 如何向前遍历:
      • 从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的entry-len

Redis 线程模型

Redis并非单线程:

  • 接收客户端请求 -> 解析请求 -> 进行数据的读写操作 -> 发送数据给客户端这个过程是由一个线程,主线程来完成的
  • 在2.6版本,会启动2个后台线程,分别用来处理关闭文件、AOF刷盘
  • 在4.0之后,新增了一个后台线程,用来异步释放Redis内存,也就是lazyfree线程

Redis单线程模式是怎么样的:

  • 使用了epoll,IO多路复用来监听连接事件,读事件,写事件,在监听到事件后,调用对应的事件处理函数

为什么Redis是单线程,但还是这么快

  • Redis大部分操作都在内存中完成,并且采用了高效的数据结构
  • Redis采用单线程模型避免了多线程之间的竞争
  • Redis采用了I/O多路复用机制来处理大量的客户端的Socket请求

为什么Redis要使用单线程

  • CPU并不是制约Redis性能表现的瓶颈所在
  • 引入多线程,会增加系统复杂度,同时可能存在线程切换、加锁解锁、死锁造成的性能损耗

为什么Redis6.0之后引入了多线程

  • 在Redis6.0之后,采用了多个I/O线程来处理网络请求,因为随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上

Redis 持久化

Redis如何实现数据不丢失

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

AOF日志实现方式:

  • Redis在执行完一条命令后,会把命令以追加的方式,写入到一个文件里,然后Redis重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复
  • 为什么先执行命令,再把数据写入日志呢?
    • 好处:
      • 避免额外的检查开销
      • 不会阻塞当前写操作命令的执行
    • 弊端:
      • 数据可能会丢失
      • 可能会阻塞其他操作
  • 写入日志的实现过程:
    • 命令追加到server.aof_buf缓冲区(用户态)
    • I/O系统调用(write())切换到内核态
    • 写入到内核缓冲区
    • 由内核发起写操作,写入磁盘
      • 什么时候写入磁盘,由操作系统内核决定
      • Redis.conf提供了3种写入磁盘的参数:
        • Always:每次写操作完毕后,同步将AOF日志数据写回磁盘
        • EverySec:每次写操作命令执行完后,先将命令写入到AOF文件的内核缓冲区,然后每隔1秒将缓冲区的内容写回磁盘
        • No:不由Redis控制写回磁盘的时机,由操作系统控制
  • AOF日志过大,触发重写机制:
    • 重写机制会读取所有key最新的value,然后用一条命令记录到新的AOF文件,之前的一个命令就没有必要记录了,重写完成后,会将新的AOF文件覆盖现在的AOF文件,这相当于压缩了AOF文件,使得AOF文件的体积变小了
    • 重写的过程:
      • 启动一个重写子进程,共享物理内存
      • 子进程对内存只读,重写子进程读取内存所有键值对,逐一转换成命令,再将命令写到新的日志
      • 重写过程中,主进程依然可以正常处理命令,但主进程修改已经存在的key-value会导致写时复制,此时这个key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了
        • 为了解决这种数据不一致问题,Redis设置了一个AOF重写缓冲区
        • 重写AOF期间,当Redis执行完一个写命令之后,它会同时将这个写命令写入到「AOF缓冲区」和「AOF重写缓冲区」
      • 当子进程完成AOF重写工作会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
      • 调用一个信号处理函数,该函数主要做以下工作:
        • 将AOF重写缓冲区中的所有内容追加到新的AOF的文件中,使得新旧两个AOF文件所保存的数据库状态一致
        • 新的AOF的文件进行改名,覆盖现有的AOF文件。

RDB快照

  • RDB快照是以二进制的形式直接保存Redis数据区域的所有数据
    • 如果使用SAVE则会阻塞主线程
    • 如果使用BGSAVE则不会
  • 在执行RDB的期间,数据可以修改吗?
    • 可以,RDB会使用一种Copy-On-Write技术,在主线程修改数据的时候,会产生一个原数据的备份,RDB快照会使用这个备份进行,也就是保证RDB快照保存的是启动时候的数据

混合持久化

  • 在持久化时,fork出的子进程,会首先将共享内存区域的内存数据以RDB快照的方式写入AOF文件,期间主线程的操作命令会被放入AOF重写缓冲区,重写缓冲区的增量命令会以AOF格式写入到AOF文件,之后会使用含有RDB和AOF的AOF文件替换原文件
  • 好处:
    • 由于前部分是RDB,所以数据加载恢复会很快
    • 加载完RDB内容后,才会加载AOF的内容,减少了大量数据的丢失风险
  • 缺点:
    • 由于AOF文件添加了RDB的内容,让AOF的文件可读性变得很差
    • 兼容性差,如果开启了混合持久化,那么混合持久化的AOF文件是不能用在Redis 4.0之前的版本的

Redis 集群-主从复制

Redis的高可用,要从Redis的多服务节点来考虑,比如Redis的主从复制,哨兵模式,切片集群等。

当 Redis 的 key 分散在多个机器上,某些机器存有大量的热点 key,导致访问请求不均衡时,可以通过以下几种方法来均衡访问请求:

1. 哈希槽(Hash Slot)均衡

Redis 集群模式使用一致性哈希将 key 映射到不同的节点上,但某些热点 key 可能集中在一个节点。可以通过以下策略来均衡:

  • 预分配哈希槽:手动调整集群的哈希槽分配,使得负载较高的节点占用更多的哈希槽,分散负载。
  • 再平衡节点:Redis 集群支持在线调整哈希槽,可以根据热点 key 分布和流量情况动态调整节点的哈希槽分配,平衡负载。

2. 缓存分片(Sharding)

通过将数据进行水平分片,均衡地将不同的 key 存储到多个节点上。每个 key 根据某种规则(如一致性哈希或范围分片)被分配到不同的 Redis 实例。这样可以分散热点 key 的请求。

  • 一致性哈希分片:在一致性哈希环上增加虚拟节点,可以帮助更细致地分散 key,避免部分节点负载过高。
  • 平衡数据分布:在现有的分片机制上进行再分配,将热点 key 迁移到负载较低的节点上。

3. 使用代理层(如 Twemproxy、Codis)

通过使用代理层来对 key 的访问进行均衡管理。代理层可以智能地将请求路由到不同的 Redis 节点,减少对某个节点的集中访问压力。例如:

  • Twemproxy:可以作为 Redis 访问的中间层,通过哈希算法将请求分发到不同的 Redis 实例。
  • Codis:支持分布式 Redis 集群,帮助实现动态扩容和均衡访问。

4. 热点 key 削峰(Hot Key Mitigation)

对于一些热点 key,可以通过一些削峰手段来减少对某个 Redis 实例的压力:

  • 缓存副本:为热点 key 设置多个副本,将读取流量分发到不同的 Redis 实例中,减少对单个节点的压力。
  • 多级缓存:在 Redis 之前增加一级本地缓存,或使用更高级别的缓存系统(如 CDN),减少对 Redis 的直接访问。
  • 请求合并:对于多个访问同一个热点 key 的请求,合并为一个请求,减少 Redis 的压力。可以通过消息队列或异步队列实现。

5. 批量请求

将多个小请求合并成一个批量请求,减少对 Redis 节点的请求频率,缓解热点问题。例如,利用 Redis 的 MGETMSET 来批量处理多个 key。

6. 分布式锁和限流

对于高频写操作,可以使用分布式锁控制对热点 key 的并发写入,或对请求进行限流,避免某个 key 被过度访问。

一致性哈希(Consistent Hashing) 是一种用于分布式系统中的负载均衡算法,广泛应用于分布式缓存、数据库和分布式存储等场景。它通过将数据和节点映射到哈希环上,从而在数据节点的增加或减少时尽可能减少数据的重新分布,以提高系统的扩展性和负载均衡能力。

一致性哈希的核心思想

一致性哈希的基本思想是将所有的节点和数据映射到一个哈希环(Hash Ring)上,当系统中的节点发生变化(比如增加或减少节点)时,只需要重新分配少部分的数据,而不是对所有数据进行重新映射。

哈希环

  • 一致性哈希的环通常被表示为一个 0 到 2³² - 1 的整数范围,首尾相接形成一个圆环。系统中的每个节点(服务器、缓存、数据库等)通过哈希函数映射到环上的某个位置。
  • 同时,数据的 key 也会通过相同的哈希函数映射到哈希环上。

数据分配

  • 数据的 key 会按照顺时针方向存储在第一个大于或等于该 key 哈希值的节点上。如果环中没有更大的节点,则数据存储在第一个节点上。
  • 当一个节点失效或新节点加入时,只需要重新分配该节点的前一个节点或后一个节点之间的数据,其他数据不需要重新分配。

一致性哈希的优点

  1. 减小节点变化时的数据迁移量:与传统哈希算法(如简单取模)不同,增加或删除节点时,一致性哈希只影响少部分节点上的数据,而不是所有节点上的数据。例如,当节点数量从 n 增加到 n+1 时,取模算法需要重新分配几乎所有数据,而一致性哈希只需重新分配少部分数据。
  2. 负载均衡性:如果节点均匀分布在哈希环上,一致性哈希能够使数据在各个节点之间尽可能平衡地分布。
  3. 动态扩展性:一致性哈希的扩展性非常好,支持节点的动态增加和减少,适用于节点频繁变动的分布式系统。

虚拟节点(Virtual Nodes)

由于一致性哈希在节点不均匀分布时可能导致负载不均衡,通常使用“虚拟节点”来进一步优化负载均衡性。

  • 虚拟节点的概念:每个物理节点可以对应多个虚拟节点,这些虚拟节点通过哈希函数映射到环上的不同位置,实际上是对物理节点的多重表示。
  • 优势:虚拟节点通过增加节点的分布密度,使得数据的分布更加均匀,避免某些物理节点承受过多的数据请求。

使用场景

一致性哈希主要用于分布式系统中的负载均衡和数据分布场景,常见的使用场景包括:

  • 分布式缓存:如 Memcached、Redis 集群中,保证缓存节点的均匀分布和动态扩展。
  • 分布式存储:如 Cassandra、Amazon DynamoDB,保证数据分布在不同的存储节点中。
  • 负载均衡:在高可用的服务架构中,均衡请求在不同的服务实例之间的分布

主从复制:

全量复制

  • 一般采用一主多从的模式,主节点负责读写操作,从节点只负责读操作,同时主节点会将写操作同步到从节点
  • 主节点和从节点的写操作同步是异步进行的,难免会出现数据不一致的情况,是无法保证强一致性的。
  • 过程:
    • 第一阶段:建立链接,协调同步
      • 从服务器会执行replicaof命令和主服务器建立主从关系,接着从服务器会向主服务器发送psync命令,表示要进行数据同步,主服务器收到psync后,会以FULLRESYNC响应回应对方,表示采用的是全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器
    • 第二阶段:主服务器同步数据给从服务器
      • 主服务器执行BGSAVE命令来生成RDB文件,然后把文件发送给从服务器,从服务器收到RDB文件后,会清空数据库,然后载入RDB文件
      • 在生成RDB文件后执行的命令,无法写入到生成的RDB文件中,这就导致了主从不一致
      • 为了保证主从一致性,主服务器会将三个时间间隙中收到的写操作命令,写入replication buffer中:
        • 主服务器生成RDB文件期间
        • 主服务器发送RDB文件给从服务器期间
        • 从服务器加载RDB文件期间
    • 第三阶段:主服务器发送新的写操作命令给从服务器
      • 在从服务器同步RDB文件完成后,会向主服务器发送确认消息,接着主服务器会将replication buffer中的所有命令发送给从服务器,这时,主从服务器的数据就一致了

命令传播

  • 主服务器在完成第一次同步后,双方会维护一个TCP链接,后续主服务器可以通过这个链接将写操作命令传播给从服务器

增量复制:

  • 如果在网络链接断开又恢复后,Redis主从节点之间会采用增量复制
  • 过程:
    • 在命令传播的过程中,主服务器的命令会写入到repl-backlog-buffer中,这是一个环形缓冲区,用于主从服务器断开后,从中找到差异数据,主服务器使用master_repl_offset记录写的位置,从服务器使用slave_repl_offset记录读的位置
    • 从服务器连上主服务器后,从服务器会将自己的复制偏移量slave_repl_offset发送给主服务器,主服务器根据自己的偏移量和从服务器偏移量的差值,判断要执行的操作:
      • 如果从服务器要读取的数据还在缓冲区中,就采取增量同步
      • 如果不在缓冲区中,就采取全量同步
    • 为了减少全量同步,可以将repl-backlog-buffer设置的大一些

如何判断Redis某个节点是否正常工作?

  • 通过基本的Ping-Pong心跳检测机制:
    • 主节点每10秒发送一次ping对从节点,判断从节点的存活性和连接状态
    • 从节点每隔1秒发送一次replconf ack,给主节点上报自己的复制偏移量
      • 实时检测主从节点网络状态
      • 上报自身复制偏移量,检查复制数据是否丢失,如果数据丢失,在从主节点缓冲区中拉取数据

主从架构中,过期Key如何处理?

  • 主节点会模拟一个DEL命令,发送给从节点

Redis是同步复制还是异步复制?

  • Redis每次收到写命令后,先写到内部缓冲区,然后异步发送给从节点

两个Buffer (replication bufferrepl-backup-buffer) 有什么区别?

  • 出现阶段:
    • Replication buffer在全量复制和增量复制都会出现,主节点会给每个连接的从节点都分配一个replication buffer
    • Repl-backup-buffer只在增量复制节点出现,一个主节点只分配一个
  • 当缓冲区满后,发送的事情不一样:
    • repl-backup-buffer满了,会直接覆盖起始数据位置
    • replication buffer满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制

如何应对主从数据不一致?

  • 尽量保证主从节点状态良好
  • 使用一个外部程序来监控主从节点的复制进度:
    • 监控主节点offset和从节点offset的差值,如果差值超出了我们的阈值,让从节点不再对外提供数据读取服务

主从切换如何减少数据丢失?

  • 异步复制同步丢失:
    • 对于服务端,配置一个min-slaves-max-lag,一旦所有的从节点数据复制和同步的延迟都超过了min-salves-max-lag的值,主节点就拒绝接收请求
    • 对于客户端,当发现主节点不可写后,采取降级措施,将数据暂时写入本地缓存和磁盘中或者写入kafka等消息队列中,等主节点恢复正常,再将数据重新写入主节点
  • 集群产生脑裂数据丢失:
    • 出现原因:当出现网络问题,哨兵节点认为主节点挂了,就会选举出一个新的主节点,但如果原来的主节点又重新连接,但因为选举了新的主节点,新的主节点就会发送数据的清空和全量同步,那么在主节点失联这个过程中的数据就会全部丢失
    • 解决方案:
      • 要求主节点必须有x个从节点连接
      • 要求主从复制同步的延迟不能超过x
      • 这样,如果主节点假掉线,因为无法保证和其他从节点连接和同步,就无法再接收新的消息,主从切换后,只有新的主节点能处理读请求,即便原主节点数据被清空,也不会导致数据丢失

如何做到故障自动切换?

  • 使用哨兵模式

Redis 过期删除和内存淘汰

Redis存储设置了过期事件的key的时候,会将key存入到一个过期字典中,Redis首先会检查key是否存在于过期字典中,如果不在就正常读取,否则判断key是否过期。

Redis使用的过期删除策略是 惰性删除 + 定期删除。

惰性删除

  • 不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key
    • 优点:每次访问时,才会检查key是否过期,该策略对CPU时间友好
    • 缺点:如果一个过期key一直没被访问,就会一直占用内存,导致浪费

定期删除

  • 每隔一段时间「随机」从数据库中取出一定数量的key进行检查,并删除其中的过期key,如果过期key的数量占比超过选取key的数量的25%,就继续选取,否则停止,等待下一轮
    • 优点 :通过限制删除执行的时长和频率,减少删除操作对CPU的影响,同时也能减少内存的占用
    • 缺点:难以确定删除操作执行的时长和频率,频率高,对CPU不好,频率低,就和惰性删除类似

Redis持久化时,对过期键如何处理?

  • RDB
    • RDB文件生成阶段,过期的键不会被保存到新的RDB文件中
    • RDB加载阶段
      • 主服务器:主服务器在加载RDB文件的时候,会对其中的键是否过期进行检查,如果过期,则不会被载入到内存中
      • 从服务器:从服务器在加载RDB文件的时候,无论键是否过期,都会全部加载到内存中,不过由于从服务器在主从同步的过程中一般会清空数据库,所以几乎没有影响
  • AOF
    • AOF写入阶段:
      • 如果某个键还没过期,AOF会将该键值对写入AOF文件,当过期键值被删除后,AOF会向AOF文件追加一条DEL命令来显式的删除该键值对
    • AOF重写阶段:
      • 已过期的键不会被写入到新的AOF文件中

Redis主从模式中,对过期键会如何处理?

  • 从库不会进行过期扫描,从库在查询过期数据的时候,会直接返回,从库的过期依靠主库在过期时对AOF日志文件写入的追加DEL命令同步

Redis的内存淘汰策略有哪些(内存占用满后)?

  • 不进行数据淘汰策略,而是直接不再提供服务
  • 进行数据淘汰策略
    • 在设置了过期时间的数据中淘汰:
      • 随机淘汰设置了过期策略的值
      • 优先淘汰更早过期的值
      • 淘汰设置了过期时间的键值中,最久未使用的键值
      • 淘汰设置了过期时间的键值对中,最少使用的键值
    • 在全部数据中淘汰:
      • 随机淘汰键值对
      • 淘汰整个键值对中最久未使用的键值
      • 淘汰整个键值对中最少使用的键值

Redis是如何实现LRU算法的?

  • 在Redis对象结构体中添加一个额外的字段lru,记录该数据的最后一次访问时间
    • 24bit用于记录访问时间
  • 进行内存淘汰时,随机取一部分值,淘汰最久未使用的那个
  • 问题:无法解决缓存污染问题

Redis是如何实现LFU算法的?

  • 可以解决缓存污染问题
  • 在Redis对象的结构体中,有一个字段lru记录key的访问频次
    • 前16bit记录访问时间,后8bit记录访问频次

Redis 缓存设计

如何避免缓存雪崩,缓存穿透,缓存击穿

缓存雪崩

  • 定义:当同一时间有大量key同时过期,而这时又有大量的用户请求,导致Redis无法完全处理,请求完全打在数据库上,导致数据库无法处理
  • 解决方法:
    • 将缓存失效的时间打散
    • 设置缓存不过期,而是手动更新缓存

缓存击穿

  • 定义:某一时间,有一个key过期了,但同时针对这个key有大量的请求访问,而无法读取缓存,直接访问数据库,导致数据库宕机
  • 解决方法:
    • 使用互斥锁,Redis中使用SETNX设置一个状态位,表示这是一种锁定状态,保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放,要么返回空值或者默认值
    • 不给热点数据设置过期时间,由后台异步更新缓存
    • 在热点数据要过期前,提前通知后台进程,更新缓存以及重新设置过期时间

缓存穿透

  • 定义:当用户访问的数据既不在缓存中,也不在数据库中,导致请求无法构建缓存,当大量这种请求到来时,数据库压力骤增
  • 解决方法:
    • 限制非法的请求,验证请求的参数是否合理,避免恶意请求进一步访问缓存和数据库
    • 设置空值或者默认值,当发现缓存穿透现象,针对要查询的数据,在缓存中设置一个空值或者默认值
    • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
      • 在写入数据的时候,用布隆过滤器做个标记,在缓存失效后,可以通过查询布隆过滤器来快速判断数据是否存在

如何设计一个缓存策略,可以动态缓存热点数据?

  • 思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据。只留下经常访问的数据
  • 实现:可以使用ZADDZRANGE来完成排序和获取数据的操作

常见的缓存更新策略

  • Cache Aside(旁路缓存策略) (实际使用)
    • 写策略:
      • 先更新数据库的值,再删除缓存中的数据
        • 顺序不能颠倒,否则会出现数据库和缓存的数据不一致问题:
          • 先删除缓存,读策略时客户端无法读取到缓存,查询数据库,回写缓存,之后写策略更新数据库,导致数据库缓存不一致
    • 读策略:
      • 缓存未命中,客户端查询数据库,回写缓存
      • 缓存命中,直接返回给用户
    • 实际上先修改数据库,再删除缓存同样会出现数据不一致的情况:
      • 请求未命中,查询数据库,写回缓存之前,用户修改数据库,导致数据库和写回缓存的数据不一致
      • 但出现概率不高,因为缓存的写入要远远快于数据库的写入
  • Read/Write Through (读穿/写穿策略)
  • Write Back (写回策略)

Redis 实战

Redis如何实现延迟队列

  • 在Redis中可以使用ZSet的方式来实现延迟消息队列,使用Score来存储延迟执行的时间
  • 使用ZADD一直向内存中生产消息,再利用ZRANGEBYSCORE查询符合条件的所有待处理任务,通过循环执行队列任务即可

Redis的大key如何处理

  • 大key定义:指的是Value很大
    • String类型的值大于10KB
    • Hash、List、Set、ZSet的元素的个数超过5000个
  • 大Key带来的影响
    • 客户端超时阻塞,Redis执行命令是单线程,操作大Key的时候会比较耗时。就会阻塞Redis
    • 引发网络阻塞。每次获取大Key产生的网络流量较大
    • 阻塞工作线程。使用DEL删除大Key的时候,会阻塞工作线程,导致无法执行后续命令
    • 内存分布不均。集群在分片均匀的情况下,会出现数据和查询倾斜的情况。部分有大Key的Redis节点占用内存多,QPS也比较大

如何查找大Key

  • 使用redis-cli --bigkeys
    • 注意事项:
      • 最好在从节点上使用,否则会阻塞主节点,或者在业务压力的低峰阶段进行扫描查询,
    • 不足:
      • 只能返回每种类型中最大的Key,无法得到大小排在前N位的bigkey
      • 对于集合来说,只能统计元素的个数大小,而不是实际占用的内存量
  • 使用SCAN命令
    • 使用SCAN扫描,然后使用TYPE获取数据类型
      • 如果是String,使用STRLEN获取的字符串长度就是内存占用的字节数
      • 如果是集合:
        • 预先在业务层获取,集合元素的平均大小,使用LLENHLENSCARDZCARD获取集合的元素个数
        • 在Redis 4.0以上,使用MEMORY USAGE查询一个键值对占用的内存空间
  • 使用RDBTools查询大Key

如何删除大key

  • 要点:本质上是内存释放的过程,内存释放后,会将空闲内存写入到空闲内存链表中,如果一次性释放太多内存,操作空闲内存链表的时间会增加,会导致Redis阻塞
  • 方法:
    • 分批次删除:
      • 对于大Hash,使用HSCAN命令,每次获取100个,再使用HDEL每次删除1个
      • 对于大List,使用LTRIM每次删除一部分
      • 对于大Set,使用SSCAN获取100个,再用SREM每次删除1个
      • 对于大ZSet,使用ZREMRANGEBYRANK,每次删除Top 100的元素
    • 异步删除:
      • 在Redis 4.0之后,使用UNLINK命令调用异步线程删除

Redis管道有什么用

  • 管道是Redis提供的批处理技术,用于解决多个命令执行时的网络等待

Redis支持事务回滚吗?

  • 使用命令开启:MULTI 提交:EXEC 抛弃:DISCARD
  • Redis事务不一定保证原子性,提交的命令即便出错,正确的依然会执行

Redis实现分布式锁

  • 分布式锁是分布式环境下的一种并发控制手段,用于控制某个资源在同一时刻只能被同一应用使用
  • 实现方式:
    • 加锁:
      • 使用SET lock_key unique_value(不同应用的唯一标识) NX(保证操作原子性) PX/EX(设置过期时间,避免异常导致锁无法释放)
    • 解锁:
      • 要保证解锁的是上锁的应用
      • 使用Lua脚本保证解锁操作的原子性
  • 优点:
    • 性能高效
    • 实现方便
    • 避免单点故障
  • 缺点:
    • 超时时间不好设置
      • 可以写一个守护线程,在锁快到期的时候,对锁进行续约,当执行完成后,销毁锁
    • 主从模式下,数据的复制是异步的,这导致了分布式锁的不可靠性
      • 如果主节点设置好锁后,宕机了,没有及时同步,那么新的主节点依然可以获取锁,导致多个应用服务同时获取到锁

Redis 如何解决集群情况下分布式锁的可靠性?

  • 使用RedLock,本质上是基于多个Redis节点的分布式锁,
  • 思路:客户端和多个独立的Redis节点依次请求申请加锁,如果客户端可以和半数以上的节点成功完成加锁,则认为加锁成功,否则失败
  • image