Redis梳理
Redis数据结构
Redis基本数据结构有:String、List、Hash、Set、SortedSet
SortedSet
操作复杂度:在SortedSet中添加、删除或更新一个成员都是非常快速的操作,其时间复杂度为logN。
SortedSet底层采用字典+跳跃表两种数据结构,来保存有序集元素。字典是通过哈希表来实现的。
常用API
Redis持久化
Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。
RDB持久化
概要
- RDB持久化是Redis默认的持久化方式;
- 工作原理是周期性地以快照的形式将数据持久化到硬盘中;
- 通过BGSAVE命令操作。
持久化过程
bgsave
命令让父进程fork一个子进程(进行一个判断,如果父进程已经有了一个子进程就直接返回);- 父进程fork完了之后处理其他任务,同时,子进程将父进程内存中的数据写到.rdb文件持久化到硬盘中;(os通过CopyOnWrite解决数据更新引发的冲突)
- 子进程持久化完了之后,通知父进程更新.rdb文件。
AOF持久化
概要
- AOF持久化以独立日志的方式,记录每次写入的命令;
- AOF持久化的实时性更好,但存在大量的冗余命令,需要通过重写机制压缩日志,恢复速度慢。
持久化过程
- 命令写入后,通过append到一个buffer中保证实时性,再同步到一个旧AOF文件中;
- 重写过程和RDB持久化过程类似,只是最后父进程写到一个buffer里,同步到一个新AOF文件中;
- 最后用新的AOF文件把旧的AOF文件替换掉,重启服务时再把新文件load进来。
RDB-AOF混合持久化
混合持久化的过程为:在AOF重写时,先执行bgsave
生成RDB,再将后处理的命令追加到rdb结尾。
Redis主从复制
Redis的主从复制是为了提升分布式系统的可用性和读写性能的。Redis通过SLAVEOF命令或slaveof配置让一个server复制另一个server的数据集和状态。Redis采用异步复制机制。
主从复制的优点:
- master可以关闭持久化机制,减少IO性能损耗;
- slave能提高读请求处理效率;
- 容灾性比较好
主从复制和集群概念上的区别:主从复制是一个master和若干个slave,一个master挂了可以从slave推选新的master,slave可以用提供故障恢复、分担读流量、备份数据;集群是数据量较大时,数据根据key根据哈希计算出slot在多个分片中进行分区(partition),客户端对key的请求会被转发到持有那个key的分片上。分片由一个master和若干个slave组成,他们之间又通过主从复制机制来同步数据。可以理解为集群是主从复制和分区的集合。
主从复制的使用
主从复制的开启,完全是在 slave 发起的,不需要我们在 master 做任何事情。slave 开启主从复制,有三种方式:
1 |
|
主从复制的原理
主从复制过程可分为三个阶段:复制初始化、数据同步和命令传播。
初始化阶段
- 执行完slaveof命令后,slave根据master地址发起socket连接,master收到socket连接之后将连接信息保存,连接建立;
- slave向master发送PING命令,结果如果返回PONG则说明master可用,否则说明master处理其他任务在阻塞中,slave断开连接并重试;
数据同步阶段
- master和slave相互确认连接信息后,slave向master发送psync命令;
- 主库收到该命令后判断是进行增量同步还是全量同步,然后根据策略进行数据的同步;
- 当master有新的写操作时候,此时进入复制第三阶段。
命令传播阶段
- 数据同步之后,master和slave通过心跳机制检测彼此是否挂了,每隔10smaster向slave发送PING命令判断slave是否在线,slave每隔1s发送replconf ack命令。
- slave发送的命令除了判断master是否在线外,还汇报了自己的复制偏移量,让master根据当前同步情况发布未同步数据的命令。
全量复制
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
发生全量复制的时候,master会在后台启动一个线程,把同步数据生成一份RDB文件,同时将新收到客户端的写命令缓存到内存中,RDB文件生成完之后发送给slave,等slave同步完RDB文件之后,再把写命令发给slave进行同步。过程中如果发生网络故障,会自动重连,master只会复制给slave部分缺少的数据。
增量复制
增量复制发生在正常工作时master发生的写操作同步到slave的过程。增量复制的过程为:master每执行一个写命令就向slave发送相同的写命令,slave接受并执行。
Redis集群模式
集群模式原理
大规模数据存储系统都会面临的一个问题就是如何横向拓展。当你的数据集越来越大,一主多从的模式已经无法支撑这么大量的数据存储,于是你首先考虑将多个主从模式结合在一起对外提供服务。
Redis采用的是一种去中心化的集群模式。集群通过分片进行数据共享,分片内采用一主多从的形式进行副本复制,并提供复制和故障恢复功能。
主从复制相当于一棵树,集群相当于是一个森林。
Redis集群模式原理
以下图为例,一主一从构成一个分片,之间通过异步进行复制,一个机房的master挂掉,会推选另一个机房的slave为master,继续提供服务。每个master负责一部分的slot,客户端通过key做映射取模得到对应的slot,连接到对应的分片,写请求一律走master, 读请求根据路由选择规则找到对应的分片节点。
故障检测
跟大多数分布式系统一样,Redis Cluster 的节点间通过持续的 heart beat 来保持信息同步,不过 Redis Cluster 节点信息同步是内部实现的,并不依赖第三方组件如 Zookeeper。集群中的节点持续交换 PING、PONG 数据,消息协议使用 Gossip,这两种数据包的数据结构一样,之间通过 type 字段进行区分。
Redis通过哨兵机制保证高可用。
哨兵机制的工作原理
- Redis Sentinel(哨兵)是一个分布式架构,它包含若干个哨兵节点和数据节点。
- 每个哨兵节点会对数据节点和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。
- 如果被标识的是主节点,它就会与其他的哨兵节点进行协商,当多数哨兵节点都认为主节点不可达时,它们便会选举出一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时地通知给应用方。
通信过程
Redis集群中的每个节点会定期向其他节点发送PING,如果超过规定时间(node_timeout)没有收到PONG,就会标记为疑似下线状态(PFAIL)。在PING的时候,会广播其他节点当前节点知道的其他节点的信息(包括疑似挂了的节点),当其他节点收到这些疑似挂了的节点之后,会做失效报告(failure report)。如果当前节点已经PFAIL了某个节点,并且大部分节点认为PFAIL的节点PFAIL了,那么当前节点就改PFAIL为FAIL并广播出去。
故障迁移
Redis引入了一个epoch的概念。在Redis集群中主要有两种epoch:curentEpoch 和 configEpoch。
currentEpoch
这是一个集群状态相关的概念,可以当做记录集群状态变更的递增版本号。每个集群节点,都会通过 server.cluster->currentEpoch 记录当前的 currentEpoch。
集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch。因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致。
currentEpoch的作用
currentEpoch 作用在于,当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加 currentEpoch 的值。目前 currentEpoch 只用于 slave 的故障转移流程,这就跟哨兵中的sentinel.current_epoch 作用是一模一样的。当 slave A 发现其所属的 master 下线时,就会试图发起故障转移流程。首先就是增加 currentEpoch 的值,这个增加后的 currentEpoch 是所有集群节点中最大的。然后slave A 向所有节点发起拉票请求,请求其他 master 投票给自己,使自己能成为新的 master。其他节点收到包后,发现发送者的 currentEpoch 比自己的 currentEpoch 大,就会更新自己的 currentEpoch,并在尚未投票的情况下,投票给 slave A,表示同意使其成为新的 master。
configEpoch
这是一个集群节点配置相关的概念,每个集群节点都有自己独一无二的 configepoch。所谓的节点配置,实际上是指节点所负责的槽位信息。
每一个 master 在向其他节点发送包时,都会附带其 configEpoch 信息,以及一份表示它所负责的 slots 信息。而 slave 向其他节点发送包时,其包中的 configEpoch 和负责槽位信息,是其 master 的 configEpoch 和负责的 slot 信息。节点收到包之后,就会根据包中的 configEpoch 和负责的 slots 信息,记录到相应节点属性中。
configEpoch的作用
configEpoch 主要用于解决不同的节点的配置发生冲突的情况。举个例子就明白了:节点A 宣称负责 slot 1,其向外发送的包中,包含了自己的 configEpoch 和负责的 slots 信息。节点 C 收到 A 发来的包后,发现自己当前没有记录 slot 1 的负责节点(也就是 server.cluster->slots[1] 为 NULL),就会将 A 置为 slot 1 的负责节点(server.cluster->slots[1] = A),并记录节点 A 的 configEpoch。后来,节点 C 又收到了 B 发来的包,它也宣称负责 slot 1,此时,如何判断 slot 1 到底由谁负责呢?
这就是 configEpoch 起作用的时候了,C 在 B 发来的包中,发现它的 configEpoch,要比 A 的大,说明 B 是更新的配置。因此,就将 slot 1 的负责节点设置为 B(server.cluster->slots[1] = B)。在 slave 发起选举,获得足够多的选票之后,成功当选时,也就是 slave 试图替代其已经下线的旧 master,成为新的 master 时,会增加它自己的 configEpoch,使其成为当前所有集群节点的 configEpoch 中的最大值。这样,该 slave 成为 master 后,就会向所有节点发送广播包,强制其他节点更新相关 slots 的负责节点为自己。
slave在发现从属master下线后,通过增加currentEpoch计数,向其他节点拉起投票,其他节点在currentEpoch更新机制下会投票给slave,从而选举其成为master。
Redis淘汰策略
当写入数据将导致超出maxmemory限制时,Redis会采用maxmemory-policy所指定的策略进行数据淘汰。
按照「数据范围」分为volatile淘汰和allkeys淘汰,按照「淘汰算法」分为随机淘汰、LRU淘汰、LFU淘汰。
LRU:按最近最少使用原则淘汰。(未被淘汰的key可能是不常使用但刚刚使用的key)
LFU:先按照使用次数淘汰,使用次数相同的key再使用LRU淘汰。(Redis4.0支持)
Redis过期策略
Redis支持两种过期策略:定期删除和惰性删除。
定期删除
Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描。
过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:
- 从过期字典中随机选择20个key;
- 删除这20个key中已过期的key;
- 如果已过期key的比例超过25%,则重复步骤1。
惰性删除
客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
缓存穿透、缓存击穿、缓存雪崩
缓存穿透
客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案:
- 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值。
- 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。
布隆过滤器
布隆过滤器可以用很低的代价,估算出数据是否真实存在。
布隆过滤器的核心包括两部分:
- 一个大型的位数组;
- 若干个不一样的哈希函数,每个哈希函数都能将哈希值算的比较均匀。
布隆过滤器的工作原理:
- 添加key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置,并将位数组中这个位置的值设置为1。
- 询问key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置。然后对比这些哈希函数在位数组中对应位置的数值:
- 如果这几个位置中,有一个位置的值是0,就说明这个布隆过滤器中,不存在这个key。
- 如果这几个位置中,所有位置的值都是1,就说明这个布隆过滤器中,极有可能存在这个key。之所以不是百分之百确定,是因为也可能是其他的key运算导致该位置为1。
缓存击穿
一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
解决方案:
- 永不过期:热点数据不设置过期时间,所以不会出现上述问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。
- 加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
缓存雪崩
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
解决方案:
- 避免数据同时过期:设置过期时间时,附加一个随机数,避免大量的key同时过期。
- 启用降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
- 构建高可用的Redis服务:采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。
Redis性能分析
Redis性能高的原因
- Redis是单线程的,可以避免线程切换的性能损耗;
- Redis大部分操作是在内存中完成的;
- Redis采用了IO多路复用机制,使其在IO操作中能并发处理大量的请求,实现高吞吐;
- Redis使用了高效的数据结构(如:在zset中使用跳表)
- Redis是C语言编写的,执行效率高
关于单线程的解释
Redis在键值对操作时,是单线程操作。涉及持久化、异步、集群数据同步等操作时,需要依赖其他线程来执行。Redis的底层其实并不完全是单线程的。
IO多路复用
TODO:
- IO多路复用
- 关于跳表结构操作细节的掌握
- redis的API的使用