“众里寻他千百度,蓦然回首,那人却在,灯火阑珊处”。多年的 IT 生涯,始终心愿本人写的程序可能有很强的健壮性,也始终心愿能找到一个高可用的标杆程序去借鉴学习,不畏惧内存溢出、磁盘满了、断网、断电、机器重启等等状况。但意想不到的是,这个标杆程序居然就是从一开始就在应用的分布式缓存——Redis。
Redis(Remote Dictionary Server ),即近程字典服务,是 C 语言开发的一个开源的高性能键值对(key-value)的内存数据库。因为它是基于内存的所以它要比基于磁盘读写的数据库效率更快。因而 Redis 也就成了大家解决数据库高并发拜访、分布式读写和分布式锁等首选解决方案。
那么既然它是基于内存的,如果内存满了怎么办?程序会不会解体?既然它是基于内存的,如果服务器宕机了怎么办?数据是不是就失落了?既然它是分布式的,这台 Redis 服务器断网了怎么办?
明天咱们就一起来看看 Redis 的设计者,一名来自意大利的小伙,是如何打造出一个超强健壮性和高可用性的程序,从而不害怕这些状况。
一、Redis 的内存管理策略——内存永不溢出
Redis 次要有两种策略机制来保障存储的 key-value 数据不会把内存塞满,它们是:过期策略和淘汰策略。
1、过期策略
用过 Redis 的人都晓得,咱们往 Redis 里增加 key-value 的数据时,会有个选填参数——过期工夫。如果设置了这个参数的值,Redis 到过期工夫后会自行把过期的数据给革除掉。“过期策略”指的就是 Redis 外部是如何实现将过期的 key 对应的缓存数据革除的。
在 Redis 源码中有三个外围的对象构造:redisObject、redisDb 和 serverCron。
- redisObject:Redis 外部应用 redisObject 对象来形象示意所有的 key-value。简略地说,redisObject 就是 string、hash、list、set、zset 的父类。为了便于操作,Redis 采纳 redisObject 构造来对立这五种不同的数据类型。
- redisDb:Redis 是一个键值对数据库服务器,这个数据库就是用 redisDb 形象示意的。redisDb 构造中有很多 dict 字典保留了数据库中的所有键值对,这些字典就叫做键空间。如下图所示其中有个“expires”的字典就保留了设置过期工夫的键值对。而 Redis 的过期策略也是围绕它来进行的。
- serverCron:Redis 将 serverCron 作为工夫事件来运行,从而确保它每隔一段时间就会主动运行一次。因而 redis 中所有定时执行的事件工作都在 serverCron 中执行。
理解完 Redis 的三大外围构造后,咱们回到“过期策略”的具体实现上,其实 Redis 次要是靠两种机制来解决过期的数据被革除:定期过期(被动革除)和惰性过期(被动革除)。
- 惰性过期(被动革除):就是每次拜访的时候都去判断一下该 key 是否过期,如果过期了就删除掉。该策略就能够最大化地节俭 CPU 资源,然而却对内存十分不敌对。因为不实时过期了,本来该过期删除的就可能始终沉积在内存外面!极其状况可能呈现大量的过期 key 没有再次被拜访,从而不会被革除,占用大量内存。
- 定期过期(被动革除):每隔肯定的工夫,会扫描 Redis 数据库的 expires 字典中肯定数量的 key,并革除其中已过期的 key。Redis 默认配置会每 100 毫秒进行 1 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是采纳了如下办法:
(1)从过期字典中随机取出 20 个键;
(server.h 文件下 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
配置 20)
(2)删除这 20 个键中过期的键;
(3)如果过期键的比例超过 25%,反复步骤 1 和 2;
具体逻辑如下图:
因为 Redis 中同时应用了惰性过期和定期过期两种过期策略,所以在不同状况下使得 CPU 和内存资源达到最优的均衡成果的同时,保障过期的数据会被及时革除掉。
2、淘汰策略
在 Redis 可能没有须要过期的数据的状况下,还是会把咱们的内存都占满。比方每个 key 设置的过期工夫都很长或不过期,始终增加就有可能把内存给塞满。那么 Redis 又是怎么解决这个问题的呢?——那就是“淘汰策略”。
官网地址:https://redis.io/topics/lru-c…
Reids 官网下面列出的淘汰策略一共有 8 种,但从本质算法来看只有两种实现算法,别离是 LRU 和 LFU。
LRU(Least Recently Used):翻译过去是最久未应用,依据时间轴来走,淘汰那些间隔上一次应用工夫最长远的数据。
LRU 的简略原理如下图:
从上图咱们能够看出,在容器满了的状况下,间隔上次读写工夫最长远的 E 被淘汰掉了。那么数据每次读取或者插入都须要获取一下以后零碎工夫,以及每次淘汰的时候都须要拿以后零碎工夫和各个数据的最初操作工夫做比照,这么干势必会减少 CPU 的负荷从而影响 Redis 的性能。Redis 的设计者为了解决这一问题,做了肯定的改善,整体的 LRU 思路如下:
(1)、Redis 里设置了一个全局变量 server.lruclock 用来寄存零碎以后的工夫戳。这个全局变量通过 serverCron 每 100 毫秒调用一次 updateCachedTime()更新一次值。
(2)、每当 redisObject 数据被读或写的时候,将以后的 server.lruclock 值赋值给 redisObject 的 lru 属性,记录这个数据最初的 lru 值。
(3)、触发淘汰策略时,随机从数据库中抉择采样值配置个数 key, 淘汰其中热度最低的 key 对应的缓存数据。
注:热度就是拿以后的全局 server.lruclock 值与各个数据的 lru 属性做比照,相差最长远的就是热度最低的。
Redis 中所有对象构造都有一个 lru 字段, 且应用了 unsigned 的低 24 位,这个字段就是用来记录对象的热度。
LFU(Least Frequently Used):翻译成中文就是最不罕用。是按着应用频次来算的,淘汰那些应用频次最低的数据。说白了就是“开端淘汰制”!
方才讲过的 LRU 依照最久未应用尽管能达到淘汰数据开释空间的目标,然而它有一个比拟大的弊病,如下图:
如图所示 A 在 10 秒内被拜访了 5 次,而 B 在 10 秒内被拜访了 3 次。因为 B 最初一次被拜访的工夫比 A 要晚,在等同的状况下,A 反而先被回收。那么它就是不合理的。LFU 就完满解决了 LRU 的这个弊病,具体原理如下:
上图是开端淘汰的原理示意图,仅是按次数这个维度做的开端淘汰,但如果 Redis 仅按应用次数,也会有一个问题,就是某个数据之前被拜访过很屡次比方上万次,但后续就始终不必了,它自身按应用频次来讲是应该被淘汰的。因而 Redis 在实现 LFU 时,用两局部数据来标记这个数据:应用频率和上次访问工夫。整体思路就是:有读写我就减少热度,一段时间内没有读写我就缩小相应热度。
不论是 LRU 还是 LFU 淘汰策略,Redis 都是用 lru 这个字段实现的具体逻辑,如果配置的淘汰策略是 LFU 时,lru 的低 8 位代表的是频率,高 16 位就是记录上次访问工夫。整体的 LRU 思路如下:
(1)每当数据被写或读的时候都会调用 LFULogIncr(counter)办法,减少 lru 低 8 位的拜访频率数值;具体每次减少的数值在 redis.conf 中配置默认是 10(# lfu-log-factor 10)
(2)还有另外一个配置 lfu-decay-time 默认是 1 分钟,来管制每隔多久没人拜访则热度会递加相应数值。这样就躲避了一个超大拜访次数的数据很久都不被淘汰的破绽。
小结:“过期策略”保障过期的 key 对应的数据会被及时革除;“淘汰策略”保障内存满的时候会主动开释相应空间,因而 Redis 的内存能够自运行保障不会产生溢出异样。
二、Redis 的数据长久化策略——宕机可立刻复原数据到内存
有了内存不会溢出保障后,咱们再来看看 Redis 是如何保障服务器宕机或重启,原来缓存在内存中的数据是不会失落的。也就是 Redis 的长久化机制。
Redis 的长久化策略有两种:RDB(快照全量长久化)和 AOF(增量日志长久化)
1、RDB
RDB 是 Redis 默认的长久化计划。RDB 快照(Redis DataBase),当触发肯定条件的时候,会把以后内存中的数据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过 dump.rdb 文件复原数据。那那个肯定的条件是啥呢?到底什么时候写入 rdb 文件?
触发 Redis 执行 rdb 的形式有两类:主动触发和手动触发
“主动触发” 的状况有三种:达到配置文件触发规定时触发、执行 shutdown 命令时触发、执行 flushall 命令时触发。
注:在 redis.conf 中有个 SNAPSHOTTING 配置,其中定义了触发把数据保留到磁盘触发频率。
“手动触发”的形式有两种:执行 save 或 bgsave 命令。执行 save 命令在生成快照的时候会阻塞以后 Redis 服务器,Redis 不能解决其余命令。如果内存中的数据比拟多,会造成 Redis 长时间的阻塞。生产环境不倡议应用这个命令。
为了解决这个问题,Redis 提供了第二种形式 bgsave 命令进行数据备份,执行 bgsave 时,Redis 会在后盾异步进行快照操作,快照同时还能够响应客户端申请。
具体操作是 Redis 过程执行 fork(创立过程函数)操作创立子过程(copy-on-write),RDB 长久化过程由子过程负责,实现后主动完结。它不会记录 fork 之后后续的命令。阻塞只产生在 fork 阶段,个别工夫很短。手动触发的场景个别仅用在迁徙数据时才会用到。
咱们晓得了 RDB 的实现的原理逻辑,那么咱们就来剖析下 RDB 到底有什么优劣势。
劣势:
(1)RDB 是一个十分紧凑 (compact 类型) 的文件,它保留了 redis 在某个工夫点上的数据集。这种文件非常适合用于进行备份和劫难复原。
(2)生成 RDB 文件的时候,redis 主过程会 fork()一个子过程来解决所有保留工作,主过程不须要进行任何磁盘 IO 操作。
(3)RDB 在复原大数据集时的速度比 AOF 的复原速度要快。
劣势:
RDB 形式数据没方法做到实时长久化 / 秒级长久化。在肯定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会失落最初一次快照之后的所有批改
2、AOF(Append Only File)
AOF 采纳日志的模式来记录每个写操作的命令,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。Redis 重启时会依据日志文件的内容把写指令从前到后执行一次以实现数据的复原工作。
其实 AOF 也不肯定是齐全实时的备份操作命令,在 redis.conf 咱们能够配置抉择 AOF 的执行形式,次要有三种:always、everysec 和 no
AOF 是追加更改命令文件,那么大家想下始终追加追加,就是会导致文件过大,那么 Redis 是怎么解决这个问题的呢?
Redis 解决这个问题的办法是 AOF 上面有个机制叫做 bgrewriteaof 重写机制,咱们来看下它是个啥
注:AOF 文件重写并不是对原文件进行重新整理,而是间接读取服务器现有的键值对,而后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。
咱们晓得了 AOF 的实现原理,咱们来剖析下它的优缺点。
长处:
能最大限度的保障数据安全,就算用默认的配置 everysec,也最多只会造成 1s 的数据失落。
毛病:
数据量比 RDB 要大很多,所以性能没有 RDB 好!
小结:因为有了长久化机制,因而 Redis 即便服务器宕机或重启了,也能够最大限度的复原数据到内存中,提供给 client 持续应用。
三、Redis 的哨兵模式——可战到最初一兵一卒的高可用集群
内存满了不会挂,服务器宕机重启也没问题。足见 Redis 的程序健壮性曾经足够弱小。但 Redis 的设计者,在面向高可用背后,仍持续向前迈进了一步,那就是 Redis 的高可用集群计划——哨兵模式。
所谓的“哨兵模式”就是有一群哨兵(Sentinel)在 Redis 服务器后面帮咱们监控这 Redis 集群各个机器的运行状况,并且哨兵间互相通告告诉,并指引咱们应用那些衰弱的服务。
Sentinel 工作原理:
1、Sentinel 默认以每秒钟 1 次的频率向 Redis 所有服务节点发送 PING 命令。如果在 down-after-milliseconds 内都没有收到无效回复,Sentinel 会将该服务器标记为下线(主观下线)。
2、这个时候 Sentinel 节点会持续询问其余的 Sentinel 节点,确认这个节点是否下线,如果少数 Sentinel 节点都认为 master 下线,master 才真正确认被下线(主观下线),这个时候就须要从新选举 master。
Sentinel 的作用:
1、监控:Sentinel 会一直查看主服务器和从服务器是否失常运行
2、故障解决:如果主服务器产生故障,Sentinel 能够启动故障转移过程。把某台服务器降级为主服务器,并发出通知
3、配置管理:客户端连贯到 Sentinel,获取以后的 Redis 主服务器的地址。咱们不是间接去获取 Redis 主服务的地址,而是依据 sentinel 去主动获取谁是主机,即便主机产生故障后咱们也不必改代码的连贯!
小结:有了“哨兵模式”只有集群中有一个 Redis 服务器还衰弱存活,哨兵就能把这个衰弱的 Redis 服务器提供给咱们(如上图的 1、2 两步),那么咱们客户端的链接就不会出错。因而,Redis 集群能够战斗至最初一兵一卒。
这就是 Redis,一个“高可用、强健壮性”的标杆程序!
作者:宜信技术学院 谭文涛