Redis-学习总结

1.Redis中常见的数据类型有哪些?

  1. String:可以存储任何类型的数据,最大长度为512MB

    • 使用场景:缓存 Session、Token、图片地址、序列化后的对象、页面单位时间的访问数、分布式锁等
  2. Set:存储无序且不重复的字符串集合,使用哈希表实现可以基于 Set 轻易实现交集、并集、差集的操作,支持快速查找和去重操作

    • 使用场景:文章点赞、共同好友(交集)、共同粉丝(交集)等
  3. zSet:存储有序的字符串集合,类似于set但是增加了权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表

    • 使用场景:各种排行榜、优先级任务队列等
  4. Hash:存储键值对,适合用于存储对象

    • 使用场景:用户信息、商品信息、文章信息、购物车信息等
  5. List:集合,使用双向链表实现,可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

    • 使用场景:最新文章、最新动态、消息队列等

2.Redis为什么这么快

主要有以下几个方面原因:

  1. 基于内存存储:Redis大部分数据都存储在内存中,相较于传统的基于磁盘的数据库系统,内存操作的读写速度要快得多,开启持久化功能(RDB 或 AOF)后,主线程仍然在内存中操作
  2. 单线程模型:Redis 采用单线程( 6.0 版本开始也可以开启多线程),避免了多线程上下文切换和锁竞争所带来的开销,简化了并发场景下的处理流程
  3. 高效的 I/O 多路复用:Redis使用单线程结合高效的 i/o 多路复用机制,在大规模并发情况下也能保持高吞吐量
  4. 高效的数据结构:Redis提供多种高效的数据结构(如String、Set、List、Hash等),这些结构经过优化,能够快速完成各种操作,Redis 会根据实际存储规模自动选择最合适的底层数据结构,节省内存和提升访问效率。

3.为什么 Redis 设计为单线程?6.0 版本为何引入多线程?

设计为单线程的主要原因是:

  1. 避免并发复杂度:多线程在共享数据结构时往往需要加锁或其他同步机制,容易带来锁竞争、线程切换等开销,从而降低整体性能,而单线程在处理请求时不需要考虑线程同步带来的额外开销,避免了并发场景下可能发生的各种复杂问题
  2. 使用I/O 多路复用:Redis 使用多路复用机制,在使用单线程的同时,一条线程就能够同时处理大量的连接请求
  3. 基于内存的数据操作:Redis 最核心的操作都在内存中完成,内存读写本身就极快,再加上内置多种优化数据结构的支持,很大程度上降低了单线程的性能瓶颈,同时单线程可以有效地利用 CPU 缓存,提高效率

6.0版本引入多线程的原因是:

  • 随着数据规模的增长、请求量的增多,Redis 6.0 引入了可选的网络 I/O 多线程,用来进一步缓解网络I/O的压力,但核心的数据操作和命令执行仍然是单线程进行

4.Redis的Hash是什么

Redis 中,Hash是一种用于存储键值对的数据结构,和传统的HashTable类似,同时,Redis 的Hash能够管理一些关联的数据,比较灵活

Hash 的特点如下:

  1. 节省内存
    当 Redis存储小数据,Hash 中的字段数量较少的时侯,Redis 会采用比如zip list来存储,节省内存
  2. 支持快速读写
    Redis支持快速地对Hash内部的字段进行增删改查的操作,适合存储对象的属性

Hash有以下几个常用的命令:

  1. 写操作

    设置 Hash 中的指定字段的值:

    1
    HSET user:1001 name "ikun" age "23"

    image-20250103160751879

    一次设置多个字段的值:

    1
    HMSET user:1001 name "ikun" age "23" city "hangzhou"

    image-20250103160811629

  2. 读操作

    获取 Hash 中指定字段的值:

    1
    HGET user:1001 name

    image-20250103160831884

    一次获取多个字段的值:

    1
    HMGET user:1001 name age

    image-20250103160857094

    获取 Hash 中所有字段和值:

    1
    HGETALL user:1001

    image-20250103160942492

    获取 Hash 中所有字段名:

    1
    HKEYS user:1001

    image-20250103161003945

    获取 Hash 中所有字段的值:

    1
    HVALS user:1001

    image-20250103161021686

  3. 删除操作

    删除一个或多个字段:

    1
    HDEL user:1001 field

    image-20250103161151434

  4. 其他常用命令

    返回 Hash 中字段的数量:

    1
    HLEN user:1001

    image-20250103161044241

    判断指定字段是否存在:

    1
    HEXISTS user:1001 field

    image-20250103161058851

    为 hash 中的字段加上一个整数值:

    1
    HINCRBY user:1001 age 1

    image-20250103161107408

实现Hash有两种方式:

  1. 在Redis 6和之前底层是zip list+HashTable实现的
  2. 在Redis 7之后是Listpack(紧凑列表)+HashTable实现的

我的Redis版本为5.0,所以只有ziplist

hash-max-ziplist-entries表示建的默认最大个数为512

hash-max-ziplist-value表示每个字段值默认最大长度为64

image-20250103162052479

当Hash的值小于以上两个值后5.0版本会用ziplist存储,7.0后用用ziplist或者listpack存储,大于这两个值会使用HashTable并且不会转化成ziplist或者listpack

我们可以使用config set命令,设置这两个默认的值大小。

image-20250103162923814

5.Zset 的实现原理

Zset是有序集合,Sorted Set,简称 Zset,由跳表和哈希表组成,Zset结合了set的特性和排序功能,是一种能够存储具有唯一性的成员并按照分数(score)进行排序的集合结构,在保持元素唯一的同时按分数排序并支持高效的增删改查

Zset内部实现主要由两个核心数据结构组成:

  1. 跳表:提供了基于排序的高效范围操作
  2. 哈希表:提供了对成员分数的快速索引

由于这两种数据结构,Zset天然的实现了排行榜延时队列等需要有序和精准查找的场景!

当Zset的元素数量较少时,Redis会使用ziplist来节省内存

  1. zset-max-ziplist-entries表示元素的默认最大个数为128
  2. zset-max-ziplist-value表示元素成员名和分值默认最大长度为64

image-20250103164854211

如果需要修改可以使用config set命令

image-20250103165151316如果超过这两个值的任意一个阈值,Zset 将使用跳表+HashTable作为底层实现

同时使用跳表+HashTable是因为:

  • 跳表与HashTable都要同时保存每个成员的分数,保证了在跳表与HashTable之间进行相互印证和快速操作,在插入(ZADD)、删除(ZREM)、查找(ZSCORE)、范围查询(ZRANGE、ZREVRANGE、ZRANGEBYSCORE)操作时都会进行相应更新,从而保持数据同步的一致性

6.Redis中跳表的实现原理

Redis 中的跳表(Skiplist)是通过多层有序链表+随机层高实现的,跳表的最底层的链表保存了所有元素,这一点与B+树类似,是Zset内部实现的两个核心数据结构之一,支持有序数据的快速范围查询快速精确定位 ,可以用在实时排名、定时调度等业务场景

跳表的结构如下所示:

最顶层链表: [10] ───────────────> [40] ───────────────> [70] ─> nil
中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────────> nil
最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─> [70] ─────────> nil

  • 最底层是完整的有序链表,存放所有节点
  • 最底层之上,会随机生成若干个索引层,用于跳过部分节点
  • 每向上一层,节点数通常会减少一半左右,通过前/后指针把这些节点连成一条索引链

插入操作:

  • 插入节点:

    • 每插入一个新节点时,会使用随机函数决定插入节点在哪一层(一般限制在1-32之间),假如随机结果允许节点具有更高层,则会在上层加一个指针,用来增加跳跃的范围,比如插入一个64的节点,假设在最顶层
    • 最顶层链表: [10] ────────────> [40]─────> [64] ──────> [70] ─> nil
      中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────> nil
      最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─────> [70] ─> nil
  • 更新索引层

    • 在插入点所在层以上的每一层,如果新节点层高足够,则会把它插入到相应层级的链表中,并更新前驱节点和新节点之间的forward指针(前进指针)、span(该层 forward 指针跨越的节点数量)等信息,对上面插入的64进行更新就变成
    • 最顶层链表: [10] ────────────> [40]─────> [64] ──────> [70] ───> nil
      中间层链表: [10] ─────> [30] ───> [40] ─────> [64] ──────> [70] ──> nil
      最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ──> [64] ───> [70] ─> nil
  • 更新 backward-后退指针(zskiplistNode的结构体数组)

    • 在最底层链表,设置新节点的后退指针指向前驱节点,如果新节点不是尾节点,也要更新后继节点的后退指针

    zskiplistNode:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    typedef struct zskiplistNode {
    //成员
    sds ele;

    //后退指针(双向链表的一部分)
    struct zskiplistNode *backward;

    //不同层级上的前进指针和跨度
    struct zskiplistLevel {
    struct zskiplistNode *forward;
    //该层 forward 指针跨越的节点数量
    unsigned long span;
    } level[];
    } zskiplistNode;

查询操作:

  • 从最高层链表开始查询:
    • 当我们进行查询操作时,从最顶层链表快速跳过大量节点,从头结点依次向前查找,当下一节点的值<=目标值时,就向前移动,一旦发现不能继续前进时,就向下降到下一层更精确地查找
  • 降到下一层链表继续查询:
    • 这一层会重复最高层链表查询时的比较-前进过程,继续向前查询
  • 到底层精确定位:
    • 直到降到最底层,就能精确地找到目标节点或确定目标节点不存在

这个过程类似在多层高速公路上寻找出口:先在最上层快速跳,接近目标区域后逐层下切换到普通道路,再进行精确查找

比如上面的例子:

  • 最顶层链表: [10] ───────────────> [40] ───────────────> [70] ─> nil
    中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────────> nil
    最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─> [70] ─────────> nil

我们要查找20这个节点,首先从最顶层链表的10这个节点开始,因为20<40那么移动到中间层的10这个节点,中间层再比较,20<30(下一节点的值<=目标值),下降到下一层到达最底层链表,接着继续比较,20==20,查找成功。

总共跳转的次数为[10]->[10] + [10]->[10] + [10]->[20] 花费三次,如果通过传统链表(最底层),只需要[10]->[20] 一次,所以跳表的查询速度并不是一定比传统链表快

删除操作:

  • 定位删除节点
    • 和查询过程相同,先在各层索引中找到目标节点的前驱节点
  • 移除指针
    • 从最高层到最底层,依次把前驱节点的forward指针改为跳过该节点,指向它的后继节点
  • 更新backward
    • 在最底层更新前驱和后继节点的backward指针
  • 释放节点
    • 释放被删除节点对应的空间,如果节点层高大于 1,需要将多层结构也清理掉

7.Redis 中如何保证缓存与数据库的数据一致性?

保证缓存和数据库的数据一致性我们可以通过以下几个方式:

  1. 先更新缓存,再更新数据库:

    • 具体方式:先把新数据写到缓存,然后再写数据库

    • 这种方式大多数情况下不推荐使用,很容易出现不一致或写失败的问题,比如说

      如果在缓存更新成功后、数据库尚未更新完成时,其他线程就来读取,会读到新缓存的值、旧数据库中的值的不一致情况

      或者数据库更新失败,导致缓存和数据库一直不一致,还有一点就是在高并发场景下,不同线程对同一条数据做更新时,执行顺序很难控制

  2. 先更新数据库,再更新缓存:

    • 具体方式:先更新数据库,再把新值写入缓存

    • 这种方式在简单场景可以使用,但不推荐,因为并发场景下也会有各种竞态问题,比如说

      如果先更新数据库但缓存里还是旧数据,且读操作在缓存更新之前到达,则会读到旧值

      假如另一个线程也在修改并更新缓存,可能出现后写覆盖先写的情况,需要引入锁或版本号来避免

  3. 先删除缓存,再更新数据库:

    • 具体方式:在写操作时,先删除缓存中的对应数据,然后再更新数据库,如果后面发现有读操作时,缓存没命中的话,再从数据库读取并回填缓存

    • 这种方式在没有并发或并发量很小的场景下能用,但对高并发场景不太友好,所以也不推荐,会产生下面的问题:

      在删除缓存后,数据库还未完成更新,就有线程来读数据,可能会把旧的数据库数据重新写回缓存,解决这个问题需要引入延时删除、分布式锁等策略

  4. 先更新数据库,再删除缓存:

    • 具体方式:先更新数据库,再删除缓存中的旧数据,后续有读请求时,发现缓存未命中,就到数据库查最新数据并回填,这也是最常见的一种做法,一般情况业务上都会使用,因为只要保证数据库更新完毕后,能及时删除对应缓存,就不会保留过期数据

    • 这种方式在并发场景下也可能出现脏数据,比如说:

      数据库更新完、缓存还没删的瞬间,被其他线程读到然后又回填

      解决办法:延时双删、分布式锁

  5. 缓存双删策略:

    • 具体方式:更新数据库之前,先删除一次缓存,更新完数据库之后,等一个短暂的时间,再删除一次缓存,能够解决数据库更新期间可能出现的竞态问题,第一次删除是为了让后续读操作强制去数据库拿最新数据,而第二次延时删除是为了防止在第一次删除后、第二次删除前的时间窗口里,有其他线程刚好把旧数据又放回缓存
    • 这种方式是高并发的写场景下的常见做法,能显著减小并发场景下短暂的不一致概率,但是需要设置合理的延时时间以及分布式锁等措施,时间过长或过短都可能出问题
  6. 使用 Binlog 异步更新缓存:

    • 具体方式:Mysql会产生Binlog日志,通过监听器监听数据库更新操作,然后通过消息队列异步地去更新或删除 Redis 中对应的缓存,保证对数据库的所有操作,都能将变动同步到缓存

    • 这种方式也会产生一些问题,比如说:

      binlog 解析并同步到缓存会有一定延迟

      如果 binlog 丢失、或者监听器出现故障,可能造成缓存与数据库再次不同步

8.Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

1.缓存击穿:

  • 定义:是指当缓存中的某个热点数据过期,导致大量请求访问了该热点数据,无法从缓存中读取,直接访问数据库,导致数据库宕机
  • 解决方案:
    • 热点数据设置永不过期策略
    • 使用互斥锁,保证同一时间只有一个线程更新缓存

2.缓存穿透:

  • 定义:是指当用户访问的数据是缓存和数据库中都没有的数据时,这个数据在缓存中没有记录,就都去访问数据库,有大量这样的请求到来时,造成数据库的压力骤增
  • 解决方案:
    • 使用布隆过滤器(bloomfilter),快速判断数据是否存在,类似hashset,让请求访问的时候先判断这个数据存不存在,存在则放行,否则直接返回
    • 对查询的结果进行缓存,即使不存在也缓存一个标识
    • 在API接口入口出进行非法请求的限制,判断请求参数是否含有非法值、请求字段是否存在

3.缓存雪崩:

  • 定义:是指多个缓存在同一时刻过期或者 Redis 故障宕机时,导致大量请求无法在 Redis 中处理,而去同时访问数据库,造成数据库瞬间压力骤增或者宕机
  • 解决方案:
    • 设置合理的过期时间
    • 使用双缓存策略
    • 使用互斥锁

9.Redis string 类型的底层实现是什么?

Redis中string类型底层基于SDS实现,SDS 是一种动态字符串结构,能够在保持c语言兼容字符串用法的同时,提供更高效的内存管理和使用体验

有以下几种编码方式:

  1. int:
    • 用于存储整数类型的字符串,内存消耗小
  2. embstr:
    • 通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS
    • 占用64Bytes的空间,存储44Bytes的数据
  3. raw:
    • 通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS
    • 存储大于44Bytes的数据

10.Redis 中如何实现分布式锁?

最常用的方式是使用 SET key uniqueValue EX seconds NX 原子地获取锁,并用 Lua 脚本保证解锁操作是解的自己加的锁

具体方式:

  1. 利用 Redis 提供的 SET key uniqueValue NX EX seconds 命令:

    • NX:互斥,确保只能有一个线程获取到锁

    • EX:设置过期时间,确保锁不会被某个客户端一直占有,避免死锁

    • 如果返回 OK,说明加锁成功;如果返回 nil,说明加锁失败,锁已被其他客户端持有

      例如:

      1
      2
      3
      4
      #key为user:1001
      #value为uniqueId(唯一标识,这里随便写了一个)
      #过期时间为 30 秒
      SET user:1001 uniqueId EX 30 NX

      image-20250105205634463

  2. 通过给锁的value设置一个唯一标识(如 UUID、雪花算法或随机字符串),确保是自己加的锁之后才可以解锁

  3. 确认好后使用 Lua 脚本来删除锁

在释放锁时,需要先比较 key 的 value 与自己持有的标识是否一致,如果一致则删除 key:

1
2
3
4
5
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
  • KEYS[1]:锁的 key
  • ARGV[1]:加锁时存储D唯一标识

还可以使用RedLock 实现分布式锁,具体方式:

  1. 准备一组相互独立的 Redis 节点(一般为 5 个),客户端依次向这些节点尝试加锁(用 SET NX EX)。

  2. 如果成功在n/2+1及以上节点上拿到锁且在过期时间内,则认为加锁成功

  3. 解锁时也需要依次向各节点发起解锁操作

11.Redis 的 Red Lock 是什么?

RedLock红锁,是一个利用多节点独立部署Redis实例来实现分布式锁的方案,比起传统的不需要使用从库和哨兵机制,

原理:在大多数节点上(> N/2)加锁成功 + 在过期时间时间内完成加锁操作,才判定加锁成功

具体方式:

  1. 准备一组相互独立的 Redis 节点(一般至少为 5 个),客户端依次向这些节点尝试加锁(用 SET NX EX

    注意必须是没有设置过期时间的情况下才能加锁`

  2. 如果成功在n/2+1及以上节点上拿到锁且在过期时间内,则认为加锁成功

  3. 解锁时也需要依次向各节点发起解锁操作,一般使用Lua脚本

总结:使用RedLock需要部署多台Redis实例,成本高,性能来说也,比不上单实例的redis,而且加锁需要依次向所有节点加锁,时间成本也高,但是可以很好的解决单点故障数据一致性安全性问题,一般业务还是推荐经典是主从+烧饼机制

12.Redis 实现分布式锁时可能遇到的问题有哪些?

  1. 业务执行异常导致的锁无法自动释放

    • 比如:业务还没执行完,锁已经过期了,无法自动释放锁

    • 解决办法:

      使用Redission的看门狗机制

      设置合理的过期时间

  2. 解锁时误释放他人锁

    • 比如:客户端 A 获取锁后,本来应该在执行任务结束后删除锁,但如果发生网络延迟或阻塞,导致锁已过期并被客户端 B 成功获取,此时客户端 A 依旧执行删除操作,错误地删除了客户端 B 的锁

    • 解决办法:

      增加唯一标识,解锁时判断一下是不是自己的锁

      使用Lua 脚本配合操作,原子地判断并删除锁,避免竞争导致的错误删除

  3. 各种故障问题

    • 比如:因为故障(单点故障、网络err、主从不同步等)导致没有正确解锁,或者设置过期时间不当,会让锁一直存在下去,出现死锁

    • 解决办法:

      避免不带过期时间的锁

      设置合理的过期时间

      对于执行时间不可预估的长任务,可以使用续约机制

  4. 时钟漂移

    • 比如:部署了多台Redis实例,不同实例因为各种原因导致实例的系统时间不一致,影响了锁的过期时间

    • 解决办法:

      所有节点上的系统时间采用NTP服务同步

  5. 锁的可重入性

    • 比如:Redis的分布式锁默认是不可重入的,同一个客户端在持有锁的情况下再次请求锁会失败

    • 解决办法:

      给锁添加计数器,在获取锁时,如果发现计数器不为零,说明当前线程已经获取到了锁,此时可以直接增加计数器并返回 true,即表明已经获取到了锁,无需再次获取,在释放锁时,需要将计数器-1,如果计数器为零,才释放锁

      增加唯一标识,每次获取锁时检查这个标识,如果请求锁的客户端已经持有了这个锁,则只更新锁的过期时间而不需要重新获取

13.Redis的持久化机制有哪些?

主要有3种:

  1. RDB:通过在特定的时间间隔或者满足特定条件时,将当前内存的数据生成快照文件(默认为 dump.rdb)来实现持久化,Redis重启,可以通过加载快照文件直接恢复到生成快照时的数据状态

    数据量不大、备份与迁移方便,但有可能丢失最近一次快照之后的数据

    • 实现方式:

      1. 手动命令:

        • save :在主线程中执行数据快照,阻塞 Redis 进程,直到快照完成
        • bgsave:Redis使用rdbSaveBackground异步执行快照,主线程可继续处理请求
      2. 自动触发:

        • 通过配置文件redis.conf设置save

          比如 save 60 1 表示60秒内至少有1次写操作,则触发一次 bgsave

  2. AOF:通过将每个写操作记录到日志文件(默认为 appendonly.aof)实现持久化,支持将所有写操作记录下来,只要在Redis重启后重新执行AOF文件中的写命令即可将数据恢复到内存中

    记录所有写命令,数据丢失极少,但文件更大、恢复更慢、IO 开销更高

    • 实现方式:
      • 三种回写策略:
        1. always:每执行一条写操作后都立刻执行一次fsync,最安全但最慢,影响吞吐量
        2. everysec(默认策略):每秒执行一次 fsync,性能和安全性较为均衡(最多丢失1秒的数据)
        3. no:由操作系统自行决定何时进行 fsync,最快但安全性低,在Redis崩溃时数据丢失
      • 重写机制:随着记录的操作增多aof文件会变大,所以Redis采用重写机制,定期对aof 文件进行压缩,把内存中的数据用最精简的写命令格式重新写入新的aof文件来瘦身
  3. RDB+AOF混合:Redis 4.0+版本新增的混合持久化机制,结合RDB与AOF优势的持久化方案,当执行AOF重写时,Redis 会先写入一段RDB格式的内容,再写剩余命令日志

    进一步提升数据安全和重启效率,在加快恢复速度的同时,保持AOF的高可用性

    在Redis同时存在RDB和AOF文件时,默认优先加载 AOF 文件,确保数据完整性

14.Redis 主从复制的实现原理是什么?

主从复制是一种非阻塞的异步数据复制机制,通过将数据复制到一个或多个从节点,在多台服务器之间保持数据一致,主从复制主要有两种数据同步方式,全量同步增量同步

优点:

  1. 读写分离:主从复制能实现读写分离,写操作请求主节点,读操作请求从节点,提高系统吞吐量
  2. 高可用性:当主节点宕机时,可手动或通过哨兵等机制进行主从切换,实现故障转移

缺点:

  1. 数据延迟:从节点接收写命令是异步的,可能会有延迟
  2. 数据丢失:主节点宕机时,没发送到从节点的增量数据可能会丢失

具体流程:

  1. 从节点发送同步请求,开始同步:
    从节点启动后,向主节点发送一个PSYNC命令,尝试与主节点进行复制同步

    PSYNC是Redis 2.8版本引入的,从节点发送 PSYNC replid offset给主节点,主节点判断能否继续进行部分同步,如果无法部分同步,则进行全量同步

    runid(复制ID):主节点的runID,当主节点发生故障切换后,新主节点会生成新的复制ID

    offset(复制进度):由于记录上次同步的最后偏移位置,第一次值为 -1

  2. 主节点响应全量同步或增量同步:

    • 全量同步:如果是第一次复制(从节点发送PYSNC ? -1),或者主节点runid为空时,会进行全量同步
    • 增量同步:全量同步后,主节点通过增量同步保持持久连接,将后续写入数据库的操作同步到从节点,保持数据一致

15.Redis 数据过期后的删除策略是什么?

Redis中,给键置了过期时间之后,不会在过期时间到达的那一刻立刻将过期的数据删除,而是采用了惰性删除和定期删除相结合的方式来清理过期数据

  1. 惰性删除:当客户端访问某个键时,Redis 会先检查该键是否过期,如果过期则会将其立即删除并返回空结果或者报错,但是如果一个过期键一直没有被访问,就不会被惰性删除,这会占用额外的内存空间

  2. 定期删除:Redis 会周期性地(默认100ms,具体可配置)随机抽样检查过期键并主动删除,通过定期,避免内存中堆积过多已过期但未被访问的键,防止内存膨胀

  3. 内存回收机制:当 Redis 开启了maxmemory配置并且达到设置的上限时,如果还有新的写操作,需要提供空间存储新数据,就会触发内存淘汰策略

    • 主要有以下几种策略:
      1. noeviction(默认策略):不删除建,新的写操作直接拒绝
      2. volatile-lru:只在设置了过期时间的键里挑选最近最少使用的键删除
      3. allkeys-lru:在所有键中挑选最近最少使用的键删除
      4. allkeys-random:随机删除任意键
      5. volatile-random:随机删除设了过期时间的键
      6. volatile-ttl:在设了过期时间的键中最先过期的键优先删除
      7. allkeys-lfu:基于访问频率在所有键中选择最不常用的键删除
      8. volatile-lfu:基于访问频率在有过期时间的键中选择最不常用的键删除

    内存回收机制主要是为了应对内存超限的情况,而过期键的删除则是一个常规的过期清理过程

16.如何解决 Redis 中的热点 key 问题?

在某些业务场景下,大多数请求会集中读写少数几个key,比如抢购、秒杀场景,访问量都会集中到特定商品id所对应的库存key。因为Redis 的核心是单线程执行命令,如果某个key在同一时刻被大量访问,就容易造成阻塞,如果所有数据被过度集中在一个key中,读写压力也会集中在单节点

解决Redis中的热点key问题可以有以下几个方法:

  1. 热点key拆分:拆分大 key,将热点数据分散,减轻单点压力

  2. 分散存储:对于高并发的计数场景,比如点赞数、浏览量等,可以采用多哥key+异步的方式

    eg:将一个全局计数器拆分成多个key,每个请求随机写入某个key,然后再定时汇总

  3. 读写分离:通过Redis主从复制,增加从节点或多级缓存

    对于读多写少的业务,使用主从复制,主节点负责写,从节点负责读,这样读请求可以分散到多个从节点,减轻主节点压力

    对于热点数据,还可以在应用内部(JVM、本地 Cache)或者CDN再加一层缓存,减少对Redis的访问次数

  4. 限流降级:在业务层面进行流量管控,减少请求

    限流:在业务层对请求进行限速或漏桶/令牌桶控制,避免短时间内大量的请求都打到Redis,减轻峰值压力,比如Nginx限流、分布式限流中间件等

    降级:当某个热点key访问过高时,可以对部分非关键功能进行降级,或者访问到热点key时直接返回固定值、或者缓存的旧值

17.Redis 集群的实现原理是什么?

Redis集群是一种分布式架构,用于在多个Redis实例下进行数据分开存储,每个实例可以有主从节点,核心是通过哈希槽(Hash Slot)机制和Gossip协议来实现的,适用于大规模、高并发的业务场景

  • 哈希槽:将键空间划分为16384个固定槽,每个实例负责一定范围(或多个范围)的哈希槽

    例如节点A负责槽0 ~ 5000,节点B负责槽5001 ~ 10000,节点C负责槽10001 ~ 16383

    每个key都会先通过哈希函数计算哈希值,然后对1638 取模,得到其所属的槽编号slot,再定位到对应的节点

  • Gossip 协议:用于节点之间交换消息和进行故障检测的协议

    节点随机挑选其它节点进行消息交换,使信息在整个集群中扩散,最后保持一致

    Gossip协议下能够自动感知并进行故障转移,无需人工干预

18.Redis 中的 Big Key 问题是什么?如何解决?

Big Key指的是某key下存储了非常大的数据量,占用空间比较大,一个超大的字符串等等,Big Key容易导致一系列性能和可用性问题,比如大数据量的读写操作阻塞、内存占用过高、查询效率变慢、客户端请求超时等等

解决办法

因为问题本质是数据过度集中在单个 key 内,所以可以从以下几个方面解决:

  1. 大key拆分:拆分大 key,将热点数据分散,减轻单点压力
  2. 优化数据结构:使用合适的数据结构就行优化,或者定期清理无用数据
  3. 使用Redis集群:采用集群的方式,把大Key拆分到不同服务器上

容易产生Big Key的场景

  1. 消息列表:Redis中用List存储用户的消息列表,时间久了之后单个List可能存储大量的数据
  2. 计数器:大量用户的计数都存到单个Hash里,数据也会变得非常庞大
  3. 排行榜:排行榜用户数骤增的情况下,Sorted Set里的元素数也会变成Big Key

怎么查找Big Key?

  1. 可以通过redis自带的命令查找

    1
    redis-cli --bigkeys

    image-20250107113830715

  2. 使用内置命令

    1
    MEMORY USAGE key

    image-20250107143844372

  3. 使用 scan/ hscan/zscan等命令