Redis基础巩固


Redis 是什么

Redis 是一种高性能、基于内存的开源键值存储数据库系统,它主要用于缓存会话管理实时分析等用途。

关键特点

  1. 键值存储:Redis 以键值对的形式存储数据。每个键都是一个唯一的标识符,与一个值相关联。
  2. 基于内存存储:Redis 所有的数据存储在内存中,这意味着它提供极高的读写速度,使得它非常适合用作缓存层,能够快速响应读取请求。
  3. 持久性:Redis 支持数据持久性,可以将数据保存到磁盘上,以便在重启后恢复数据。
  4. 多数据结构支持:Redis 支持各种数据结构,包括如字符串(string)、列表(list)、集合(set)、有序集合(sorted set)、哈希表(hash)、位图(bitmap)、超日志(hyperloglog)和地理空间索引(geospatial index)。等。这些数据结构可以用于不同的应用场景。
  5. 发布-订阅模式:Redis 支持发布-订阅消息模式,允许客户端订阅特定的频道,从而实现高效的消息通信。这对于实现实时通信和事件驱动应用程序非常有用。
  6. 事务:Redis 支持事务,可以一次执行多个命令,并保证这些命令在执行期间不会受到其他客户端的干扰。
  7. 分布式和高可用性:通过使用哨兵(Sentinel)和集群,Redis 可以提供高可用性。哨兵用于监控 Redis 服务器的健康状况并自动执行故障转移。集群则提供数据分片和自动分区,以支持更大规模的数据存储。
  8. 分片:Redis 支持数据分片,可以水平扩展存储容量和吞吐量。
  9. Lua脚本:Redis 允许使用 Lua 脚本执行高级操作,使得在服务器端可以进行复杂的逻辑,减少网络往返次数。
  10. 社区支持:Redis 社区活跃,提供了许多扩展和插件,使其适用于各种不同的用例。

数据类型

  1. 字符串(String)
    • 结构:字符串是Redis最基本的数据类型。Redis中的字符串是二进制安全的,这意味着它们可以包含任何数据,比如JPEG图像或序列化对象。
    • 实现:字符串使用简单的动态字符串(SDS,Simple Dynamic String)实现,允许快速追加操作,与Java中的StringBufferStringBuilder类似,都支持动态修改,如追加、修改等操作,而不需要每次操作都创建一个新的字符串实例。
  2. 列表(List)
    • 结构:列表是字符串元素的集合,按插入顺序排序。
    • 实现:Redis列表用双向链表或压缩列表(ziplist)实现。较小的列表通常使用压缩列表以节省空间,而较大的列表则使用双向链表。
  3. 集合(Set)
    • 结构:集合是无序的字符串集合,每个元素都是唯一的。
    • 实现:小的集合使用压缩列表(ziplist)实现,较大的集合使用散列(hash table)实现。
  4. 有序集合(Sorted Set)
    • 结构:有序集合类似于集合,但每个元素都关联一个分数。这些元素按分数有序排列。
    • 实现:有序集合通过跳跃表(skiplist)和散列结构组合实现。跳跃表用于按分数排序和范围查询,而散列用于快速访问。
  5. 哈希(Hash)
    • 结构:哈希是键值对的集合,类似于Java中的HashMap或Python中的字典。
    • 实现:小的哈希使用压缩列表实现,大的哈希使用散列结构。
  6. 位图(Bitmap)
    • 结构:位图不是独立的数据类型,而是在字符串上操作单个位(binary digit)。
    • 实现:通过对字符串类型的特殊操作实现,允许设置和查询字符串值的特定位。
  7. HyperLogLog
    • 结构:用于高效地执行基数计数(比如计算一个集合中不同元素的数量)。
    • 实现:使用近似算法,牺牲了精度以换取极高的空间效率。
  8. 地理空间索引(Geospatial)
    • 结构:用于存储地理位置信息,并进行各种地理相关的计算,如两点之间的距离。
    • 实现:基于有序集合,利用Z-order曲线在一维值中编码二维经纬度。

应用场景

  1. 缓存系统

    • 场景:减少数据库的负载,加速数据检索。
    • 实现:将经常查询的数据,如用户信息、商品详情等存储在 Redis 中。当数据被请求时,首先查询 Redis,如果找不到,再查询数据库,并将结果存回 Redis。
  2. 会话存储(Session Store)

    • 场景:用于 Web 应用的用户会话管理。
    • 实现:将用户的会话信息存储在 Redis 中,由于 Redis 的读写速度快,可以快速处理大量并发的会话数据。
  3. 消息队列

    • 场景:应用程序之间的消息传递和异步处理。
    • 实现:使用 Redis 的发布/订阅功能或列表结构实现消息队列,支持生产者-消费者模型,实现数据的异步处理。
  4. 排行榜/计数器

    • 场景:用于实现社交网络、游戏等应用的排行榜功能。
    • 实现:利用 Redis 的有序集合(sorted set),可以快速添加、更新和获取排行榜数据。
  5. 实时分析

    • 场景:网站的实时访问数据统计。
    • 实现:使用Redis的计数器功能,例如HyperLogLog来估计唯一访问者数量。
  6. 地理空间数据处理

    • 场景:例如实现基于位置的服务,如查找附近的商店或用户。
    • 实现:使用Redis的地理空间索引功能,可以存储地理位置信息,并进行范围查询和距离计算。
  7. 分布式锁

    • 场景:在分布式系统中同步不同进程或服务器之间的操作。
    • 实现:通过Redis的SETNX命令实现锁的机制,确保同一时间只有一个进程能执行特定的代码段。
  8. 数据过期处理

    • 场景:自动删除过期的数据,如临时令牌或验证码。
    • 实现:利用Redis的键过期功能,可以为存储的数据设置生存时间。

持久化机制

Redis 支持两种持久化机制,分别是RDB(Redis Database)和 AOF(Append Only File)。这两种机制可以单独使用,也可以同时使用,以便在不同的场景下平衡性能和数据安全性。

RDB(默认持久化机制)

  • 介绍

    RDB 持久化是通过创建数据集的快照来实现的。

  • 工作原理

    • 在指定的时间间隔内,Redis自动创建当前数据的快照,并将其保存在一个紧凑的二进制文件中(默认为 dump.rdb)。Redis 在默认情况下只有一个 dump.rdb 文件,意味着每次创建新的RDB快照时,都会覆盖现有的dump.rdb文件。
    • 这个 dump.rdb 就是当前redis的备份快照数据。这个RDB就是在redis启动的时候,它发现有这个rdb,就会载入到内存中,也就是恢复数据。需要注意的是,redis启动的时候,如果rdb文件很大,那么会堵塞,直到数据全部恢复到内存里。
    • 快照的创建可以通过自动或手动触发。自动触发基于配置的时间间隔和数据变化的次数。
    • RDB 是每隔一段时间做备份的机制。如果因为服务器宕机死机重启,那么内存中的数据就没了,但是他会从rdb中进行恢复。
  • 优点

    • 高效性能:RDB是一个非常高效的方式来保存大量数据的快照。
    • 灾难恢复:由于RDB文件是压缩的二进制文件,适用于需要定期备份数据的情况,非常适合灾难恢复。
  • 缺点

    • 数据丢失风险:如果Redis崩溃,自上次快照以来的所有数据都可能丢失。
    • 性能开销:在大数据集的情况下,保存快照可能会对性能产生短暂的影响。

    在 RDB 持久性机制下,Redis 确实提供了两种快照(snapshot)保存方式:SAVEBFSAVE。这两种命令都用于生成当前 Redis 数据库状态的快照,但它们在执行方式上有显著的不同。

  1. SAVE
    • 执行方式SAVE命令会创建一个快照并将其保存在磁盘上,但这个过程是同步进行的。这意味着在 SAVE命令执行期间,Redis将停止处理其他命令。
    • 使用场景:由于SAVE会堵塞所有其他客户端请求,它通常不推荐在生产环境中使用。它更适用于低流量的时段或维护期间,例如,当需要确保数据完全同步到磁盘时。
  2. BGSAVE
    • 执行方式BGSAVE命令会在后台创建一个快照。具体来说,Redis会先创建一个子进程,然后子进程负责将快照写入磁盘,而父进程(即原始的Redis服务器进程)可以继续处理客户端请求。
    • 使用场景:由于BGSAVE不会堵塞主服务进程,它更适合生产环境中使用,尤其是在需要定期快照但又不希望影响服务性能的场合。

AOF

  • 介绍

    AOF(Append-Only File)是一种将Redis操作命令以追加方式写入日志文件的机制,是追加式备份。它以文本格式记录Redis的写操作(新增、修改、删除),都会记录在这个AOF日志里。

    需要注意的是,redis是先执行写操作指令,随后再把指令追加进AOF文件中。

  • 工作原理

    • 每个写操作命令都会追加到AOF文件的末尾。
    • 类似于记录日志,把所有的写操作追加到文件。追加的形式是append,逐个命令追加,不是修改。
      • 比如说 set key1 abc, set key1 123,虽然两次设置key1的值,但不会合并,而是追加命令。
      • redis恢复的时候先恢复AOF,如果AOF有问题(比如破损),则再恢复RDB。
      • redis恢复的时候是读取AOF中的命令,从头到尾读一遍,然后数据恢复。
    • Redis启动时,通过重新执行这些命令来重建原始数据。
    • AOF 是通过Redis主线程执行的,因此每个写操作都会导致磁盘I/O,当然这是为了确保数据的持久性。
  • 优点

    • 数据完整性:与RDB相比,AOF可以提供更好的数据完整性和安全性。
    • 易于阅读:AOF文件以文本格式存储,易于阅读和维护。
    • 灵活性:提供多种同步策略,如每秒同步每修改同步等。
  • 缺点

    • 文件大小:AOF文件可能会比RDB文件大很多,因为它保存了所有的写操作。
    • 性能开销:特别是在每次修改同步的配置下,可能会对写入性能产生影响。

重写机制

AOF 的重写机制是Redis用来优化AOF文件大小和性能的重要机制。随着操作的不断累积,AOF文件可能会变得非常大,包含许多已经不再需要的命令。AOF重写机制就会创建一个新的AOF文件,其中包含了与当前数据库相同的数据,但是采用更紧凑的格式,通常比原始AOF文件要小得多。这有助于减少AOF文件的大小,提高Redis性能,以及降低恢复速度。

工作原理

  • 创建新的AOF文件:在AOF重写过程中,Redis会创建一个新的AOF文件。这个文件不是通过重放旧的AOF文件中的命令来生成的,而是直接从当前数据库的内存状态到处。
  • 最小命令集:新的AOF文件仅包含使数据库达到当前状态的最小命令集。例如,如果一个键被修改多次,新的AOF文件只会包含这个键的最终状态。

重写模式

AOF重写有两种模式,其中一种是混合模式,另一种是纯AOF模式

  • 混合模式(Mixed Mode):

    • 在混合模式下,AOF重写生成的新AOF文件既包含AOF格式的写命令,也包含RDB快照的数据。
    • 首先,redis会把当前所有的数据以rdb形式存入到AOF文件中,这些都是二进制文件,数据量小,随后新的数据会以AOF格式追加到这个AOF文件中。
    • 恢复过程会更快,因为只需要加载一个文件。
    • 纯AOF模式:
      • AOF重写生成的新AOF文件仅包含AOF格式的写命令,不包含RDB快照的数据。但是会将一些重复的,没有意义的指令给去除掉,减少文件体积。
  • 触发机制

    • 手动触发:可以通过执行BGREWRITEAOF命令手动触发AOF重写。
    • 自动触发:Redis还可以配置为在AOF文件增长到一定大小时自动触发重写。这是通过配置文件中的auto-aof-rewrite-percentageauto-aof-rewrite-min-size 指令来控制的。
  • 过程细节

    • 使用子进程:类似于BGSAVE命令,AOF重写也是在一个子进程中进行的,以避免堵塞主进程。
    • 追加写入期间的命令:在重写过程中,对Redis数据库进行的所有写操作同时会被追加到旧的和新的AOF文件中,确保数据一致性。
    • 切换文件:一旦新的AOF文件创建完成,Redis会使用新文件替换旧的AOF文件,并从此刻开始只向新文件追加新的写命令。
  • 优点

    • 减少磁盘占用:通过删除命令,AOF重写能显著减少AOF文件的大小。
    • 提高重启速度:更小的AOF文件意味着重启时重放命令的速度更快。
  • 注意事项

    • 性能影响:尽管AOF重写是非堵塞的,但它可能会增加磁盘I/O负担,因此在高负载的系统上运行时需要小心。
    • 内存影响:与BGSAVE类似,AOF重写也会临时增加Redis的内存使用,因为它需要创建一个当前数据库状态的副本。

    AOF小结

  1. AOF写入

    • AOF操作本身是将每个写命令追加到AOF文件的过程。这种追加操作通常是非堵塞的,但它的行为取决于具体的配置。

    • 在AOF配置中,有一个

      appendfsync
      

      选项,它控制着操作系统刷新数据到磁盘的时机,这个选项有三个设置:

      • always(每修改同步):每个写命令都同步写入磁盘,这可能导致堵塞,尤其是在磁盘I/O性能较差的时候。
      • everysec(每秒同步):在默认情况下,大多数Redis设置会使用这个选项。大约每秒同步一次,这是一种平衡性能和数据安全性的做法,通常不会引起显著的堵塞。
      • no:交给操作系统决定何时进行数据写入,这种方式下写入操作是非堵塞的,但在系统崩溃的情况下可能会丢失更多数据。
  2. AOF重写

    • AOF重写操作是非堵塞的。在执行AOF重写时,Redis会启动一个子进程来进行重写工作,而主进程继续处理客户端的请求。
    • 由于AOF重写是在子进程中完成的,它不会堵塞正在进行的客户端命令处理。不过,它可能会对系统的整体性能产生影响,主要是因为磁盘I/O和额外的CPU负载。

两种持久化机制如何选择

选择RDB还是AOF持久化取决于应用的需求。通常情况下,两者可以结合使用以获得更好的性能和持久性。例如,可以启用AOF来记录最近的写操作,并同时使用RDB来提供定期的全数据快照。

总的来说,RDB适合对定期备份敏感数据集较大的场景,而AOF适合对实时性要求较高数据恢复性要求非常高的场景。具体选择应该根据应用程序的性质和需求来确定。

淘汰策略

Redis 的数据淘汰策略是指当内存使用达到一定阈值时,Redis如何选择删除一些数据以释放内存的方法。这些策略主要用于当Redis被用作缓存时,帮助管理内存的使用。

  1. noeviction(无淘汰策略):当内存使用达到限制时,对写入操作返回错误,但允许读操作。这是默认策略
  2. allkeys-lru(最近最少使用):在内存达到限制时,在所有键中移除最近最少使用的键。适用于通用缓存场景。
  3. volatile-lru(过期时间中最少使用):仅淘汰设置了过期时间的键中的最近最少使用的键。
  4. allkeys-random(随机):在内存达到限制时,在所有键中随机移除键。
  5. volatile-random(过期时间中随机):仅随机移除设置了过期时间的键。
  6. allkeys-lfu:在所有键中移除最不经常使用的键。
  7. volatile-lfu(最近最少频繁使用):从已设置过期时间的键中,移除最不经常使用的键。
  8. volatile-ttl(最短剩余时间):从已设置过期的键中,移除即将到期的键。

应用场景:

选择哪种淘汰策略取决于具体的使用场景和需求。例如,如果使用Redis作为缓存,并且希望在内存不足时自动删除老旧数据,可以选择allkeys-lru策略。

对于关键数据,可能更倾向于使用noeviction策略,并在应用层面控制内存使用。

非阻塞I/O模型

在此之前,我们先了解下什么是I/O模型。I/O(输入/输出)模型描述的是程序如何处理输入和输出操作。在计算机系统中,I/O操作通常是指与外部设备(如硬盘、网络接口等)的数据交换。I/O模型决定了程序在等待I/O操作完成时的行为,这对程序的性能和响应能力有重要影响。

主要有以下几种I/O模型:

  1. 堵塞I/O(Blocking I/O)

    • 在这种模型中,应用程序发起I/O请求后,必须等待数据准备就绪并完成操作,期间应用程序被堵塞,不能执行其他任务。
    • 例如,读取文件操作会一直等待,直到有数据可以读取。
  2. 非堵塞I/O(New I/O)

    • 应用程序发起I/O请求后,如果数据未准备好,操作系统会立即返回一个错误(通常是“资源暂时不可用”),应用程序可以继续执行其他任务。
    • 应用程序需要不断地询问操作系统数据是否准备好,这个过程为“轮询(polling)”。
  3. I/O复用(I/O Multiplexing)

    • 应用程序通过一个API(如select、poll、epoll)监控多个I/O流,一旦其中一个或多个I/O流准备好,操作系统通知应用程序。
    • 这种模型允许单个线程同时管理多个I/O操作,而不是为每个I/O操作创建单独的线程。
  4. 信号驱动I/O(Signal-driver I/O)

    • 应用程序告诉操作系统启动一个操作,并让操作系统在数据准备好时通过信号来通知它。
    • 与非堵塞I/O不同,信号驱动I/O不需要应用程序不断地检查数据是否准备好。
  5. 异步I/O(Asynchronous I/O)

    • 应用程序发起I/O操作后,可以立即开始执行下一个指令。操作系统将完成整个I/O操作(包括数据传输)并在操作完成后通知应用程序。
    • 这种模型下,应用程序无需等待I/O操作的完成。

接下来让我们通过一个关于Redis如何利用非阻塞I/O处理客户端请求的例子来更好地理解非堵塞I/O模型概念。

场景:客户端请求处理

传统堵塞I/O模型的限制

在传统的堵塞I/O模型下,当Redis服务器接收来自一个客户端的请求时,它必须等待整个请求的处理完全完成(包括等待所有必要的数据被读取或写入),在此期间,它不能处理来自其他客户端的任何其他请求。这意味着如果某个请求的处理需要一些时间(例如,一个复杂的查询或大量数据的读取),其他客户端必须等待,这降低了整体的响应性和吞吐量。

Redis的非堵塞I/O模型

现在再来看看Redis是如何使用非堵塞I/O来优化这个过程的:

  1. 多个客户端同时连接:多个客户端同时向Redis服务器发送请求。

  2. 非堵塞I/O操作

    • 当Redis服务器接收到一个请求时,它会开始处理这个请求。如果在处理过程中需要进行I/O操作(比如读取磁盘上的数据),Redis服务器不会在这个操作完成前被堵塞。
    • 相反,如果数据尚未准备好,Redis可以暂时停止处理这个请求,并转而处理其他客户端的请求。
  3. I/O多路复用

    • 在后台,Redis使用I/O多路复用技术(如epoll)来有效地监控所有活跃的客户端连接。
    • 一旦某个请求的I/O操作完成(例如,所需数据已准备好读取),I/O多路复用机制会通知Redis服务器,然后Redis可以继续处理这个请求。
  4. 高效并发处理

    • 通过这种方式,Redis可以在单个线程中高效地处理多个并发请求,而无需为每个请求或连接创建单独的线程。
    • 这提高了服务器的响应性和吞吐量,即使在面对大量并发请求时也能保持高性能。

结论:Redis 正是使用了非堵塞I/OI/O多路复用,才能够快速、高效地处理成千上万的并发连接和请求,而无需创建和管理多个线程,从而大大提高了资源利用率和性能。

事件驱动模型

  • 非堵塞的事件循环模型

  • Redis 采用了

    非堵塞的事件循环模型

    ,允许多个客户端并发请求。虽然Redis在任何给定时间点只能处理一个请求,但它可以快速轮询多个客户端请求,以确保高吞吐量。

    • 事件驱动

      • Redis的核心运行机制基于事件驱动的。意味着Redis的主要功能是接收和处理客户端请求、执行命令、数据持久化等,都是通过响应各种事件来完成的。
      • 在事件驱动模型中,Redis不需要为每个任务或请求创建新的线程,而是在单线程中异步处理这些事件。这种方式使得Redis能够高效地处理大量并发请求,同时保持简单的架构和低延迟。
    • 事件循环:Redis的核心是一个事件循环,也称为事件驱动循环。这个事件循环不断地检查并处理发生的事件,而不会堵塞整个系统。它会轮询各个已注册的事件,如客户端连接事件、套接字可读事件、套接字可写事件等。

    • 事件监听:Redis使用操作系统提供的多路复用机制,如epoll(Linux)kqueue(BSD)select等,来监听多个套接字上的事件。这使得Redis能够同时处理多个客户端连接而无需为每个连接创建一个新线程。

    • 协程和事件处理:在一些版本的Redis中,引入了协程(Coroutine)和事件处理机制,可以更有效地处理多个客户端请求和数据库操作,提高了并发性能。

Redis的操作是同步还是异步

先说结论,基于内存操作是同步,基于网络I/O或磁盘I/O是异步

同步

  • 当Redis接收到像get key1, set key1这样的命令时,如果数据在内存中(即不需要从磁盘加载),Redis会立即处理这个命令并同步返回结果。即使是在这种同步情况下,操作也是非常迅速的,因为它是在内存中进行的,几乎没有什么延迟。
  • 因此,对于大多数常规操作,Redis的处理是足够快的,以至于即使这些操作都是同步进行的,也不会对性能造成显著影响。

异步

  • 在网络I/O方面,Redis使用非堵塞I/O机制。这意味着Redis可以在等待一个网络请求的数据准备好时,同时处理其他网络请求或内部任务。
  • 在磁盘I/O方面,需要区分不同类型的操作:
    • RDB持久化:当执行如BGSAVE命令进行RDB持久化时,Redis会在后台创建一个数据快照。这是一个异步操作,不会堵塞主事件循环。
    • AOF持久化:对于AOF持久化,其行为取决于配置。如果配置为appendfsync always,则每次写入都会同步到磁盘,这可能是同步的。如果配置为appendfsync everysecappendfsync no,则写入操作是异步的,因为实际的磁盘写入是延迟或由操作系统管理的。
    • 磁盘读取:对于磁盘读取(如重启时从RDB或AOF恢复数据),这通常是同步进行的,因为Redis需要这些数据来恢复其状态。

Redis 单线程为什么这么快

Redis 采用单线程模型的优势在于它能够避免多线程带来的复杂性,降低了线程切换和锁竞争的开销,以及更容易实现一些关键操作的原子性。

  1. 内存操作
    • 快速数据访问:Redis是一个基于内存的数据存储系统。内存操作比磁盘操作快得多,避免了磁盘寻址和磁盘I/O的开销。
  2. 高效的数据结构
    • 优化的实现:Redis内部使用高效的数据结构(如哈希表、跳跃表等),这些结构经过优化,能够快速执行数据操作,如添加、删除、查找和访问。
  3. 单线程模型
    • 避免上下文切换:多线程程序需要处理上下文切换的开销。Redis的单线程模型避免了这种开销,从而提高了效率。
    • 无需锁机制:Redis是单线程的,不需要担心数据同步和锁的问题。
  4. 非堵塞I/O
    • I/O多路复用:Redis利用I/O多路复用技术(如epoll、kqueue)来同时监听多个网络连接,从而提高网络通信效率。
    • 事件驱动模型:Redis使用基于非堵塞I/O的事件驱动模型。这意味着即使在执行I/O操作(如网络请求)时,Redis也不会被堵塞,而是能够继续处理其他任务。
  5. 优化的命令执行
    • 快速命令处理:Redis的大多数命令非常简单,可以在常数时间内完成(例如O(1)或O(log n))。
    • 管道化和批量操作:Redis支持管道化,允许客户端一次性发送多个命令,然后Redis以此快速处理,减少了网络往返延迟。

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

缓存雪崩

原理:缓存雪崩是指在缓存系统中,大量的缓存数据几乎同时失效(过期),导致所有的请求都直接落到数据库上,从而可能引起数据库压力过大、甚至宕机的问题。 解决方法

  1. 设置不同的过期时间:为缓存数据设置略微不同的过期时间,防止大量数据同时过期。
  2. 使用持久化:确保缓存层有持久化机制,这样即使缓存服务器重启,也可以从持久化存储中恢复数据。
  3. 设置备用缓存:建立备用缓存或多级缓存策略,当主缓存不可用时,可以使用备用缓存。
  4. 限流和降级:在系统架构中实现限流策略,以及在高负载时启用服务降级策略。

缓存击穿

原理:缓存击穿是指对某个热点 key 非常频繁的访问,在这个 key 突然失效的瞬间,大量请求直接达到数据库上,可能导致数据库短时间内承受巨大压力。 解决方法

  1. 设置热点数据永不过期:对于这些非常热门的数据,可以将它们设置为永不过期。
  2. 使用互斥锁:当缓存失效时,不是所有请求都去数据库加载数据,而是使用某种互斥锁机制确保只有一个请求去请求数据库加载数据库并回填到缓存中。
  3. 提前更新:监控这些热点key的访问频率和模式,根据预测在它们即将过期时提前更新它们的值。

缓存穿透

原理:缓存穿透是指查询一个一定不存在的数据,由于缓存不会命中,每次都要到数据库去查询,可能会被恶意利用,对数据库造成压力。 解决方法

  1. 布隆过滤器:在缓存之前使用布隆过滤器,它可以快速判断一个数据是否在数据集中,如果不存在,则无需查询数据库,直接返回空响应即可。
  2. 缓存空对象:即使某个数据在数据库中不存在,也将这个“空”结果缓存起来,避免对同一个不存在的数据发起多次查询。
  3. 参数校验:增加严格的参数校验,避免非法参数查询。

布隆过滤器是什么

布隆过滤器是一种空间效率很高的概率型数据结构,用于测试一个元素是否是一个集合的成员。它的主要特点是:

  1. 如果布隆过滤器判断元素不存在:那么该元素一定不存在于集合中。
  2. 如果布隆过滤器判断元素存在:则该元素可能存在于集合中。也就是说,存在一定的误判概率,即布隆过滤器可能会错误地判断某个不存在的元素为存在(称为假阳性)。

布隆过滤器的这种特性使其非常适合用于那些不需要100%准确性但对空间效率有较高要求的场景,如网络应用中的缓存穿透问题、垃圾邮件检测等。

原理简述

布隆过滤器通过多个独立的哈希函数对元素进行处理。当添加一个元素时,它会被所有的哈希函数分别哈希,然后在对应的位置上做标记。在查询时,会对元素使用相同的哈希函数,检查所有对应的位置是否都被标记过。如果所有位置都被标记,布隆过滤器判断元素“可能存在”;如果任何一个位置未被标记,则元素“一定不存在”。

场景

应用:缓存穿透问题

	**问题**:缓存穿透是指缓存和数据库中都没有的数据,但请求者故意进行重复请求的现象。如这些请求数据由于不存在,每次请求都要访问数据库然后返回,这将导致数据库压力过大。

	**措施**:布隆过滤器可以用来防止缓存穿透。方法是将所有可能查询的数据哈希到一个足够大的布隆过滤器中。查询时,先查询布隆过滤器,如果布隆过滤器说数据不存在,那么肯定不存在,请求可以拒绝,从而避免对底层数据源的查询压力。如果布隆过滤器认为数据可能存在,请求才会被进一步的查询数据库或缓存系统。

Redis和数据库如何做到一致性

  1. 缓存失效策略
    • 主动失效:每当数据库更新时,立即删除或更新缓存中的相应数据。
    • 延迟双删:在更新数据库之前和之后的适当时间间隔内,两次删除缓存中的数据。第一次删除是为了防止在更新数据库过程中有新的读请求得到旧的缓存数据,第二次删除是为了处理在更新数据库与第一次删除缓存间隙期间产生的旧缓存数据。
  2. 读写分离和最终一致性
    • 对于某些非关键性应用,可以接受最终一致性而不是强一致性。
    • 通过设置合理的缓存过期时间,可以在一定时间内自动更新缓存,减少数据不一致的时间窗口。
  3. 事务和锁机制
    • 在更新数据时使用事务和锁来确保数据库操作和缓存操作的原子性。
    • 这种方法适用于要求严格一致性的场景,但可能会影响系统的性能。

主从复制(读写分离)

// TODO

哨兵模式

// TODO

Redis 分布式锁

实现分布式锁

Redis 分布式锁底层实现的关键点在于Redis的事务性操作和原子性保证

Redis 分布式锁的原理主要基于它的命令, SETNX (SET if not exists),Redis 使用单线程处理命令,因此在执行 SETNX 命令期间不会发生竞态条件(线程竞争),即使多个客户端同时尝试设置同一个键,Redis也会确保只有一个客户端成功设置。这使得 SETNX 命令成为实现分布式锁的理想选择,因为它可以安全地用于多个客户端之间的协调。

  1. SETNX(SET if Not exists)命令:Redis中通常使用 SETNX 命令来尝试设置一个键的值,但仅在该键不存在时才设置成功。这一特性使得可以将键视为锁,当 SETNX 成功时标识锁被获取。

  2. 内部执行过程

    • 检查键是否存在:在执行 SETNX 命令时,Redis首先会检查指定的键是否已经存在于数据库中。
    • 如果键不存在,设置键的值:如果检查发现键不存在,Redis会执行设置操作,将指定的键设置为指定的值。这个设置操作是原子的,即Redis确保在多个客户端同时尝试设置相同键时,只有一个客户端会成功设置,而其他客户端将失败。
    • 返回结果:SETNX命令会根据操作的结果返回一个布尔值,通常是标识为1代表成功获取锁,0代表锁已被其他客户端持有。客户端可以根据这个返回值来确定是否成功获取锁或设置键的值。
  3. 设置超时时间:为了防止分布式锁被永远持有,可以为其设置一个超时时间(过期时间),以确保即使持有者在某些情况下崩溃或无法释放锁,锁最终也会自动释放。

  4. 锁的释放:锁的持有者完成其任务后应该释放锁,这通常通过删除键来实现:del lock_key

为什么Redis适合做分布式锁

  • 原子性操作:Redis的SETNX命令和EXPIRE命令是原子操作,这意味着它们在执行时不会受到竞态条件的影响。这使得在多个客户端之间安全地实现分布式锁非常容易。
  • 高性能:Redis是一个内存数据库,可以在微秒级别执行操作,因此在获取和释放锁时非常快速。这使得Redis适合用作分布式锁。
  • 可用性:Redis支持主从复制和分片,因此即使某个Redis节点发生故障,其他节点仍然可以提供服务。这确保了分布式锁的可用性。
  • 超时和自动释放:Redis可以为锁设置超时时间,确保锁在一段时间后自动释放,防止死锁情况的发生。
  • 简单易用:Redis的API自带了SETNX和EXPIRE命令,创建锁和管理锁非常容易。

Redis 6.0 多线程的改动

从 Redis 6.0版本开始,Redis开始引入了多线程模型来处理网络I/O。这是Redis架构中的一个重要变化,但需要注意的是,这个多线程模型主要用于网络请求的读写操作,并不涉及到数据的实际读写操作,也就是说,数据的实际读写还是单线程在处理。

Redis多线程模型的特点

  1. 限于网络I/O多线程在Redis中主要用于处理客户端请求的接受和响应的发送,即网络I/O操作。数据的读取和写入仍然在主线程中单线程执行,以保证原子性和一致性。
  2. 配置可调整:用户可以配置线程数量,以优化网络I/O的性能。默认情况下,多线程不会启用,需要用户配置。

为什么采用这种多线程设计

通过使用多线程处理网络I/O,Redis可以更好地利用现代多核CPU的能力,提高在高并发情况下的性能,尤其是在处理大量网络请求时。


  目录