1. 数据类型
常用的Redis数据类型有5种,分别是:
- String、List、Set、SortedSet、Hash
还有一些高级数据类型,比如Bitmap、HyperLogLog、GEO等,其底层都是基于上述5种基本数据类型。因此在Redis的源码中,其实只有5种数据类型。
不管是任何一种数据类型,最终都会封装为RedisObject格式,它是一种结构体,C语言中的一种结构,可以理解为Java中的类。
2. 持久化方案
2.1 Redis的持久化方案有哪些?
Redis 提供了两种主要的持久化方案来确保数据的安全性和稳定性:
-
RDB(Redis Database)持久化:RDB 是通过创建数据的快照来进行持久化的。在指定的时间点,Redis 会将内存中的数据集快照保存到磁盘上的一个二进制文件中。这种方式适用于备份和灾难恢复,因为它能够提供某一时间点的数据快照。RDB 持久化可以是自动触发的,也可以是手动触发的。自动触发可以通过配置文件中的
save
选项设置,而手动触发可以通过save
或bgsave
命令实现。bgsave
命令会创建一个子进程来处理快照,而不会阻塞主进程。 -
AOF(Append Only File)持久化:AOF 持久化记录了服务器接收到的每一个写操作命令,并将这些命令追加到文件的末尾。在 Redis 重启时,它会重放这些命令来重建原始数据。这种方式提供了更好的持久性保证,因为它记录了所有的写操作。AOF 文件的持久化可以通过
appendfsync
选项配置,该选项可以设置为always
(每次写操作后都执行 fsync),everysec
(每秒执行一次 fsync),或者no
(从不主动 fsync,由操作系统决定何时同步)。 -
Redis 4.0 引入了 RDB-AOF 混合持久化 方案,结合了 RDB 和 AOF 的优点。在这种模式下,AOF 文件的开始部分包含了 RDB 快照的数据,之后是 RDB 快照创建之后的所有写命令。这样可以在保证数据安全性的同时,加快数据恢复的速度。根据不同的需求和场景,可以选择合适的持久化方案或者将两者结合使用,以达到数据持久化的目的。
2.2 每种持久化方式的原理是分别是什么?
每种Redis持久化方式的原理如下:
2.2.1 RDB持久化原理
- 快照保存(Snapshotting):RDB持久化是通过周期性地将内存中的数据集快照保存到磁盘上的一个二进制文件来实现的。这个二进制文件包含了一个特定时间点的数据库状态。
- 触发方式:RDB持久化可以通过时间条件自动触发,也可以手动触发。自动触发是通过在配置文件中设置特定时间间隔和数据变化次数来实现的。例如,可以配置“若10分钟内至少有1个键被改变,则进行快照保存”。
- 子进程创建:为了避免阻塞主进程,Redis使用
fork()
系统调用创建一个子进程来处理快照的创建。父进程继续处理命令请求,而子进程则负责将内存数据写入磁盘。 - 压缩和替换:生成的RDB文件是压缩过的,以减少磁盘占用和加快加载速度。一旦子进程完成快照文件的写入,新文件会替换掉旧的快照文件。
- 恢复:当Redis重启时,它会加载RDB文件,将数据恢复到内存中。
2.2.2 AOF持久化原理
-
命令追加(Append-only):AOF持久化记录了服务器接收到的每一个可能改变数据库状态的写操作命令,并将这些命令追加到文件的末尾。
-
命令日志:这些命令是文本格式的,可以被人类阅读,并且可以在需要时手动执行。
-
写回策略:AOF持久化通常配置为每秒一次将操作从缓冲区写回磁盘(
fsync
操作)。这可以减少数据丢失的风险,同时保持较好的性能。 -
日志重放:在Redis重启时,它会重放AOF日志中的命令,以此来恢复数据到内存中。
-
日志压缩(Rewrite):随着时间的推移,AOF文件可能会变得非常大。为了优化性能,Redis提供了AOF重写功能,它会创建一个新的AOF文件,其中只包含恢复当前数据集所需的最小命令集合。
2.2.3 RDB-AOF混合持久化原理
- 结合两者:混合持久化结合了RDB和AOF两种持久化方式的优点。
- RDB快照开头:在AOF文件的开始部分,存储了RDB快照数据,这代表了数据库的一个完整状态。
- AOF日志追加:紧随RDB数据之后,存储了RDB快照创建之后的所有写操作命令。
- 恢复过程:在Redis重启时,它会优先使用AOF文件进行数据恢复,因为AOF文件包含了自快照以来的所有命令,可以提供更完整的数据恢复。
- 效率提升:由于AOF文件的前半部分是RDB数据,这使得恢复过程可以很快开始,而不需要等待完整的AOF日志重放。
2.3 分别有什么优缺点?
2.3.1 RDB持久化
优点:
- 适合做备份:RDB是数据的快照,可以作为冷备份使用。
- 恢复速度快:因为是全量数据,所以恢复时间比AOF快。
- 对性能影响小:创建快照时主进程不需要做磁盘IO。
缺点:
- 数据安全性较低:只记录了快照的数据,快照间隔内的数据可能会丢失。
- 占用磁盘空间:RDB文件可能会比AOF大,尤其是数据量大时。
2.3.2 AOF持久化
优点:
- 数据安全性高:记录了所有写操作,最多丢失1秒的数据。
- 适合灾难恢复:AOF文件是追加写入的,适合用于数据恢复。
- 可以配置不同的fsync策略:可以平衡数据安全性和性能。
缺点:
- 启动速度慢:需要重放所有命令,尤其是日志文件大时。
- 占用磁盘空间:AOF文件可能会比RDB持久化大。
2.3.3 RDB-AOF混合持久化
优点:
- 结合了RDB和AOF的优点:既保证了数据安全性,又提高了数据恢复效率。
- 启动速度快:利用RDB快照快速启动,然后应用AOF增量日志。
缺点:
- 配置复杂:需要同时管理RDB和AOF的配置。
- 兼容性问题:较新的Redis特性,需要Redis 4.0以上版本。
2.3 如果服务器宕机了数据怎么恢复?
RDB持久化: 如果配置了RDB持久化,在宕机前生成的最后一次数据快照将被用来恢复数据。Redis重启时,可以自动加载这个快照文件,恢复到宕机前的数据状态。
AOF持久化: 如果配置了AOF持久化,可以通过重放AOF日志文件中的所有写操作命令来恢复数据。Redis提供了appendonly yes配置项来启用AOF持久化,并在重启时自动进行数据恢复。
RDB-AOF混合持久化:Redis 4.0及以上版本支持RDB和AOF的混合持久化。在这种配置下,AOF文件的开始部分包含了RDB快照的数据,之后是RDB快照创建之后的所有写操作命令。Redis重启时,会优先使用AOF文件进行数据恢复,因为它包含了自快照以来的所有命令,可以提供更完整的数据恢复。
恢复步骤
- 确保持久化文件未损坏:在尝试恢复之前,确认RDB快照文件或AOF日志文件没有损坏。
- 启动Redis服务器:通常,Redis在启动时会自动检测到持久化文件,并根据配置的持久化方式进行数据恢复。
- 手动恢复:如果Redis没有自动恢复,或者需要在不重启Redis的情况下恢复数据,可以手动执行恢复操作。对于RDB,这通常意味着将快照文件放到Redis的数据目录下;对于AOF,可以使用redis-cli --appendonly yes命令加载AOF文件。
- 验证数据:恢复完成后,应该检查数据是否已经正确恢复。
3. 高可用方案
3.1.Redis有哪些高可用方案?
3.1.1 主从复制
- 主节点负责写操作,从节点负责读操作,实现读写分离,提高性能。
- 支持数据的自动同步,当主节点数据发生变化时,会自动同步到从节点。
- 在主节点故障时,需要手动或通过哨兵机制进行故障转移。
工作原理
:从节点启动后,会向主节点发送SLAVEOF命令,请求数据同步。主节点接收到请求,会执行一个RDB快照,并将快照发送给从节点。从节点接收并应用RDB快照后,主节点继续将新的写入命令发送给从节点,从节点应用这些命令以保持数据的最终一致性。
3.1.2 哨兵模式(Sentinel)
- 哨兵系统可以监控主节点的状态,并且在主节点发生故障时自动进行故障转移。
- 能够将一个从节点提升为新的主节点,并将其他从节点重新配置为新主节点的从节点。
- 增加了系统的可用性,但仍然存在数据同步的问题。
工作原理
:在主从模式的基础上添加了一个哨兵节点,用于监控主节点状态,当主节点故障时,自动进行故障转移,将一个从节点提升为新的主节点,并更新其他节点的配置。
3.1.2.1 哨兵是如何监控主节点并实现故障转移的?
Redis哨兵(Sentinel)是一种用于监控Redis主节点和从节点运行状况的系统。它主要负责两个任务:监控主节点的状态以预防故障,以及在检测到故障时自动进行故障转移。以下是哨兵监控主节点状态并实现故障转移的基本过程:
- 监控: 哨兵系统会定期地检查主节点和从节点是否运行正常。这通过向这些节点发送
PING
命令并等待响应来实现。 - 主观下线:如果哨兵发现主节点没有响应,它会认为主节点进入了“主观下线”状态,即哨兵个体认为主节点已经下线。
- 客观下线:为了确认故障不是由于本机的问题导致的误判,哨兵会询问其他哨兵节点对主节点的观察情况。如果大多数哨兵都认为主节点没有响应,那么它们会达成共识,将主节点标记为“客观下线”。
- 故障转移:一旦主节点被确认为客观下线,哨兵会开始故障转移过程。它会从当前的从节点中选举出一个来作为新的主节点。选举过程通常考虑从节点的优先级和复制的进度。
- 数据迁移:选中新的主节点后,哨兵会将这个从节点提升为新的主节点,并且更新其他从节点的配置,让它们开始复制新的主节点。
- 配置传播:故障转移完成后,哨兵会将新的主节点信息传播给其他哨兵节点,并且通知客户端新的主节点地址。
- 旧主节点恢复:如果旧的主节点重新上线,它会被哨兵配置为新的主节点的从节点。
哨兵系统通过上述机制实现了对Redis主节点的高可用性支持。在哨兵的帮助下,即使主节点发生故障,Redis集群也能继续提供服务,而不需要人工干预故障转移的过程。
3.1.3 Redis Cluster
- 通过数据分片(sharding)实现高可用性,将所有的数据分布在多个节点上。
- 每个节点负责存储一部分数据(一个或多个哈希槽)。
- 当一个节点故障时,其他节点可以继续处理命令请求,并且可以在不停止服务的情况下,自动将故障节点的数据迁移到其他节点。
A. 故障转移
分片集群的节点之间会互相通过ping的方式做心跳检测,超时未回应的节点会被标记为下线状态。当发现master下线时,会将这个master的某个slave提升为master。
B. Redis分片集群如何判断某个key应该在哪个实例?
- Redis分片集群共有16384个插槽,集群会将16384个插槽分配到不同的实例上。
- 根据key计算哈希值,对16384取余。
- 余数作为插槽,寻找插槽所在实例即可。
4. 内存回收
4.0 Redis有哪些内存淘汰策略?
Redis 提供了多种数据淘汰策略,用于在内存不足时决定哪些数据应该被移除。以下是 Redis 支持的数据淘汰策略:
-
volatile-lru:
- 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
-
allkeys-lru:
- 从所有数据集中挑选最近最少使用的数据淘汰,不考虑数据的过期时间。
-
volatile-random:
- 从已设置过期时间的数据集中随机挑选数据淘汰。
-
allkeys-random:
- 从所有数据集中随机挑选数据淘汰,不考虑数据的过期时间。
-
volatile-ttl:
- 从已设置过期时间的数据集中挑选将要过期的数据淘汰。
-
noeviction:
- 不淘汰任何数据。当内存限制达到时,写入操作将被拒绝。
每种淘汰策略都有其适用场景。例如,volatile-lru
和 volatile-random
主要用于希望保留较活跃数据的场景,而 allkeys-lru
和 allkeys-random
则不考虑数据的过期时间,适用于需要平等对待所有数据的情况。volatile-ttl
适用于希望尽快回收即将过期数据的场景。noeviction
策略通常用于对数据完整性要求极高的场景,但需要注意,如果持续写入,内存可能会被耗尽。
在配置 Redis 时,可以根据业务需求选择合适的数据淘汰策略。通过在配置文件中设置 maxmemory-policy
参数,可以指定 Redis 使用哪种数据淘汰策略。
4.1 Redis如何判断KEY是否过期?
在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。
4.2 Redis是何时删除过期KEY的?
Redis并不会在KEY过期时立刻删除KEY,因为要实现这样的效果就必须给每一个过期的KEY设置时钟,并监控这些KEY的过期状态。无论对CPU还是内存都会带来极大的负担。
Redis 清理过期键(KEY)主要发生在以下几种情况:
过期检查:Redis 会周期性地检查键的过期时间,这通常由一个定时任务执行,该任务的运行频率可以通过配置文件中的 hz 参数设置。
惰性删除:当对一个键执行操作时(如获取键的值),如果该键已经设置了过期时间,Redis 会首先检查该键是否已经过期。如果是,Redis 会先删除该键,然后再执行相应的操作。
定时清理:Redis 通过一个后台进程周期性地清理过期键。这个清理过程不是一次性检查所有键,而是按照一定的频率进行抽查,以减少CPU的使用。
内存淘汰:当 Redis 的内存占用超过 maxmemory 限制时,Redis 会根据配置的淘汰策略(如 volatile-lru 或 allkeys-lru)来选择并删除一部分KEY。
4.3 当Redis内存不足时会发生什么?
这取决于配置的内存淘汰策略,Redis支持很多种内存淘汰策略,例如LRU、LFU、Random. 但默认的策略是直接拒绝新的写入请求。而如果设置了其它策略,则会在每次执行命令后判断占用内存是否达到阈值。如果达到阈值则会基于配置的淘汰策略尝试进行内存淘汰,直到占用内存小于阈值为止。
4.4 谈谈LRU、LFU
LRU是最近最久未使用。Redis的Key都是RedisObject,当启用LRU算法后,Redis会在Key的头信息中使用24个bit记录每个key的最近一次使用的时间lru。每次需要内存淘汰时,就会抽样一部分KEY,找出其中空闲时间最长的,也就是now - lru结果最大的,然后将其删除。如果内存依然不足,就重复这个过程。
由于采用了抽样来计算,这种算法只能说是一种近似LRU算法。因此在Redis4.0以后又引入了LFU算法,这种算法是统计最近最少使用,也就是按key的访问频率来统计。当启用LFU算法后,Redis会在key的头信息中使用24bit记录最近一次使用时间和逻辑访问频率。其中高16位是以分钟为单位的最近访问时间,后8位是逻辑访问次数。与LFU类似,每次需要内存淘汰时,就会抽样一部分KEY,找出其中逻辑访问次数最小的,将其淘汰。
4.5 逻辑访问次数是如何计算的?
答:由于记录访问次数的只有8bit,即便是无符号数,最大值只有255,不可能记录真实的访问次数。因此Redis统计的其实是逻辑访问次数。这其中有一个计算公式,会根据当前的访问次数做计算,结果要么是次数+1,要么是次数不变。但随着当前访问次数越大,+1的概率也会越低,并且最大值不超过255.
除此以外,逻辑访问次数还有一个衰减周期,默认为1分钟,即每隔1分钟逻辑访问次数会-1。这样逻辑访问次数就能基本反映出一个key的访问热度了。
5. 缓存问题
5.1 缓存穿透
缓存穿透是指当执行查询请求缓存未命中时,请求直接透过缓存层到达后端数据库。如果数据库中也不存在该数据,那么这个请求将不会在缓存中留下任何记录,导致后续相同请求仍然直接访问数据库,而不是被缓存所拦截。
这种情况在以下场景中尤为常见:
- 查询数据库中不存在的数据。
- 缓存中的数据由于过期或被清除而不可用。
- 系统遭受恶意攻击,攻击者故意请求大量不存在的数据,以此增加数据库的负载。
缓存穿透的风险在于,如果大量这样的请求发生,数据库可能会因为高负载而变得缓慢甚至宕机,这严重影响了系统的性能和稳定性。
针对缓存穿透问题,以下是三种常见的应对方案:
缓存空对象:
当数据库查询未命中时,仍然将这个“空结果”作为缓存对象写入缓存系统,但通常会设置一个较短的过期时间。这样,未来的请求会先命中缓存,即使数据实际上并不存在。这种方法简单易实现,但可能会占用额外的缓存空间,并可能导致数据一致性问题。
布隆过滤器:
在访问缓存之前,使用布隆过滤器来检查请求的数据是否存在于数据库中。布隆过滤器是一种空间效率很高的概率型数据结构,用来判断一个元素是否在一个集合中。它能够保证所有在数据库中存在的数据都能够被准确识别,而不在数据库中的查询则能够被迅速排除,从而避免了对数据库的无效访问。
互斥锁或分布式锁:
当缓存未命中时,使用互斥锁来保证只有一个线程去数据库查询数据,其他线程则等待或重试。这种方法可以避免多个线程对数据库进行重复查询,但可能影响性能,因为请求需要排队等待。
这三种方案各有优缺点,需要根据具体的业务场景和系统要求来选择最合适的策略。在实际应用中,可能会结合使用这些方案,以提供更全面的保护。例如,结合使用缓存空对象和布隆过滤器,可以既减少缓存空间的无效占用,又减少对数据库的不必要访问。
5.2 缓存击穿
缓存击穿是指当一个非常热门的缓存数据在某个时间点过期时,大量的请求几乎同时到达,这些请求发现缓存中的数据已经不存在,就会直接向数据库请求数据,这可能导致数据库在瞬间承受巨大的压力,从而可能导致数据库服务变慢甚至宕机。
应对策略:
互斥锁:使用互斥锁(如Redis分布式锁)确保同一时间只有一个请求可以访问数据库并重建缓存。
多级缓存:使用多级缓存策略,比如在应用层和分布式缓存层之间加入本地缓存。
5.3 缓存雪崩
缓存雪崩是指由于缓存系统中大量缓存数据在相近的时间过期或由于缓存服务不可用,导致大量请求几乎同时直接访问数据库,从而对数据库系统造成的巨大压力,甚至导致数据库系统宕机现象。
应对策略:
缓存雪崩的常见原因有两个,第一是因为大量key同时过期。针对问这个题我们可以可以给缓存key设置不同的TTL值,避免key同时过期。
第二个原因是Redis宕机导致缓存不可用。针对这个问题我们可以利用集群提高Redis的可用性。也可以添加多级缓存,当Redis宕机时还有本地缓存可用。
5.4 如何保证缓存的双写一致性?
缓存的双写一致性很难保证强一致,只能尽可能降低不一致的概率,确保最终一致。我们项目中采用的是Cache Aside模式。简单来说,就是在更新数据库之后删除缓存;在查询时先查询缓存,如果未命中则查询数据库并写入缓存。同时我们会给缓存设置过期时间作为兜底方案,如果真的出现了不一致的情况,也可以通过缓存过期来保证最终一致。
为什么不采用延迟双删机制?
延迟双删的第一次删除并没有实际意义,第二次采用延迟删除主要是解决数据库主从同步的延迟问题,我认为这是数据库主从的一致性问题,与缓存同步无关。既然主节点数据已经更新,Redis的缓存理应更新。而且延迟双删会增加缓存业务复杂度,也没能完全避免缓存一致性问题,投入回报比太低。