论程序的健壮性——就看Redis
“众里寻他千百度,蓦然回首,那人却在,灯火阑珊处”。多年的IT生涯,一直但愿本身写的程序可以有很强的健壮性,也一直但愿能找到一个高可用的标杆程序去借鉴学习,不畏惧内存溢出、磁盘满了、断网、断电、机器重启等等状况。但意想不到的是,这个标杆程序居然就是从一开始就在使用的分布式缓存——Redis。redis
Redis(Remote Dictionary Server ),即远程字典服务,是 C 语言开发的一个开源的高性能键值对(key-value)的内存数据库。因为它是基于内存的因此它要比基于磁盘读写的数据库效率更快。所以Redis也就成了你们解决数据库高并发访问、分布式读写和分布式锁等首选解决方案。
算法
那么既然它是基于内存的,若是内存满了怎么办?程序会不会崩溃?既然它是基于内存的,若是服务器宕机了怎么办?数据是否是就丢失了?既然它是分布式的,这台Redis服务器断网了怎么办?数据库
今天咱们就一块儿来看看Redis的设计者,一名来自意大利的小伙,是如何打造出一个超强健壮性和高可用性的程序,从而不害怕这些状况。缓存
1、 Redis的内存管理策略——内存永不溢出
Redis主要有两种策略机制来保障存储的key-value数据不会把内存塞满,它们是:过时策略和淘汰策略。安全
一、 过时策略
用过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没有再次被访问,从而不会被清除,占用大量内存。ide
- 按期过时(主动清除):每隔必定的时间,会扫描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 和内存资源达到最优的平衡效果的同时,保证过时的数据会被及时清除掉。
二、淘汰策略
在Redis可能没有须要过时的数据的状况下,仍是会把咱们的内存都占满。好比每一个key设置的过时时间都很长或不过时,一直添加就有可能把内存给塞满。那么Redis又是怎么解决这个问题的呢?——那就是“淘汰策略”。
官网地址:https://redis.io/topics/lru-cache
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的内存能够自运行保证不会产生溢出异常。
2、 Redis的数据持久化策略——宕机可当即恢复数据到内存
有了内存不会溢出保障后,咱们再来看看Redis是如何保障服务器宕机或重启,原来缓存在内存中的数据是不会丢失的。也就是Redis的持久化机制。
Redis 的持久化策略有两种:RDB(快照全量持久化)和AOF(增量日志持久化)
一、 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掉的话,就会丢失最后一次快照以后的全部修改
二、 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继续使用。
3、Redis的哨兵模式——可战到最后一兵一卒的高可用集群
内存满了不会挂,服务器宕机重启也没问题。足见Redis的程序健壮性已经足够强大。但Redis的设计者,在面向高可用面前,仍继续向前迈进了一步,那就是Redis的高可用集群方案——哨兵模式。
所谓的“哨兵模式”就是有一群哨兵(Sentinel)在Redis服务器前面帮咱们监控这Redis集群各个机器的运行状况,而且哨兵间相互通告通知,并指引咱们使用那些健康的服务。
Sentinel工做原理:
一、 Sentinel 默认以每秒钟1次的频率向Redis全部服务节点发送 PING 命令。若是在down-after-milliseconds 内都没有收到有效回复,Sentinel会将该服务器标记为下线(主观下线)。
二、 这个时候Sentinel节点会继续询问其余的Sentinel节点,确认这个节点是否下线, 若是多数 Sentinel节点都认为master下线,master才真正确认被下线(客观下线),这个时候就须要从新选举master。
Sentinel的做用:
一、监控:Sentinel 会不断检查主服务器和从服务器是否正常运行
二、故障处理:若是主服务器发生故障,Sentinel能够启动故障转移过程。把某台服务器升级为主服务器,并发出通知
三、配置管理:客户端链接到 Sentinel,获取当前的 Redis 主服务器的地址。咱们不是直接去获取Redis主服务的地址,而是根据sentinel去自动获取谁是主机,即便主机发生故障后咱们也不用改代码的链接!
小结:有了“哨兵模式”只要集群中有一个Redis服务器还健康存活,哨兵就能把这个健康的Redis服务器提供给咱们(如上图的一、2两步),那么咱们客户端的连接就不会出错。所以,Redis集群能够战斗至最后一兵一卒。
这就是Redis,一个“高可用、强健壮性”的标杆程序!
做者:宜信技术学院 谭文涛