Redis

Redis缓存基础

Redis之所以能够实现高性能,主要得益于其单线程模型、内存操作、IO多路复用机制

  • 单线程:由于所有命令都在同一个线程中顺序执行,因此不需要复杂的锁机制来保护共享资源,避免了多线程环境下的竞态条件和死锁问题。减少上下文切换开销。

  • 基于内存操作:RAM访问时间远低于磁盘I/O。

  • 非阻塞IO多路复用:Redis采用了基于epoll的事件驱动架构,可以在不阻塞主线程的情况下同时监听多个客户端连接的状态变化。通过异步读取和写入网络套接字,Redis可以最大限度地利用可用带宽,而不必等待每一个单独的操作完成。

常用数据类型

Redis常见数据类型:字符串,哈希,列表,集合,有序集合

String(字符串)不仅可以存储字符串, 还可以存储整数或浮点数。适用于缓存热点数据,实现各种计数器功能(如网站访问统计、点赞次数等)

bash
# 设置指定键的值,并可选地设置过期时间(秒或毫秒) |  获取指定键的值
SET mykey "Hello" EX 10 NX
GET mykey

# 将键所存储的数值加一/减一。如果键不存在,则认为它的值是 0
INCR counter
DECR counter
# 将键所存储的数值加上/减去指定增量。同样,若键不存在则视为 0
INCRBY score 5
DECRBY score 2
  • 底层实现: SDS (Simple Dynamic String) Redis 使用自己实现的简单动态字符串结构来表示 String 类型的数据。

  • 对于较短的字符串(小于等于39个字节),Redis 采用 embstr 编码, 对于较长的字符串,则使用普通的 raw 编码。

String 实际上可以容纳任意二进制数据,可达 512MB。

Hash(哈希) 是一种键值对集合的数据结构,非常适合==存储对象==(可存取对象中的属性,并且支持原子性操作),如购物车对象

  • 压缩列表(ziplist):为了节省内存空间,当 Hash 中的元素数量较少且单个元素大小较小时,Redis 会使用压缩列表作为底层实现。(在 Redis 7.0 及以上版本中,ziplist 被 listpack 所取代)

  • 哈希表(hashtable):随着 Hash 中元素数量的增长或单个元素变大,Redis 会自动将压缩列表转换为更高效的哈希表结构。

bash
# 用户 user123 添加了两个 item456 商品
HSET cart:user123 item456 2

# 查看用户 user123 的购物车物品
HGETALL cart:user123
# 查询 user123 的 item456 数量
HGET cart:user123 item456

# 用户 user123 移除了一个 item456 商品(注意确保不会出现负数)
HINCRBY cart:user123 item456 -1
# 完全删除某商品
HDEL cart:userid itemid
# 一次性清除所有商品
DEL cart:userid

# 统计购物车中商品种类数
HLEN cart:userid
# 分别统计购物车中每种商品的数量
HVALS cart:userid

哈希表是一种通过哈希函数将键映射到特定位置的数据结构,使得查找、插入和删除操作的时间复杂度接近 O(1)。

Redis 中的 List 是一种双向链表(linked list)数据结构,它可以在列表的头部和尾部高效地添加或移除元素。可利用List实现分布式的 栈和队列

栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构

bash
LPUSH key value    # 将一个值插入到列表的头部(左端)
LPOP key           # 移除并返回列表的第一个元素(左端的元素)

队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构

bash
LPUSH key value    # 将一个值插入到列表的尾部(右端)
RPOP key           # 移除并返回列表的第一个元素(左端的元素)

# 阻塞队列 示例:
LPUSH key value
BRPOP key [key ...] timeout  # 列表为空则阻塞连接直到等待超时或元素入列

其他应用场景

  • 任务队列:可以用来管理待处理的任务,确保任务按照一定的顺序被执行。
  • 社交网络的消息流:例如 微博,可以通过 List 来保存用户发布的新鲜事。
  • 缓存最近/最频繁使用的数据:例如,可以维护一个最近浏览商品的列表。

Set 是一种无序且不允许重复元素的数据结构,非常适合用于存储唯一性的数据集合。

  • Set 支持高效的成员添加、删除和检查操作,并且能够执行交集、并集和差集等集合运算。

  • Set 的底层实现是基于哈希表(hash table),这使得其成员性检查、添加和移除操作的时间复杂度接近于 O(1)。对于小集合,为了节省内存,当集合中元素的数量很少(默认 小于64 个元素)且成员为整数时,Redis 使用一种叫做 ==intset(整数集合)== 的紧凑编码方式来表示集合

应用场景: 去重,点赞,关注模型,抽奖, 下为一个简单的抽奖示例:

bash
# 1. 初始化奖池
SADD raffle_pool participant1 participant2 participant3 ... participantN

# 2. (可选) 查看所有参与抽奖者
SMEMBERS raffle_pool

# 3. 抽取幸运儿
# 方法一: 使用 SPOP 直接抽取并移除
winner=$(SPOP raffle_pool)

# 方法二: 使用 SRANDMEMBER 和 SREM 组合
# winner=$(SRANDMEMBER raffle_pool)    # 返回一个随机的参与者,但不移除它
# SREM raffle_pool "$winner"           # 将指定的参与者从奖池中移除

# 4. 记录中奖结果
RPUSH winners "$winner"  # 使用 List 记录中奖者顺序
# 或者
SADD winners_set "$winner"  # 使用 Set 确保唯一性

ZSet(Sorted Set,有序集合)每个成员关联一个分数(score),使其可以根据分数进行排序

  • 成员数量较少时使用压缩列表(ziplist / listpack)以节省内存
  • 当成员数量或大小超过一定阈值时,则采用跳跃表(skiplist)结合哈希表(hash table)来保证高效的操作和成员唯一性

应用场景:排行榜

在 Redis 中,TYPEOBJECT ENCODING 提供了关于键的不同层次的信息:

  • TYPE 查询这个键代表的是什么类型的数据结构(如字符串、列表、集合等)
  • OBJECT ENCODING 展示 Redis 内部是如何实际存储这些数据结构的(使用哪种编码方式)
bash
HSET user:1000 name "Alice"

# 查询该键的类型
TYPE user:1000                       # hash

# 查询 user:1000 的底层编码方式
OBJECT ENCODING user:1000            # listpack

底层数据结构

Redis内部使用了多种底层数据结构: 哈希表, 双向链表, 整数集合, 压缩列表, 跳表

哈希表(Hash Table)

主要用于实现 Redis 的 Hash 数据类型,以及 ZSet 和 Set 内部成员唯一性的保证。

  • 提供 O(1) 时间复杂度的插入、查找和删除操作。
  • 使用拉链法解决哈希冲突问题。

应用场景:适合需要快速查找、添加或移除元素的场景,如缓存、会话管理等。

双向链表(Doubly Linked List): 用于实现 Redis 的 List 数据类型。

  • 每个节点包含指向前驱和后继节点的指针。
  • 支持两端高效地插入和删除操作。

应用场景:适用于队列、栈等先进先出(FIFO)或后进先出(LIFO)的数据处理模式。

整数集合(Intset): 在小集合且所有成员均为整数时,用作 Set 的底层实现。

  • 只能存储 16 位、32 位或 64 位整数,并根据需要自动升级内部表示形式。
  • 确保成员唯一性并维持有序排列。
  • 高效利用内存空间。

应用场景:特别适合存储少量整数值的小型集合。

压缩列表(Ziplist / Listpack): 用于优化小集合或短序列的存储。

  • 将所有元素连续地存储在一个内存块中,避免指针带来的额外开销。
  • 使用变长编码表示每个元素及其长度信息。
  • 允许在头部或尾部高效地插入和删除元素。
  • 支持不同类型的数据(整数、字符串),并通过混合编码方式选择最合适的表示方法。

应用场景:适用于存储小集合或短序列的数据,如小型 List、Set 和 ZSet

跳跃表(Skiplist): 作为 Redis ZSet 的主要底层实现之一,确保成员按照分数排序。

  • 通过多层索引加速查找、插入和删除操作,平均时间复杂度为 O(log N)。
  • 新节点插入时,随机决定其层级,确保跳跃表高度合理。
  • 每个节点不仅存储实际的数据项(成员及其分数),还维护指向同一层下一个节点及下一层相同位置节点的指针。

应用场景:非常适合处理有序集合中的各种操作,如排行榜、优先级队列等。

为什么选择跳表?==> 相对于红黑树实现更为简单,多线程环境中并发更友好 (内存效率更高?)

压缩列表通过紧凑存储减少内存开销,适用于小集合;跳表则通过多层索引加速查找、插入和删除操作,适用于需要高效排序和随机访问的大规模有序数据集。

持久化策略

  • Redis 的持久化机制有哪些?
    • 常见的持久化机制包括:
      • RDB:定期将内存中的数据快照保存到磁盘。
      • AOF:记录服务器执行的所有写操作命令,以便恢复数据。
  • Redis 在生成 RDB 文件时如何处理请求?
    • 生成RDB文件时,Redis会创建一个子进程来执行快照操作,父进程继续处理请求,子进程完成后将快照文件写入磁盘。

击穿/穿透/雪崩

  • Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
    • 缓存击穿:某个热点Key突然大量请求,导致缓存失效,所有请求直接打到数据库。
    • 缓存穿透:查询一个不存在的Key,导致每次查询都直接打到数据库。
    • 缓存雪崩:大量缓存在同一时间失效,导致所有请求直接打到数据库。
  • Redis 中如何保证缓存与数据库的数据一致性?
    • 常见的方法包括:
      • 双写:同时写入缓存和数据库。
      • 失效时间:为缓存设置合理的失效时间。
      • 缓存更新策略:使用更新或删除策略保持缓存和数据库一致。

大Key和热点Key

  • Redis 中的 Big Key 问题是什么?如何解决?
    • Big Key问题:大Key占用大量内存,影响Redis性能。
    • 解决方法
      • 分片:将大Key拆分成多个小Key。
      • 使用更合适的数据结构:例如使用Ziplist或Hash。
  • 如何解决 Redis 中的热点 key 问题?
    • 热点Key问题:某些Key被频繁访问,导致性能瓶颈。
    • 解决方法
      • 数据分片:将热点Key分散到多个节点。
      • 缓存层:在应用层或CDN层缓存热点Key。
      • 布隆过滤器:减少不必要的查询。

内存淘汰策略

  • Redis 中有哪些内存淘汰策略?
    • 常见的内存淘汰策略包括:
      • noeviction:不淘汰数据,达到最大内存限制时返回错误。
      • allkeys-lru:使用LRU算法淘汰任意键。
      • volatile-lru:使用LRU算法淘汰设置了过期时间的键。
      • allkeys-random:随机淘汰任意键。
      • volatile-random:随机淘汰设置了过期时间的键。
      • volatile-ttl:优先淘汰TTL值最小的键。
  • Redis 中的内存碎片化是什么?如何进行优化?
    • 内存碎片化:由于频繁的内存分配和释放,导致内存空间分散。
    • 优化方法
      • 重启Redis:定期重启Redis实例,回收内存。
      • 调整内存分配策略:使用更合理的内存分配策略。
      • 使用内存碎片整理工具:使用第三方工具进行内存碎片整理。

分布式锁

  • Redis 中如何实现分布式锁?

    • 可以使用Redis的SETNX命令实现简单的分布式锁。
    • 示例:
      java
      if (redis.setnx("lock", "value")) {
          // 获取锁成功,执行业务逻辑
          try {
              // 业务逻辑
          } finally {
              // 释放锁
              redis.del("lock");
          }
      } else {
          // 获取锁失败
      }
  • 分布式锁在未完成逻辑前过期怎么办?

    • 可以在设置锁时设置一个合理的超时时间,并在释放锁时进行双重检查。
    • 示例:
      java
      long expireTime = System.currentTimeMillis() + 30000; // 30秒超时
      if (redis.setnx("lock", String.valueOf(expireTime))) {
          // 获取锁成功
      } else {
          String existingExpireTime = redis.get("lock");
          if (existingExpireTime != null && Long.parseLong(existingExpireTime) < System.currentTimeMillis()) {
              // 锁已过期,尝试重新获取锁
              String currentExpireTime = redis.getSet("lock", String.valueOf(expireTime));
              if (currentExpireTime.equals(existingExpireTime)) {
                  // 成功获取锁
              } else {
                  // 获取锁失败
              }
          } else {
              // 获取锁失败
          }
      }
  • Redis 的 Red Lock 是什么?你了解吗?

    • Red Lock是一种改进的分布式锁算法,通过多个Redis实例来提高锁的可靠性和可用性。
    • 示例:
      java
      RedissonClient redisson = Redisson.create();
      RLock lock = redisson.getLock("myLock");
      lock.lock();
      try {
          // 业务逻辑
      } finally {
          lock.unlock();
      }
  • Redis 实现分布式锁时可能遇到的问题有哪些?

    • 常见问题包括:
      • 锁过期:锁在业务逻辑未完成前过期。
      • 死锁:多个节点竞争锁导致死锁。
      • 网络分区:网络问题导致锁无法正常释放。
  • 说说 Redisson 分布式锁的原理?

    • Redisson分布式锁:基于Redis实现的分布式锁,提供了丰富的功能和更高的可靠性。
    • 原理
      • 可重入锁:支持可重入,多次获取锁不会阻塞。
      • 公平锁:支持公平锁,按请求顺序获取锁。
      • 锁过期保护:通过WatchDog机制自动续期,防止锁过期。
    • 使用示例
      java
      RedissonClient redisson = Redisson.create();
      RLock lock = redisson.getLock("myLock");
      lock.lock();
      try {
          // 业务逻辑
      } finally {
          lock.unlock();
      }

性能及应用场景

  • Redis 通常应用于哪些场景?
    • 常见的应用场景包括:
      • 缓存:作为高速缓存层,提高数据访问速度。
      • 消息队列:实现消息传递和任务队列。
      • 会话存储:存储用户会话信息。
      • 计数器:统计网站访问量、用户行为等。
      • 排行榜:实现实时排行榜功能。
      • 地理位置:存储和查询地理位置信息。

快/源码设计

  • Redis 为什么这么快?

    • Redis之所以快,主要有以下几个原因:
      • 内存存储:数据存储在内存中,访问速度快。
      • 单线程模型:避免了多线程间的上下文切换开销。
      • 高效的内存管理:使用自定义的数据结构和内存分配算法。
      • 网络通信优化:使用多路复用IO模型,处理大量并发连接。
  • Redis 源码中有哪些巧妙的设计,举几个典型的例子?

    • 内存管理:使用自定义的内存分配器,减少内存碎片。
    • 多路复用IO:使用epoll/kqueue等高性能IO多路复用技术。
    • 事件驱动:使用事件驱动模型,处理大量并发连接。
    • 数据结构优化:使用Ziplist、Quicklist等优化数据结构,减少内存占用。

性能优化

  • Redis 性能瓶颈时如何处理?
    • 常见的性能优化方法包括:
      • 增加内存:扩大Redis实例的内存容量。
      • 优化数据结构:选择合适的数据结构,减少内存占用。
      • 使用Pipeline:减少网络往返次数,提高性能。
      • 分片:将数据分散到多个Redis实例。
      • 监控和调优:使用监控工具定期检查性能,进行调优。

多线程和虚拟内存

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

    • 单线程设计:避免多线程间的上下文切换开销,提高性能。
    • 6.0版本引入多线程:为了处理IO密集型任务,如网络IO和文件IO,提高整体性能。
  • Redis 的虚拟内存(VM)机制是什么?

    • 虚拟内存机制:允许Redis将部分数据换出到磁盘,减少内存占用。
    • 工作原理:Redis将不常用的数据换出到磁盘,当需要访问时再换入内存。

高级功能和架构

主从复制

  • Redis 主从复制的实现原理是什么?
    • 主从复制通过主节点将数据同步到从节点。主节点将写操作记录到replication buffer中,从节点定期请求同步数据,主节点将数据发送给从节点。
  • Redis 主从复制的常见拓扑结构有哪些?
    • 常见的拓扑结构包括:
      • 单主单从:一个主节点和一个从节点。
      • 单主多从:一个主节点和多个从节点。
      • 级联复制:从节点再作为其他从节点的主节点。

集群和哨兵机制

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

    • Redis集群通过分片(sharding)技术将数据分布到多个节点上。每个节点负责一部分数据,通过哈希槽(hash slot)来确定数据的归属。
  • Redis 集群会出现脑裂问题吗?

    • Redis集群通过哨兵机制和多数派选举来避免脑裂问题。只有超过半数的节点同意,才能进行主从切换。
  • 在 Redis 集群中,如何根据键定位到对应的节点?

    • 通过哈希槽(hash slot)来定位键。每个键通过CRC16算法计算哈希值,然后取模得到哈希槽编号,根据哈希槽编号找到对应的节点。
  • Redis 的哨兵机制是什么?

    • 哨兵机制用于监控和管理Redis集群的高可用性。哨兵节点会定期检查主节点的健康状况,如果主节点宕机,哨兵会自动进行主从切换。

Redis事务

  • Redis 支持事务吗?如何实现?
    • Redis支持事务,通过MULTIEXECDISCARDWATCH命令来实现事务。
    • MULTI:开始事务。
    • EXEC:执行事务。
    • DISCARD:取消事务。
    • WATCH:监视键的变化,如果在事务执行前键被修改,事务将被取消。

Lua脚本

  • Redis 的 Lua 脚本功能是什么?如何使用?
    • Redis支持使用Lua脚本执行复杂的操作。通过EVAL命令执行Lua脚本,可以实现原子操作。
    • 示例:
      lua
      EVAL "return redis.call('INCR', KEYS[1])" 1 counter

Pipeline管道

  • Redis 的 Pipeline 功能是什么?
    • Pipeline功能允许客户端一次性发送多个命令,减少网络往返次数,提高性能。
    • 示例:
      java
      redis.pipelined(jedis -> {
          jedis.set("key1", "value1");
          jedis.set("key2", "value2");
      });

Redis客户端

  • 你在项目中使用的 Redis 客户端是什么?
    • 常见的Redis客户端包括Jedis、Lettuce、Redisson等。
    • Jedis:Java客户端,轻量级,使用广泛。
    • Lettuce:Java客户端,支持Netty,性能较好。
    • Redisson:Java客户端,提供了丰富的高级功能,如分布式锁。