NoSQL数据库——Redis缓存(3)

上篇我们讲了非关系型数据库的简介,有兴趣的朋友可以点击链接:

NoSQL数据库——简介(1)

NoSQL数据库——Redis(2)

这篇我们讲讲redis的缓存机制

一、持久化机制

我们知道redis是一个内存数据库,数据保存在内存中,也容易发生丢失。在了解redis的缓存机制之前,我们先来看看,redis如何确保数据不会丢失。只有我们将数据存储在计算机的内存中时,才能确保数据被写入到磁盘中。而如何确保数据在存储之前,redis不会挂、数据不会丢呢,我们分步来看。

不同的节点之间,如何备份、传输数据呢,Redis为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File),就是如何确保我们的数据在主从节点一致,且能被写入到磁盘不丢失。

1.1、RDB机制(异步)

RDB其实就是在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。

RDB原理

save, shutdown, slave 命令会触发一个操作,父进程执行fork操作创建子进程,遍历hash table,子进程创建RDB数据文件,系统利用写时复制(copy-on-write)机制,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换(定时一次性将所有数据进行快照生成一份副本存储在硬盘中)

RDB特性:

  • 粒度比较大,如果crash后,执行save, shutdown, slave 命令,则中间的操作没办法恢复。
  • RDB 是一个非常紧凑(compact)的文件,非常适合用于灾难恢复,能存储到别的数据中心,恢复大数据集比AOF要快。
  • RDB 的缺点是,会丢失数据,由于备份有时间间隔,一旦故障,会丢失最后一次备份点后的数据
  • 数据集较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户

1.2、AOF机制:

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF原理

在写、删除操作指令,子进程持续的写(写时复制机制)到一个类似日志临时文件里,不需要进行 seek。AOF 文件体积变得过大时,自动地在后台对 AOF 进行绝对安全的重写,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 新 AOF 文件写完,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并对新 AOF 文件进行追加操作。

AOF特性:

  • 粒度较小,crash之后,只有crash之前没有来得及做日志的操作没办法恢复。
  • AOF 让redis的数据非常耐久性,默认策略为每秒钟 fsync 一次,可以保持良好的性能,如果发生故障停机,也最多只会丢失一秒钟的数据
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行安全的重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 
  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积,关闭 fsync 可以让 AOF 的速度和 RDB 一样快。
  • AOF会因为阻塞命令 BRPOPLPUSH 引起无法将数据集恢复成保存时的原样 bug,而RDB绝对不会

一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。

1.3、混合持久化:

5.0默认开启混合模式,通过 bgrewriteaof 构建,子进程将内存数据以RDB格式写入aof临时文件,子进程将aof缓冲数据追加到aof临时文件,子进程通知主进程,主进程修改临时文件为aof文件

二、redis的分布式

先来简单了解下redis中提供的集群策略,虽然redis有持久化功能能够保障redis服务器宕机也能恢复并且只有少量的数据损失,但是由于所有的数据在一台服务器上,如果服务器出现故障,就算是有备份也不可避免数据丢失的问题。

怎么确保redis节点不会挂

随着业务数据量的上升,对缓存数据的CRUD操作也迅速增加,那么单节点的redis服务器是支撑不了庞大的数据存储需求的,所以得部署多台机器。分布式部署多台机器一般分两种(或三种)集群模式,一般的文档,都把redis的集群方式分成三种:主从、哨兵、集群(这里的集群只是广义集群的一种)。但是这么分类很不严谨,哨兵模式,单独使用是没有意义的,哨兵的作用有:监控、提醒、故障迁移,还是得配合主从模式,所以我们讨论主从+哨兵模式、Cluster集群两种情况

2.1、主从+哨兵模式

主从复制(异步)

与mysql的主从复制原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。 

全量复制和增量复制

Redis 在进行全量复制时,master 会将内存数据通过 bgsave 落地到 rdb,同时,将构建 内存快照期间 的写指令,存放到复制缓冲中,当 rdb 快照构建完毕后,master 将 rdb 和复制缓冲队列中的数据全部发送给 slave,slave 完全重新创建一份数据。传递 rdb 时还会占用大量带宽,对整个系统的性能和资源的访问影响都比较大。

而增量复制,master 只发送 slave 上次复制位置之后的写指令,不用构建 rdb,而且传输内容非常有限,对 master、slave 的负荷影响很小,对带宽的影响可以忽略,整个系统受影响非常小。

psync(复制积压缓冲)

在 Redis 2.8 之前,Redis 基本只支持全量复制。在 slave 与 master 断开连接,或 slave 重启后,都需要进行全量复制。在 2.8 版本之后,Redis 引入 psync,增加了一个复制积压缓冲,在将写指令同步给 slave 时,会同时在复制积压缓冲中也写一份。在 slave 短时断开重连后,上报master runid 及复制偏移量。如果 runid 与 master 一致,且偏移量仍然在 master 的复制缓冲积压中,则 master 进行增量同步。但如果 slave 重启后,master runid 会丢失,或者切换 master 后,runid 会变化,仍然需要全量同步。

在Redis 4.0 中强化了 psync,引入了 psync2。使用 replid(即复制id) 来作为复制判断依据。Redis 启动后,会创建一个长度为 40 的随机字符串,作为 replid 的初值,同时 Redis 实例在构建 rdb 时,会将 replid 作为 aux 辅助信息存入 rbd。重启时,加载 rdb 时即可得到 master 的复制 id。从而在 slave 重启后仍然可以增量同步。在建立主从连接后,会用 master的 replid 替换自己的 replid。同时会用 replid2 存储上次 master 主库的 replid。即便 slave 汇报的复制 id 与新 master 的 replid 不同,但和新 master 的 replid2 相同,同时复制偏移仍然在复制积压缓冲区内,仍然可以实现增量复制。

Redis 复制流程

在设置 master、slave 时,首先通过配置或者命令 slaveof no one 将节点设置为主库。然后其他各个从库节点,通过 slaveof $master_ip $master_port,将其他从库挂在到 master 上。

哨兵模式

哨兵(sentinel) 是一个分布式系统,可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master.每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置.

在这里插入图片描述

主从切换

当主节点客观下线时就需要进行主从切换,主从切换的步骤为:

  • 选出领头哨兵
  • 领头哨兵从在线的从数据库中,选择优先级最高的从数据库。优先级可以通过slave-priority选项设置。
  • 如果优先级相同,则从复制的命令偏移量越大(即复同步数据越多,数据越新),越优先。
  • 如果以上条件都一样,则选择run ID较小的从数据库。

2.2、Cluster集群

redis最开始使用主从模式做集群,若master宕机需要手动配置slave转为master;后来为了高可用提出来哨兵模式,该模式下有一个哨兵监视master和slave,若master宕机可自动将slave转为master,但它也有一个问题,就是不能动态扩充;所以在3.x提出cluster集群模式。Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

其结构特点:
1、所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2、节点的fail是通过集群中超过半数的节点检测失效时才生效。
3、客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
4、redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。
5、Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。

Cluster主从模式

为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份。我们需要保证集群中至少应该有奇数个节点,所以至少有三个节点,每个节点至少有一个备份节点,主节点、备份节点由redis-cluster集群确定,采用哈希槽 (hash slot)的方式来分配16384个slot 给节点。

三、Redis的策略

redis中有读写、过期、淘汰、持久化等策略,持久化机制上面已经讲过,这一节,我们就根据CRUD的顺序来讲讲这几种策略。

3.1、redis的读写策略

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

Cache-Aside可能是最常用的缓存策略,比较适合读请求比较多的场景。在这种策略下,应用程序(Application)会与缓存(Cache)和数据源(Data Source)进行通信,应用程序会在命中数据源之前先检查缓存。

  • 失效:应用程序先从cache取数据,没有得到,从数据库中取数据成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

缺陷:

  • 首次请求数据一定不在 cache 的问题——解决:将热点数据提前放入cache 中

  • 写操作比较频繁的话导致cache中的数据会被频繁被删除,影响缓存命中率 ——解决(1)加分布式锁来保证更新cache不存在线程安全问题 (2)类似乐观复制,允许短暂时间内,redis和数据库的不一致

Read/Write Through Pattern(读写穿透)

服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。这种缓存读写策略在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存Redis 并没有提供 cache 将数据写入DB的功能。

Write Through:

  • 先查 cache,cache 中不存在,直接更新 DB。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)

Read Through:

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。

Write Behind Pattern(异步缓存写入)

Write Behind Pattern 和 Read/Write Through Pattern 很相似,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

3.2、redis的过期策略

如果我们设置了Redis的key-value的过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用 定期删除 + 惰性删除 两种数据过期清除策略。

定期删除

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

1.从过期字典中随机 20 个 key;
2.删除这 20 个 key 中已经过期的 key;
3.如果过期的 key 比率超过 1/4,那就重复步骤 1;

redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。

惰性删除

所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。

3.3、redis的内存淘汰算法LRU/LFU

LRU(最近最少使用)/LFU(最不经常使用页置换算法)是很经典的内存淘汰算法,并不局限于用在redis中,在我们的计算机系统中,也会经常用到。这里我们仅仅探讨这两个内存淘汰算法在redis中的应用。

LRU算法

全称是 Least Recently Uses,按照最近最少使用的原则来筛选数据,LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。

如果有一个新数据 45 要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么LRU 算法做两件事:数据 45 是刚被访问的,所以它会被放到 MRU 端;算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。LRU 算法在实际实现时,需要用链表管理所有的缓存数据,移除元素时直接从链表队尾移除,增加时加到头部就可以了,但这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。Redis 默认会记录数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这里的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 N 个,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。

LFU算法

LFU,最不经常使用页置换算法是在Redis4.0后出现的,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。LFU算法能更好的表示一个key被访问的热度。

Redis 只用了 24bit (server.lruclock 也是24bit)来记录被访问的热度信息,16bit : 上一次递减时间 (解决访问次数太大一直占用着内存问题),8bit : 访问次数 (解决 key 访问次数无限大耗费存储空间的问题)由于新加入的 key 访问次数很可能比不被访问的老 key小,为了不被马上淘汰,新key访问次数设为 5。

3.4、redis的缓存淘汰策略

Redis通过maxmemory设指定最大内存,当内存占用超过阀值,按策略进行数据淘汰,淘汰策略通过maxmemory-policy设置

淘汰方式:

  • 同步删除 :直接从内存删除
  • 异步删除:开启延迟删除,集合类元素大于64 延迟
  • 删除配置 lazyfree_lazy_expire/lazyfree_lazy_eviction

淘汰策略

  • ​​​noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  • volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-random:加入键的时候如果过限,从所有key随机删除
  • allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键

通常情况下推荐优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。如果没有设置过期时间的键值对,那么 volatile-lru,volatile-lfu,volatile-random 和 volatile-ttl 策略的行为, 和 noeviction 基本上一致。

3.5、redis和数据库双写一致性

其实并没有一个准确的针对redis的更新策略,我们更多讨论的是,如何确保数据库与redis的一致,也就是Redis和数据库双写一致性问题。

针对这个问题,有很多解决方案来实现,我们这里只稍微列举出来,提供一种思路。

  • 使用多线程和队列方案解决
  • 写后延时双删
  • 一级缓存放热点数据(Caffeine)二级缓存(redis)放非热点数据的二级缓存设计
  • Redis订阅广播实现多级缓存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值