Redis 是一个开源的、高性能的键值对存储数据库,支持多种数据结构。
一、什么是 Redis,它有哪些特点?
Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
其特点包括:
高性能: 数据存储在内存中,读写速度极快,每秒可处理大量的读写请求。
数据结构丰富: 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。
持久化: 支持 RDB(Redis Database)和 AOF(Append Only File)两种持久化方式,保证数据在服务器重启后不会丢失。
分布式: 支持主从复制、哨兵模式和集群模式,可实现高可用和分布式部署。
原子性: 所有 Redis 操作都是原子性的,确保数据操作的一致性。
二、Redis 的常见数据结构及其原理。
字符串(String)
字符串是 Redis 中最基本的数据结构,它可以存储任何形式的字符串,包括二进制数据。 在 Redis 内部,字符串使用简单动态字符串(SDS)来实现。
简单动态字符串(SDS):SDS 是一种可变长度的字符串结构, 其定义如下:
struct sdshdr {
int len; // 字符串的实际长度
int free; // 字符串剩余的空闲空间
char buf[]; // 存储字符串的字符数组
};
优点:
获取字符串长度的时间复杂度为 O (1): 因为 len 字段记录了字符串的实际长度,无需像 C 语言字符串那样遍历整个字符串。
二进制安全: 可以存储任意二进制数据,因为 SDS 是通过 len 字段来判断字符串的结束,而不是依赖于空字符 ‘\0’。
减少内存分配次数: 当字符串需要扩展时,SDS 会预先分配一些额外的空间,避免频繁的内存分配。
应用场景
缓存: 将数据库查询结果存储为字符串,减少数据库访问次数。
计数器: 使用 INCR、DECR 等命令对字符串进行原子性的增减操作。
哈希(Hash)
哈希是一个键值对的集合,其中键和值都是字符串。 在 Redis 中,哈希可以使用两种数据结构来实现:压缩列表(ziplist)和哈希表(hashtable)。
压缩列表(ziplist): 当哈希中的键值对数量较少且每个键值对的长度较短时,Redis 会使用压缩列表来存储哈希。压缩列表是一种连续的内存块,将键和值依次存储在一起,通过偏移量来访问每个元素。
哈希表(hashtable): 当哈希中的键值对数量较多或每个键值对的长度较长时,Redis 会使用哈希表来存储哈希。哈希表使用数组和链表实现,通过哈希函数将键映射到数组的某个位置,如果发生哈希冲突,则使用链表来解决。
应用场景
存储对象:可以将一个对象的各个属性存储为哈希的键值对,方便对对象的属性进行单独操作。
列表(List)
列表是一个有序的字符串元素集合,支持在列表的两端进行插入和删除操作。 在 Redis 中,列表可以使用两种数据结构来实现:压缩列表(ziplist)和双向链表(linkedlist)。
压缩列表(ziplist): 当列表中的元素数量较少且每个元素的长度较短时,Redis 会使用压缩列表来存储列表。
双向链表(linkedlist): 当列表中的元素数量较多或每个元素的长度较长时,Redis 会使用双向链表来存储列表。双向链表的每个节点包含指向前一个节点和后一个节点的指针,方便在列表的两端进行插入和删除操作。
应用场景
消息队列:使用 LPUSH 和 RPOP 命令实现消息的入队和出队操作。
最新消息列表:使用 LPUSH 命令将最新的消息添加到列表的头部,使用 LRANGE 命令获取最新的消息列表。
集合(Set)
集合是一个无序的、唯一的字符串元素集合。 在 Redis 中,集合可以使用两种数据结构来实现:整数集合(intset)和哈希表(hashtable)。
整数集合(intset): 当集合中的元素都是整数且元素数量较少时,Redis 会使用整数集合来存储集合。整数集合是一个有序的整数数组,通过二分查找来查找元素。
哈希表(hashtable): 当集合中的元素包含非整数或元素数量较多时,Redis 会使用哈希表来存储集合。哈希表使用数组和链表实现,通过哈希函数将元素映射到数组的某个位置,如果发生哈希冲突,则使用链表来解决。
应用场景
去重: 利用集合的唯一性,对数据进行去重操作。
交集、并集、差集运算: 使用 SINTER、SUNION、SDIFF 等命令进行集合的交集、并集、差集运算。
有序集合(Sorted Set)ZSET
有序集合是一个有序的、唯一的字符串元素集合,每个元素都关联一个分数(score),根据分数对元素进行排序。 在 Redis 中,有序集合使用跳跃表(skiplist)和哈希表(hashtable)来实现。
跳跃表(skiplist): 跳跃表是一种有序的数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而可以在 O (log n) 的时间复杂度内完成插入、删除和查找操作。跳跃表的每个节点包含一个分数和一个成员,根据分数对节点进行排序。
哈希表(hashtable): 哈希表用于快速查找成员对应的分数,通过成员作为键,分数作为值存储在哈希表中。
应用场景
排行榜: 根据元素的分数对元素进行排序,实现排行榜功能。
范围查找: 使用 ZRANGE、ZREVRANGE 等命令根据分数范围获取元素。
三、Redis 速度快的原因
基于内存操作
Redis 是一个内存数据库,数据都存储在内存中,与传统的磁盘数据库相比,内存的读写速度要快得多。例如,机械硬盘的随机读写速度通常在几十毫秒级别,而内存的读写速度可以达到纳秒级别,因此 Redis 可以快速地处理数据的读写请求。
单线程架构
Redis 的核心是单线程的,即同一时间只处理一个客户端的请求。单线程架构避免了多线程带来的上下文切换和锁竞争问题,减少了系统开销。同时,Redis 使用了高效的事件驱动模型,通过 I/O 多路复用技术(如 select、poll、epoll 等)来同时监听多个客户端的连接和请求,提高了并发处理能力。
高效的数据结构
Redis 采用了上述多种高效的数据结构,这些数据结构针对不同的应用场景进行了优化,能够在常数时间复杂度或对数时间复杂度内完成常见的操作。例如,哈希表的查找、插入和删除操作的平均时间复杂度为 O (1),跳跃表的插入、删除和查找操作的时间复杂度为 O (log n)。
优化的内存管理
Redis 实现了自己的内存分配器,如 jemalloc、tcmalloc 等,这些内存分配器可以更高效地管理内存,减少内存碎片的产生。同时,Redis 还支持内存淘汰策略,当内存使用达到一定阈值时,会自动删除一些过期或不常用的数据,以释放内存空间。
异步操作
Redis 支持异步操作,如异步删除、异步持久化等。在进行一些耗时的操作时,Redis 可以将这些操作放到后台线程中执行,避免阻塞主线程,从而提高了系统的响应速度。例如,在进行 AOF 重写时,Redis 会在后台线程中进行,不会影响主线程的正常处理。
四、持久化机制
Redis 的 RDB 和 AOF 有什么区别?如何选择?
特性 | RDB | AOF |
---|---|---|
持久化方式 | 快照(内存数据的二进制备份) | 日志(记录所有写操作命令) |
恢复速度 | 快(直接加载二进制文件) | 慢(需重放所有命令) |
数据安全性 | 低(最后一次快照后的数据丢失) | 高(默认每 fsync 一次) |
文件大小 | 小(紧凑) | 大(命令日志) |
适用场景 | 主从复制、快速恢复 | 数据安全性要求高的场景 |
选择建议:
同时开启 RDB 和 AOF,RDB 用于快速恢复,AOF 保证数据不丢失。
对数据安全性要求极高的场景,可调整 AOF 的 fsync 策略(如 appendfsync everysec)。
RDB 快照是如何生成的?会阻塞主线程吗?
Redis 使用 fork() 系统调用创建子线程,子线程负责将内存数据写入临时文件,完成后替换旧的 RDB 文件。
主线程在 fork 时会短暂阻塞(取决于内存大小),但子线程写入时不影响主线程。
五、Redis 常问面试题
Redis 是什么?它的特点是什么?
Redis 是一个开源的内存键值数据库,支持多种数据结构(如 String、Hash、List、Set、Sorted Set 等),具有高性能、持久化、分布式等特点。
核心特性:
内存存储,读写速度快(10 万 QPS 以上)。
单线程架构,避免线程切换开销。
支持数据持久化(RDB 和 AOF)。
支持发布订阅、Lua 脚本、事务等功能。
Redis 有哪些数据结构?分别适用于什么场景?
String: 缓存、计数器、分布式锁。
Hash: 存储对象(如用户信息)。
List: 消息队列(LPUSH + RPOP)、最新消息列表。
Set: 去重、交集 / 并集 / 差集运算。
Sorted Set: 排行榜、带权重的任务队列。
Redis 的有序集合(ZSET)为什么使用跳跃表而不是二叉搜索树?
跳跃表的插入、删除、查询时间复杂度为 O (log n),与二叉搜索树(如红黑树)相当。
跳跃表的实现更简单,且支持范围查询(如 ZRANGEBYSCORE),而二叉搜索树需遍历所有节点。
Redis 的 ZSET 底层采用 跳跃表 + 哈希表 组合,哈希表存储成员与分数的映射,跳跃表负责排序。
Redis 的哈希表(Hash)是如何解决哈希冲突的?
Redis 使用 链地址法(链表)解决哈希冲突。
当多个键哈希到同一槽位时,这些键以链表形式存储。
当链表长度超过阈值(如 5)时,会转换为红黑树以提高性能。
如何优化 Redis 的内存使用?
使用压缩数据结构(如 ziplist、intset)。
合理设置键的过期时间,避免内存泄漏。
启用内存淘汰策略(max - memory - policy),如 allkeys - lru。
减少大键的使用,避免内存碎片。
什么是缓存雪崩、穿透、击穿?如何解决?
缓存穿透: 指查询一个不存在的数据,导致请求直接访问数据库。
解决方案包括:
布隆过滤器: 在缓存前面添加布隆过滤器,判断请求的数据是否可能存在。如果不存在,直接返回,避免访问数据库。
空值缓存: 当查询的数据不存在时,也将空值存入缓存,并设置一个较短的过期时间,避免频繁查询不存在的数据。
缓存击穿: 指一个热点 key 在缓存过期的瞬间,大量请求同时访问该 key,导致请求直接访问数据库。
解决方案包括:
互斥锁: 在缓存过期时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个请求去更新缓存,其他请求等待缓存更新完成后再获取数据。
热点 key 永不过期: 对于一些热点 key,不设置过期时间,而是通过定时任务或者其他方式来更新缓存。
缓存雪崩: 指大量缓存 key 在同一时间过期,导致大量请求直接访问数据库。
解决方案包括:
过期时间随机化: 为每个缓存 key 的过期时间添加一个随机值,避免大量 key 在同一时间过期。
多级缓存: 使用多级缓存,如本地缓存和 Redis 缓存,当 Redis 缓存失效时,先从本地缓存获取数据,减少对数据库的压力。
限流和熔断: 在缓存雪崩发生时,对请求进行限流或者熔断,保护数据库不被大量请求压垮。
如何保证缓存和数据库的数据一致性?
更新策略: 先更新数据库,再更新缓存:这种方式可能会导致并发问题,不建议使用。
先更新数据库,再删除缓存:这是比较常用的方式。当更新数据库成功后,删除对应的缓存,下次查询时再从数据库中获取数据并更新缓存。
延迟双删: 为了避免删除缓存失败或者数据库更新和缓存删除操作之间的短暂不一致,可以采用延迟双删策略。即先删除缓存,再更新数据库,然后等待一段时间后再次删除缓存。
异步消息队列: 使用消息队列来保证缓存和数据库的更新操作的顺序和可靠性。当数据库更新成功后,发送一条消息到消息队列,由消息队列的消费者负责删除缓存。
请解释 Redis 的主从复制、哨兵模式和集群模式
主从复制: 机制:主从复制是指将一个 Redis 服务器(主节点)的数据复制到其他 Redis 服务器(从节点)。从节点会定期从主节点同步数据,保持数据的一致性。
作用:提高 Redis 的读写性能,分担主节点的读压力;实现数据的备份,提高数据的安全性。
哨兵模式: 机制:哨兵模式是在主从复制的基础上,增加了哨兵进程来监控主从节点的状态。当主节点出现故障时,哨兵会自动将一个从节点提升为主节点,实现故障自动转移。
作用:提高 Redis 的高可用性,确保在主节点故障时系统能够自动恢复。
集群模式: 机制:集群模式是将多个 Redis 节点组成一个集群,数据会被分片存储在不同的节点上。客户端可以直接连接到集群中的任意节点,集群会自动将请求路由到正确的节点上。
作用:提高 Redis 的可扩展性和容错性,支持大规模数据存储和高并发访问。
说说 Redis 哈希槽的概念?
在 Redis 集群中,整个键空间被划分为 16384 个哈希槽(编号从 0 到 16383)。每个键都会根据其哈希值被映射到这 16384 个哈希槽中的某一个,集群中的每个节点负责处理一部分哈希槽。也就是说,集群中的所有节点共同管理这 16384 个哈希槽,以此实现数据的分布式存储。
哈希槽(Hash Slot)是 Redis 集群模式特有的数据分片和管理机制,在其他同步机制(如主从复制、哨兵模式)中并不会使用
怎么理解 Redis 事务
原子性(部分保证)
Redis 事务在执行过程中不会被其他客户端的命令插入打断,要么所有命令都执行,要么所有命令都不执行。但需要注意的是,如果事务队列中的某个命令执行失败(例如语法错误),Redis 并不会回滚之前已经执行的命令,也不会停止后续命令的执行,所以它只提供了部分原子性保证。
一致性
由于事务中的命令按顺序执行,且不会被其他客户端的命令插入打断,所以在事务执行前后,数据的状态是符合业务逻辑的,保证了数据的一致性。
隔离性
在事务执行过程中,其他客户端的命令不会插入到事务队列中执行,保证了事务的隔离性。
持久性(依赖持久化策略)
Redis 事务的持久性取决于 Redis 的持久化策略。如果使用的是 RDB 持久化,那么在事务执行过程中如果 Redis 发生崩溃,可能会丢失部分数据;如果使用的是 AOF 持久化,且 appendfsync 配置为 always,那么可以保证事务的持久性。
局限性
不支持回滚: 如前面所述,Redis 事务在命令执行出错时不会回滚已经执行的命令,需要开发者在编写代码时自行处理错误情况。
单线程执行: Redis 事务是单线程执行的,在事务执行期间,其他客户端的命令需要等待事务执行完毕,可能会影响系统的并发性能。
应用场景
批量操作: 当需要对多个键进行批量操作时,可以使用事务将这些操作组合在一起,减少网络开销。
乐观锁场景: 通过 WATCH 命令可以实现乐观锁机制,用于解决多个客户端同时修改同一组键时可能出现的数据冲突问题。
Redis 如何做内存优化?
合理选择数据结构
对于哈希表(Hash)和列表(List),当元素数量较少且元素大小较小时,Redis 会使用 ziplist 来存储,它是一种紧凑的连续内存数据结构,能有效节省内存。
对于集合(Set),如果元素都是整数且数量较少,Redis 会使用 intset 存储,它同样是一种紧凑的整数集合结构。
优化键名和值
键名尽量简短: 键名是会占用内存空间的,应使用简短且有意义的键名。例如,使用 u:1:name 而不是 userid:1:username。
避免存储大对象: 尽量将大对象拆分成多个小对象进行存储。比如一个大的 JSON 对象,可以将其拆分成多个字段分别存储。
使用压缩算法: 对于一些文本类型的数据,可以在存储前进行压缩,取出时再解压缩。不过这会增加 CPU 开销,需要权衡使用。
过期键处理
合理设置过期时间: 对于一些缓存数据,应根据业务需求合理设置过期时间,避免无用数据长期占用内存。可以使用 EXPIRE、PEXPIRE 等命令为键设置过期时间
集群和分片
数据分片: 对于大规模数据,可以使用 Redis 集群或分片技术将数据分散到多个节点上,避免单个节点内存压力过大。每个节点只负责部分数据的存储和处理,提高了系统的可扩展性和内存利用率。
读写分离: 通过主从复制实现读写分离,将读操作分散到多个从节点上,减轻主节点的压力,也可以在一定程度上优化内存使用。
Redis 的事务支持哪些命令?是否支持回滚?
事务通过 MULTI、EXEC 开启和提交,支持批量执行命令。
不支持回滚,但可以通过 DISCARD 取消事务。
若某个命令执行失败,其他命令仍会继续执行(除非使用 WATCH 监控键)。
Redis 的过期键如何处理?
惰性删除: 访问键时检查是否过期,过期则删除。
定期删除: 后台线程定期扫描过期键,删除一部分。
内存淘汰: 当内存不足时,根据策略(如 LRU)删除键。
Redis 是单进程单线程的,redis 利⽤队列技术将并发访问变为串⾏访问,消除了传统数据库串⾏控制的开销。
⼀个字符串类型的值能存储最⼤容量是512M
Redis 集群最⼤节点个数是16384 个。
Redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销
传统数据库的串行控制及开销
并发访问问题
在传统数据库(如 MySQL、Oracle 等)中,当多个客户端同时对数据库进行读写操作时,可能会出现数据不一致的问题。例如,多个事务同时修改同一行数据,可能会导致数据丢失、脏读、不可重复读等问题。
串行控制机制
为了解决并发访问带来的问题,传统数据库通常采用锁机制来实现串行控制。常见的锁类型包括行级锁、表级锁等。当一个事务需要对某行数据进行写操作时,会先获取该行数据的锁,其他事务必须等待该锁被释放后才能对该行数据进行操作。
串行控制的开销
锁机制虽然可以保证数据的一致性,但会带来一些开销:
上下文切换开销: 当一个事务获取不到锁时,会进入阻塞状态,操作系统需要进行上下文切换,将 CPU 资源分配给其他线程或进程,这会增加系统的开销。
死锁问题: 多个事务相互等待对方释放锁时,可能会导致死锁,需要额外的机制来检测和解决死锁问题。
锁竞争开销: 多个事务竞争同一把锁时,会导致性能下降,尤其是在高并发场景下,锁竞争会成为系统的瓶颈。
Redis 利用队列技术的处理方式
队列技术
Redis 可以利用其列表(List)数据结构来实现队列。客户端的请求可以依次放入队列中,Redis 按照队列的顺序依次处理这些请求,从而将并发访问变为串行访问。
消除传统数据库串行控制的开销
无锁机制: Redis 采用单线程模型处理请求,同一时间只处理一个请求,不需要使用锁机制来保证数据的一致性,因此消除了锁竞争和死锁问题带来的开销。
减少上下文切换: 由于 Redis 是单线程处理请求,不存在多线程之间的上下文切换,减少了系统的开销。
高效处理: Redis 基于内存操作,处理请求的速度非常快,队列的入队和出队操作时间复杂度都是 O (1),可以高效地处理大量请求。
综上所述,Redis 利用队列技术将并发访问变为串行访问,避免了传统数据库中锁机制带来的开销,提高了系统的性能和并发处理能力。
布隆过滤器
布隆过滤器是一种概率型数据结构,用于快速判断一个元素是否存在于集合中。
它的核心逻辑是:
若布隆过滤器判断元素不存在 → 该元素一定不存在于数据库中(无漏判)。
若布隆过滤器判断元素存在 → 该元素可能存在于数据库中(可能误判)。
因此,布隆过滤器的作用是过滤掉 “一定不存在” 的查询,从而减少对数据库的访问次数。