Redis详知详解
来自知乎文章:Redis详知详解 - 知乎 (zhihu.com)在这里做一些改动方便阅读
Redis学习
三大主线:
1.高性能:线程模型,数据结构,持久化,网络框架
2.高可靠性:主从复制,哨兵机制
3.高可拓展:数据分片,负载均衡
01 Redis 基本架构
可以存储的数据
Redis是一个键值数据库。基本数据模型是key-value模型;
不同的键值数据库所支持的key类型差异不大,但是在对value类型支持时却有着较大的差别,所以在对键值数据库进行选择时,一个重要的考虑因素是它所支持的value类型。例如,Memcached支持的value类型只有string,而Redis支持的value类型包括了string,哈希表,列表,集合等。
可以对数据进行的操作
PUT:新写入或者更新一个key-value对
GET:根据key值读取相应的value值
DELETE:根据key值删除整个key-value对
SCAN:根据一段的key值范围返回相应的value值。
采用了内存,键值数据库包括了访问框架,索引模块,操作模块,储存模块。
访问模式:
通过函数库调用的方式供外部应用使用
通过网络框架以Socket通信的形式对外提供键值对操作
RocksDB以动态链接库的形式使用,而Memcached和Redis通过网络框架访问;
索引方式:
Memcached和Redis采用了哈希表作为key-value的索引
Redis 主要通过网络框架进行访问,而不再是动态库了,这也使得 Redis 可以作为一个基础性的网络服务进行访问,扩大了 Redis 的应用范围。
Redis 数据模型中的 value 类型很丰富,因此也带来了更多的操作接口,例如面向列表的LPUSH/LPOP,面向集合的 SADD/SREM 等。
Redis的持久化模块能支持两种方式:日志(AOF)和快照(RDB),这两种持久化方式具有不同的优劣势,影响到 Redis 的访问性能和可靠性。
SimplekV 是个简单的单机键值数据库,但是,Redis 支持高可靠集群和高可扩展集群,因此,Redis 中包含了相应的集群功能支撑模块
02 Redis数据结构
值的组织方式:
Redis底层对键值对中值的保存形式,其中String对应底层的简单动态字符串;其他的List,Hash,Set等都对应了两种数据结构;
而List Hash set Sorted Set对应四种数据结构都对应了底层两种实现方式,称为集合类型,特点是一个键对应了一个集合的数据。
键和值的组织结构
为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有的键值对。(全局哈希表)
是键到集合类型指针地址的映射;
哈希冲突:不同的key经过哈希映射之后,到了同一个哈希桶中。
当哈希表过长时,就会对哈希表进行rehash操作,就是增加目前hash桶的数量,让逐渐增多的entry元素能够在更多的桶之间分散保存,减少单个桶的元素数量。
rehash过程:
1.给哈希表2分配更大的空间,例如是当前哈希表1的两倍
2.将哈希表1的数据拷贝到哈希表2
3.释放哈希表1
4.完成后,下一次哈希表1就可以作为扩容使用。
存在问题:如果随着业务增长此时进行拷贝,由于数据量过多,会造成Redis线程阻塞?(迁移数据时无法进行其他请求)
渐进式rehash:在原有的rehash中,我们的操作是统一在一段时间中进行rehash,此时我们的操作是,当客户对这个key进行访问时,我们对这个key对应的桶进行rehash,分摊了开销。
集合数据的操作效率
与底层数据结构有关,使用哈希表比链表效率高
压缩列表和跳表
压缩列表
其中前三个字段分别是:列表长度,列表尾的偏移量,列表中的entry个数,zlend表示列表结束。在列表中如果查找第一个或者最后一个元素,可以只通过表头的三个字段的长度直接定位,复杂度是O(1),而查找其他元素时,就需要逐个查找。
跳表:顾名思义,跳表就是能跳!!!
不同操作的复杂度
单元操作(对每一种集合的单个数据实现增删改查)O(1)
范围操作,指集合类型中的遍历操作,返回集合中的所有数据,此类操作一般是O(N)
统计操作,统计集合中所有元素的个数,如果采用的是压缩列表的数据结构,O(1)
对头尾操作,压缩列表O(1)
03 Redis高性能IO模型
前言
通常所说的Redis单线程,指的是Redis的网络IO和键值对的读写是由一个线程来完成的,这是Redis对外提供键值存储服务的主要流程。其他功能,如持久化,异步删除,集群数据同步等,是由其他额外的线程执行的。
Redis为什么用单线程
多线程的巨大开销:
一般情况下,在系统可用资源数足够时,我们如果能够通过,增加线程数量可以增大我们的吞吐量,但是由于多个线程对共享资源的同时访问,就需要额外的机制来维护共享资源的正确性,这就导致了额外的开销。
为了保证队列长度的正确性,大量线程被强制串行化。
单线程的Redis为什么快?
通常来说,单线程的处理能力比多线程差很多,但是Redis却能够使用单线程模型,达到每秒十万级的处理能力。
1.Redis是内存操作,而且采用了更加高效的数据结构。例如哈希表和跳表。
2.网络端采用了多路复用机制。
在阻塞模式下,Redis线程有很大机率会阻塞在accept和recv。这是阻塞模式,而Socket也提供了非阻塞模式。
非阻塞模式:在accpet和recv上设置非阻塞模式,Redis线程可以在此时执行其他操作。
基于多路复用的高性能I/O模型
这就是我们经常听到的select/epoll机制,简单来说,在Redis运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或者数据请求。一旦请求到达,就会交给Redis线程处理。
事件队列,储存在操作系统的内核内存中。
04 AOF日志
主要问题:如何实现Redis的持久化?
1. AOF日志是如何实现?
数据库是写前日志,而Redis的AOF是写后日志。
数据库记录,修改后的数据的值。Redis的AOF记录修改的语句。
为了避免开销,在记录日志时,并不会对语法进行检查。所以采用写后日志,避免了语法出错。
缺点:
虽然不会阻塞当前对Redis的写入操作,但是会阻塞下一个对Redis的写入操作。
如果程序在写入日志前宕机,则会丧失此时日志,如果用作数据库则会丧失数据。
2. 三种写回策略
Always,同写同回,写完立马同步到磁盘上。
Evevrysec,每秒写回
No,操作系统控制写回,先写入内存缓冲区,再由操作系统决定何时将缓冲区内容写回。
3.日志文件过大
4.重写会不会阻塞主线程
每次AOF重写,Redis都会先执行一个内存拷贝,用于重写;然后使用两个日志来保证在重写过程中新写入的数据不会丢失。
AOF是写日志,但是如果要恢复的话,就需要一条一条执行,如果我们有快照,就不需要管理这个过程,而是能够直接获取状态了。
05 内存快照 RDB
主要问题:
为什么需要内存快照?为了解决AOF数据恢复过慢问题。
要对哪些数据进行快照?
做快照时如何数据还能够被增删改吗?
给哪些数据进行快照?
Redis执行的是全量快照。
save在主线程中执行,会导致阻塞。
bgsave创建一个子线程,专门用于写入RDB文件,避免了主线程的阻塞。
做快照时如何数据还能够被增删改吗?在RDB时可以进行写操作,这是由于Copy-On-Write机制。
如何确定快照频率?频率过高,带来了巨大开销,磁盘以及fork阻塞,以及内存等等问题。
Redis4.0提出了混合使用AOF日志和内存快照的方式。
在两次快照之间使用AOF记录这期间的所有命令操作。
所以一旦发生宕机,可以使用最近快照+AOF的形式恢复数据。
06 数据同步:主从库的数据一致性
为什么需要主从数据库?一个实例在恢复期间是无法服务新来的数据存取请求的。为了保证服务的质量一般都采用增加副本冗余量,将一个数据保存在多个实例上。
·读操作:主库、从库都可以接收。
·写操作:首先到主库执行,然后,主库将写操作同步给从库。
主从库间如何第一次同步?
当启动多个Redis实例时,可以通过replicaof命令形成主库和从库关系。之后会按照三个阶段完成数据的第一次同步。
例如现在有实例1(172.16.19.3)和实例2(172.16.19.5)
replicaof 172.16.19.3
此时实例2就会成为实例1的从库,并从实例1上复制数据。
第一次同步的三个阶段:
第一阶段是主从库建立连接、协商同步。
1.发送psync命令,表示要进行数据同步,主库根据这个命令参数来启动复制。psync命令包含了主库的runID,和复制进度offset两个参数。此时由于不知道主库ID和复制进度,所以runID设置为?,offset设置为-1表示第一次复制。
2.主库响应:FULLRESYNC,表示这是一次全量复制,主库把当前的数据都复制给从库。
第二阶段:主库把本地RDB文件同步给从库。在此期间产生的新的数据,会储存在专门的replication buffer内存中,在RDB完成发送后,再同步给从库。
主从级联模式:
在主-从关系模式中,所有的从库都是和主库进行连接,所有的全量复制和RDB文件传输也都是在主库进行。导致主库不断地fork生成子线程,可能会导致主库阻塞。通过主-从-从的模式,我们可以将主库生成RDB和传输RDB的压力以级联的方式分散到从库上。
基于长连接的命令传播
不可避免的网络阻塞或者断开
主从库网络断了之后
Redis2.8前重新进行全量复制
Redis2.8后
断开后,写入操作不仅写入replicationbuffer中还写入repl_backlog_buffer中,连接后从库发送自己的位置,与主库对比,主库将之间的差距写给从库。
如果读取速度过慢则会出现数据被覆写的,导致主从库数据不一致问题。
07 哨兵机制
主库崩溃后如何进行写操作?
哨兵机制的基本流程:
1.监控
哨兵是一个运行在特殊模式下的Redis进程,主从库实例运行时,他也在运行。哨兵对所有主从库发送PING命令,如果主从库没有在规定时间内响应则会被标记为下线。如果主库下线则会自动切换主库。
2.选主
按照一定规则选择一个从库作为新的主库。
3.通知
执行通知任务时,哨兵会把新主库的连接信息发送给其他从库,让他们执行replicadf命令和新主库建立连接。同时把连接信息发送给客户端,让他们把请求发送到新主库上。
主观下线和客观下线
主观下线,PING命令没回应,对于从库没影响直接下线就好。
对于主库如果误判则会产生极大开销。为了避免该问题一般采用哨兵集群来进行判断。多数同意下线则才是客观下线。
如何选择新的主库?
筛选+打分:
优先级最高的从库得分高
例如内存大的优先级高
和旧主库同步程度最近的从库得分高
slave_repl_offset和master_repl,从库中根据slave_repl_offset来决定谁更新
ID号小的从库得分高
优先级和复制进度相同的情况下,ID号最小的从库得分最高。
08 哨兵集群
在进行哨兵集群配置,并不需要知道其他哨兵实例的IP端口。
基于pub/sub机制的哨兵集群
发布订阅机制,只要哨兵和主库进行连接后,就可以在主库上发布消息,也可以订阅消息,当多个哨兵实例都连接上了主库,他们之间就能够彼此知道IP地址和端口。
和kafka一样只有订阅一个频道的应用才能通过发布消息进行信息交换。其中哨兵之间的信息交换频道是“sentinel:hello”.
哨兵通过INFO命令向主库请求slave列表
切换主库后客户端如何知道从库和新主库信息?
哨兵实例也提供了订阅发布机制
客户端读取哨兵配置文件,客户端就可以从哨兵处订阅以上信息。
由哪个哨兵实际进行主从切换?
上一节中讲到了客观下线,此时主库下线的话无法通过主库进行消息订阅和发布。当主库下线后,哨兵给其他哨兵发送Y或N,当一个哨兵获得了仲裁所需的票数后就可以标记主库为客观下线。此时这个哨兵发送命令希望由自己来自行主从库的切换,称为leader选举。
成为leader哨兵条件:
拿到半数以上赞成票
拿到的票数大于配置文件中的quorum值
如果有两个哨兵实例则需要配置quorum值为2,因为如果是1,如果有个哨兵挂掉此时拿不到半数以上赞成票。
假设有一个Redis集群是"一主四从",同时配置包含了5个哨兵实例的集群,quorum值为2,那么在运行过程中,如果3个哨兵实例发生故障,此时Redis主从如果有故障,还能正确判断主库"客观下线"嘛?还能进行主从库切换嘛?是不是哨兵实例越多越好?如果调大 down-after-milliseconds 值,能否减少误判?
可以判断客观下线,因为quorum=2,当一个哨兵判断主库"主观下线"后,询问另一个哨兵,当2个哨兵都判定"主观下线",满足quorum值,因此主库"客观下线"。
不能完成客观下线,必须达到半数以上才能选出leader。
哨兵在判断"主观下线"和选举"哨兵领导者"的时候都需要和其他节点进行通信,交换信息。哨兵实例越多,通信次数也就越多,部署越多哨兵在不同机器上,节点越多带来的机器故障风险越大,这些都会影响到哨兵的通信和选举。出问题时候也会意味着选举时间变长,切换主从的时间变长。
适当调大down-after-milliseconds值,当哨兵与主库之间网络存在短时波动时,可以降低误判的概率。但是调大down-after-milliseconds值也意味着主从切换的时间会变长,对业务的影响时间越久,我们需要根据实际场景进行权衡,设置合理的阈值。
09 切片集群
e.g.:用单个实例储存较大数据,此时Redis fork子线程进行RDB持久化时,会耗时十分严重。fork操作和储存数据所占内存大小成正比,所以一般来说对该Redis实例进行切片组建切片集群。
主要问题:
数据切片后如何实现实例的分布?
客户端如何定位所要访问的数据在哪个实例上?
数据切片和实例的对应分布关系
切片集群是一种保存大量数据的通用机制,该机制可有大量不同的实施方案。
Redis3.0后官方提供了Redis Cluster机制。
Redis Cluster采用了哈希槽的机制来处理数据和实例之间的映射关系。Cluster方案中一个切片集群有16384个哈希槽,每个键值对都会根据他的key映射到一个哈希槽中。
在创建切片集群时,Cluster会将哈希槽分配到不同的实例上。
在客户端和实例建立连接后就会发送哈希槽分配信息给客户端,当集群刚刚建立时,每个实例只知道自己被分配了哪些哈希槽,而不知道其他哈希槽信息。Redis实例会发送自己的哈希槽信息给其他实例,每个实例都具有了哈希槽映射关系。
客户端会在本地耶缓存哈希槽信息。
集群中有实例增加和删除,Redis重新分配哈希槽。
为了负载均衡需要把哈希槽重新分配一次。
客户端此时缓存是失效的,Cluster提供了重定向机制,如果在该实例没有找到key值的哈希槽,会参考这个实例上最新的哈希槽对照表,重定向到正确的实例。
MOVED:重定向
ASK:哈希槽中的数据还没有迁移完毕