在前两篇,我们学习了一下Redis的相关数据类型、底层实现、持久化、集群分区等知识,这一篇我们重点搞懂一下Redis的内存淘汰机制,用于容错的哨兵机制以及非常重要的应用场景。
Redis内存淘汰机制
使用
# maxmemory <bytes>
内存淘汰的过程是:
- 客户端发起了需要申请更多内存的命令(如set)。
- Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略(就是配置的maxmemory这个值)来淘汰内存(key),从而换取一定的内存。
- 如果上面都没问题,则这个命令执行成功。
内存淘汰策略
- EXPIRE 将key的生存时间设置为ttl秒 EXPIRE key ttl
- PEXPIRE 将key的生成时间设置为ttl毫秒
- EXPIREAT 将key的过期时间设置为timestamp所代表的的秒数的时间戳
- PEXPIREAT 将key的过期时间设置为timestamp所代表的的毫秒数的时间戳
Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:
- noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
- allkeys-lru:在主键空间中,优先移除最近未使用的key。
- volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
- allkeys-random:在主键空间中,随机移除某个key。
- volatile-random:在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
# maxmemory-policy noeviction
接下来我们来看这些策略适用的此场景是什么:
- allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
- allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
- volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。
非精准的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主从复制可以根据是否是全量分为全量同步和增量同步。全量同步
1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis的哨兵机制
- 不时地监控redis是否按照预期良好地运行;
- 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
- 能够进行自动切换(进行主备切换)。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。
很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:
- 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;
- 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);
- 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。
哨兵机制的运行过程
如上图所示,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集群?