Java八股文整理:Redis篇
3 - Redis 篇
3.1 Redis 基础
3.1.1 什么是 Redis?使用场景?
1. 什么是 Redis?
- Redis 是一种基于内存的数据库,因此读写速度非常快,常用于 缓存,消息队列、分布式锁 等场景。
- Redis 数据类型的操作都是 原子性 的,因为执行命令由单线程负责的,不存在并发竞争的问题。
2. Redis 使用场景?
- Redis 支持事务 、持久化、Lua 脚本、多种集群(主从复制、哨兵)、发布/订阅模式,内存淘汰、过期删除等机制。
- 缓存:数据存储:频繁访问、不经常变更、读取远多于写入的数据,复杂查询结果、会话信息等。数据读取:当应用接收到数据请求时,首先检查 Redis 缓存是否有该数据。过期策略:通过 TTL(Time to Live)设置数据的过期时间,以确保缓存数据的时效性。
- 消息队列:List:使用两个 List(发布+消费)。生产者将消息 LPUSH 到一个 List,消费者使用 RPOP 从另一个 List 读取消息。Pub/Sub:生产者发布消息到一个频道。消费者订阅这个频道,一旦有新消息发布,消费者就会收到通知。
- 排行榜:使用 Sorted Set 存储排行榜数据,可以设置成员的分数,Redis 根据分数自动排序。提供了丰富的操作命令,如 ZADD、ZRANK、ZREVRANK、ZRANGE 等。
- Lua 脚本: 这允许用户编写复杂的逻辑,而不需要在客户端和服务器之间多次交换数据。
- Lua 脚本可以原子性地执行,这意味着在执行脚本的过程中,不会有其他命令被执行。
- 多种集群模式: Redis 的主从复制允许数据从一个主节点复制到一个或多个从节点。提供数据的冗余和备份,还可以分摊读操作的压力。
- Redis 哨兵系统用于监控 Redis 主节点和从节点的运行状态,并在主节点故障时自动进行故障转移。
3.1.2 Redis 和 Memcached 有什么区别?
- 数据结构:Redis 支持更丰富的数据结构,如字符串、列表、集合、有序集合、散列等,而 Memcached 只支持简单的键值对。
- 持久化:Redis 提供了持久化机制,可以将内存中的数据保存到磁盘,支持 RDB 和 AOF,而 Memcached 不支持持久化。
- 主从复制:Redis 支持主从复制,可以进行数据的备份和故障恢复,Memcached 不支持。
- 事务:Redis 支持事务,可以保证一系列操作的原子性执行,Memcached 不支持。
- 内存管理:Redis 有更复杂的内存管理机制,如内存淘汰策略等,Memcached 的内存管理相对简单。
- 网络模型:Redis 使用的是自己的事件处理模型,而 Memcached 使用的是 libevent 库。
3.1.3 为什么用 Redis 作为 MySQL 的缓存?
- 提高性能:减少数据库的访问次数,通过缓存热点数据减少数据库的压力。
- 读写分离:减轻数据库的读操作压力,提高数据库的写操作性能。
- 数据一致性:通过缓存和数据库的同步机制,保证数据的最终一致性。
- 容错机制:缓存可以作为数据库的一种容错机制,当数据库不可用时,仍然可以提供部分数据服务。
3.1.4 如何保证 Redis 和 MySQL 的一致性?
1. 缓存同步策略
- 设置有效期:给缓存设置有效期,到期后自动删除,再次查询时更新。(要求低)优势:简单、方便缺点:时效性差,缓存过期之前可能不一致
- 同步双写:在修改数据库的同时,直接修改缓存。(要求低)优势:时效性强,缓存与数据库强一致缺点:有代码侵入,耦合度高
- 异步通知:修改数据库时发送事件通知,相关服务监听到后修改缓存数据。(微服务)优势:低耦合,可以同时通知多个缓存服务缺点:时效性一般,可能存在中间不一致状态
2. 延迟双删 + 消息队列
- 通过延迟双删策略来保证一致性,并使用 MQ 异步通知数据库变更,以提高性能并减少对主业务流程的影响。
- 延迟双删:更新数据库时,先删除缓存,然后更新数据库,等待一小段时间后,再次删除缓存。这可以减少在数据库更新和缓存删除之间并发访问导致的不一致问题。
- 消息队列:数据库更新后,将更新操作记录发送到消息队列。消费者服务监听这些消息,异步地根据数据库的变更来更新或删除缓存,确保缓存最终与数据库保持一致。
3.1.5 本地缓存和 Redis 缓存的区别?
- 本地缓存:本地应用的内存,重启后丢失数据。单机应用缓存数据。速度非常快,直接访问内存。容量受限于应用的内存。
- Redis 缓存:服务器的内存,重启后可恢复数据。多机多应用共享缓存。速度快,通过网络但优化。容量受限于服务器配置。
3.2 Redis 数据结构
3.2.1 Redis 常见数据类型?使用场景?
1. 常见数据类型
- String(字符串):缓存数据、计数器、简单的消息存储。
- Hash(哈希):存储 对象 的字段和对应值,比如用户 ID 和用户名。
- List(列表):简单 消息队列(需注意全局唯一 ID 和消费组限制)。
- Set(集合):聚合计算(并集、交集、差集)、点赞、共同关注、抽奖等。
- Zset(有序集合):排序场景,如 排行榜、按电话号码或姓名排序等。
2. 更新后的数据类型
- BitMap(2.2 新增):二值状态统计,如用户 签到、登录状态、连续签到用户统计等。
- HyperLogLog(2.8 新增):基数统计,如大规模的独立访客数统计(UV)、独立 IP 数统计等。
- GEO(3.2 新增):地理位置 信息存储,如滴滴打车的车辆位置、附近的司机查询等。
- Stream(5.0 新增):高级消息队列,支持自动生成全局唯一消息 ID 和以消费组形式消费数据。
3.2.2 Redis 数据类型的底层结构?
1. String 类型: 使用 简单动态字符串(SDS)。
- SDS 记录了长度信息,不需要遍历整个字符串。时间复杂度:O(1)
2. List 类型: 使用 快速列表(quicklist)。
- 替代了早期的双向链表和压缩列表。时间复杂度:O(1)
3. Hash 类型: 使用 紧凑列表(listpack)或 哈希表。
- 元素个数小于 512 且值小于 64 字节时使用 listpack,否则使用哈希表。时间复杂度:O(1)
3. Set 类型: 使用 整数集合 或 哈希表。
- 元素个数小于 512 且值为整数使用整数集合,否则使用哈希表。时间复杂度:O(1)
5. ZSet 类型: 使用 紧凑列表(listpack)或 跳表。
- 元素个数小于 128 且值小于 64 字节时使用 listpack,否则使用跳表。时间复杂度:增删 O(log N),查 O(log N + M)优点:跳表通过 多层索引 优化查找速度,可以在高层快速定位节点所在的范围,然后逐层降低,最终精确查找数据位置。缺点:跳表相比于链表,由于增加了多层索引,会消耗更多的内存空间。
3.2.3 Redis 时间复杂度为 O(n) 的命令?
1. 时间复杂度为 O(n) 的命令
List
:lindex、lset、linsertHash
:hgetall、hkeys、hvalsSet
:smembers、sunion、sunionstore、sinter、sinterstore、sdiff、sdiffstoreZset
:zrange、zrevrange、zrangebyscore、zrevrangebyscore、zremrangebyrank、zremrangebyscore2. 优化策略
- 使用扫描命令:HSCAN、SSCAN、ZSCAN,避免全表扫描。
- 谨慎使用 O(n) 命令:在大数据集上,这些命令可能影响性能。
- 客户端聚合操作:避免使用 SORT、SUNION 等聚合命令。
3.3 Redis 线程模型
3.3.1 Redis 线程模型?
Redis 单线程:主线程负责处理客户端请求的接收、解析、数据读写和响应发送等过程。
- 版本变迁:2.6: 启动两个后台线程处理耗时任务,包括关闭文件和 AOF 刷盘。3.0 后: 新增了一个额外的后台线程,用于异步释放 Redis 内存,即 lazyfree 线程。
- 优势:将它们交给后台线程处理,可以避免主线程因此类操作阻塞而无法处理新的请求。通过异步处理,Redis 在执行耗时操作时不会影响到实时的请求响应,提升了整体性能和响应速度。
- 后台线程任务队列:BIO_CLOSE_FILE 队列: 处理关闭文件的任务,使用 close(fd) 完成。BIO_AOF_FSYNC 队列: 当 AOF 配置为 everysec 时,主线程将 AOF 写入操作封装成任务,调用 fsync(fd) 刷盘。BIO_LAZY_FREE 队列: 处理 lazy free 相关任务,包括释放对象、删除数据库对象、释放跳表对象等。
3.3.2 Redis 单线程模式是怎样的?
- Redis 的单线程模式指的是 Redis 处理客户端请求的主线是单线程的,即所有的命令执行都在一个线程中按顺序执行。
- 这种模式下,Redis 通过非阻塞 I/O 多路复用技术来处理多个客户端连接。
3.3.3 Redis 为什么这么快?
- 单线程避免了线程切换开销:由于 Redis 是单线程的,避免了多线程环境下的上下文切换和锁竞争开销。
- 非阻塞 I/O:Redis 使用非阻塞 I/O 和事件驱动模型,可以高效地处理大量并发连接。
- 内存操作:Redis 操作主要在内存中进行,内存访问速度远高于磁盘。
- 数据结构优化:Redis 的数据结构设计优化,如跳表实现的有序集合,可以提供快速的数据访问。
3.3.4 Redis 6.0 之前为什么使用单线程?
- 性能:单线程模型在多核处理器上可能不是最优的,但在 Redis 的使用场景下,单线程已经能够提供非常高的性能。
- 简单性:单线程模型简化了代码的复杂度,减少了并发编程中的问题,如死锁、竞态条件等。
- 资源利用:在 Redis 的使用场景中,CPU 通常不是瓶颈,瓶颈更多在于内存和网络带宽。
3.3.5 Redis 6.0 之后为什么引入了多线程?
- 提高多核处理器的利用率:随着多核处理器的普及,引入多线程可以更好地利用多核资源,提高性能。
- 处理大容量数据:在处理大容量数据时,多线程可以并行处理,提高效率。
- 减轻主线程压力:通过多线程处理一些耗时的操作,如 RDB 和 AOF 的文件写入,可以减轻主线程的压力。
3.3.6 常见的 IO 模型?多路复用的系统调用?
- 阻塞IO:当进程进行IO操作时,进程会被阻塞,直到IO操作完成。 优点:模型简单,易于理解和实现。缺点:进程在IO操作期间不能进行其他工作,导致资源浪费。
- 非阻塞IO:进程发起IO请求后,系统会立即返回,不会等待IO操作完成。 优点:进程可以在等待IO操作完成的同时进行其他工作。缺点:需要频繁检查IO操作是否完成,增加了系统的复杂性。
- IO多路复用:通过一个线程监视多个文件描述符,一旦某个文件描述符就绪,系统会通知程序进行IO操作。 select():监视多个文件描述符,等待它们中的一个或多个变为就绪状态。 缺点:文件描述符数量受限于系统限制,效率较低。poll():与select类似,但poll没有文件描述符数量的限制。 缺点:效率仍然不高,因为每次调用都需要复制文件描述符集合。epoll():更高效的IO多路复用机制,它不需要在每次调用时复制文件描述符集合,且可以处理更多的文件描述符。 优点:高效,可以处理大量文件描述符,且资源消耗较小。
3.4 Redis 持久化
3.4.1 Redis 如何实现数据不丢失?
AOF(Append Only File)
: 记录每次写操作,并将其追加到文件中。 优点: 数据持久性高,可以通过重放命令恢复数据。缺点: 文件较大,恢复速度相对慢。RDB(Redis Database Backup)
: 定期快照,在指定的时间间隔内将内存中的数据保存为二进制文件。 优点: 数据恢复速度快,适合大规模数据。缺点: 在快照期间,数据的变更不会被记录,可能导致数据丢失。
你平时是怎么使用 RDB 和 AOF 的?
- RDB:当对数据一致性的要求不是特别严格,允许数据丢失时,RDB 是一个较好的选择。 配置:通过 redis.conf 设置。例如:SAVE 900 1,900秒内至少有1个写操作则保存快照。
- AOF:适合对数据持久化要求高的场景,尤其是需要尽可能减少数据丢失的场合。例如,关键业务数据存储。 配置:通过 redis.conf 设置。例如:appendonly yes,appendfsync everysec,每秒同步一次 AOF 文件。
3.4.2 AOF 日志是如何实现的?
3.4.3 RDB 快照是如何实现的呢?
RDB(Redis DataBase)快照是 Redis 使用的一种持久化机制,用于将当前内存中的数据以快照的形式 保存到硬盘 上的文件中。
与之相对的是 AOF(Append-Only File)持久化方式,它记录的是操作日志而不是 实际数据。
通过 RDB 快照生成的文件,Redis 会定期创建新的 RDB 文件(
SAVE
或BGSAVE
),实现数据的持久化和备份。
- 定期快照:SAVE 或 BGSAVE 根据配置触发。优点:减少性能影响。缺点:数据丢失风险。
- 即刻快照:SAVE 立即生成,BGSAVE 后台生成。优点:数据一致性高。缺点:性能开销较大。
3.4.4 为什么会有混合持久化?
混合持久化(RDB 和 AOF)是为了结合两者的优点:
- 快速启动:RDB 可以提供快速的启动恢复。
- 数据完整性:AOF 可以提供更好的数据完整性,记录每次写操作,减少数据丢失的风险。
- 灵活性:混合持久化允许用户根据需要选择数据恢复的粒度,可以在快速启动和数据完整性之间取得平衡。
3.5 Redis 集群
3.5.1 Redis 如何实现服务高可用?
- 主从复制:最基础的高可用保障。它通过将一台 Redis 主服务器的数据同步到多个从服务器上,形成 一主多从 的架构。工作方式:主服务器可以进行读写操作,所有的写操作会被异步地复制到从服务器上,从服务器则只能执行读操作。优点:提高了读取性能,并提供了故障恢复的能力。缺点:异步复制导致主从数据之间可能存在短暂的不一致。
- 哨兵模式:用于监控 Redis 主从集群的健康状况,当主服务器宕机时自动完成 故障转移,选举新的主服务器。工作方式:哨兵进程周期性地检查 Redis 服务器的健康状态,并在发现故障时触发自动故障转移。优点:自动化了故障恢复,提高了系统的可用性。缺点:需要额外的资源来运行哨兵进程,并且配置和管理较为复杂。
- 切片集群模式:当单个 Redis 服务器无法处理整个数据集时,可以将数据分布在多个节点上,通过分片来提高性能和可用性。工作方式:使用哈希槽来管理数据的分布和节点之间的映射关系,每个节点负责一部分哈希槽。优点:增加了水平扩展性,使得 Redis 能够处理更大的数据集和负载。缺点:需要在应用层实现对 Redis Cluster 的支持,同时要确保哈希槽的平衡和节点的健康。
3.5.2 集群脑裂导致数据丢失怎么办?
- 配置优化:合理配置集群参数,如设置合理的 slave 节点数量和分区超时时间。
- 监控和报警:实施实时监控,一旦发现脑裂现象,立即发出报警并采取措施。
- 数据备份:定期进行数据备份,以防万一发生数据丢失时可以恢复。
- 人工干预:根据集群状态决定如何处理,如强制某些节点下线,或者等待网络恢复后再进行数据同步。
- 使用 Redis 4.0 及以上版本:这些版本提供了更好的脑裂解决方案,如使用 Raft 协议来处理分区问题。
3.6 Redis 过期删除与内存淘汰
3.6.1 Redis 使用的过期删除策略是什么?
Redis 允许对 key 设置过期时间,为此需要有效的删除策略来处理已过期的键值对。
- 惰性删除策略:不主动删除过期键,而是在每次访问 key 时检查过期字典,判断是否过期并删除。优点:对 CPU 时间友好,因为仅在访问时才进行检查和删除操作。缺点:内存不友好,如果过期 key 一直未访问,仍占据内存空间。
- 定期删除策略:每隔一段时间,Redis 从数据库中随机选择一些 key 进行过期检查和删除。优点:控制删除操作的时长和频率,减少对 CPU 的影响。缺点:难以确定合适的删除频率,可能出现 CPU 开销过大或内存占用过高的情况。
3.6.2 Redis 持久化时,对过期键会如何处理?
1. RDB 文件
- 生成阶段:过期的键不会被保存到新的 RDB 文件中。
- 加载阶段(主服务器):过期的键不会被载入到数据库中。
- 加载阶段(从服务器):过期的键也会被载入到数据库中。
2. AOF 文件
- 写入阶段: 键过期后,AOF 文件会追加一条 DEL 命令。
- 重写阶段: 过期的键不会被保存到重写后的 AOF 文件中。
3.6.3 Redis 主从模式中,对过期键会如何处理?
- 主节点:主节点会定期检查键是否过期,并在过期时删除它们。这个过程是异步的,通过一个定时任务来完成。
- 从节点:从节点不会主动删除过期键,它们依赖主节点的数据同步来更新数据。当主节点删除过期键后,从节点会通过复制操作来同步这些更改。
- 内存释放:过期键被删除后,它们占用的内存会被释放。
3.6.4 Redis 内存满了,会发生什么?
- 拒绝命令:Redis 会拒绝写入操作,返回错误信息,如“OOM command not allowed when used memory > 'maxmemory'”。
- 只读模式:在某些配置下,Redis 可能会进入只读模式,只响应读取命令,直到内存使用量降低。
- 触发内存淘汰:根据配置的内存淘汰策略,Redis 会开始淘汰旧数据以释放内存。
3.6.5 Redis 内存淘汰策略有哪些?
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- allkeys-ttl:从所有数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中随机挑选数据淘汰。
- allkeys-random:从所有数据集中随机挑选数据淘汰。
- volatile-lfu:从已设置过期时间的数据集中挑选使用频率最低的数据淘汰。
- allkeys-lfu:从所有数据集中挑选使用频率最低的数据淘汰。
3.6.6 LRU 算法和 LFU 算法有什么区别?
- LRU(Least Recently Used):最近最少使用算法,淘汰最长时间未被访问的数据。它是基于时间的,不考虑数据的使用频率。
- LFU(Least Frequently Used):最少使用频率算法,淘汰使用次数最少的数据。它基于数据的使用频率,不考虑时间因素。
3.7 Redis 缓存设计
3.7.1 缓存雪崩、缓存击穿、缓存穿透?
1. 缓存雪崩:指大量缓存同时失效,导致请求直接打到数据库,造成数据库压力过大。
- 失效时间随机化: 在设置失效时间时加入随机值,使得缓存过期时间分散开来,不会在同一时间大规模失效。
- 永不过期: 对于不易变化的数据,可以设置永不过期,通过后台服务更新缓存,避免缓存大面积同时失效。
2. 缓存击穿:指某个热点数据缓存失效后,大量并发请求直接绕过缓存直接访问数据库,造成数据库压力过大。
- 互斥锁: 使用互斥锁来保证同一时间只有一个线程去加载数据到缓存,未获取到锁的线程可以等待。
- 永不过期: 对于热点数据,可以设置永不过期或在数据即将过期前异步更新缓存。
3. 缓存穿透:指恶意或者无效的请求访问不存在于缓存和数据库中的数据,造成数据库压力过大。
- 设置空值或默认值: 针对查询无效数据的请求,在缓存中设置空值或默认值,避免对数据库的无效查询。
- 使用布隆过滤器: 使用布隆过滤器标记存在的数据,来快速判断请求的数据是否存在,避免对数据库的查询压力。
3.7.2 如何设计一个缓存策略,动态缓存热点数据?
由于数据存储受限,并不是所有数据都需要存放到缓存中,而只是将热点数据缓存起来,所以要设计一个动态缓存的策略。
总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
例子:电商平台场景中,要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:
- 先通过缓存做一个排序队列(比如存放 1000 个商品),越是最近访问的商品排名越靠前。(
zadd
)- 然后定期过滤掉队列中排名最后的 200 个商品,再从数据库随机读取 200 个商品加入队列中。(
zrange
)- 这样每次请求会先从队列中获取商品 ID,如果命中就根据 ID 从另一个缓存中读取实际的商品信息,并返回。
3.7.3 说说常见的缓存更新策略?
- 懒加载(Lazy Loading):只有当需要数据时才去加载数据到缓存中。
- 时间过期(Time-based Expiration):设置缓存数据的过期时间,过期后自动从缓存中删除。
- 写入时更新(Write-Through):数据更新时同时更新缓存和数据库。
- 失效策略(Cache Aside):从缓存读取数据,如果未命中,则从数据库加载数据并更新缓存。
- 双写一致性(Read-Through and Write-Behind Caching):读取时更新缓存,写入时异步更新缓存和数据库。
- 缓存预热(Cache Warm-up):在系统启动时预先加载热点数据到缓存中。
- 批量更新(Batch Updates):对于批量数据更新操作,可以一次性更新缓存和数据库。
3.7.4 位图 BitMap、布隆过滤器 Bloom filter?
1. 位图(BitMap)
- 用途:适用于数据量大且连续的场景,如判断一个整数是否在某个范围内。
- 优点:相比传统数组,BitMap 更加节省空间。
- 实现:可以使用 hutool 工具包中的 IntMap 或 LongMap 来实现。
2. 布隆过滤器(Bloom filter)
- 用途:适用于判断元素是否可能存在于一个集合中,特别是当集合元素非常多时。
- 原理:使用多个哈希函数来减少冲突,提高判断的准确性。
- 特点:时间和空间效率高,有一定的误判率(哈希冲突),但可以调整参数来控制。
3.8 Redis 实战
3.8.1 Redis 如何实现延迟队列?
- 使用 Sorted Set:利用 Sorted Set 的分数作为延迟时间,将任务存储在 Sorted Set 中,通过分数范围查询来获取到期的任务。
- 使用 Pub/Sub:通过发布/订阅模式,将消息发送到一个频道,消费者在后台监听并处理消息,实现异步处理。
- 使用 Keyspace Notifications:开启 Redis 的 Keyspace Notifications 功能,监听特定模式的键的过期事件,实现延迟队列。
3.8.2 Redis 的大 key 如何处理?
1. 什么是 Redis 大 Key?
大 key 并不是指 key 很大,而是指 key 对应的 value 或元素数量 很大。具体定义如下:
- String 类型的值大于 10 KB
- Hash、List、Set、ZSet 类型的元素个数超过 5000 个
大 key 会导致 Redis 性能降低、数据倾斜、主从同步 等问题。
2. 如何解决?
- 可删除:使用 UNLINK 命令可以安全地删除大 key。
- 不可删除:不可删除的话就将大 key 拆分 为多个小 key。
3.8.3 Redis 管道有什么用?
管道技术(Pipeline)是客户端提供的一种 批处理 技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
管道本质上是客户端的功能,而非 Redis 服务器端的功能。同时要避免发送的命令过大,或管道内数据太多而导致网络阻塞。
3.8.4 Redis 事务支持回滚吗?
#redis##八股文##笔记#Redis 的事务不支持传统数据库中的回滚操作。Redis 的事务主要是通过
MULTI
、EXEC
、WATCH
和DISCARD
命令来实现的:
- MULTI:开始一个事务。
- EXEC:执行事务中的所有命令。
- WATCH:监视一个或多个键,如果在执行
EXEC
之前这些键被其他命令修改,则事务会失败。- DISCARD:取消当前事务,放弃事务中的所有命令。如果事务中的某个命令失败,Redis 会继续执行其他命令,而不是回滚。