Redis理解

针对小林Coding 图解Redis 做的自己的理解:

图解 Redis

数据类型篇

Redis常用数据类型和应用场景

String

String 是最基本的 key-value 结构,value是具体的值,不仅是字符串,也可以是数字(整数或浮点数),value 最多可以容纳的数据长度为512M。

应用场景

  • 缓存对象

    • 直接缓存整个对象的JSON ,SET user:1 '{"name":"koyal","age":"18"}'
    • 采用将 key 进行分离为 user:ID 属性,使用 MSET 存储,用 MGET 获取各属性值,MSET user:1:name koyal user:1:age 18 user:2:name xxx user:2:age 20
  • 常规计数:计算访问次数、点赞、转发、库存数量等。

  • 分布式锁:SET 命令有个 NX 参数可以实现 key不存在才插入,可以实现分布式锁。

    • 如果 key 不存在,则插入成功,表示加锁成功
    • 如果 key 存在,则插入失败,表示加锁失败
  • 共享 Session 信息:在分布式系统之下,各个服务器都会去同一个 Redis 获取相关的 Session 信息。

List

List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。

应用场景

  • 消息队列:消息队列在存取消息时,必须要满足三个需求,分别是消息保存、处理重复的消息和保证消息可靠性。

    • 缺陷:List不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其他消费者再次消费。

Hash

Hash 是一个键值对(key-value)集合,value的形式:value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合存储对象。

应用场景

  • 缓存对象:Hash类型的(key,field,value)的结构与对象的(对象id,属性,值)的结构相似,可以用来存储对象。

  • 购物车:以用户 id 为 key ,商品 id 为 field,商品数量为 value,构成购物车的三要素。

Set

Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储2^32-1个元素,可以交集、并集、差集等等。

应用场景

集合的无序、不可重复、支持并交差等特性,比较适合用来数据去重和保证数据唯一性,还可以用来统计多个集合的交集、错集和并集等(Set的差集、并集和交集的计算复杂度较高,在数据量大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞)

  • 点赞:Set 类型可以保证一个用户只能点一个赞,key 为文章id,value 为用户id。

  • 共同关注:Set 类型支持交集运算,可以用来计算共同关注的好友、公众号等。key 可以为用户id,value 为已关注的公众号id。

  • 抽奖活动:Set 类型因为有去重功能,可以保证同一个用户不会中将两次。

Zset

Zset 类型(有序集合类型)相比 Set类型多个排序属性 score(分值),每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。

应用场景
Zset 类型可以根据元素的权重来排序。

  • 排行榜:学生成绩的排行榜,游戏积分排行榜等。

  • 电话、姓名排序:使用有序集合中的ZRANGEBULEXZREVRANGEBYLEX可以帮助我们实现电话号码或姓名的排序。

BitMap

BitMap,一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素,BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

应用场景

BitMap 类型非常适合二值状态统计的场景,其只有0和1两种,在记录海量数据时,可以有效节省内存空间。

  • 签到统计:可以用1表示签到,0表示未签到

  • 判断用户登录态:可以用1表示登录,0表示未登录

HyperLogLog

Redis 2.8.9 版本新增的数据类型,是一种用于统计基数的数据集合类型,基数统计是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog的统计规则是基于概率完成的,不是非常准确,标准误算率是0.81%。

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^63个不同元素的基数,和元素越多就越消费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

应用场景

  • 百万级网页 UV 计数:HyperLogLog 优势在于只需要花费 12KB 内存,就可以计算接近2^64个元素的基数。

GEO

GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

应用场景

  • 滴滴打车:GEOADDGEORADIUS这两个命令。

Stream

Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。其支持消息的持久化、支持自动生成全局唯一ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

应用场景

  • 消息队列:自动生成全局唯一消息ID,支持以消费组形式消费数据。

持久化篇

AOF持久化是怎么实现的

AOF 日志

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件中,然后重启 Redis 的时候,先去读取这个文件中的命令,并且执行它,那么就恢复了缓存数据。

这种保存写操作命令到日志的持久化方式,就是 Redis 中的 AOF(Append Only File)持久化功能,只会记录写操作命令,读操作命令是不会被记录的,因为没有意义。

Redis 实现执行写操作后,才将该命令记录到 AOF 日志里的,这样做有两个好处:

  • 避免额外的检查开销:在执行命令的时候,只有在该命令执行成功后,才将命令记录到 AOF 日志里,这样不同额外的检查开销,保证记录在 AOF 日志里的命令都是可执行并且正确的。
  • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

AOF 持久化功能也有潜在风险:

  • 数据丢失的风险:执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机。
  • 给下一个命令带来阻塞风险:再将日志内容写入到硬盘时,服务器的硬盘的I/O压力太大,就会导致写硬盘的速度很慢,进而阻塞住了,导致后续的命令无法执行

三种写回策略

AOF 将命令写回到硬盘的策略(在 redis.conf 配置文件中配置):

  • Always:每次写操作命令执行完成后,同步 AOF 日志数据写会硬盘。
  • Everysec:每次写操作命令执行完成后,先将命令写入到 AOF 文件的内核缓冲区,然后每个一秒将缓冲区里的内容写回到硬盘。
  • No:每次写操作命令执行完成后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

Always 策略可以最大程度保证数据不丢失,由于它没执行一条写操作命令就同步 AOF 内容写回硬盘,所以是不可避免会影响主进程的性能。

No 策略相比于 Always 策略性能较好,但是操作系统写回硬盘的时机是不可预知的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。

Everysec 策略是一种折中的方式,避免了 Always 策略的性能开销,也比 No 策略更能避免数据丢失(如果上一秒的写操作命令日志没有写回到硬盘,发生了宕机,这一秒内的数据自然也会丢失。)

总结:

  • 如果要高性能,就选择 No 策略
  • 如果要高可靠,就选择 Always 策略
  • 如果允许数据丢失一点,又想性能高,就选择 Everysec 策略

AOF 重写机制

AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小也会越来越大,会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个回复的过程会很慢。

Redis 为了避免 AOF 文件越来越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 会启用 AOF 重写机制,来压缩 AOF 文件。

AOF 重写机制就是在重写时,读取当前数据库中的所有键值对,然后将每个键值对用一条命令记录到新的 AOF 文件,等到全部记录完后,就会将新的 AOF 文件替换调现有的 AOF 文件。

为什么重写 AOF 的时候,不直接复用现有的 AOF 文件,而是先写到新的 AOF文件在覆盖过去?
因为如果 AOF 文件重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。

RDB 快照是怎么实现的

RDB 快照

Redis 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。

Redis 提供了两个命令来生成 RDB 文件,分别是 savebgsave,区别在于是否在主线程中执行。

  • 执行了 save 命令,会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,如果写入 RDB文件的时间过长,会阻塞主线程。
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞。

RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。

缺点:

  • 在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则影响 Redis 性能,AOF 日志可以以秒级的方式记录操作命令,丢失的数据相对更少。

执行快照时,数据能被修改吗?

在执行 bgsave 过程中,Redis 依然可以继续处理操作,也就是数据是能被修改的。关键的技术在于写时复制技术。

RDB 和 AOF 合体

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销

在 Redis 4.0 版本中,将 RDB 和 AOF 合体使用,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化,其工作在 AOF 日志重写过程。

使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

但是这样的话文件可读性会变差,而且只有 Redis 4.0 之后的版本才支持混合持久化。

Redis 大 Key 对持久化有什么影响。

大 Key 对 AOF 日志的影响

在使用 Always 策略的时候,主线程在执行命令后,会把数据写入到 AOF 日志文件,然后会调用fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。

  • 当使用 Always 策略的时候,如果写入的是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
  • 当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。
  • 当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。

大 Key 对 AOF 重写和 RDB 的影响

当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 AOF 重写机制。

AOF 重写机制和 RDB 快照(bgsave命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。父进程对共享内存中的大 Key 进行修改,那么内核就会发生写时复制,会把物理内存复制一份,由于大 Key 占用的物理内存是比较大的,那么在复制物理内存这一过程中,也是比较耗时的,于是父进程(主线程)就会发生阻塞。

大 Key 除了会影响持久化之外,还会有以下的影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 Key 是会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 Key 产生的网络流量较大,如果一个 Key 的大小是 1MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 Key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 Key 的 Redis 节点占用内存多,QPS也会比较大。

如何避免大 Key 呢?
最好在设计阶段,就把大 Key 拆分成一个一个小 Key。或者,定时检查 Redis 是否存在大 Key,如果该大 Key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 Key,因为该命令的删除过程是异步的,不会阻塞主线程。

功能篇

Redis 过期删除策略和内存淘汰策略

过期删除策略

Redis 是可以对 Key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。

如何设置过期时间?

设置 key 过期时间的命令一共有 4 个

  • expire <key> <n>:设置 key 在 n 秒后过期
  • pexpire <key> <n>:设置 key 在 n 毫秒后过期
  • expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期
  • pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期

在设置字符串时,也可以同时对 key 设置过期时间,共有 3 中命令

  • set <key> <value> ex <n>:设置键值对的同时,同时指定过期时间(精确到秒)
  • set <key> <value> px <n>:设置键值对的同时,同时指定过期时间(精确到毫秒)
  • setex <key> <n> <value>:设置键值对的同时,同时指定过期时间(精确到秒)

如何判定 key 已过期了?

每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典中,过期字典中保存了数据库中所有 key 的过期时间。

字典实际上是哈希表,哈希表的最大好处就是让我们可以用O(1)的时间复杂度来快速查找,当查询一个 key 时,Redis 首先检查该 key 是否存在于过期时间中

  • 如果不在,则正常读取键值。
  • 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 过期。

过期删除策略有哪些?

常见的三种过期删除策略:

  • 定时删除:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
    • 优点:可以保证过期 key 会被尽快删除,也就是内存可以被尽快的删除,定时删除是对内存最有好的。
    • 缺点:在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。
  • 惰性删除:不主动删除过期间,每次从数据库访问 key 时,都检查 key 是否过期,如果过期则删除该 key 。
    • 优点:这种策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
    • 缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会被释放,造成了一定的内存空间浪费,惰性删除策略对内存不友好。
  • 定期删除:每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key。
    • 优点:通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
    • 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
      • 难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对 CPU 不友好,如果执行的太少,那有和惰性删除一样了,过期 key 占用的内存不会及时得到释放。

Redis 过期删除策略是什么?

Redis 选择 惰性删除和定期删除 这两种策略配合使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

内存淘汰策略

当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此保障 Redis 高效的运行。

如何设置 Redis 最大运行内存?

在配置文件 redis.conf 中,可以通过参数maxmemory <bytes>来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略。

不同的操作系统中,maxmemory 的默认值是不同的。

  • 64 位操作系统,maxmemory 的默认值为0,表示没有内存大小限制,不管存放多少数据到 Redis 中,Redis 也不会对可用内存进行检查,直到 Redis 实例因内存不足而崩溃也无作为。
  • 32 位操作系统,maxmemory 的默认值是 3G,因为 32 位的机器最大支持 4GB的内存,系统本身也需要一定的内存资源来支持运行,这样可以避免因为内存不足导致 Redis 实例崩溃。

Redis 内存淘汰策略有哪些?

共八种,大体分为不进行数据淘汰进行数据淘汰两类策略。

不进行数据淘汰的策略

  • noeviction(Redis 3.0 之后,默认的内存淘汰策略):表示当运行内存超过了最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作的。

进行数据淘汰的策略(细分为在设置了过期时间的数据中进行淘汰在所有范围内进行淘汰

在设置了过期时间的数据中进行淘汰

  • volatile-random:随机淘汰设置了过期时间的任意键值
  • volatile-ttl:优先淘汰更早过期的键值
  • volatile-lru:(Redis 3.0 之前,默认的内存淘汰策略)淘汰所有设置了过期时间的键值中,最久未使用的键值
  • volatile-lfu:(Redis 4.0 之前新增的内存淘汰策略)淘汰所有设置了过期时间的键值中,最少使用的键值

在所有范围内进行淘汰

  • allkeys-random:随机淘汰任意键值
  • allkeys-lru:淘汰整个键值中最久未使用的键值
  • allkeys-lfu:(Redis 4.0 后新增的内存淘汰策略)淘汰整个键值中最少使用的键值

LRU 算法和 LFU 算法有什么区别?

LRU 全称为 Least Recently Used 最近最少使用,会选择淘汰最近最少使用的数据。

Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机去5个值(可配置),然后淘汰最久没有使用的那个。

Redis 实现的 LRU 算法的优点:

  • 不同为所有的数据维护一个大链表,节省了空间占用
  • 不用在每次数据访问时都移动链表项,提升了缓存的性能

LRU 的问题,无法解决缓存污染问题

LFU 全称为 Least Frequently Used 最仅最不常用,根据数据访问次数来淘汰数据,核心思想是:“如果数据过去被访问多次,那么将来被访问的频率也更高”。

LFU 算法相比于 LRU 算法的实现,多记录了数据的访问频次的信息。

Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。

  • 在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis 可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
  • 在 LFU 算法中,Redis 对象头的 24 bits 的 lru 字段被分成两段来存储,高 16 bit存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
    • ldt 是用来记录 key 的访问时间戳
    • logc 是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的 logc 初始值为 5。logc 会随时间推移而衰减的

高可用篇

主从复制是怎么实现的?

当将数据存储在一台服务器上

  • 如果服务器发生了宕机,由于数据恢复是需要点时间,那么这个期间是无法服务新的请求的。
  • 如果这台服务器的硬盘出现了故障,可能数据就都丢失了。

要避免这种单点故障,最好的方法就是将数据备份到其他服务器上,让这些服务器同样可以对外提供服务,这样即使有一台服务器出现了故障,其他服务器仍然可以继续提供服务。

在这里插入图片描述
Redis 提供了 主从复制模式 来保证这些服务器之间的数据一致性,且主从服务器之间采用的是读写分离的方式。

主服务器进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。

在这里插入图片描述

第一次同步

可以使用replicaof(redis 5.0 之前使用 slaveof)命令形成主服务器和从服务器的关系

比如,现有服务器 A 和服务器 B ,在服务器 B 上执行下面这条命令

# 服务器 B 执行这条命令
replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>

服务器 B 会变成服务器 A 的 从服务器,然后与主服务器进行第一次同步:

  • 第一阶段是建立连接,协商同步
  • 第二阶段是主服务器同步数据给从服务器
  • 第三阶段是主服务器发送新写操作命令给从服务器
    在这里插入图片描述

命令传播

主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接

在这里插入图片描述
主服务器通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。

这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。

分摊主服务器的压力

主从服务器在第一次数据同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。

主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,带来的问题:

  • 由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器会忙于使用 fork() 创建子进程,如果主服务器的内存数据非常大,在执行 fork() 函数时会阻塞主线程,从而使得 Redis 无法正常处理请求。
  • 传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。

为了解决这些问题,我们可以在从服务器上执行下面这条命令,使其作为目标服务器的从服务器:
replicaof <目标服务器的IP> 6379
如果目标服务器本身也是从服务器,那么该目标服务器不仅可以接受主服务器同步的数据,也会把数据同步给自己旗下的从服务器,从而减轻主服务器的负担。

增量复制

主从服务器在完成第一次同步后,就会基于长连接进行命令传播。

如果主从服务器间的网络连接断开了,那么就无法进行命令传播了,这是从服务器的数据就没办法和主服务器保存一致了,客户端就可能从从服务器读到旧的数据。
请添加图片描述
如果此时断开的网络,又恢复正常了,要怎么继续保证主从服务器的数据一致性呢?

  • 在 Redis 2.8 之前,从服务器会和主服务器重新进行一次全量复制,这样的开销太大了。

  • 在 Redis 2.8 之后,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
    请添加图片描述
    主服务器怎么知道要将哪些增量数据发送给从服务器呢?

  • repl_backlog_buffer,一个 环形 缓冲区,用于主从服务器断连后,从中找到差异的数据。

  • replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己 写 到的位置,从服务器使用 slave_repl_offset 来记录自己 读 到的位置。

Redis 主从节点是长连接还是短连接?

长连接

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

通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

  • Redis 主节点默认每个 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数 repl-ping-slave-period 控制发送频率。
  • Redis 从节点每个 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:
    • 实时检测主从节点网络状态
    • 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据
主从复制架构中,过期 key 如何处理?

主节点处理了一个 key 或者通过淘汰算法淘汰了一个 key ,这个时间主节点模拟一条 del 命令发送给从节点,从节点收到该命令后,就进行删除 key 的操作。

Redis 是同步复制还是异步复制?

Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。

主从如何做到故障自动切换?

Redis 哨兵机制,哨兵在发现主节点出现故障时,自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。

缓存篇

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

缓存雪崩

通常为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一些列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

发生缓存雪崩有两个原因:

  • 大量数据同时过期
  • Redis 故障宕机

针对不同的原因解决方法:

大量数据同时过期

  • 均匀设置过期时间:
  • 互斥锁:
  • 双 key 策略:
  • 后台更新缓存:

1、均匀设置过期时间:

如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间,可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样可以保证数据不会在同一个时间过期。

2、互斥锁:

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,要将数据更新到 Redis 里),缓存构建完后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

3、后台更新缓存
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。

事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。

解决上面的问题的方式有两种。

第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。

这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。

第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。

在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。

Redis 故障宕机

  • 服务熔断或请求限流机制:
  • 构建 Redis 缓存高可靠集群:

1、服务熔断或请求限流机制

可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库。然后等到 Redis 恢复正常后,在允许业务应用访问缓存服务。

为了减少对业务的影响,可以启用请求限流机制只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。

2、构建 Redis 缓存高可靠集群:

服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过主从节点的方式构建 Redis 缓存高可靠集群。

如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题

缓存击穿

业务中会有几个数据被频繁地访问,比如秒杀活动,这类被频繁地访问的数据被称为热点数据。

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

缓存击穿和缓存雪崩很相似,可以认为击穿是雪崩的一个子集。

应对缓存击穿可以采取前面说到两种方案:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

缓存穿透

当用户访问的数据,即不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透的发生一般有这两种情况:

  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据。
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务。

应对缓存穿透的三种常见方案:

  • 非法请求的限制
  • 缓存控制或者默认值
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

第一种方案,非法请求的限制

当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

第二种方案,缓存空值或者默认值

当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值