在前两篇,我们学习了一下Redis的相关数据类型、底层实现、持久化、集群分区等知识,这一篇我们重点搞懂一下Redis的内存淘汰机制,用于容错的哨兵机制以及非常重要的应用场景。
Redis内存淘汰机制
Redis是内存数据库,我们能
时时刻刻能感受到Redis作者为更好地使用内存而费尽各种心思,例如最明显的是对于同一种数据结构在不同应用场景下提供了基于不同底层编码的实现(如压缩列表、跳跃表等)。
Redis最常见的两种应用场景为缓存和持久存储,当Redis做缓存时,
有一个Redis服务器,服务器物理内存大小为1G的,我们需要存在Redis中的数据量很小,这看起来似乎足够用很长时间了,随着业务量的增长,我们放在Redis里面的数据越来越多了,数据量大小似乎超过了1G,但是应用还可以正常运行,这是因为操作系统的可见内存并不受物理内存限制,而是虚拟内存,物理内存不够用没关系,操作系统会从硬盘上划出一片空间用于构建虚拟内存,比如32位的操作系统的可见内存大小为2^32,而用户空间的可见内存要小于2^32很多,大概是3G左右。好了,我们庆幸操作系统为我们做了这些,但是我们需要知道这背后的代价是不菲的,不合理的使用内存有可能发生频繁的swap,频繁swap的代价是惨痛的。所以回过头来看,作为有追求的程序员,我们还是要小心翼翼地使用好每块内存,把用户代码能解决的问题尽量不要抛给操作系统解决。
所以,为了更好的利用内存,使Redis存储的都是缓存的热点数据,Redis设计了相应的内存淘汰机制(也可以叫做缓存淘汰机制)。
****
使用
我们可以通过配置redis.conf中的maxmemory这个值来开启内存淘汰功能,我们可以通过这个值来设置内存淘汰算法
1
2 1# maxmemory <bytes>
2
内存淘汰的过程是:
- 客户端发起了需要申请更多内存的命令(如set)。
- Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略(就是配置的
maxmemory这个值)来淘汰内存(key),从而换取一定的内存。
- 如果上面都没问题,则这个命令执行成功。
maxmemory为0的时候表示我们对Redis的内存使用没有限制。
内存淘汰策略
Redis 为我们提供了多种内存淘汰策略,我们可以根据不同的应用场景应用不同的策略。
这里补充一下主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。我们来看如何设置key的过期时间,通过以下的指令:
- EXPIRE 将key的生存时间设置为ttl秒
EXPIRE key ttl
- PEXPIRE 将key的生成时间设置为ttl毫秒
- EXPIREAT 将key的过期时间设置为timestamp所代表的的秒数的时间戳
- PEXPIREAT 将key的过期时间设置为timestamp所代表的的毫秒数的时间戳
ttl key 可获得对应的key还有多少时间过期。
接下来,我们继续分析Redis的内存淘汰策略:
Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:
- noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
- allkeys-lru:在主键空间中,优先移除最近未使用的key。
- volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
- allkeys-random:在主键空间中,随机移除某个key。
- volatile-random:在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
淘汰策略的选择可以通过配置配置文件来指定:
1
2 1# maxmemory-policy noeviction
2
1
2 1 接下来我们来看这些策略适用的此场景是什么:
2
- allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
- allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
- volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。
另外,volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候(存储一段时间),然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。
非精准的LRU
上面提到的LRU(Least Recently Used)策略,实际上Redis实现的LRU并不是可靠的LRU,也就是名义上我们使用LRU算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的,这里涉及到一个权衡的问题,如果需要在全部键空间内搜索最优解,则必然会增加系统的开销,Redis是单线程的,也就是同一个实例在每一个时刻只能服务于一个客户端,所以耗时的操作一定要谨慎。为了在一定成本内实现相对的LRU,早期的Redis版本是基于采样的LRU,也就是放弃全部键空间内搜索解改为采样空间搜索最优解。自从Redis3.0版本之后,Redis作者对于基于采样的LRU进行了一些优化,目的是在一定的成本内让结果更靠近真实的LRU。
LRU的实现(也是经典的页面置换算法),可以用LinkedHashMap来实现,可以通过指定按什么顺序来访问实现,构造函数中,有一个参数:accessOrder,当被设置为false 时是基于插入顺序 true 基于访问顺序(get一个元素后,这个元素被加到最后,使用了LRU 最近最少被使用的调度算法)。
如果想自己实现LRU缓存策略,我们可以参考LinkedHashMap实现原理 与 LinkedHashMap的使用与实现。
Redis主从同步原理
和MySQL主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,另外也是为了保证HA。
Redis主从复制可以根据是否是全量分为全量同步和增量同步。
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis的同步策略是:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
Redis的哨兵机制
Redis-Sentinel也就是哨兵机制,是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。
它的主要功能有以下几点
- 不时地监控redis是否按照预期良好地运行;
- 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
- 能够进行自动切换(进行主备切换)。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。
Sentinel支持集群
很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:
- 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;
- 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);
- 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。
看到这,我们会想,Redis提供的哨兵机制和Zookeeper的应用场景类似,利用ZK也可以监控服务器运行状态,并在有宕机情况发生时完成master选举,从而进行故障转移。
哨兵机制的运行过程
我们可以简单用图来表示一下Sentinel的作用:
如上图所示,Redis为了保证高可用性,采用Master-slave形式部署,采用AOF或RDB进行持久化,采用集群culster机制来分布式存储。多个哨兵,不仅同时监控主从数据库,而且哨兵之间互为监控,并且哨兵不会存在单点问题。哨兵会监控主从Redis服务器是否正常,当发现有问题时,会进行通知,并进行故障转移。当监控到master节点宕机后,会进行maser选举出新的master节点,并进行主从切换,并将其他节点作为新选举出的master节点的slave。哨兵Sentinel用到的分布式一致性算法是Raft分布式算法。
redis利用比较易于实现的raft协议实现了节点宕机的自动化处理,保障了集群的高可用性。Raft协议与paxos算法类似,但比paxos算法好理解,我们都知道,paxos算法是经典的分布式一致性算法,zookeeper的ZAB协议也是在其基础上设计的。ZAB,paxos,Raft都是采用过半策略。关于相关分布式算法,我会在后边的系列,Zookeeper系列进行介绍。对于Redis的监控过程,Leader选举,主备切换,数据同步等过程,跟Zookeeper类似,我们可以把zookeeper的实现方式与其进行类比。可以把哨兵集群当做一个Zk集群?
可参考:
Redis 哨兵机制与用法说明
Raft与paxos算法的对比
在本篇文章中,我们分析了Redis的内存淘汰机制,Redis的容灾策略即哨兵机制,我是把Redis的哨兵集群想象成了一个zk集群,只不过他使用的算法是Raft算法,zk使用的ZAB协议,但是他们的核心原理是类似的,如有偏差,请大家指出。在后边的系列种,我们会详细介绍Zookeeper的实现原理及相关分布式一致性算法如:2pc,3pc,paxos,zab等。但Redis系列还没完成,因为有最重要的还没有介绍,下一篇,我会详细介绍一下Redis常见的应用场景。