Redis

Posted by 佳运 Blog on April 16, 2025

Redis

redis为什么这么快

  • Redis 基于内存,内存的访问速度比磁盘快很多
  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
  • Redis 内置了多种优化过后的数据类型/结构实现,性能非常高
  • Redis 通信协议实现简单且解析高效

IO多路复用

是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利 用CPU资源。目前的l/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的 Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。(由于redis是基于内存的,它的速度瓶颈从来都不是执行速度,而是在io读取上)

Redis 如何实现数据不丢失?

Redis 是一种高性能的键值存储系统,它支持多种数据结构,并且广泛用于缓存、消息队列等场景。为了实现数据不丢失,Redis 提供了多种持久化机制,主要包括 RDB(Redis Database)、 AOF(Append-Only File)和混合持久化,接下来我会详细讲述这三种机制。

  • 首先是 RDB 持久化,RDB 是 Redis 的一种快照持久化方式,它会定期将内存中的数据保存到磁盘上的一个二进制文件中。RDB 快照可以通过手动命令(如 SAVE 或 BGSAVE)或配置文件中的时间策略(如每隔一定时间或一定写操作次数后自动触发)来生成。

    它的执行过程主要分为三步,

    • 第一步是当触发 RDB 持久化时,Redis 会 fork 出一个子进程
    • 第二步是子进程负责将当前内存中的数据完整地写入到一个临时文件中
    • 第三步是在写入完成后,子进程用这个临时文件替换掉旧的 RDB 文件,完成持久化

    RDB 文件是紧凑的二进制格式,适合备份和恢复,尤其是在恢复大数据集时性能较好,但是数据可能会有一定的丢失风险,因为它是基于时间点的快照,两次快照之间的数据可能无法保存。

  • 其次是 AOF 持久化,AOF 是 Redis 的另一种持久化方式,它通过记录每个写操作命令来保证数据的完整性。

    它的工作流程主要分为三步,

    • 第一步是记录写操作,每当 Redis 执行一个写操作时,该操作会被追加到 AOF 文件的末尾;
    • 第二步是文件重写,随着写操作的增加,AOF 文件可能会变得很大,Redis 提供了 AOF 重写机制,通过创建一个新的 AOF 文件来压缩历史记录,只保留最终状态;
    • 第三步是恢复数据,当 Redis 启动时,会读取 AOF 文件并重新执行其中的命令,从而恢复数据。

    AOF 的数据安全性更高,可以配置为每次写操作都同步到磁盘,几乎不会丢失数据,而且它是可读的文本文件,便于调试和分析,但是它的文件体积通常比 RDB 大,恢复速度较慢,且频繁的磁盘写入可能会影响性能。

  • 最后是混合持久化,在 Redis 4.0 及之后版本中,引入了混合持久化的方式,这种方式首先以 RDB 格式保存当前数据的快照,然后再追加后续的写操作命令,这种方式既能快速加载数据,又能减少文件体积,同时保持较高的数据安全性。

AOF持久化的执行流程

AOF持久化的流程包括命令追加、文件写入与同步、文件重写以及重启加载。每个阶段的具体操作如下:

(1)命令追加(Append)

在执行写命令时,Redis会将命令首先追加到AOF缓冲区(aof_buf)。这个缓冲区暂时存储命令,以减少磁盘I/O的频率。通过事件循环机制,写命令会被转换为Redis协议格式,并追加到缓冲区中,确保命令执行效率不受影响。

(2)文件写入与同步(Write & Sync)

AOF缓冲区中的命令并不会立即写入磁盘,而是根据配置的同步策略来决定写入时机。Redis提供三种同步策略:

  • always:每次写命令后立即写入并同步到AOF文件。虽然数据安全性高,但性能大幅下降,不推荐使用。
  • everysec:每秒将AOF缓冲区中的命令写入并同步到文件,兼顾了性能与数据安全性,是默认推荐的策略。
  • no:不进行主动同步,操作系统决定写入时机,可能导致数据丢失,适用于对数据安全性要求不高的场景。

文件写入和同步的区别在于,写入操作仅将数据写入操作系统的缓冲区,而同步操作会确保数据实际写入磁盘。Linux系统中,写入操作通过write调用,同步通过fsync或fdatasync完成。

(3)文件重写(Rewrite)

随着Redis运行时间的增加,AOF文件会逐渐增大,可能导致占用大量磁盘空间并影响重启加载速度。为了解决这一问题,Redis引入了AOF重写机制。AOF重写会根据当前数据集生成一个新的AOF文件,仅保留恢复当前数据所需的最小命令集合,显著减少文件大小。该过程是通过fork一个子进程来进行,子进程根据内存中的数据生成新的AOF文件,而主进程继续处理客户端请求。当重写完成后,主进程将会用新文件替换旧文件。通过配置auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,可以自动触发AOF重写,或者使用BGREWRITEAOF命令手动触发。

(4)重启加载(Load)

在Redis重启时,如果启用了AOF持久化功能,系统会通过加载AOF文件来恢复数据库状态。Redis通过创建伪客户端读取AOF文件并执行每个写命令来恢复数据。如果AOF文件损坏,Redis会停止启动并显示错误信息,此时可以使用redis-check-aof工具进行修复。

Redis的缓存策略

在现代分布式系统中,缓存是提升性能和减轻数据库负载的关键组件。Redis 作为一种高性能的内存数据库,被广泛应用于缓存层。本文将深入探讨几种常用的 Redis 缓存策略,包括旁路缓存模式(Cache-Aside Pattern)、读穿透模式(Read-Through Cache Pattern)、写穿透模式(Write-Through Cache Pattern)和异步缓存写入模式(Write-Behind Pattern)。一,旁路缓存模式(Cache-Aside Pattern)

一、旁路缓存模式(Cache-Aside Pattern)

1. 描述

旁路缓存模式是最常见的缓存策略之一。在这种模式下,应用程序直接与缓存和数据库进行交互。具体流程如下:

  • 读取数据:应用程序首先从缓存中获取数据。如果缓存未命中,则从数据库中读取数据,并将其写入缓存。

  • 写入数据:应用程序先更新数据库,然后删除缓存中的数据,以确保数据的一致性。

2. 优点

  • 简单易懂,易于实现。
  • 适用于读多写少的场景。

3. 缺点

  • 每次写操作都需要同时更新数据库和缓存,增加了写操作的复杂性。

4. 适用场景

  • 数据读取频繁但写入较少的场景。

二、读穿透模式(Read-Through Cache Pattern)

1. 描述

在读穿透模式中,应用程序不直接与数据库交互,而是通过缓存代理进行所有的读操作。具体流程如下:

  • 读取数据:如果缓存中不存在,缓存代理会从数据库中加载数据,并将其写入缓存,然后返回给应用程序。

2. 优点

  • 应用程序逻辑简化,只需与缓存交互。
  • 缓存代理自动处理缓存未命中情况。

3. 缺点

  • 需要缓存代理支持读穿透逻辑。
  • 对缓存代理的性能要求较高。

4. 适用场景

  • 需要简化应用程序缓存操作的场景。

三、写穿透模式(Write-Through Cache Pattern)

1. 描述

在写穿透模式中,应用程序不直接与数据库交互,而是通过缓存代理进行所有的写操作。具体流程如下:

  • 写入数据:缓存代理会同时更新缓存和数据库,确保数据的一致性。

2. 优点

  • 应用程序逻辑简化,只需与缓存交互。
  • 数据一致性较好。

3. 缺点

  • 写操作的性能可能受到影响,因为需要同时更新缓存和数据库。
  • 需要缓存代理支持写穿透逻辑。

4. 适用场景

  • 需要简化应用程序缓存操作并确保数据一致性的场景。

四、异步缓存写入模式(Write-Behind Pattern)

Write-Behind和Write-Through在”程序只和缓存交互且只能通过缓存写数据“这方面很相似。不同点在于Write-Through会把数据立即写入数据库中,而Write-Behind会在一段时间之后(或是被其他方式触发)把数据一起写入数据库,这个异步写操作是Write-Behind的最大特点。

  1. 描述 在异步缓存写入模式中,应用程序将数据写入缓存,缓存代理会在后台异步地将数据写入数据库。具体流程如下:

​ 写入数据:应用程序只需将数据写入缓存,缓存代理负责异步将数据写入数据库。

  1. 优点 写操作性能高,因为应用程序只需与缓存交互。 适用于写操作频繁的场景。
  2. 缺点 数据一致性较难保证,因为数据写入数据库是异步进行的。 需要处理缓存和数据库之间的同步问题。
  3. 适用场景 写操作频繁且对写操作性能要求高的场景。

Redis的过期删除策略

Redis 是一种高性能的键值存储系统,它支持为每个键设置过期时间(TTL,Time To Live)。为了高效地管理这些带有过期时间的键,Redis 采用了三种主要的过期删除策略:惰性删除、定时删除和定期删除。接下来我会详细讲述这三种策略的触发时机、执行过程和优缺点。

  • 第一个是惰性删除(Lazy Deletion)

    当客户端尝试访问某个键时会被触发,它的执行过程主要分为三步,

    • 首先,Redis 会检查该键是否设置了过期时间。
    • 其次,如果当前时间已经超过该键的过期时间,则 Redis 会立即删除该键,并返回空值给客户端。
    • 最后,如果键未过期,则正常返回键对应的值。

    惰性删除的优点是不会额外占用 CPU 资源,只有在访问键时才会触发删除操作。当时,它的缺点是,当某些过期键如果长时间未被访问时,仍然会一直占用内存,进而导致内存浪费。

  • 第二个是定时删除(Immediate Deletion)

    当键的过期时间到达时会被触发,Redis 立即删除该键。它的执行过程只有两步,

    • 首先,让 Redis 使用一个内部定时器来监控键的过期时间。
    • 然后,当某个键的过期时间到达时,Redis 会立即将其从内存中删除。

    定时删除的优点是能保证过期键在过期瞬间被清理,避免内存浪费。当时,它的缺点是,采用此策略对 CPU 的消耗较大,尤其是在大量键同时过期时,可能会影响 Redis 的性能。

  • 第三个是定期删除(Periodic Deletion)

    让 Redis 在后台定期运行一个任务来清理过期键。它执行过程主要分为三步,

    • 首先,Redis 会从设置了过期时间的键集合中随机抽取一部分键。
    • 其次,检查这些键是否已经过期,如果过期则删除。
    • 最后,根据配置的频率和限制条件,控制每次删除的数量,避免对性能造成过大影响。

    定期删除的优点是能在一定程度上主动清理过期键,减少内存浪费。且通过随机抽样和限制删除数量,平衡了性能和内存使用。但是,它的缺点是,删除操作很随机,无法保证所有过期键都能被及时清理。

Redis是如何判断数据是否过期的呢?

Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。

过期字典是存储在redisDb这个结构里的:

typedef struct redisDb {
    ...
    
    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

Redis 过期删除策略和内存淘汰策略有什么区别?

Redis 过期删除策略和内存淘汰策略是两种不同的机制,分别用于处理 Redis 数据的过期和内存管理问题。它们的区别如下:

(1)过期删除策略(Expiration Policy)

过期删除策略是 Redis 用来自动删除已设置过期时间(TTL)的键的机制。当某个键的生存时间(TTL)到期时,Redis 会根据配置的过期删除策略自动删除该键。它主要是解决数据本身过期的问题。

主要特点:

触发条件:当某个键的过期时间到期,Redis 会删除这个键。

过期时间设置:可以通过 EXPIRE 或 SET 命令设置键的过期时间。

(2)内存淘汰策略(Eviction Policy)

内存淘汰策略是 Redis 在内存达到最大限制时,用来决定如何处理多余数据的机制。当 Redis 的内存使用达到设置的上限(maxmemory 配置项),Redis 会选择根据配置的淘汰策略删除一些键,以腾出空间。

主要特点:

触发条件:当 Redis 占用的内存超过配置的最大内存限制时,触发内存淘汰策略。

内存限制设置:可以通过 maxmemory 配置项设置 Redis 的最大内存。

常见的内存淘汰策略:

volatile-random:随机淘汰设置了过期时间的任意键。适用于没有其他明确需求时,随机删除一些数据以腾出内存。

volatile-ttl:优先淘汰那些即将过期的键。对于设置了过期时间的键,系统会倾向于删除最早过期的键,确保数据不会被长时间保留。

volatile-lru:淘汰所有设置了过期时间的键中,最久未使用的键。LRU(Least Recently Used)算法会选择最近最少访问的数据进行淘汰。Redis 3.0 之前默认使用此策略。

volatile-lfu:淘汰所有设置了过期时间的键中,最少使用的键。LFU(Least Frequently Used)算法会选择访问频率最低的键进行淘汰。此策略是在 Redis 4.0 后新增的。

allkeys-random:从所有键中随机淘汰任意一个键。这种方式不考虑键是否设置了过期时间,适用于不关心哪些键被淘汰的场景。

allkeys-lru:淘汰所有键中最久未使用的键。LRU 算法会选择那些最久没有访问过的数据进行淘汰,适用于内存压力较大的场景。

allkeys-lfu:淘汰所有键中最少使用的键。LFU 算法会选择访问频率最低的键进行淘汰,这也是 Redis 4.0 后新增的一种策略,能够更细粒度地管理内存。

noeviction:当 Redis 达到最大内存限制时,不进行任何淘汰操作,而是直接返回错误。这种策略适用于对内存限制非常严格的场景,确保不会丢失任何数据。

Redis缓存失效会不会立即删除

不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。

  • 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
  • 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

怎么判断 Redis 某个节点是否正常工作?

判断 Redis 某个节点是否正常工作是一个常见的运维和开发需求。接下来我会详细讲述判断 Redis 节点正常工作五种常见方式。

  • 第一种是采用 PING 命令,它是 Redis 内置命令,用于测试 Redis 服务是否可用。如果 Redis 节点正常工作,执行 PING 命令会返回 PONG。如果未收到 PONG 或连接超时,则说明节点可能存在问题。
  • 第二种是采用 INFO 命令,它也是 Redis 内置命令,INFO 命令可以返回 Redis 节点的详细运行信息,包括内存使用、连接数、持久化状态等。通过解析这些信息,可以判断节点是否处于正常状态。例如,检查 role 字段可以确认节点是主节点还是从节点,检查 connected_clients 可以确认是否有过多的客户端连接。
  • 第三种是采用 CLUSTER INFO 命令(集群模式),它还是 Redis 内置命令,如果 Redis 运行在集群模式下,可以使用 CLUSTER INFO 命令查看集群的状态。重点关注 cluster_state 字段,如果值为 ok,则表示集群正常;如果是 fail,则说明集群中有节点不可用。
  • 第四种是采用 Telnet 或 Netcat,它们属于外部工具,用于测试 Redis 节点的端口是否可达。例如,尝试连接到 Redis 的默认端口 6379,如果连接失败,说明节点可能宕机或网络有问题。
  • 第五种是采用监控系统,配置 Prometheus、Grafana 等监控工具,实时监控 Redis 的性能指标(如内存使用率、QPS、延迟等)。如果某些指标超出阈值或出现异常波动,可能是节点出现问题。

Redis常见的数据结构

Redis 是一种高性能的键值存储系统,它支持多种数据结构,这些数据结构使得 Redis 能够灵活地应对不同的应用场景。接下来我会详细讲述 Redis 常见的五种数据结构及其特点。

  • 第一个是字符串(String),它是 Redis 最基础的数据结构,它可以存储文本、数字或二进制数据。它的特点是一个键对应一个值,且支持原子操作,比如递增(INCR)、递减(DECR)等,主要用于缓存简单的键值对数据,比如用户会话信息或计数器。
  • 第二个是列表(List),它是一个有序的字符串集合,支持从两端插入和删除元素。它的特点是底层采用双向链表,插入和删除操作非常高效。另外,支持队列和栈的操作,比如先进先出(FIFO)或后进先出(LIFO)。常用于消息队列、任务调度等场景。
  • 第三个是集合(Set),它是一个无序且不允许重复元素的字符串集合。它的特点是底层采用哈希表,在查找和插入操作的时间复杂度为 O(1)。且支持集合运算,比如交集(SINTER)、并集(SUNION)和差集(SDIFF)。常用于去重、标签系统等场景。
  • 第四个是哈希(Hash),它是一种键值对集合,适合存储对象。它的特点是允许在一个键下存储多个字段和值,类似于 Java 中的 Map。其底层实现为哈希表,适合存储结构化数据,比如用户信息。它支持对单个字段的操作,比如 HGET 和 HSET。
  • 第五个是有序集合(Sorted Set),它也是一种集合,但它的每个元素都关联一个分数,元素按照分数排序。它的特点是在底层实现结合了跳表(多层有序链表)和哈希表,支持高效的范围查询。且元素唯一且有序,适合需要排序的场景,比如排行榜。还支持按分数范围获取元素,比如 ZRANGE 和 ZRANGEBYSCORE。

除了以上5种数据结构,还有哪些数据结构

(1)Bitmaps(位图)

描述:位图是一种基于字符串的特殊数据结构,用于高效存储布尔值(0 和 1)。

特点:每个位表示一个布尔值。支持高效的位操作。

常用命令:

SETBIT key offset value:设置指定偏移量的位值。

GETBIT key offset:获取指定偏移量的位值。

BITCOUNT key:统计位图中为 1 的位数。

使用场景:

用户签到记录(每天用 1 位表示是否签到)。

统计在线用户数量。

(2)HyperLogLog(基数统计)

描述:HyperLogLog 是一种用于估算集合基数(唯一元素数量)的数据结构。

特点:使用极小的内存空间进行基数统计。结果是近似值,误差率约为 0.81%。

常用命令:

PFADD key element:向 HyperLogLog 中添加元素。

PFCOUNT key:获取基数估计值。

PFMERGE destkey sourcekey:合并多个 HyperLogLog。

使用场景:

网站 UV(独立访客)统计。

大规模数据的去重统计。

(3)Streams(流)

描述:Streams 是 Redis 5.0 引入的一种数据结构,用于处理消息流。

特点:消息是有序的,按时间戳排序。支持消费者组(Consumer Groups),允许多个消费者协作处理消息。

常用命令:

XADD key ID field value:向流中添加消息。

XREAD key:读取消息。

XGROUP CREATE key groupname ID:创建消费者组。

XREADGROUP GROUP groupname consumer COUNT count:从消费者组中读取消息。

使用场景:

消息队列(如 Kafka 的轻量级替代方案)。

日志收集和分析。

(4)Geospatial (地理位置)

描述:Redis的Geo在Redis3.2版本就推出了这个功能,这个功能可以推算出地理位置的信息,两地之间的距离,方圆几里的人。

常用命令:

geoadd命令:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。

geopos命令:从key里返回所有给定位置元素的位置(经度和纬度)。

geodist命令:返回两个给定位置之间的直线距离,如果两个位置之间的其中一个不存在,那么命令就返回空值。

georadius命令:以给定的经纬度为中心,找出某一半径内的元素。

georadiusbymember命令:找出位于指定范围内的元素,中心点是由给定的位置元素决定。

底层实现原理:

GEO底层的实现原理其实就是Zset,我们可以使用Zset命令来操作GEO。

渐进式rehash

为了能更清楚地展示dict(redis的hash表)的数据结构定义,用一张结构图来表示dict(字典)的构成。如下图:

以下是哈希表渐进式rehash的详细步骤:

(1)为ht[1]分配空间,让dict字典同时持有 ht[0] 和 ht[1] 两个哈希表

(2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。

(3)在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引(table[rehashidx]桶上的链表)上的所有键值对rehash到ht[1]上,当rehash工作完成之后,将rehashidx属性的值增一,表示下一次要迁移链表所在桶的位置。

(4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有桶对应的键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。(因为redis是单线程的,如果一次性rehash回阻塞其他线程,影响执行速度)

缓存穿透、缓存击穿、缓存雪崩

缓存是提升系统性能的重要手段,但在使用缓存时可能会遇到一些问题,比如缓存穿透、缓存击穿和缓存雪崩。这些问题如果处理不当,可能会导致系统性能下降甚至崩溃。接下来我会详细讲述这三种问题的定义、成因、影响以及解决方案。

  • 缓存穿透,它是指查询一个不存在的数据,这个数据既不在缓存中,也不在数据库中,导致每次请求都会直接打到数据库上。

    造成这个问题的原因主要有两个,一个是用户恶意构造不存在的查询条件(如随机生成的无效 ID);另一个是数据库中确实没有对应的数据,而缓存中也没有命中。

    缓存穿透会让数据库承受大量无效请求,导致性能下降或崩溃。解决方案主要有两种,

    • 一个是使用布隆过滤器,在请求到达缓存之前,先用布隆过滤器判断数据是否存在。
    • 另一个是缓存空值,当查询结果为空时,将空值写入缓存,并设置较短的过期时间。这样可以避免重复查询数据库。
  • 缓存击穿,它是指某个热点数据在缓存中过期后,大量并发请求同时访问该数据,导致所有请求都打到数据库上。

    造成这个问题的原因主要有两个,一个是热点数据的缓存过期时间设置不合理。另一个是大量用户在同一时间访问同一个热点数据。

    缓存击穿会让数据库瞬间承受高并发压力,导致性能下降或崩溃。解决方案主要有两种,

    • 一个是采用互斥锁(Mutex Lock),在缓存失效时,只允许一个线程去加载数据并更新缓存,其他线程等待。可以通过分布式锁(如 Redis 的 SETNX 命令)实现。
    • 另一个是采用永不过期策略(或者逻辑过期),对于热点数据,可以不设置过期时间,而是通过后台定时任务主动刷新缓存。
  • 缓存雪崩,它是指大量缓存在同一时间过期,导致大量请求直接打到数据库上。

    造成这个问题的原因主要有两个,一个是缓存的过期时间设置为相同的值,导致集中失效。另一个是缓存服务宕机或不可用,导致所有请求都直接访问数据库。

    缓存雪崩会使数据库瞬间承受巨大的请求压力,导致系统崩溃。解决方案主要有三种,

    • 第一个是为缓存设置随机的过期时间,避免集中失效。例如,在基础过期时间上加上一个随机值。
    • 第二个采用是多级缓存,使用本地缓存(如 Guava Cache)作为一级缓存,Redis 作为二级缓存,减少对数据库的直接访问。
    • 第三个是采用降级策略,在缓存不可用时,返回默认值或静态页面,避免请求直接打到数据库。

布隆过滤器的原理、优缺点和应用场景

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于快速判断一个元素是否存在于集合中。它通过牺牲一定的准确性(存在一定的误判率)来换取极低的内存占用和高效的查询性能。

(1)布隆过滤器的核心原理

基本结构:

由一个位数组(Bit Array)和多个哈希函数(Hash Functions)组成。

位数组初始化时所有位均为 0。

当插入元素时,通过多个哈希函数计算出多个哈希值,将这些哈希值对应的位数组位置设为 1。

查询元素时,通过相同的哈希函数计算哈希值,检查对应的位是否全为 1:如果存在 0,则元素一定不在集合中。如果全为 1,则元素可能在集合中(存在误判的可能)。

(2)布隆过滤器的优缺点

优点:

空间效率高:相比哈希表或集合,布隆过滤器占用的内存极小。例如,存储 1 亿个元素,误判率 1% 时仅需约 1.2GB 内存。

查询速度快:插入和查询时间复杂度为 O(k)(k 为哈希函数数量),与数据规模无关。

无须存储元素本身:仅保存哈希后的位数组,适合隐私敏感场景。

缺点:

存在误判率(False Positive):可能错误地判断一个不存在的元素为“存在”(无法避免)。误判率与位数组长度、哈希函数数量、元素数量相关。

无法删除元素:由于位数组是多个元素共享的,直接删除某个元素会破坏其他元素的判断(可通过计数布隆过滤器改进)。

不支持动态扩容:位数组长度固定,无法动态扩展。

(3)布隆过滤器的应用场景

缓存穿透防护:在 Redis 中,将查询过的不存在的 key 存入布隆过滤器。当请求到来时,先通过布隆过滤器判断是否存在,避免查询数据库。

大数据去重:例如统计独立用户 ID、网页爬虫 URL 去重。

垃圾邮件过滤:快速判断邮件地址是否在黑名单中。

分布式系统协调:检查某个资源是否已被其他节点处理。

Redis实现分布式锁的原理

Redis 实现分布式锁依赖于其原子操作能力,通过确保在分布式环境中只有一个客户端能够成功获得锁,从而协调多个客户端对共享资源的安全访问。接下来,我会从加锁和解锁这两个方面说一下单个 Redis 实现分布式锁。

第一个方面是加锁,Redis 提供了 SET resource_name my_random_value NX PX timeout 命令,其中my_random_value是客户端生成的唯一标识,用于区分不同客户端的锁操作,而 NX 参数确保命令仅当键不存在时执行(即创建锁),还有 PX timeout 设置了锁的有效期。这一命令是原子性的,意味着它作为一个不可分割的整体被执行,要么全部完成,要么完全不发生,从而保证了即使在高并发情况下也只有一个客户端能成功获取锁。

第二个方面是解锁,当需要解锁时,不能简单地调用 DEL 命令删除键,因为这可能会误删其他客户端持有的锁。正确的方式是使用 Lua 脚本原子性地检查当前锁是否由该客户端持有(即比较键的值是否等于最初设置的 my_random_value),并且只在匹配的情况下删除键。使用 Lua 脚本的好处在于它是原子性的,避免了读取-检查-删除过程中可能出现的竞争条件。

如何保证Redis与MySQL的数据一致性(双写一致性)

  • 第一种是先写MySQL,再写Redis,这种方式存在较大风险,尤其是Redis更新失败时,缓存与数据库数据不一致。
  • 第二种是先写Redis,再写MySQL,这种方案虽然提高了性能,但存在数据丢失的风险,尤其是在Redis出现故障或丢失数据时,可能导致数据不一致。
  • 第三种是先删除Redis,再写MySQL,虽然一定程度上避免了缓存数据不一致的问题,但会影响性能,因为每次更新都需要重建缓存,同时也无法保证缓存更新及时性,尤其是在高并发情况下。
  • 第四种是先删除Redis,再写MySQL,再删除Redis,这种方案会影响性能,尤其是在高负载环境中频繁操作缓存和数据库时。
  • 第五种是先写MySQL,再删除Redis,虽然保证了数据库一致性,但可能会出现缓存未及时更新的情况,导致短时间内缓存数据过期,影响系统的响应速度。

以上几种在并发情况下都不能保证数据一致性,以下两种方式可以保证数据(最终)一致:

  • 允许延时一致的业务:采用异步通知
    • 删除缓存重试策略(消息队列
    • 订阅 binlog,再删除缓存(Canal+消息队列
  • 强一致的,采用Redisson的读写锁
    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
    • 排他锁:独占锁writeLock,加锁之后,阻塞其他线程读写操作

Redis主从一致

全量同步和增量同步

全量同步:

  • 1.从节点请求主节点同步数据(发送replication id、offest)
  • 2.主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id、offest)
  • 3.在主节点执行bgsave,生成rdb文件之后发送给从节点去执行
  • 4.在rdb生成期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
  • 5.把生成之后的命令日志文件发送给从节点同步

增量同步:

  • 1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次请求就获取从节点的offset值
  • 2.主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

Redis哨兵模式

在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。

哨兵节点主要负责三件事情:监控、选主、通知

当redis集群的主节点故障时,Sentinel集群将从剩余的从节点中选举一个新的主节点,有以下步骤:

  • 故障节点主观下线

    Sentinel集群的每一个Sentinel节点会定时对redis集群的所有节点发心跳包检测节点是否正常。如果一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。

  • 故障节点客观下线

    当节点被一个Sentinel节点记为主观下线时,并不意味着该节点肯定故障了,还需要Sentinel集群的其他Sentinel节点共同判断为主观下线才行。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。

    如果客观下线的redis节点是从节点或者是Sentinel节点,则操作到此为止,没有后续的操作了如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。

  • Sentinel集群选举Leader

    每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。

    如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader否则重新进行选举。

  • Sentinel Leader决定新主节点

    当Sentinel集群选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点:

    1. 过滤故障的节点
    2. 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
    3. 选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续
    4. 选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点

Redis的分片集群

redis的分片集群有什么作用

  • 集群中有多个master,.每个master保存不同数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

Redis分片集群中数据是怎么存储和读取的

  • Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽
  • 将16384个插槽分配到不同的实例
  • 读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例