目录
一、主从同步
1.CAP原理
2.增量同步
3.快照同步
4.无盘复制
4.wait指令
二、Sentinel(哨兵监控)
三、Redis Cluster
四、key过期策略
五、懒惰删除
在大数据高并发场景下,单个Redis实例往往是不够的,首先单个Redis的内存不宜过大,内存过大会导致快照文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗好长的数据加载时间。其次在CPU利用率上,单个Redis实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作,压力会非常大。基于此就需要做Redis集群,将众多小内存的Redis实例整合起来,将分布在多台机器上的众多CPU核心的计算能力聚集到一起,完成海量数据存储和高并发读写操作。
如果使用了Redis的持久化功能,就必须认真对待主从同步,它是系统数据安全的基础保障。主从同步也就是主从复制,Redis用于存储数据,集群中不同的Redis主机可能负责存储不同的数据,如果某个Redis主机挂掉,那么它在没有从机的情况下,这部分数据就只能丢失了,所以每个主机配置一些从机将主机的数据备份,倘若主机挂掉,数据还在从机上,最起码这部分数据还是可用的。所以主从同步就是主从复制,它主要是保证高可用性。
CAP是分布式存储的理论基石,它是如下三个单词的首字母:
分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫做网络分区。
如上,在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的一致性将无法满足,因为两个分布式节点的数据不再保持一致。除非牺牲可用性,也就是暂停分布式节点服务,在网络分区发生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续提供服务。CAP原理可以用一句话概括:当网络分区发生时,一致性和可用性两难全。
Redis的主从数据是异步同步的,所以分布式的Redis并不满足一致性要求。当客户端再Redis的主节点修改了数据之后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以Redis满足可用性。
Redis保证最终一致性,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态保持一致。如果网络断开,主从节点的数据将会出现大量不一致,但一旦网络恢复,从节点会采用多种策略努力追赶,继续尽力保持和主节点一致。
Redis同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存buffer中,然后异步将buffer中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里。由于内存的buffer是有限的,所以Redis主节点不能将所有的指令都记录在内存buffer中。Redis的复制内存buffer是一个定长的环形数组,若数组满了,就会从头开始覆盖前面的内容。对于主节点中那些没有同步的指令在buffer中可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制---快照同步。
快照同步是一个非常耗资源的操作,他首先需要在主节点上将当前内存的数据全部快照到磁盘中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接收完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空,加载完毕后通知主节点继续进行增量同步。
在整个快照同步进行的过程中,主节点的buffer还在不停地往前移动,如果快照同步地时间过长或者buffer太小,都会导致同步期间的增量指令在buffer中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,这样很有可能陷入快照同步的死循环中。所以务必配置一个合适的buffer大小参数,避免快照复制的死循环。
当从节点刚加入到集群时,他必须先进行一次快照同步,同步完成后再继续进行增量同步。
主节点在进行快照同步时,会进行很耗时的IO操作,特别是当系统正在进行AOF的fsync操作时,如果发生快照同步,fsync将会被推迟,这就会严重影响主节点的服务效率。所以从Redis2.8.18开始,Redis支持无盘复制。所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一边将序列化的内容发送到从节点。
Redis的复制是异步进行的,Redis3.0版本以后提供了wait指令可以让异步复制变为同步复制,确保系统的强一致性。
wait提供两个参数:
两个参数的含义是:等待wait指令之前的所有写操作同步到N个从节点,最多等待时间t,如果时间t=0,表示无限等待直到N个从节点同步完成。
Sentinel负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。当客户端连接集群时,会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向Sentinel要地址,Sentinel会将最新的主节点地址告诉客户端。如此应用程序将无须重启即可自动完成节点切换。
如上图,若主节点挂掉,原先的主从复制也就断开了,客户端和损坏的主节点断开并和新的主节点建立交互,其他节点开始和新的主节点建立复制关系。当旧的主节点恢复后会变成从节点。
Redis采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能特别多。Sentinel无法保证消息完全不丢失,但是也尽可能保证消息少丢失。他有两个选项可以限制主从延迟过大。
集群下的分布式锁
在Sentinel集群中,当主节点挂掉时,从节点会取而代之,但客户端却没有明显感知。比如,原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了,然后从节点变成了主节点,这个新节点内部没有这个锁,所以当另外一个客户端请求加锁时,立刻批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。不过这种不安全仅在主从发生failover的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。
Redis Cluster是Redis作者提供的Redis集群化方案,该集群由三个Redis节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样,这三个节点相互连接组成一个对等的集群,他们之间通过一种特殊的二进制协议交互集群信息。
Redis Cluster将所有数据划分为16384个槽位,每个节点负责其中的一部分槽位。槽位信息存储于每个节点中。当Redis Cluster的客户端来连接集群时,也会得到一份集群的槽位配置信息。这样当客户端要查找某个key时,可以直接定位到目标节点。客户端为了可以直接定位某个具体的key所在的节点,需要缓存槽位相关信息,这样才可以准确快速定位到相应的节点。同时因为可能会存在客户端与服务器存储槽位不一致的情况,还需要纠正机制来实现槽位信息的效验调整。另外,Redis Cluster的每个节点会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的。
Redis Cluster默认会对key值使用crs16算法进行hash,得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位。
当客户端向一个错误节点发出指令后,该节点会发现指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,即告诉客户端去连接这个节点以获取数据。
Redis Cluster可以为每个主节点设置若干个从节点,当主节点发生故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。不过Redis也提供一个参数cluster-require-full-coverage可以允许部分节点发生故障,其他节点还可以继续提供对外访问。
那么如何判断某个节点发生故障了,如果只是网络抖动,比如突然间部分连接变得不可访问,然后很快又恢复了。Redis Cluster提供一种选项cluster-node-timeout,表示当某个节点持续tomeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换。
Redis所有的数据结构都可以设置过期时间,时间到了,Redis会自动删除相应的对象。Redis会将每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除了定时遍历之外,它还会使用惰性策略来删除过期的key。所谓惰性策略(懒汉式)就是在客户端访问这个key的时候,检查该key是否过期,如果过期了就立即删除。
Redis默认每秒进行10次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略,步骤如下:
为了保证过期扫描不会出现循环过度,导致线程卡死的现象,算法还增加了扫描时间的上限,默认不会超过25ms。
如果在同一时间有大批量的key过期,那么Redis过持续扫描过期字典(循环多次),直到过期字典中过期的key变得稀疏,才会停止。这就会导致线上读写请求出现明显卡顿现象。导致这种卡顿的另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的CPU消耗。
当客户端请求到来时,服务器如果正好进入过期扫描状态,客户端的请求将会等待至少25ms后才会进行处理,如果客户端将超时时间设置得比较短(比如读写超时时长),比如10ms,那么就会出现大量得链接因为超时而关闭,业务端就会出现很多异常,而且这时你还无法从Redis得slow log中看到慢查询记录,因为慢查询指得是逻辑处理过程慢,不包含等待时间。
所以如果有大批量得key过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。
Redis从节点不会进行过期扫描,从节点对过期得处理是被动得。主节点在key到期时,会在AOF文件里增加一条del指令,同步到所有的从节点,从节点通过执行这条del指令来删除过期的key。
因为指令同步是异步进行的,所以如果主节点过期的key的del指令没有及时同步到从节点,就会出现从数据的不一致,主节点没有的数据在从节点里还存在。
我们都直到Redis是单线程的,但是起始Redis内部并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时操作。
删除指令del会直接释放对象的内存,如果被删除的key是一个非常大的对象,比如包含了上千万个元素的hash,那么删除操作就会导致单线程卡顿。为了解决该问题,在4.0版本中引入了unlink指令,他能对删除操作进行懒处理,丢给后台线程来异步回收内存。
UNLINK key
Redis提供了flushdb和flushall指令,用来清空数据库,这也是及其缓慢的操作。同样的这两个指令也可以异步化,在指令后面增加async参数就可以交给后台线程异步回收。
对于异步操作,主线程会先将key的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。异步队列被主线程和异步线程同时操作,所以异步队列是一个线程安全的队列。
AOF Sync
Redis需要每秒1(可设置)次同步AOF日志到磁盘,确保消息尽量不丢失,需要调用sync函数,这个操作比较耗时。会导致主线程的效率下降,所以Redis也将这个操作移到异步线程来完成。执行AOF Sync操作的线程是一个独立的异步线程,和前面的懒惰删除线程不是一个线程,同样它也有一个属于自己的任务队列,队列只用来存放AOF Sync任务。
参考《Redis深度历险》