44 连问深度剖析 Redis,近 2 万字总结干货满满

文档大纲

1、什么是Redis?

Redis是一款完全开源免费且遵循BSD协议的高性能key-value数据库。它具有以下特点:

  • 支持数据持久化:Redis能够将内存中的数据保存至磁盘,在重启时可重新加载并继续使用。
  • 丰富的数据结构:除了简单的key-value类型数据,Redis还提供了list、set、zset、hash等多种数据结构的存储支持。
  • 数据备份功能:Redis支持数据备份,采用master - slave模式实现数据备份。

2、Redis的数据类型?

  • string(字符串):这是Redis最基础的数据结构。其值可以是简单字符串、复杂字符串(如JSON、XML)、数字(整数、浮点数),甚至二进制数据(如图片、音频、视频),但单个值最大不能超过512MB。典型使用场景包括:
    • 缓存功能
    • 计数
    • 共享Session
    • 限速
  • hash(哈希):哈希类型的键值本身是一个键值对结构。典型应用场景有:
    • 缓存用户信息
    • 缓存对象
  • list(列表):列表(list)类型用于存储多个有序的字符串,它非常灵活,可充当栈和队列。主要使用场景如下:
    • 消息队列
    • 文章列表
  • set(集合):集合(set)类型同样用于保存多个字符串元素,与列表不同的是,集合中不允许有重复元素,且元素无序。常见使用场景有:
    • 标签(tag)
    • 共同关注
  • zset(有序集合):有序集合中的元素可进行排序,与列表以索引下标排序不同,它通过为每个元素设置权重(score)来排序。主要应用场景为:
    • 用户点赞统计
    • 用户排序
  • HyperLogLog
  • pub/sub

3、Redis有哪些优缺点?

优点:

  • 读写性能卓越:Redis读速可达110000次/s,写速可达81000次/s。
  • 支持数据持久化:提供AOF和RDB两种持久化方式。
  • 支持事务:Redis所有操作均为原子性,还支持将多个操作合并后原子性执行。
  • 数据结构丰富:除string类型的value外,还支持hash、set、zset、list等多种数据结构。
  • 支持主从复制:主机可自动将数据同步至从机,实现读写分离。

缺点:

  • 内存受限:数据库容量受物理内存限制,不适用于海量数据的高性能读写,主要适用于较小数据量的高性能操作与运算场景。
  • 容错与恢复不足:Redis不具备自动容错和恢复功能,主机或从机宕机均会导致前端部分读写请求失败,需等待机器重启或手动切换前端IP才能恢复。
  • 数据一致性问题:主机宕机时,宕机前部分数据可能未及时同步到从机,切换IP后会引发数据不一致问题,降低系统可用性。
  • 在线扩容困难:Redis较难支持在线扩容,集群容量达到上限时,在线扩容操作复杂。为避免该问题,运维人员上线系统时需预留足够空间,造成资源浪费。

4、为什么要用Redis做缓存?

主要从“高性能”和“高并发”两个方面来看:

  • 高性能:当用户首次访问数据库中的某些数据时,由于数据从硬盘读取,过程较慢。将这些数据存入缓存,下次访问时可直接从缓存获取。操作缓存即操作内存,速度极快。数据库中对应数据变更后,同步更新缓存中的数据即可。
  • 高并发:直接操作缓存所能承受的请求量远高于直接访问数据库。因此,可将数据库中的部分数据转移至缓存,让部分用户请求直接在缓存处处理,无需经过数据库。

5、Redis为什么这么快?

  1. 基于内存的高效操作:Redis完全基于内存运行,绝大多数请求都是单纯的内存操作,这使得其运行速度极快。它的数据存储结构类似于HashMap,而HashMap的显著优势在于,其查找和操作的时间复杂度均为O(1),这就保证了Redis在数据读写上的高效性。
  2. 简洁的数据结构设计:Redis的数据结构经过专门设计,极为简单,相应地,对数据的操作也很简便。这种简洁的数据结构设计,使得Redis在处理数据时能够更加高效,减少了不必要的计算和处理开销。
  3. 单线程机制的优势:Redis采用单线程模型,这种设计避免了不必要的上下文切换和竞争条件。由于不存在多进程或多线程切换所导致的CPU消耗,并且无需考虑各种锁的问题,也没有加锁和释放锁的操作,因此不会因可能出现的死锁情况而造成性能损耗,从而能够更专注地处理数据请求,提升整体性能。
  4. 多路I/O复用模型的应用:Redis使用多路I/O复用模型以及非阻塞I/O技术。这使得它能够在单个线程中同时处理多个I/O流,极大地提高了I/O的利用率和响应速度,能够高效地处理大量并发请求,而不会被I/O操作所阻塞。
  5. 独特的底层模型:Redis的底层实现方式与其他系统不同,它直接构建了自己的VM(Virtual Memory,虚拟内存)机制。相比一般系统调用系统函数时,会浪费一定时间在数据移动和请求上,Redis的这种底层实现方式减少了这些额外的时间消耗,进一步提升了数据处理的速度。

6、Redis的应用场景

  1. 计数器功能:通过对String类型数据进行自增或自减运算,Redis能够轻松实现计数器的功能。由于Redis是内存型数据库,具备极高的读写性能,非常适合存储那些需要频繁读写的计数量,例如网站的访问量统计、帖子的点赞数等。
  2. 缓存应用:将热点数据存储在Redis内存中,并设置合理的内存最大使用量以及淘汰策略,以此来确保缓存的命中率。这样可以大大减少对后端数据库的访问压力,提高系统的响应速度,例如缓存网站的页面片段、数据库查询结果等。
  3. 会话缓存管理:Redis可用于统一存储多台应用服务器的会话信息。当应用服务器不再负责存储用户的会话信息时,它就不再具有状态性,用户可以向任意一台应用服务器发起请求,这种方式更易于实现系统的高可用性和可伸缩性,方便用户在不同服务器之间进行无缝切换。
  4. 全页缓存(FPC):除了基本的会话token之外,Redis还提供了便捷的FPC平台。以Magento电商平台为例,它提供了一个插件,能够使用Redis作为全页缓存的后端,提升页面加载速度。对于WordPress用户而言,Pantheon的wpredis插件也是一个很好的选择,它可以帮助用户快速加载曾经浏览过的页面,优化用户体验。
  5. 查找表的使用:诸如DNS记录等数据非常适合使用Redis进行存储。查找表的应用和缓存类似,都利用了Redis快速查找的特性。但查找表与缓存的区别在于,查找表中的内容通常不会失效,因为它被视为可靠的数据来源,而缓存中的数据则可能会根据设置的规则失效。
  6. 消息队列(发布/订阅功能):Redis的List数据结构是一个双向链表,通过lpush和rpop命令可以方便地写入和读取消息,从而实现消息队列的功能。不过,在一些对消息处理要求较高的场景中,使用Kafka、RabbitMQ等专业的消息中间件可能更为合适。Redis的发布/订阅功能有着广泛的应用场景,不仅可以作为基于发布/订阅的脚本触发器,甚至还能够用来构建简单的聊天系统。
  7. 分布式锁实现:在分布式场景中,单机环境下的锁无法对多个节点上的进程进行同步控制。此时,可以利用Redis自带的SETNX命令来实现分布式锁。此外,Redis官方还提供了RedLock分布式锁实现方案,能够更好地满足分布式系统中对锁的需求,确保在分布式环境下数据的一致性和操作的正确性。
  8. 排行榜构建:Redis提供的列表和有序集合数据结构,为构建各种排行榜系统提供了便利。通过合理运用这些数据结构,可以轻松实现如游戏排行榜、商品销量排行榜等功能,满足用户对数据排名和展示的需求。
  9. 社交网络应用:在社交网络场景中,Redis可用于实现赞/踩功能、粉丝关系管理、共同好友或喜好的查找、消息推送以及下拉刷新等功能。例如,通过Redis可以快速记录用户的点赞和踩的操作,高效管理用户之间的关注和粉丝关系,为社交网络应用提供强大的支持。

Redis的应用通常会结合具体项目需求来考量,以一个电商项目的用户服务为例:

  • Token存储:当用户在电商平台登录成功后,使用Redis存储用户的Token,以便在后续的请求中验证用户身份,提高认证的效率和安全性。
  • 登录失败次数计数:利用Redis的计数功能,记录用户登录失败的次数。当登录失败次数超过一定阈值时,自动锁定账号,防止恶意登录行为,保障用户账号的安全。
  • 地址缓存:对电商平台中的省市区等地址数据进行缓存,减少对数据库的频繁查询,加快页面加载速度,提升用户体验。
  • 分布式锁:在分布式环境下,针对用户的登录、注册等操作添加分布式锁。例如,在商品抢购等场景中,通过分布式锁防止一人多卖的情况发生,保证交易的一致性和准确性。
  • ……

7、什么是持久化?

持久化是指将Redis内存中的数据写入磁盘的过程。这一操作的主要目的是防止因服务宕机等意外情况导致内存数据丢失。通过持久化,即使Redis服务出现故障,在恢复后也能够从磁盘中重新加载数据,保证数据的完整性和可用性。

8、Redis 的持久化机制是什么?各自的优缺点?

持久化机制:

  • RDB(默认):即Redis DataBase,是Redis默认采用的持久化方式。
  • AOF:全称Append Only File,以日志形式记录数据操作。

RDB:

RDB是Redis默认的持久化方式,其工作机制为:每隔一段时间,Redis就会将内存中的数据保存到硬盘上的指定文件中,对应的产生的数据文件为dump.rdb。 触发RDB的方式主要有手动触发和自动触发这两种:

  • 手动触发:对应savebgsave命令。save命令会阻塞当前Redis服务器,直到RDB过程完成为止,对于内存较大的实例而言,会造成较长时间的阻塞,因此不建议在线上环境中使用;bgsave命令则是让Redis进程执行fork操作创建子进程,由子进程负责RDB持久化过程,完成后自动结束,阻塞仅发生在fork阶段,一般时间很短。
  • 自动触发:以下场景会自动触发RDB持久化:
    1. 配置save相关参数,如“save m n”,这表示在m秒内数据集若存在n次修改时,就会自动触发bgsave
    2. 当从节点执行全量复制操作时,主节点会自动执行bgsave生成RDB文件,并发送给从节点。
    3. 执行debug reload命令重新加载Redis时,也会自动触发save操作。
    4. 默认情况下,执行shutdown命令时,如果没有开启AOF持久化功能,那么就会自动执行bgsave

优点

  • 文件单一,便于持久化:RDB持久化仅生成一个文件dump.rdb,在进行数据持久化存储和管理时较为方便。
  • 容灾性良好:单个dump.rdb文件便于保存到安全的磁盘位置,在发生灾难等情况时,能够较为容易地恢复数据。
  • 性能最大化:通过fork子进程来完成写操作,主进程可以继续处理命令请求,实现了IO操作的最大化。由于使用单独子进程进行持久化,主进程无需进行任何IO操作,从而保证了Redis的高性能。
  • 启动效率较高:当数据集较大时,相较于AOF方式,RDB的启动效率更高,能够更快地恢复数据。

缺点

  • 数据安全性较低:RDB是间隔一段时间进行一次持久化操作,如果在两次持久化之间Redis发生故障,就会导致数据丢失。因此,这种方式更适用于对数据完整性要求不是非常严苛的场景。例如,在1:00进行了一次快照,而在1:10即将进行下一次快照时Redis宕机,那么就会丢失这10分钟内的数据。
  • 可能影响服务:每次RDB操作通过fork子进程生成快照文件时,如果文件规模特别大,可能会导致客户端服务暂停数毫秒甚至数秒,影响服务的连续性。

AOF:

AOF是以日志的形式记录每个写操作,每当对数据进行新建、修改等操作时,都会将相应的命令保存到指定文件中。当Redis重新启动时,会读取该文件,并重新执行其中的新建、修改数据命令,以此来恢复数据。当RDB和AOF两种方式同时开启时,在数据恢复过程中,Redis会优先选择AOF来恢复数据。

优点

  • 数据安全性高:AOF持久化可以通过配置appendfsync属性来控制写入频率。若设置为always,则每进行一次命令操作,就会将其记录到AOF文件中一次,大大降低了数据丢失的风险。
  • 数据一致性有保障:采用append模式写文件,即使中途服务器宕机,也可以通过redis-check-aof工具来解决数据一致性问题,确保数据的完整性。
  • 便于数据恢复:AOF日志文件中的命令以非常可读的方式进行记录,这对于灾难性的误删除操作来说,非常适合进行紧急恢复。例如,若有人不小心执行了flushall命令清空了所有数据,只要此时还未执行rewrite操作,就可以直接从日志文件中删除flushall命令,然后进行数据恢复。

缺点

  • 文件体积较大:对于同一份数据,AOF文件通常要比RDB数据快照文件大,这会占用更多的磁盘空间。
  • 写性能相对较低:开启AOF后,其支持的写QPS(每秒查询率)会比RDB低。这是因为AOF一般会配置成每秒执行一次fsync操作,而每秒的fsync操作开销相对较高,影响了写操作的性能。
  • 数据恢复速度慢:由于AOF文件记录的是操作命令,在进行数据恢复时需要依次执行这些命令,因此数据恢复的速度相对较慢,不太适合作为冷备方案。

9、RDB和AOF到底如何选择?

如何看待数据“绝对”安全?

Redis作为一款内存数据库,从本质上来说,若要保证高性能,就难以做到数据的“绝对”安全。无论是RDB还是AOF,它们都是在兼顾性能的前提下,尽可能地降低数据丢失的风险。一旦真的发生数据丢失问题,也只能尽量减少损失。在整个项目的架构体系中,Redis大多情况下扮演着“二级缓存”的角色。适合作为二级缓存保存的数据通常具有以下特点:

  • 读多写少:经常需要进行查询操作,而很少被修改的数据。
  • 允许一定风险:不是非常重要的数据,即使偶尔出现并发问题,也不会对业务产生严重影响。
  • 数据独立性:不会被其他应用程序随意修改的数据。 当Redis作为缓存服务器时,意味着数据在像MySQL这样的传统关系型数据库中存在正式版本,数据最终以MySQL中的为准。

RDB和AOF到底如何选择?

  • 不建议单独使用RDB:仅仅使用RDB会导致在Redis故障时丢失大量数据,无法满足对数据完整性有一定要求的场景。
  • 不建议单独使用AOF:单独使用AOF存在两个问题。其一,通过AOF做冷备时,其数据恢复速度没有RDB快;其二,RDB每次生成数据快照的方式较为简单直接,在数据恢复的健壮性方面更具优势。
  • 综合使用两者:综合考虑AOF和RDB两种持久化方式,建议以AOF来保证数据不丢失,将其作为恢复数据的首要选择;同时利用RDB来进行不同程度的冷备,当AOF文件丢失或损坏无法使用时,可以借助RDB快速恢复数据。 官方推荐:如果对数据的敏感度较低,可以选择单独使用RDB;不建议单独使用AOF,因为可能存在潜在的Bug;如果Redis仅作为纯内存缓存使用,那么RDB和AOF都可以不使用。

10、Redis持久化数据和缓存怎么做扩容?

  • 作为缓存使用时:当Redis被用作缓存时,可以使用一致性哈希算法来实现动态扩容和缩容。一致性哈希能够有效地将数据均匀分布在不同的节点上,并且在节点数量发生变化时,尽可能减少数据的重新分布,从而保证系统的稳定性和性能。
  • 作为持久化存储使用时:如果Redis被用作持久化存储,必须使用固定的keys-to-nodes映射关系,并且节点的数量一旦确定就不能随意更改。否则(即Redis节点需要动态变化的情况),就必须使用一套能够在运行时进行数据再平衡的系统,而目前只有Redis集群能够满足这一要求,实现动态的节点变化和数据再平衡。

11、Redis 过期键的删除策略?

  • 定时删除:为每个设置了过期时间的键都创建一个定时器,一旦到达过期时间,就会立即清除该键。这种策略的优点是能够及时释放过期键所占用的内存,对内存管理非常友好;然而,其缺点也很明显,会占用大量的CPU资源去处理过期数据,进而影响缓存的响应时间和吞吐量,降低系统的整体性能。
  • 惰性删除:只有在访问某个键时,才会判断该键是否已经过期,如果过期则将其清除。这种策略的最大优势在于能够最大化地节省CPU资源,因为它不需要专门的线程去主动检查过期键;但它对内存不太友好,在极端情况下,可能会出现大量的过期键长时间未被再次访问,从而一直占用内存空间,导致内存浪费。
  • 定期过期:每隔一定的时间间隔,Redis会扫描一定数量的数据库中的expires字典(expires字典保存了所有设置了过期时间的键的过期时间数据,其中,键是指向键空间中某个键的指针,值是该键的毫秒精度的UNIX时间戳表示的过期时间,键空间则是指该Redis集群中保存的所有键),并清除其中已过期的键。这种策略是前两种策略的折中方案,通过合理调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同的应用场景下实现CPU资源和内存资源的最优平衡。

PS:在Redis中,同时采用了惰性删除和定期删除这两种过期策略,以在节省CPU资源和释放内存空间之间找到一个较好的平衡点,保障系统的性能和资源利用率。

12、Redis的内存淘汰策略有哪些?

Redis的内存淘汰策略,是指当Redis用于缓存的内存空间不足时,对于需要新写入且要申请额外空间的数据所采取的处理方式。

全局的键空间选择性移除

  • noeviction:当内存无法容纳新写入的数据时,新的写入操作将会报错,以此阻止数据被强制删除,保证已存在数据的完整性。
  • allkeys-lru:这是最常用的内存淘汰策略。当内存不足以存放新写入的数据时,Redis会在整个键空间中,移除最近最少使用(Least Recently Used,LRU)的键,从而为新数据腾出空间。这种策略基于一种假设,即最近最少使用的数据在未来被访问的可能性也较低。
  • allkeys-random:当内存不足时,在整个键空间中,随机地移除某个键来释放内存,以满足新数据的写入需求。这种策略相对简单,但可能会移除一些仍有访问价值的数据。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存无法容纳新写入的数据时,Redis会在设置了过期时间的键空间中,选择移除最近最少使用的键。该策略与allkeys-lru类似,只是范围限定在设置了过期时间的键上。
  • volatile-random:在内存不足且需要写入新数据时,从设置了过期时间的键空间中,随机地移除某个键。这种方式同样简单,但对于数据的选择缺乏针对性。
  • volatile-ttl:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,优先移除那些过期时间更早的键。这种策略有助于及时清理即将过期的数据,提高内存的使用效率。

PS:需要注意的是,Redis的内存淘汰策略的选取并不会影响对过期键的处理。内存淘汰策略主要用于应对内存不足时新数据的写入需求,而过期策略则是专门用来处理已经达到过期时间的缓存数据。

13、Redis如何做内存优化?

为了优化Redis的内存使用,可以充分利用Hash、list、sorted set、set等集合类型的数据结构。通常情况下,许多小的Key-Value对可以以更紧凑的方式存储在一起。应尽可能使用散列表(hashes),特别是当散列表中存储的数据较少时,其占用的内存非常小。因此,在设计数据模型时,应尽量将相关数据抽象到一个散列表中。例如,在一个web系统中,对于用户对象,不应为其名称、姓氏、邮箱、密码等属性分别设置单独的键,而是应该将该用户的所有信息整合存储到一张散列表中,这样可以有效减少内存的占用,提高内存的使用效率。

14、什么是事务?

事务是一个具有单独隔离性的操作单元。在事务中,所有的命令都会被序列化,并按照顺序依次执行。在事务的执行过程中,不会受到其他客户端发送来的命令请求的干扰。同时,事务也是一个原子操作,这意味着事务中的命令要么全部被成功执行,要么全部都不执行,不存在部分执行的情况,保证了数据操作的完整性和一致性。

15、Redis事务的概念

Redis事务的本质是通过一组命令(MULTI、EXEC、WATCH等)的集合来实现的。它支持一次性执行多个命令,在事务执行过程中,所有命令会被序列化。并且,事务会按照顺序串行化地执行队列中的命令,其他客户端提交的命令请求不会插入到正在执行的事务命令序列中。简单来说,Redis事务就是以一次性、顺序性和排他性的方式执行一个队列中的一系列命令,确保事务内的操作不受外部干扰,保证数据的一致性。

16、Redis事务的三个阶段

  • 事务开始(MULTI):通过执行MULTI命令开启一个事务,该命令总是返回OK,表示事务已成功开始,此后客户端发送的命令将被加入到事务队列中。
  • 命令入队:在事务开启后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被依次放入事务队列中,等待后续的执行。
  • 事务执行(EXEC):当客户端调用EXEC命令时,事务队列中的所有命令会按照入队的顺序依次执行,并返回所有命令的执行结果,按命令执行的先后顺序排列。如果在执行过程中事务被打断,则返回空值nil

17、 Redis事务相关命令

Redis事务功能主要通过MULTIEXECDISCARDWATCH这四个原语来实现,在事务执行时,Redis会将其中的所有命令进行序列化,然后按顺序执行。

  1. Redis不支持回滚:“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”,这种设计使得Redis的内部结构保持简单且运行快速。因为回滚操作需要额外的资源和复杂的处理逻辑,不支持回滚可以减少这些开销。
  2. 命令错误时的处理:如果在一个事务中的命令存在语法等错误,那么整个事务中的所有命令都不会被执行。这是为了保证事务的完整性,避免错误命令对数据造成不一致的影响。
  3. 运行错误时的处理:如果在一个事务中出现运行时错误(例如对类型不匹配的数据进行操作),那么已经正确入队的命令会被执行,而出现错误的命令之后的命令将被跳过。

WATCH命令是一个乐观锁机制,它可以为Redis事务提供check-and-set(CAS)行为。通过监控一个或多个键,一旦其中有一个键被修改(或删除),在调用EXEC命令时,之后的事务就不会执行,监控状态会一直持续到EXEC命令被执行。 MULTI命令用于开启一个事务,总是返回OK。执行MULTI之后,客户端可以继续发送多条命令,这些命令会被暂存到事务队列中,等待EXEC命令触发执行。 EXEC命令用于执行所有事务块内的命令,并返回事务块内所有命令的返回值,按照命令执行的先后顺序排列。当事务执行过程被打断时,返回空值nilDISCARD命令可以让客户端清空事务队列,并放弃执行事务,同时客户端会从事务状态中退出,回到正常的命令执行状态。 UNWATCH命令用于取消WATCH对所有键的监控,使得后续的事务不再受之前监控键的状态变化影响。

18、 事务管理(ACID)概述

  • 原子性(Atomicity):原子性意味着事务是一个不可分割的工作单元,事务中的所有操作要么全部成功执行,要么全部不执行,不存在部分执行的情况,保证了数据操作的完整性。
  • 一致性(Consistency):事务前后数据的完整性必须保持一致,即事务执行前后,数据库中的数据应满足预先定义的完整性约束条件,确保数据的正确性和有效性。
  • 隔离性(Isolation):在多个事务并发执行时,一个事务的执行不应受到其他事务的干扰,每个事务都感觉像是在独立地访问和修改数据库,保证了事务之间的相互独立性。
  • 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,即使后续数据库发生故障(如系统崩溃、断电等),也不会影响到已提交事务对数据的修改,确保数据的可靠性。

Redis的事务总是具备ACID特性中的一致性和隔离性,而其他特性(原子性和持久性)在某些情况下是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值设置为always时,事务才具有耐久性,即事务提交后的数据修改能够持久化保存。

19、Redis事务支持隔离性吗

Redis是单进程程序,并且在执行事务时,它能够保证事务不会被中断,事务会一直运行直到执行完事务队列中的所有命令。由于Redis的单进程特性以及事务执行的连续性,其他事务无法在当前事务执行过程中插入干扰,因此,Redis的事务总是带有隔离性的,确保了事务执行的独立性和数据的一致性。

20、主从复制了解吗?

Redis主从复制

在Redis架构中,主从复制指的是将一台Redis服务器的数据复制到其他Redis服务器的过程。其中,提供数据的服务器被称作主节点(master),而接收数据的服务器则被称为从节点(slave)。需要注意的是,数据复制方向是单向的,即只能由主节点流向从节点。Redis的主从复制模式涵盖主从同步与从从同步两种类型,从从同步这一功能是在Redis后续版本中新增的,其目的在于减轻主节点在数据同步时的负担。 主从复制具备以下重要作用:

  • 数据冗余:通过主从复制,实现了数据的热备份机制。这是一种在持久化手段之外的数据冗余方式,能够有效防止数据丢失,确保数据的安全性与可靠性。
  • 故障恢复:一旦主节点出现故障,从节点能够迅速接替其工作,提供服务。这一过程实际上是一种服务冗余策略,能够实现快速的故障恢复,保障系统的可用性。
  • 负载均衡:基于主从复制架构,配合读写分离策略,即写操作由主节点负责(应用在写入Redis数据时连接主节点),读操作由从节点承担(应用在读取Redis数据时连接从节点),可以有效分担服务器的负载压力。尤其是在写操作相对较少、读操作频繁的场景下,借助多个从节点分担读负载,能够显著提升Redis服务器的并发处理能力。
  • 高可用基石:主从复制不仅具备上述功能,更是哨兵和集群得以实现的基础。从这个层面来讲,主从复制是构建Redis高可用架构的核心要素。

21、Redis主从有几种常见的拓扑结构?

Redis的复制拓扑结构支持单层或多层的复制关系,依据拓扑的复杂程度,主要可分为以下三种类型:一主一从、一主多从、树状主从结构。

  • 一主一从结构:这是最为简单的复制拓扑结构形式,主要用于在主节点发生宕机时,从节点能够及时提供故障转移支持,维持系统的基本运行。
  • 一主多从结构:也被称为星形拓扑结构。在这种结构中,应用端可利用多个从节点实现读写分离。对于读操作占比较大的业务场景,将读命令发送至从节点执行,能够有效分担主节点的压力,提升系统整体性能。

  • 树状主从结构:又称树状拓扑结构。在此结构下,从节点不仅能够复制主节点的数据,还能够作为其他从节点的主节点,进一步向下层进行数据复制。通过引入这样的复制中间层,能够显著降低主节点的负载,同时减少需要传输给从节点的数据量,提升数据复制效率与系统扩展性。

22、Redis的主从复制原理了解吗?

Redis的主从复制原理具体如下:

  1. 保存主节点信息:从节点首先会保存主节点的相关信息,其中包括主节点的IP地址与端口号,为后续建立连接做好准备。
  2. 主从建立连接:从节点在发现新的主节点后,会尝试与主节点建立网络连接,以此搭建数据传输通道。
  3. 发送ping命令:连接建立成功后,从节点会向主节点发送ping请求,进行首次通信。此举主要用于检测主从节点之间的网络套接字是否正常可用,以及主节点当前是否能够正常接收和处理命令。
  4. 权限验证:若主节点设置了密码验证机制,从节点必须输入正确密码才能通过验证,从而保障数据传输的安全性与合法性。
  5. 同步数据集:当主从复制连接正常通信后,主节点会将自身持有的全部数据发送给从节点,使从节点获取与主节点一致的数据副本。
  6. 命令持续复制:完成初始数据同步后,主节点会持续将写命令发送给从节点,确保主从节点之间的数据始终保持一致,实现数据的实时更新。

23、主从数据同步的方式?

自Redis 2.8版本起,使用psync命令来完成主从数据同步,同步过程主要包含全量复制与部分复制两种方式:

  • 全量复制:通常应用于初次复制场景。在Redis早期版本中,仅支持全量复制这一种方式,它会将主节点的全部数据一次性传输给从节点。当数据量较大时,这种方式会给主从节点以及网络带来较大的开销。其完整运行流程如下:

1. 从节点发送`psync`命令进行数据同步。由于是首次复制,从节点没有复制偏移量以及主节点的运行ID,因此发送`psync - 1`。
2. 主节点接收到`psync - 1`后,解析得知当前为全量复制,随即回复`+FULLRESYNC`响应。
3. 从节点接收主节点的响应数据,并保存主节点的运行ID以及偏移量`offset`。
4. 主节点执行`bgsave`命令,将RDB文件保存至本地。
5. 主节点将RDB文件发送给从节点,从节点接收后将其保存在本地,并直接作为自身的数据文件。
6. 在从节点开始接收RDB快照直至接收完成期间,主节点依然会响应读写命令。主节点会将这期间产生的写命令数据存储在复制客户端缓冲区内。待从节点加载完RDB文件后,主节点再将缓冲区内的数据发送给从节点,以确保主从节点之间的数据一致性。
7. 从节点接收完主节点传送的全部数据后,会清空自身原有的旧数据。
8. 从节点清空数据后,开始加载RDB文件。
9. 若从节点开启了AOF持久化功能,在成功加载完RDB文件后,会立即执行`bgrewriteaof`操作,目的是确保全量复制后AOF持久化文件能够立即投入使用。
  • 部分复制:部分复制是Redis为解决全量复制开销过高问题而推出的优化措施,通过psync{runId}{offset}命令实现。当从节点复制主节点数据过程中,若出现网络闪断或命令丢失等异常情况,从节点会向主节点请求补发丢失的命令数据。若主节点的复制积压缓冲区内存在这部分数据,便会直接发送给从节点,以此维持主从节点复制的一致性。具体流程如下:
    1. 当主从节点之间的网络出现中断,若持续时间超过repl - timeout设定的时间,主节点会判定从节点故障,并中断复制连接。
    2. 在主从连接中断期间,主节点依然能够响应命令,但由于复制连接中断,命令无法发送给从节点。不过,主节点内部的复制积压缓冲区能够保存最近一段时间内的写命令数据,默认最大缓存为1MB。
    3. 当主从节点网络恢复后,从节点会重新连接主节点。
    4. 主从连接恢复后,从节点会将之前保存的自身已复制的偏移量以及主节点的运行ID作为psync参数发送给主节点,请求进行部分复制操作。
    5. 主节点接收到psync命令后,首先核对参数runId是否与自身一致。若一致,表明之前复制的是当前主节点;接着根据参数offset在自身的复制积压缓冲区中查找。若偏移量之后的数据存在于缓冲区中,则向从节点发送+CONTINUE响应,表明可以进行部分复制。
    6. 主节点依据偏移量,将复制积压缓冲区中的数据发送给从节点,使主从复制恢复至正常状态。

24、主从复制存在哪些问题呢?

主从复制虽然优势显著,但也存在一些不容忽视的问题:

  • 当主节点出现故障时,需要人工手动将一个从节点晋升为主节点,同时还得修改应用方的主节点地址,并通过命令让其他从节点去复制新的主节点,整个过程全程需要人工干预,缺乏自动化机制。
  • 主节点的写能力受限于单机性能,无法充分利用分布式环境下的多机资源来提升写操作的效率。
  • 主节点的存储能力同样受限于单机的物理存储空间,难以应对海量数据存储的需求。

上述第一个问题属于Redis的高可用范畴,而第二、三个问题则归属于Redis的分布式问题。

25、Redis为什么早期选择单线程?

官方在https://redis.io/topics/faq给出的解释是,由于Redis主要是基于内存的操作,在多数情况下,CPU并非Redis的性能瓶颈,内存大小或网络限制才更有可能成为瓶颈。如果希望充分利用CPU资源,可以在一台机器上启动多个Redis实例。 PS:网上有一种观点认为,官方的这个解释略显敷衍,实际上是历史原因所致,开发者觉得多线程实现较为麻烦,后来CPU的利用问题就留给了使用者去解决。 同时,FAQ里还提到,Redis 4.0之后开始引入多线程机制。除了主线程外,也有后台线程负责处理一些相对耗时的操作,比如清理脏数据、释放无用连接、删除大Key等。

26、Redis6.0使用多线程是怎么回事?

Redis6.0引入多线程机制,主要用于处理数据的读写和协议解析工作,但Redis执行命令依旧采用单线程模式。这样设计的原因在于,Redis的性能瓶颈主要在于网络IO,而非CPU运算。通过使用多线程来提升IO读写效率,进而全面提高Redis的整体性能。

27、Redis Sentinel(哨兵)了解吗

主从复制存在一个明显的缺陷,即无法自动完成故障转移。为了解决这一问题,Redis推出了Redis Sentinel(哨兵)方案。

Redis Sentinel由两部分构成,分别是哨兵节点和数据节点:

  • 哨兵节点:哨兵系统由一个或多个哨兵节点组成,这些哨兵节点是特殊的Redis节点,它们不存储数据,主要职责是对数据节点进行实时监控。
  • 数据节点:主节点和从节点都属于数据节点。

在主从复制的基础上,哨兵实现了自动化的故障恢复功能。官方对哨兵功能的描述如下:

  • 监控(Monitoring):哨兵会持续不断地检查主节点和从节点是否正常运行。
  • 自动故障转移(Automatic failover):当主节点出现异常无法正常工作时,哨兵会启动自动故障转移操作。它会从失效主节点的从节点中挑选一个,将其升级为新的主节点,并让其他从节点转而复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获取当前Redis服务的主节点地址。
  • 通知(Notification):哨兵能够将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,确保了哨兵能够及时察觉主节点故障并完成转移操作。而配置提供者和通知功能,则需要在与客户端的交互过程中得以体现。

28、Redis Sentinel(哨兵)实现原理知道吗?

哨兵模式通过哨兵节点实现对数据节点的监控、下线以及故障转移。

定时监控

Redis Sentinel借助三个定时监控任务来完成对各个节点的发现与监控:

  • 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令,以此获取最新的拓扑结构信息。
  • 每隔2秒,每个Sentinel节点会向Redis数据节点的sentinel:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点自身的信息。
  • 每隔1秒,每个Sentinel节点会向主节点、从节点以及其余Sentinel节点发送一条ping命令,进行心跳检测,以确认这些节点当前是否可达。

主观下线和客观下线

主观下线指的是某个哨兵节点自行判断某个节点存在问题;客观下线则是指超过一定数量的哨兵节点都认为主节点存在问题。

  1. 主观下线:每个Sentinel节点每隔1秒会对主节点、从节点以及其他Sentinel节点发送ping命令进行心跳检测。当这些节点在down - after - milliseconds时间内没有给出有效回复时,Sentinel节点就会判定该节点失败,这一行为称为主观下线。
  2. 客观下线:当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is - master - down - by - addr命令向其他Sentinel节点询问对主节点的判断。当认为主节点有问题的Sentinel节点数量超过个数时,该Sentinel节点会做出客观下线的决定。

领导者Sentinel节点选举

Sentinel节点之间会开展领导者选举工作,选出一个Sentinel节点作为领导者,负责后续的故障转移操作。Redis采用了Raft算法来实现领导者选举。

故障转移

由选举出的领导者Sentinel节点负责执行故障转移,具体过程如下:

  1. 从从节点列表中挑选出一个节点作为新的主节点,这一步相对复杂。
  2. Sentinel领导者节点会对第一步选出的从节点执行slaveof no one命令,使其转变为主节点。
  3. Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点。
  4. Sentinel节点集合会将原来的主节点更新为从节点,并持续关注它。当原来的主节点恢复后,会命令它去复制新的主节点。

29、领导者Sentinel节点选举了解吗?

Redis采用Raft算法来实现领导者选举,其大致流程如下:

  1. 每个在线的Sentinel节点都具备成为领导者的资格。当某个Sentinel节点确认主节点主观下线时,会向其他Sentinel节点发送sentinel is - master - down - by - addr命令,请求将自身设置为领导者。
  2. 收到该命令的Sentinel节点,如果此前没有同意过其他Sentinel节点的sentinel is - master - down - by - addr命令,就会同意此次请求;否则,予以拒绝。
  3. 如果某个Sentinel节点发现自己获得的票数大于或等于max(quorum, num(sentinels)/2 + 1),那么它将成为领导者。
  4. 若在本次选举过程中未能选出领导者,则会进入下一次选举。

30、新的主节点是怎样被挑选出来的?

  1. 过滤:排除那些“不健康”的从节点,包括处于主观下线状态、在5秒内未回复Sentinel节点ping响应,以及与主节点失联超过down - after - milliseconds * 10秒的从节点。
  2. 按优先级筛选:优先选择slave - priority(从节点优先级)最高的从节点列表。若存在这样的节点,则直接返回;若不存在,则继续下一步。
  3. 选择复制偏移量最大的节点:挑选复制偏移量最大的从节点,因为该节点的数据复制最为完整。若存在符合条件的节点,则返回;若不存在,则进行下一步。
  4. 选择runid最小的节点:最终选择runid最小的从节点作为新的主节点。

31、Redis 集群了解吗?

前面提到主从架构存在高可用和分布式方面的问题,哨兵解决了高可用问题,而集群则是能够一举解决高可用和分布式问题的终极方案。

  1. 数据分区:数据分区(也称数据分片)是集群的核心功能。集群将数据分散存储到多个节点,一方面突破了Redis单机内存大小的限制,大幅增加了存储容量;另一方面,每个主节点都能对外提供读服务和写服务,显著提高了集群的响应能力。
  2. 高可用:集群支持主从复制以及主节点的自动故障转移(与哨兵机制类似)。当集群中任一节点发生故障时,整个集群仍可对外提供服务,保障了系统的高可用性。

32、集群中数据如何分区?

在分布式存储系统中,需要依据分区规则将数据集映射到多个节点。常见的数据分区规则有以下三种:

  • 哈希取余分区

- **原理**:假设有一个包含2亿条`k,v`记录的数据集,单机无法满足存储需求,需采用分布式多机存储。若由3台机器构成集群,用户每次进行读写操作时,会依据公式`hash(key) % N`(`N`为机器台数)计算哈希值,以此决定数据映射到哪个节点。
- **优点**:该方法简单直接且有效。只需提前预估好数据量并规划好节点数量(如3台、8台、10台等),就能在一定时期内支撑数据存储。通过Hash算法可使特定部分的请求固定落到同一台服务器上,每台服务器处理部分请求并维护相关信息,实现了负载均衡和分而治之。
- **缺点**:若要对原规划好的节点进行扩容或缩容,操作较为麻烦。因为每次数据变动导致节点数量改变时,数据的映射关系都需重新计算。例如,原本的取模公式`Hash(key)/3`在节点数量变化后会变为`Hash(key) /?`,地址取余运算结果将大幅改变,根据公式获取的服务器也变得不可控。而且,若某个Redis机器宕机,由于机器台数变化,会致使哈希取余后的全部数据重新分配。
  • 一致性哈希算法分区

- **作用**:一致性哈希算法于1997年由麻省理工学院提出,旨在解决分布式缓存中数据变动和映射问题。因为当某个机器宕机,机器数量改变,传统取余方法便不再适用。其设计目标是在服务器个数发生变动时,尽量减少对客户端到服务器映射关系的影响。
- **步骤**:
    - **构建一致性哈希环**:一致性哈希算法通过特定的hash函数产生哈希值,所有可能的哈希值构成一个全量集,形成一个hash空间`[0,2^32 - 1]`。从逻辑上,将这个线性空间首尾相连(`0 = 2^32`),使其成为一个环形空间。该算法也是基于取模方法,与传统节点取模法不同的是,它是对`2^32`取模。简单来说,就是将整个哈希值空间构建成一个虚拟圆环。假设某哈希函数`H`的值空间为`0 - 2^32 - 1`(即哈希值是一个32位无符号整形),整个哈希环按顺时针方向组织,圆环正上方的点代表0,右侧第一个点代表1,依此类推,直到`2^32 - 1`,且`0`点左侧第一个点代表`2^32 - 1`,`0`和`2^32 - 1`在零点方向重合。

      ![](https://study-cdn.disign.me/article/202501/assets/60_b1e3cd5bd2559bd7c5842134a9451f27.webp)

    - **服务器IP节点映射**:将集群中各个IP节点映射到环上的特定位置。通过对服务器的IP或主机名进行哈希计算,确定每台机器在哈希环上的位置。例如,假设有4个节点`NodeA`、`B`、`C`、`D`,经过对IP地址的哈希函数`hash(ip)`计算后,在环空间上的位置得以确定。
    - **key落到服务器的落键规则**:当存储一个`kv`键值对时,先计算`key`的哈希值`hash(key)`,确定此数据在环上的位置,然后沿环顺时针方向寻找,遇到的第一台服务器就是该数据应定位到的服务器,并将键值对存储在该节点上。例如,有`Object A`、`Object B`、`Object C`、`Object D`四个数据对象,经哈希计算后在环空间上有各自位置,根据一致性Hash算法,数据`A`会被定位到`Node A`上,`B`被定位到`Node B`上,`C`被定位到`Node C`上,`D`被定位到`Node D`上。

      ![](https://study-cdn.disign.me/article/202501/assets/60_521630b323ef58a43450f569fe12a79f.webp)

- **优点**:
    - **容错性**:假设`Node C`宕机,此时对象`A`、`B`、`D`不受影响,只有`C`对象被重定位到`Node D`。一般而言,在一致性Hash算法中,若一台服务器不可用,受影响的数据仅为此服务器到其环空间中前一台服务器(即沿逆时针方向行走遇到的第一台服务器)之间的数据,其他数据不受影响。简单说,就是`C`挂了,受影响的只是`B`、`C`之间的数据,且这些数据会转移到`D`进行存储。

      ![](https://study-cdn.disign.me/article/202501/assets/60_155426a10e9d196b32a37b6e1cc0ce35.webp)

    - **扩展性**:当数据量增加需要添加一台节点`NodeX`,且`X`的位置在`A`和`B`之间时,受影响的仅是`A`到`X`之间的数据。只需将`A`到`X`的数据重新录入到`X`上即可,不会像哈希取余那样导致全部数据重新分配。

      ![](https://study-cdn.disign.me/article/202501/assets/60_e3b3f6aa7db8e2a48638e82f4c13b7f0.webp)

- **缺点**:一致性哈希算法在服务节点较少时,容易因节点分布不均匀而出现数据倾斜问题,即被缓存的对象大部分集中缓存在某一台服务器上。例如,当系统中只有两台服务器时,可能会出现这种情况。

  ![](https://study-cdn.disign.me/article/202501/assets/60_80acabfd7a6ef49d23fd41fe32e180fe.webp)

- **总结**:一致性哈希算法为了在节点数目发生改变时尽可能少地迁移数据,将所有存储节点排列在收尾相接的Hash环上,每个`key`在计算Hash后会沿顺时针方向找到临近的存储节点存放。当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点。其优点是加入和删除节点只影响哈希环中顺时针方向的相邻节点,对其他节点无影响;缺点是数据的分布与节点位置有关,由于节点并非均匀分布在哈希环上,所以数据存储时难以达到均匀分布的效果。
  • 哈希槽分区

    • 原理:哈希槽本质上是一个数组,数组[0,2^14 - 1]形成hash slot空间。其作用是解决一致性哈希算法的数据倾斜问题。为实现数据的均匀分配,在数据和节点之间增加了一层,即哈希槽(slot),用于管理数据和节点的关系。此时,节点上放置的是槽,槽里存放数据。槽解决的是粒度问题,相当于增大了粒度,便于数据移动;哈希解决的是映射问题,通过计算key的哈希值来确定所在的槽,便于数据分配。
    • 哈希槽的计算:Redis集群内置了16384个哈希槽,Redis会根据节点数量大致均匀地将哈希槽映射到不同节点。当在Redis集群中存储一个key - value时,Redis先对key使用crc16算法算出结果,然后将结果对16384求余数,这样每个key都会对应一个编号在0 - 16383之间的哈希槽,也就是映射到某个节点上。例如,key之AB映射到Node2key之C映射到Node3

33、什么是缓存击穿、缓存穿透、缓存雪崩?

  • 缓存穿透:当请求的key对应的数据在数据源中不存在时,每次针对该key的请求都无法从缓存获取数据,进而请求会直接到达数据源。这种情况可能导致数据源负载过高甚至被压垮。例如,使用一个不存在的用户id去获取用户信息,缓存和数据库中均无此数据。若黑客利用这一漏洞进行攻击,可能会致使数据库崩溃。
  • 缓存击穿key对应的数据原本存在,但在Redis中缓存过期。此时若大量并发请求同时到来,这些请求发现缓存过期后,通常会从后端数据库加载数据并重新设置到缓存。在这种情况下,大并发的请求可能瞬间使后端数据库承受巨大压力,甚至导致数据库崩溃。
  • 缓存雪崩:指在某一时刻,发生大规模的缓存失效现象。例如,缓存服务宕机、缓存服务器重启,或者大量缓存集中在某一个时间段内过期。当缓存失效时,会给后端系统(如数据库)带来极大的压力。

缓存击穿解决方案

  • 加锁更新:例如,当请求查询数据A时,若发现缓存中没有该数据,对A这个key加锁,同时去数据库查询数据,将查询结果写入缓存后,再返回给用户。如此一来,后续的请求便可从缓存中获取数据。

  • 异步刷新过期时间:将过期时间组合写在value中,通过异步方式不断刷新过期时间,以此防止缓存击穿现象的发生。

    • 互斥锁:互斥的概念是,不同线程竞争进入临界区(共享的数据和硬件资源)。为防止访问冲突,在有限时间内只允许其中一个线程独占共享资源,比如不允许同时进行写操作。具体流程如下:

    1. 线程1发起请求,查询缓存未命中,随后获取互斥锁。获取成功后,去查询数据库重建缓存数据,写入缓存并释放锁。
    2. 线程2在线程1未释放锁之前发起请求,查询缓存未命中,尝试获取互斥锁。由于互斥锁已被线程1占用,获取失败,线程2休眠一段时间后重新尝试获取锁(直至线程1释放锁),最终缓存命中。
    
    • 逻辑过期:逻辑过期是指,原本在Redis中存储数据时,存储的是k:v键值对。采用逻辑过期方式时,需手动为value增加一个expire时间,例如:{"name":"Jack", "age":21, "expire":152141223}。具体流程如下:

    1. 线程1发起请求,查询缓存发现逻辑时间已过期,获取互斥锁。同时,线程1开启一个新线程2(用于查询数据存入缓存),先返回过期的数据。
    2. 线程2查询数据库后,重建缓存数据,写入缓存并重置逻辑过期时间,最后释放锁。
    3. 线程3发起请求(与线程1同步),查询缓存发现逻辑时间已过期,获取互斥锁失败,先返回旧数据。
    4. 线程4查询缓存,此时线程2已释放锁,缓存命中且逻辑过期时间未过期,直接返回数据。
    
    • 比较
解决方案 优点 缺点
互斥锁(一致性) 无额外内存消耗、保证一致性、实现简单 线程需等待,性能受影响,可能存在死锁风险
逻辑过期(性能) 线程无需等待,性能较好 不保证一致性、有额外内存消耗、实现复杂

缓存穿透解决方案

  • 缓存空对象:当客户端请求到达Redis后,若未命中缓存,去查询数据库。若数据库查询结果为null,则将null缓存起来。

    • 优点:实现简单,维护方便。
    • 缺点
      • 额外内存消耗:若客户端请求大量不存在的数据,Redis会缓存大量null数据。
      • 可能造成短期不一致:客户端请求不存在的数据后,Redis缓存null数据并设置了超时时间。若此时新增了一条数据,在缓存null数据的TTL时间内再次查询,结果仍为null,只有当缓存过期后,才能查询到新增数据。不过,可在新增数据时同步更新缓存,解决短期不一致问题。
  • 布隆过滤器:布隆过滤器是一个bit向量或bit数组(长度必须足够长)。其原理是将所有可能存在的数据哈希到一个足够大的bitmap中,这样一个一定不存在的数据会被bitmap拦截,从而避免对底层存储系统产生查询压力。

    • 优点:内存占用较少,不会产生多余的key
    • 缺点:实现复杂,存在误判的可能性。

解决缓存穿透 适用场景 维护成本
缓存空对象 数据命中不高;数据频繁且实时性高 代码维护简单;需要较多缓存空间;存在数据不一致问题
布隆过滤器 数据命中不高;数据相对固定且实时性低 代码维护复杂;缓存空间占用少

缓存雪崩解决方案

  • 提高缓存可用性
    1. 集群部署:通过集群方式提升缓存可用性,可利用Redis自身的Redis Cluster或第三方集群方案(如Codis等)。
    2. 多级缓存:设置多级缓存,在第一级缓存失效时,访问二级缓存。各级缓存的失效时间各不相同。
  • 优化过期时间
    1. 均匀过期:为避免大量缓存同一时间过期,可将不同key的过期时间随机生成,防止过期时间过于集中。
    2. 热点数据永不过期:对于热点数据,设置永不过期策略。
  • 熔断降级
    1. 服务熔断:当缓存服务器宕机或超时响应时,为防止整个系统出现雪崩,暂时停止业务服务对缓存系统的访问。
    2. 服务降级:当大量缓存失效且处于高并发、高负荷情况时,在业务系统内部暂时舍弃对一些非核心接口和数据的请求,直接返回预先准备好的错误处理信息。
  • 添加多级缓存:在业务中添加多级缓存机制。

34、如何保证缓存和数据库数据的⼀致性?

根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性。因此,缓存和数据库的绝对一致无法实现,只能尽可能保证它们的最终一致性。

选择合适的缓存更新策略

  • 删除缓存而非更新缓存:当一个线程对缓存的key进行写操作时,如果其他线程同时进来读取数据库,可能会读到脏数据,从而产生数据不一致问题。相比之下,删除缓存的速度更快,耗时更短,读脏数据的概率也更低。
  • 先更新数据,后删除缓存:先更新数据库还是先删除缓存,这是个关键问题。更新数据的耗时可能是删除缓存的百倍以上。当缓存中不存在对应的key,且数据库尚未完成更新时,如果有线程进来读取数据并写入缓存,那么在数据库更新成功后,该key对应的就是脏数据。显然,先删除缓存再更新数据库,会使缓存中key不存在的时间更长,产生脏数据的概率更大。目前最流行的缓存读写策略cache - aside - pattern采用的是先更新数据库,再删除缓存的方式。

缓存不一致处理

如果并发程度不高且对缓存依赖性不强,一定程度的不一致是可以接受的。但如果对一致性要求较高,就必须采取措施保证缓存和数据库中数据一致。

缓存和数据库数据不一致常见的原因有两个:

  • 缓存key删除失败
  • 并发导致写入脏数据

解决方案

  • 消息队列保证key被删除:可以引入消息队列,将需要删除的key或删除失败的key放入消息队列。利用消息队列的重试机制,重试删除对应的key。该方案的缺点是对业务代码有一定侵入性。

  • 数据库订阅 + 消息队列保证key被删除:可以使用一个服务(如阿里的canal)监听数据库的binlog,获取需要操作的数据。然后通过一个公共服务获取订阅程序传来的信息,进行缓存删除操作。这种方式降低了对业务的侵入性,但系统复杂度有所提升,更适合基建完善的大型企业。

  • 延时双删防止脏数据:还有一种情况是在缓存不存在时写入了脏数据,这种情况在先删除缓存再更新数据库的缓存更新策略下较为常见。解决方案是延时双删,即在第一次删除缓存之后,经过一段时间再次删除缓存。不过,这种方式的延时时间设置需要仔细考量和测试。

  • 设置缓存过期时间兜底:这是一种简单但有效的方法,给缓存设置一个合理的过期时间。即使出现缓存数据不一致问题,也不会永远不一致,缓存过期时自然会恢复一致。

35、缓存预热怎么做呢?

缓存预热,即提前将数据库中的数据加载到缓存中,常见方法如下:

  1. 手动操作:编写一个缓存刷新页面或接口,在上线时手动执行,以此将数据刷入缓存。
  2. 项目启动加载:若数据量不大,可在项目启动阶段自动加载数据到缓存,确保缓存中预先存有数据。
  3. 定时任务刷新:通过设置定时任务,定期刷新缓存,保证缓存数据的时效性。

36、热点key重建?问题?解决?

在开发中,常采用“缓存 + 过期时间”的策略,此策略既能加快数据读写速度,又能确保数据定期更新,基本可满足大多数场景需求。但当以下两个问题同时出现时,可能引发较大问题:

  • 热点key高并发:当前key是热点key(例如热门娱乐新闻相关key),会承受非常高的并发访问量。
  • 重建缓存耗时久:重建缓存的操作无法在短时间内完成,这可能涉及复杂计算,如复杂的SQL查询、多次IO操作或依赖多个其他服务。在缓存失效瞬间,大量线程同时尝试重建缓存,会导致后端负载急剧增加,甚至可能使应用崩溃。

怎么处理呢?

解决该问题的关键要点在于:

  • 减少重建次数:尽量降低重建缓存的操作频率。
  • 保障数据一致:确保缓存数据与数据库数据尽可能保持一致。
  • 降低潜在风险:减少因缓存重建带来的潜在风险。

通常采用以下方式:

  1. 互斥锁(mutex key):利用互斥锁机制,仅允许一个线程进行缓存重建操作。其他线程等待该线程重建完成后,再从缓存中获取数据。
  2. 永远不过期:“永远不过期”包含两层含义:
    • 缓存层面:从缓存设置角度,不设置过期时间,避免热点key过期引发的问题,即实现“物理”上不过期。
    • 功能层面:为每个value设置逻辑过期时间。一旦发现超过逻辑过期时间,启动单独线程进行缓存构建。

37、Redis报内存不足怎么处理?

当Redis出现内存不足的情况时,可采用以下几种处理方式:

  • 调整配置参数:修改Redis配置文件redis.conf中的maxmemory参数,增加Redis的可用内存。
  • 动态设置内存上限:也可通过命令set maxmemory动态设置内存上限,灵活调整Redis的内存使用。
  • 优化内存淘汰策略:修改内存淘汰策略,使Redis能更及时地释放内存空间,以满足新数据的存储需求。
  • 采用集群模式扩容:使用Redis集群模式,通过横向扩展节点,增加Redis整体的内存容量。

38、大key问题了解吗?

在Redis使用过程中,有时会出现大key情况,具体表现为:

  • value的简单key:单个简单key存储的value过大,其大小超过10KB。
  • 集合类型元素过多:在hashsetzsetlist等集合类型中存储了过多元素(以万为单位)。

大key会造成什么问题呢?

  • 客户端响应延迟:导致客户端读取数据时耗时增加,甚至可能出现超时情况。
  • 资源占用严重:对大key进行IO操作时,会大量占用带宽和CPU资源,影响Redis整体性能。
  • 集群数据倾斜:在Redis集群中,大key可能导致数据分布不均衡,即数据倾斜问题。
  • 阻塞风险:无论是主动删除大key,还是被动删除(如因内存淘汰策略触发的删除),都可能引发Redis阻塞。

如何找到大key?

  • 使用bigkeys命令:借助bigkeys命令,以遍历方式分析Redis实例中的所有Key,并返回整体统计信息以及每个数据类型中排名第一的大Key
  • 利用redis-rdb-toolsredis - rdb - tools是由Python编写的工具,用于分析Redis的rdb快照文件。它能将rdb快照文件转换为json文件或生成报表,便于深入分析Redis的使用详情,从而找出大key

如何处理大key?

  • 删除大key
    • Redis 4.0及以上版本:可使用UNLINK命令安全删除大Key。该命令以非阻塞方式逐步清理传入的Key,避免阻塞Redis服务。
    • Redis 4.0以下版本:避免使用阻塞式的KEYS命令。建议通过SCAN命令执行增量迭代扫描key,判断后再进行删除操作。
  • 压缩和拆分key
    • string类型value的处理:若valuestring类型且较难拆分,可采用序列化、压缩算法,将key大小控制在合理范围,但序列化和反序列化操作会增加时间开销。若压缩后仍为大key,则需进行拆分,将一个大key拆分为不同部分,并记录每个部分的key,通过multiget等操作实现事务读取。
    • 集合类型value的处理:当valuelist/set等集合类型时,根据预估的数据规模进行分片,通过计算将不同元素分配到不同的分片。

39、Redis常见性能问题和解决方案?

  1. Master持久化策略:Master节点最好不要进行任何持久化操作,包括内存快照和AOF日志文件的生成。尤其要避免启用内存快照进行持久化,因为这可能会对Master的性能产生较大影响。
  2. Slave数据备份:若数据较为关键,可在某个Slave节点开启AOF备份数据,建议采用每秒同步一次的策略,既能保障数据安全性,又可在一定程度上减少性能损耗。
  3. 主从网络配置:为提升主从复制的速度及连接稳定性,Slave和Master最好部署在同一个局域网内,以减少网络延迟和丢包等问题。
  4. 主库压力控制:尽量避免在负载压力较大的主库上添加从库。因为添加从库可能会导致主库在数据同步过程中,进一步增加CPU和网络等资源的消耗,从而影响整体性能。
  5. AOF重写影响:Master在调用BGREWRITEAOF命令重写AOF文件时,会占用大量的CPU和内存资源。这可能致使服务的load值过高,出现短暂的服务暂停现象,影响业务的连续性。
  6. 主从结构优化:为确保Master的稳定性,主从复制不宜采用图状结构,使用单向链表结构更为稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3… 。这种结构不仅便于解决单点故障问题,实现Slave对Master的替换,比如当Master出现故障时,可立即启用Slave1作为新的Master,且其他节点的连接关系无需变动。

40、使用Redis 如何实现异步队列?

  • 基于list的基本队列实现
    • lpush生产消息,rpop消费消息:采用list作为队列,通过lpush命令生产消息,rpop命令消费消息。消费者使用死循环执行rpop从队列中获取消息。然而,即便队列中没有消息,也会持续执行rpop操作,这会导致Redis CPU资源的浪费。虽然可以通过让消费者休眠的方式来处理,但这又会带来消息延迟的问题。
    • lpush生产消息,brpop消费消息brpoprpop的阻塞版本,当list为空时,它会一直保持阻塞状态,直至list中有新值或者达到超时时间。不过,这种方式仅能实现一对一的消息队列。
  • 基于pub/sub的发布/订阅模式:使用Redis的pub/sub功能进行消息的发布与订阅。发布者将消息发布到指定的频道(channel),订阅了相应频道的客户端都能接收到消息,从而实现1:N的消息发布/订阅。但该模式并非可靠的消息队列解决方案,它既不能保证订阅者一定能收到消息,也不会对消息进行存储。因此,在实际应用中,一般的异步队列通常还是交由专业的消息队列来实现。

41、Redis 如何实现延时队列?

利用zset的排序特性:可以借助zset数据结构,将设置好的时间戳作为score进行排序。通过zadd score1 value1 ....命令不断向内存中生产消息。然后利用zrangebysocre命令查询符合条件的所有待处理任务,通过循环执行队列任务,从而实现延时队列的功能。

42、Redis和Lua脚本的使用了解吗?

Redis自身的事务功能相对简单,在日常开发中,可利用Lua脚本来扩展Redis的命令功能。

Lua脚本在Redis开发与运维中具有诸多优势:

  • 原子执行:Lua脚本在Redis中以原子方式执行,在执行过程中不会被其他命令打断,确保了操作的原子性和数据的一致性。
  • 定制与复用:开发和运维人员可利用Lua脚本创建自定义命令,并将这些命令常驻于Redis内存中,实现复用,提高开发效率和代码的可维护性。
  • 减少网络开销:Lua脚本能够将多条命令打包一次性执行,有效减少了网络请求次数,降低了网络开销,提升了系统性能。

例如,以下是一段经典的用于秒杀系统中利用lua扣减Redis库存的脚本:

    -- 库存未预热
    if (redis.call('exists', KEYS[2]) == 1) then
         return -9;
     end;
     -- 秒杀商品库存存在
     if (redis.call('exists', KEYS[1]) == 1) then
         local stock = tonumber(redis.call('get', KEYS[1]));
         local num = tonumber(ARGV[1]);
         -- 剩余库存少于请求数量
         if (stock < num) then
             return -3
         end;
         -- 扣减库存
         if (stock >= num) then
             redis.call('incrby', KEYS[1], 0 - num);
             -- 扣减成功
             return 1
         end;
         return -2;
     end;
     -- 秒杀商品库存不存在
     return -1;

43、Redis回收进程如何工作的?

  1. 新命令执行与数据添加:客户端执行新的命令,向Redis中添加新的数据。
  2. 内存使用检查与回收:Redis实时检查内存使用情况,当内存使用量超过maxmemory设定的限制时,会依据预先设定好的内存回收策略,对数据进行回收操作,以释放内存空间。
  3. 持续的命令执行与内存管理:随着新的命令不断被执行,Redis持续监控内存使用情况,不断重复上述内存检查与回收的过程。即不断地在内存使用量达到限制边界时,通过回收操作使内存使用量回到边界以下。

如果某个命令的执行结果导致大量内存被占用(例如对很大的集合进行交集运算,并将结果保存到一个新的键),那么很快内存使用量就会超过限制边界,触发回收机制。

44、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

可以使用keys指令来扫描出符合指定模式的key列表。但需要注意的是,如果该Redis实例正在为线上业务提供服务,使用keys指令会带来问题。因为Redis是单线程的,keys指令的执行会导致线程阻塞一段时间。在这段时间内,线上服务会停顿,直到指令执行完毕,服务才能恢复正常运行。此时,更合适的做法是使用scan指令。scan指令能够无阻塞地提取出符合指定模式的key列表,不过该指令可能会返回一定数量的重复结果。在客户端对结果进行一次去重操作即可解决该问题。虽然整体操作所花费的时间会比直接使用keys指令长,但不会造成服务中断,更适用于线上业务场景。

总结

Redis在实际应用中存在多种性能问题及相应解决方案,Master节点应避免持久化操作以保障性能,关键数据可在Slave节点采用每秒同步一次的AOF备份策略,同时主从节点宜部署在同一局域网且避免在高负载主库添加从库,还应优化主从结构。在异步队列实现上,基于list的不同操作方式各有优劣,pub/sub模式虽能实现1:N消息发布但不可靠,通常异步队列由专业消息队列完成;延时队列可借助zset结构利用时间戳排序实现。开发中Lua脚本可增强Redis命令,具备原子执行、可定制复用及减少网络开销的优点。Redis回收进程会在内存超限时依策略回收。对于大量key的查找,线上服务场景下scan指令优于keys指令,虽耗时较长但不会阻塞服务。