Redis进阶 一篇怼完

文章目录

      • Redis 过期的策略
        • 定期删除
        • 惰性删除
        • 淘汰机制
        • LUR 算法原理简单说明
      • 怎么保证 Redis 高可用?
        • 主从复制原理
        • 端点续传
          • 同步(sync)
          • 命令传播方式(command propagate)
        • 哨兵机制(sentinel)
          • 哨兵核心原理
          • slave 选举算法
          • sentinel的自动纠正
          • 选举条件
        • 数据丢失情况
          • 异步复制数据丢失
          • 集群脑裂
          • 解决
      • 持久化
        • RDB
          • 说明
          • RDB 自动保存
          • RDB 文件结构
        • AOF
      • 可能出现问题的使用场景
        • 雪崩
        • 穿透
        • 击穿
        • 缓存一致性
        • 并发竞争问题
        • 总结

如果有理解错误的地方可以在评论指出,个人再做下功课

Redis 过期的策略

     可能有时候你会遇到一个场景,在 Redis 中大量的 key 明明已经过期了,结果发现 Redis 的占用还是很高?如果要明白为什么,那就要知道Redis 的过期策略。策略分为 2 种,定期删除与惰性删除。

定期删除

    假设在 Redis 中存在 10w 个设置了过期的 key,如果此时 Redis 每隔指定毫秒每隔都扫描一遍看下有没有过期,那这个时候 Redis 基本就废了,CPU 负载会贼高,性能全花在了扫描过去可以上了。
     而 Redis 使用定期删除,默认每隔 100ms 就随机抽取一些设置了过期时间的 key,检查是否过期,过期就删除。所以你会发现设置了过期时间但是为什么没有在指定时间点删除,就是因为这个。

惰性删除

    定期删除会导致可能很多过期 key 没有被删除,那怎么办?所以这里就配合和惰性删除,在你获取某个 key 时候, redis 会检查一些这个 key 如果设置了过期时间并且删除了,那么就会删除掉,不会返回任何东西。


通过上面 2 中情况来保证 key 被干掉,但是还是有一种情况。如果 key 没被定期删除,也比较少访问这个 key 也没被惰性删除,然后导致大量的无效 key 堆积,导致内存贼高,最后直接超出内存耗尽,这个时候怎么办?这里就要用到淘汰机制了。

淘汰机制

如果 redis 内存占用超过了设置的阀值后,已经当不足以容纳新写入的数据时候,就会进行内存淘汰,策略如下:

  1. noeviction:新写入操作会报错。
  2. allkeys-lru:在键空间中,移除最近最少使用的 key(常用,下面有 LRU 算法原理说明)
  3. allkeys-random:随机移除某个 key,这个没必要使用吧。
  4. volatile-lru:只限于设置了 expire 的key,随机移除一个
  5. volatile-ttl:只限于设置了 expire 的key,优先删除剩余时间(time to live,TTL) 短的key。
  6. volatile-lru:只限于设置了 expire 的key, 优先删除最近最少使用(less recently used ,LRU) 的 key。

最近才知道在 5.0 时候又添加了 LFU (最少使用的)算法,为每个key维护一个计数器。每次key被访问的时候,计数器增大。计数器越大,可以等于访问越频繁。

LUR 算法原理简单说明

LUR 算法就是淘汰最少使用的数据,比如历史记录。核心思想就是 如果数据最近被访问过,那么将来被访问的几率也更高

比较常见的实现就是使用链表结构来实现,写数据时候就压栈,访问数据时候,将原本数据取出,再放入链表中的表头。这个时候就能保证最少使用会在表的尾部。如下图:
Redis进阶 一篇怼完_第1张图片

怎么保证 Redis 高可用?

如果 Redis 要实现高可用,首先需要实现读写分离,为了避免读写分离中出现的 master 挂掉导致 redis 不可用。需要使用 哨兵机制(Sentinel )配合。来达到 master 挂掉后再从 slave 中选取一个作为主节点。此时还要再考虑一个问题,如果 Sentinel 为单节点,也挂掉了,那不就又会造成服务不同啊,所以 Sentinel 也要搭建集群。需要注意的是,redis 与 Sentinel 都要最少三个节点
Redis进阶 一篇怼完_第2张图片

主从复制原理

主从全量复制大致流程图:
Redis进阶 一篇怼完_第3张图片

细节点的流程说明:

  1. slave 启动时候,就只是保存了 master 的信息包括 host 和 ip,但是复制的流程还没有开始
  2. slave 内部有个定时任务,每秒检查是有新的 master 要连接 和 复制,如果有就跟 master 建立 scoket 网络连接
  3. slave 给 master 节点发送 ping 命令,然后进行口令认证(如果有)
  4. 如果第一个连接到 master 就会触发 full resynchronization
  5. master 会启动一个后台线程执行 bgsave 命令生产 rdb 文件,然后再发给 slave
  6. slave 收到快照文件后,先写到磁盘。然后再装在 rdb 数据

在认识到复制过程后,再引入 2 个概念配置
无磁盘化复制
针对上图第 2 步时候,生产 rdb 时,可以在配置文件中指定生成 rdb 时候,是在内存中生成然后直接发送,还是先保存到磁盘中然后在发送过去。通过如下配置:
repl-diskless-sync:
no:会开启一个线程,在内存中生成 rdb
yes:子进程生成 rdb,然后落到磁盘,再发给slave
repl-diskless-sync-delay:等待多时秒后开始给 slave 发送rdb文件,设置这个时间因为,好不容易生成一个 rdb,要等待更多的 salve 重新连接后再复制过去(上图第2步时候,为什么说等待指定时间再给 slave)

内存缓冲区超出
继续看第 2 步,在生成 rdb 快照的过程中,客户端新来的 写命令 都会写到内存缓冲区中,客户端有2种情况会直接将将缓冲区写满(配合文件中设置),导致复制失败。默认为如下:

  1. 60s 内缓冲区大小一直超过 64m
  2. 或者内存缓冲区突然暴涨,超过了 256 m。

修改配置内容如下,需要根据实际情况配置:
client-output-buffer-limit slave 256MB 64MB 60

端点续传

上面是第一次时候复制说明会进行全量复制,如果要是主从突然断开了,然后又重新连接上了,这个时候该怎么同步怎?这里就要明白断点续传的原理了,在 redis 2.8 之前使用的是同步(sync) 之后就是命令传播方式(command propagate)

同步(sync)

这种方式性能不是太好,原理比较简单,基本类似于上面全量同步,就是在重新连接后,发送一个全量的 rdb 快照。假如在断开时候, master 就收到 2 个写命令,那这个时候如果弄一个全量的 rdb 文件发过去,那就太 2 了。而且这样代价也非常大。

  1. master 生成 rdb 会耗费服务器大量的 CPU、内存和磁盘IO
  2. master 发给 slave 时候会产生大量网络 IO
  3. slave 在载入 rdb 文件时候会阻塞处理命令的请求
命令传播方式(command propagate)

这里主要说明下命令传播方式,在了解命令传播方式时候,需要明白 3 个概念:
复制偏移量 (replication offset)
执行复制的双方(主、从)两端都会维护一个复制偏移量,从服务器在连接的时候,将 offset 发送过去,以此来判断这个从服务器是否与 master 数据一直,那么说了那么多,这个偏移量到底是啥玩意呢?如下图:
Redis进阶 一篇怼完_第4张图片

客户端发送添加命令 set key ‘a’,假设 ‘a’ 就 2 个字节,此时 master 的就是偏移量就等于 offset = 2,然后再向从服务器去发送 2 个字节,从服务器同步完后在此时偏移量为 2。
Redis进阶 一篇怼完_第5张图片
slave1 突然与 master 连接中断了,并且中断过程中 master 又写了 N 条数据。
Redis进阶 一篇怼完_第6张图片
当 slave1重新连接时候,将自己的 offset 发送给 master,master 一比较,发现从服务数据缺了一部分,然后就从缺的 offset+1 到 6,发送给 slave1 ,以此来达到增量的效果。

所以 offset 就是做用来记录数据量的一个测量标准吧(佩服自己说话都那么专业了),并且可以在主从断点续传时候的偏移量差值获取到少的一部分数据。

复制积压缓冲区 (replication backlog)
    复制积压缓冲区就是维护的一个固定长度 先进先出的 队列,默认大小为 1 MB。假设队列长度为3,这个时候客户端发送来了一批请求,需要存储 a b c d,此时如图:
Redis进阶 一篇怼完_第7张图片

    上面只是说明 复制积压缓冲区 究竟是什么,其实就是一个队列,用来存储客户端发来的写命令。其实这个队列又与偏移量绑定,最终存储形式为:
在这里插入图片描述

看到这个是不是明了偏移量 与 复制缓冲区的作用,master 中维护着复制缓冲区,然后 slave 维护着自己的 offset,在同步的时候 master 才知道该从哪里开始复制。



那么复制缓冲区该怎么调整?
    如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,缓冲区被写满替换掉了一部分数据,那么这个大小也许并不合适。因此,正确估算和设置复制积压缓冲区的大小非常重要。复制积压缓冲区的最小的容量可以根据公式:second * write_size_per_second 来估算
        □ 其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算)。
        □ 而write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)。

    例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。

    为了安全起见,可以将复制积压缓冲区的大小设为 2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。至于复制积压缓冲区大小的修改方法,可以参考配置文件中关于repl-backlog-size选项的说明。

服务器的运行id (run id)
    这个就好理解了,无论是 master 还是 slave 都会维护一个 run id,在服务器启动时候就自动生成了,例如:53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3。当 slave 对 master 初次复制时候,master 会将自己的 run id 给 slave。
    slave 在重新连接 master 时候,会将之前连接的 master 的 run id 也发送过去。当时 master 端收到后,和自己的一比较。如果一样说明之前这个 slave 一直连接的就是自己,只需要增量复制即可。反之就全量复制。

哨兵机制(sentinel)

    只有主从复制还是不能够达到高可用的,因为一旦 master 挂了之后,还是导致 redis 会崩溃,此时所以的请求全部打到了数据库,很大可能直接把数据库给打死。所以这里需要哨兵机制去监视 master 的状态。
    作为监听 master 状态的,那么如果 sentinel 单机的话,挂掉了,此时 master 也挂掉了,那该怎么办?所以这种情况都是一环套一套的,A 依赖 B,如果 B 挂了,那么对 A 那不是又是灾难性的嘛。所以 sentinel 还要搭建集群。

哨兵核心原理

哨兵主要功能如下:
集群监控:负责监控 redis master 和 slave 进程是否正常工作
消息通知:如果某个 redis 示例有故障,那么哨兵负责发送消息作为报警通知给管理员
故障转移:如果 master node 挂掉后,会自动转移到 slave node 上
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

1. sdown 和 odown 转换机制
sdown:
    是主观宕机,就是一个哨兵自己觉得 master 宕机了,那么就是主观宕机。所以达成条件很简单,如果一个 ping 一个 master ,超过了 is-master-down-after-milliseconds 指定的毫秒数后,就主观的认为 master 宕机了。

odown:
    是客观宕机,如果一个 指定数据(quorum) 的哨兵都觉得一个 master 宕机了,那么就是 客观宕机。所以从 sdown 转换到 odown时候,条件就是一个哨兵在指定时间内,收到了其他哨兵也认为那个 master 主观宕机了,那么就认为客观宕机。

2. 哨兵 和 salve 集群的自动发现机制
    哨兵与哨兵之间的发现,是通过 redis 中创建 pub/sub 发布订阅模式实现的,每个哨兵都会向 sentinel:hello 这个 channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对 master 的监控配置。
Redis进阶 一篇怼完_第8张图片
3 个定时任务
每隔10s,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
Redis进阶 一篇怼完_第9张图片

每隔 2 秒,向频道中发送发:当前哨兵的信息 + 当前哨兵对主节点的判断。每隔哨兵都会订阅到该频道,然后去了解其他信息对主节点的判断。(sentinel 同时也是订阅者)
Redis进阶 一篇怼完_第10张图片

每隔1秒,每个sentinel节点会向主节点、从节点、其余sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。
Redis进阶 一篇怼完_第11张图片

slave 选举算法

当 sentinel 客观认为 master 废了的时候,就会进行选举算法,让一个 slave 做为主节点。而哨兵搭建的需要最少 3 个节点。这些因为客观宕机需要最少 2 个 sentinel 都认为宕机了才行。那么假设现在就 2 个 sentinel,挂了一个,还怎么满足客观宕机的条件?所以说需要最少 3 台机。

sentinel的自动纠正

哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要潜在的 master 候选人,哨兵会确保 slave 咋复制现有 master 的数据。
如果一个 slave 连接到一个错误的 master 上,比如故障转移之后,那么哨兵会确保它连接到正确的 master上。

选举条件

选举leader。当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个 Sentinel 会进行协商,选举出一个领头 Sentinel,并由领头 Sentinel 对下线主服务器执行故障转移操作。选举 leader 规则为:

  1. 所有 sentinel 都有资格被设置为 leader

  2. 每个发现 master 进入客观下线的 Sentinel 都会要求其他 Sentinel 将自己设置为局部领头 Sentinel。

  3. 一旦称为了 leader 后,就有权利将某个 sentinel 设置为局部领头的机会,一旦设置就无法更改。
    当sentinel 被选举为 leader(A) 后,向以另外一个 sentinel(B) 发送一个 is-master-down-by-addr,并且命令中的 runid参数是字节的 run id,表示要设置 B 为局部领头的 sentinel。如果一个 sentinel 被半数多的 sentinel 设置成了局部领导,那么这个 sentinel 下次就会成为 leader

  4. 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

选举slave。如果一个 master 被认为 客观宕机了,而且多数哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来会经历以下步骤:

  1. sentinel 会维护一个 slave 列表,先将下线的的 slave 删,保证列表中都是在线的
  2. 删除列表中 5s 内没有回复 sentinel info命令的 salve。
  3. 删除所有与主服务器连接断开超过指定时间( down-after-milliseconds10 )的 salve。
    down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds
    10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。
  4. 根据 slave 的优先级进行排序。会按照 偏移量、run id排序。

数据丢失情况

异步复制数据丢失

上面的情况看着是解决了高可用很美好,很强。但是细品下好像还是会有点问题,在master 节点收到了 N 个写的命令,还在内存中,没有去做同步给 slave 节点,恰恰就在此时,主节点嘎嘣挂了。而 Sentinel 检测到有新选择了个主节点,此时就会导致数据丢失。
Redis进阶 一篇怼完_第12张图片

集群脑裂

这个脑裂就要留意下这种情况的发生了。再假设一个场景,还是一主双从,但是有点不一样的是,主节点 与 从节点不在一个局域网中。在 sentinel 检查 master 是否挂时候,出现了网络问题导致 sentinal 误认为 master 挂了,然后又选举了个主节点,这个时候可就存在了 2 个主节点啦。
然后更恶心的是,一个客户端还在向老的 master 写数据。
Redis进阶 一篇怼完_第13张图片

解决

通过下面配置来解决这两种情况的丢失问题

min-slaves-to-write 1
min-slaves-max-lag 10

配置说明:要求至少有1个slave,在数据复制和同步的延迟不能超过10秒。如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了

此时无论是异步复制 还是 脑裂情况,都会丢失的10s 的数据。丢失数据情况不能百分之解决,但是可以避免减少这种情况

持久化

RDB

说明

    在执行 SAVE 命令或者 BGSAVE 命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。RDB 文件会对应多个,每个数据文件都代表了某一个时刻中的 redis 数据。子进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行持久化。对于已过期的键不会假造到 RDB 中
    服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止

RDB 自动保存

    Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

save 900 1
save 300 10
save 60 10000

那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
❑服务器在900秒之内,对数据库进行了至少1次修改。
❑服务器在300秒之内,对数据库进行了至少10次修改。
❑服务器在60秒之内,对数据库进行了至少10000次修改。
举个例子,以下是Redis服务器在60秒之内,对数据库进行了至少10000次修改之后,服务器自动执行BGSAVE命令时打印出来的日志:

[5085] 03 Sep 17:09:49.463 * 10000 changes in 60 seconds. Saving...
[5085] 03 Sep 17:09:49.463 * Background saving started by pid 5189
[5189] 03 Sep 17:09:49.522 * DB saved on disk
[5189] 03 Sep 17:09:49.522 * RDB: 0 MB of memory used by copy-on-write
[5085] 03 Sep 17:09:49.563 * Background saving terminated with success

自动保存实现原理
记录更改次数与时间: redis 里面有 2 个变量 dirty 计数器 和 lastsave属性:

  1. dirty计数器:记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
  2. lastsave属性:是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。

假设 set test “xxx” 时候计数器就会 +1, 而 SADD
testKey a b c 此时计数器增加 3。

检查是否满足保存条件:Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。

RDB 文件结构

RDB 文件结构总分为 5 部分,为了方便区分变量、数据、常量,图10-10中用全大写单词标示常量,用全小写单词标示变量和数据。本章展示的所有RDB文件结构图都遵循这一规则:
在这里插入图片描述

REDIS:
    RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着“REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。说白了,就是在读取时候,如果数据文件里面以这个开头就表示是一个 RDB 文件。

db_version:
    长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号

database:
    包含着零个或多个 Redis 数据库,以及各个数据库中的键值对数据。如果所有数据库都是空的,那么这个部分也为空,长度为0字节。如果至少一个数据库非空,那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。

EOF:
    常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了。

check_sum:
    是一个8字节长的整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现

AOF

    aof 方式是以日志形式记录,在收到客户端的写操作时候,将写操作按顺序记录在日志中,启动时候在执行一遍日志中的命令。针对过期键会生成一个 DEL 命令。因此,数据库中包含过期键不会对AOF重写造成影响。当 RDB 与 AOF 都开启是会先加载 AOF

    为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果服务器突然宕机,那么保存在内存缓冲区里面的写入数据将会丢失。为此,系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

AOF日志分为三种同步方式:
    appendfsync=always:
        将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,所以always的效率是appendfsync选项三个值当中最慢的一个,但从安全性来说,always也是最安全的。网上很多说不会丢失数据,其实还是会丢一个事件循环中产生的数据(虽然对事件循环不太了解,留给后面的自己再去参悟吧[笑脸])

    appendfsync=everysec:
        将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。

    appendfsync=no:
        将命令写入到 aof_buf 缓冲区中,而何时同步到磁盘,则由操作系统控制。当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据。

可能出现问题的使用场景

雪崩

服务器在某个时间点,因为某些原因导致缓存不可用了。此时所有的请求都会打到 DB,很可能导致 DB 挂掉。或者大量热点数据在同一时间失效了,流量都打到了 DB,这个时候很可能把 BD 给打挂。

解决方案:

  1. 首先肯定要搭建服务的高可用
  2. 虽然搭建了集群,如果还是挂了,就需要走备选方案了。此时就需要借助内存缓存去抗下高流量避免全部打到 DB,但是在刚使用内存缓存时候,需要去数据库查,这个时候需要限流+降级处理了,降级的用户再次刷新还是可以看到。
    Redis进阶 一篇怼完_第14张图片

穿透

请求的 key 在数据不存在,没有将 null 放入缓存,导致会一直查数据库。如果被人利用恶意的攻击,发送大量查询这个 key 的请求,就会导致数据库直接挂掉
当 testkey 不存时候,每秒请求 5k 次去请求这个key,此时如果不做处理,会给数据库造成较大压力,还有一种情况,攻击者会根据 id 获取数据时候,传入 -1, -2, -3 这些不存的数据,需要防范
Redis进阶 一篇怼完_第15张图片

解决方案:

  1. 对查询结果为 null 的的数据进行缓存(设置过期时间)
  2. 提前对数据进行提前预热,使用bitmap、或者布隆过滤器 去看存不存在
  3. key加密

击穿

请求的 key 存在,但是这个数据是热点数据,在缓存过期的一小段时间内。大量请求瞬间打到 DB,将 DB 打垮。
Redis进阶 一篇怼完_第16张图片

解决方案:

  1. 能不能设置为不过期,如果更新数据,就发送到消息队列,由消费者去更新数据。

  2. 加锁,使用分布式锁,防止击穿。因为本身就是热点数据,如果加锁,可能会造成性能瓶颈。(个人感觉应该限流 + 锁 + 降级,限流避免请求大量在阻塞,造成严重后果,而且保证一部分人此次还是可以看到接口数据的。另外一部分人走降级策略吧,再刷新下又可以看到了)

  3. (个人想法)热点数据如果硬件资源比较好一点,可以使用 内存缓存 + Redis。例如 Redis 为 100s 过期,那么内存缓存设置 120s。如果 redis 失效了,此时就走内存缓存,并且同时去查询数据库(需要保证只会查一次数据库)。这样可以实现在 key 过期时候,内存缓存去抗下高流量。查完后同时再将 redis 与 内存缓存的数据都更新

缓存一致性

一般我们操作缓存时候大致的场景为以下场景:
1. 先更新数据库,然后获取最新的数据,再对应的 修改/删除。
Redis进阶 一篇怼完_第17张图片
当接收到一个请求时候,首先去改了数据库,此时数据库修改成功了。但是去修改缓存时候失败了。导致数据不一致情况。所以说将修改缓存放入数据库操作后,会出现一些不可意料的问题。

2. 先删缓存,然后再更新数据库 (Cache Aside Pattern)
Cache Aside Pattern 原则其实就是一下2点:

  1. 读的时候,先读缓存,如果缓存没有,那么就读数据库,然后取出数据后放入缓存。
  2. 更新的时候,先删除缓存,然后再更新数据库

可以看出第一点的方式会出现不一致问题,那么为什么先删除缓存然后再更新就不会出问题了?
这是因为删除缓存后,去更新数据库,不管更新数据库成功还是失败了。反正下次查询缓存时候,发现没有缓存都会查询一新的数据。

| | |

客户端 redis 数据库
发送扣库存 num - 1
删除key num
修改 num -1 = 99
客户端1查询 num key
发现没有num,然后去查询 num = 99。所以就算数据库之前修改错误也不慌

这种方案问题:
其实这种方案还是存在问题的,那就是删除完缓存后,当正在更新数据库,这个时候来了查询缓存的,直接查出来又设置进去缓存了,又出现了一致性请求,情况流程如下:

客户端 redis 数据库
发送扣库存 num - 1
删除key num
修改数据库正在进行中
客户端1查询 num key
发现没有num,然后去查询 num = 100
修改数据库完毕!num = 99

畅想解决方案:
    我们在这里总结下,其实导致上面 2 个解决方案不可用的最根本问题就是,并发时候的又读又写,而且没有保证到原子性。如果说真的就是要做到那么强一致性,那就要设计一套缓存存储了。需要强一致性的地方的数据,保证原子性,或者说需要 读写互斥。其实可以借助队列在写的时候就放入队列,然后读时候查看队列中是否存在这个 id。

并发竞争问题

并发竞争有些场景下多个客户端都需要修改某个key,正常情况下按发送请求的时间来执行结果为 1、 2、3、4,但是并发出现了:4、 3、 2、1。也就是说,最后结果应该是 4 的,但是成了 1 了。

这种情况下使用以下解决方案:

  1. 使用 分布式锁+时间戳,通过上锁来执行。如以下:
    1 10:00:00
    2 10:00:01
    3 10:00:02
    4 10:00:03
    假设2、 3、 4 都执行了,现在执行到了 1 了,这个时候查看下 reids 中存的时间戳与 1 的时间戳是否一致,如果不一致那就不更新了

  2. 使用消息队列串行执行

总结

针对上面的这些问题,还是建议实现一套自己的缓存系统。无论是并发环境下各种 key 的写入问题,还是热点数据 key 查询出现时候的一系列问题。将问题归纳整理后,问题的本质都是相似的,而这个系统就作为这些场景的一系列解决方案或者不同的解决方案。(有感而发,其实我也没做过[笑脸],只是感觉应该这样去做,而不应该零零散散的去解决这些问题,这样只会让系统越来越臃肿,应该化零为整)

你可能感兴趣的:(redis)