大家好,我是Leo。
结束了漫长了MySQL,开始步入了Redis的殿堂。最近在做Redis技术输出时,明显发现进一步熟悉MySQL之后,对Redis的理解容易了许多。或许这就是进步吧!
下面的思路部分,可以帮助你更好的理解这篇文章的知识体系。
思路
整体结构
Redis主要是由访问框架,操作模块,索引模块,存储模块,高可用集群支撑模块,高可用扩展支撑模块等组成,
Redis还有一些,丰富的数据类型,数据压缩,过期机制,数据淘汰策略,分片机制,哨兵模式,主从复制,集群化,高可用,统计模块,通知模块,调试模块,元数据查询等辅助功能。
接下来的Redis学习之路,主要是围绕介绍上述模块,功能,策略,机制,算法等知识的输出。
五大类型
String
String类型应该是我们用的最多的一种类型,它的底层是由简单的动态字符串实现的。
hash
hash类型也是我们用的最多的一种类型了,它是由压缩列表+哈希表共同实现的一种数据类型
list
list它是一种列表类型,也是我们常用类型之一,它是由双向链表+压缩列表共同实现的一种数据类型
set
set集合和上述类型不同,他不允许重复,所以一些特定的场景会优先考虑set类型,它是由整数数组+哈希表共同实现的一种数据类型
sort set
sortset是在set的基础上,做的一个提升,不允许重复的时候,还可以处理有序。主要应用与排序表之类的场景需求,它是由压缩链表+跳表实现的一种数据类型
数据结构
哈希表会在下文rehash那里详细介绍一下。
整数数组和双向链表也很常见,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低。
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。在下述文章中的第五章节介绍过了跳表的相关说明。
哈希为啥变慢了
Redis在处理一个键值对时,会进行一次hash处理,把键处理成一个地址码写入Redis的存储模块,随着我们key的越来越多,有一些key会存在同一个地址码的情况。(我在写hashmap的时候就介绍过hash碰撞的问题)
出现这种情况之后Redis作了一个键值对的扩展,也就是键值对+链表的方式。如下图,多个数据经过hash处理之后,都落到了key1值上。一个卡槽不可能存放两个值,于是就在这个卡槽存了指向一个链表的指针,通过链表存储多个值。
哈希链表
链表处理的就是多个key一样的问题,随着数据量的发展,哈希碰撞的情况越来越频繁,链表的数据也就越来越多。hash的性能是O(1),链表的性能是O(n)。所以整体的性能被拖下来了。为了改变这一现状,Redis引入了rehash。
rehash
rehash就是增加现有的哈希桶的数量,让逐渐增多的元素能在更多的哈希桶之间分散保存。从而减少单个桶的链表的元素数量,同时也减少单个桶的冲突。
首先Redis会先创建两个全局哈希表,我们这里定义为哈希表A,哈希表B。我们在插入一个数据时,先先存入A,随着A越来越多,Redis开始执行rehash操作。主要分为三步:
- 给B分配更多的空间,一般都是A的两倍
- 把A中的数据全部拷贝到B中
- 释放A
上述rehash流程我们可以看出,当A中存在大量的数据,拷贝的效率是非常慢的!因为Redis的单线程性还会造成阻塞,导致Redis短时间无法提供服务。为了避免这一问题,Redis在rehash的基础上,采用了渐进式rehash。
渐进式 rehash
进化点就是在第二步拷贝的时候,并不是一次性拷贝的,而是分批次拷贝。在处理一个请求时,从A中的第一个索引位置开始,顺带着将这个索引位置上的所有元素拷贝到B中。等下一个请求后,再从A表中的下一个索引位置继续拷贝操作。这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
Redis单线程还是多线程
先来普及一下多线程的知识,一个CPU在运行多个线程时,会有一个多线程调用的消耗问题,而且还有多个线程调用时数据一致性的问题。这些都要单独处理,单独处理又会消耗性能。于是Redis统筹兼顾采用了单,多线程并用的思路。
在处理数据写入,读取属于键值对数据操作,采用单线程操作。在请求连接,从socket中读取请求,解析客户端发送请求,采用多线程操作。
Redis巧妙的把所有需要延迟等待的操作全部转交给了多线程处理,在不需要等待的全部单线程处理。个人感觉这种设计思路很棒
tip:如果不按照这种方式设计的,连接之后等待,发送等待,接收等待估计要等死你哦。造成Redis线程阻塞,无法处理其他请求。
多路复用机制
IO多路复用机制是指一个线程处理多个IO流,也是我们经常听到的select/epoll机制。那么那些连接,等待的操作Redis都是如何处理的呢?
在Redis只运行单线程的情况下,同一时间存在多个监听套接字,和已连接的套接字,内核会一直监听这些连接请求和数据请求。一旦客户端发送请求就会以事件的方式通知Redis主线程处理。这就是Redis线程处理多个IO流的效果。
上文说到以事件方式通知Redis这里我们做一个扩展,select/epoll提供了基于事件的回调机制,不同的事件会调用相应的处理函数。一旦请求来了,立刻加到事件队列中,Redis单线程就会源源不断的处理该事件队列。解决了等待与扫描的资源浪费问题。
安全机制
Redis的持久化安全机制主要有两大块,一块是AOF日志,一块是RDB快照,接下来我们聊聊AOF与RDB的一些区别吧
AOF
Redis为了提升性能采用的是写后日志,先执行命令,后写日志,这样做的好处主要有两点
- 只有当命令执行成功之后才会写入日志。这样就避免了写入日志之后,命令执行错误还要把日志删掉的问题。
- 先执行写入操作,后写日志,这样同时也避免了阻塞当前的写操作
坏处是:
- 如果一个命令执行完后,还没记录日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
- AOF虽然避免了对当前命令的阻塞,但可能会对下一个操作带来阻塞风险。因为AOF日志也是在主线程中执行的,并且是
- 写入磁盘。
文件格式:
Redis收到一个 "set huanshao 公众号欢少的成长之路" 命令后,AOF的日志内容是,"*3" 表示当前命令有三个部分,每部分都是由
+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“
3 set”表示这部分有 3 个字节,也就是“set”命令。
AOF写入策略
AOF提供了三种appendfsync可选值
- Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
- Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
- No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
这三种都无法做到两全其美,同步写会可以做到数据一致性,但是写入磁盘的这个性能对比内存来说太差了,如果是每秒写的话,就会丢失1秒的数据,如果No配置的话宕机后丢失数据比较多。
最后三种配置如何选择,应该根据特定的业务场景。如果数据安全性过高就选择同步写回,如果适中就每秒写回,没安全性的话就选择No。
AOF重写机制
AOF日志是追加形式的,避免不了的就是文件过大之后,再写入日志的性能会有所下降,Redis为了解决这一难题,引入了重写机制。
重写机制主要做的事情是记录一个key值的最终修改结果,修改的历史记录一律排除。这样一来,一个命令就只有一个日志。如果要拿AOF日志恢复数据的话也能恢复出正确的数据。
重写机制流程就是主线程fork出一个后台子线程 bgrewriteaof后,fork会把主线程的内存拷贝一份给子线程bgrewriteaof,这样子线程就可以在不影响主线程阻塞的情况下进行重写操作了。
在这段期间,如果有新的请求写入过来,Redis会有两个日志,一个日志指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。另一处日志指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
RDB
RDB是一种内存快照,它是系统某一刻的数据备份写到磁盘上。这样就可以达到宕机后,可以恢复某一刻之前的所有数据。
生成RDB的两种方式
- save:在主线程中执行,会导致阻塞;
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的 默认配置。
写时复制技术
首先介绍一下写时复制技术的由来,在Redis做RDB快照时(当前RDB还没有做完),来了一个修改数据的请求。如果把这个请求写入快照,那么就不符合那一刻的数据一致性。如果不写入快照把他丢弃,就会造成数据丢失还是会有数据一致性的问题。所以Redis借助操作系统提供的写时复制技术,在执行快照的同时,正常处理写操作。
处理流程
主线程fork创建子线程bgsave,可以共享主线程的所有内存数据,bgsave子线程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。如果主线程对这些数据都是读操作,那么互不影响。如果是修改操作的话就会把这块数据复制一份,生成该数据的副本。然后主线程在这个副本上进行修改。同时bgsave 子进程可以继续把原来的数据写入 RDB 文件。
这样保证了快照的数据一致性,也保证了快照期间对正常业务的影响。
既然RDB那么牛逼,可否用RDB做持久化呢?
如果我们采用RDB做持久化的话,那么就要一直进行RDB快照,如果每2秒做一次快照的话,最坏的打算就要少50%的数据量,如果每秒做一次快照,可以完全保证数据的一致性但是带来的负面影响也是非常大的。
- 频繁快照,导致磁盘IO占用影响,且磁盘内存开销非常大
- RDB由bgsave处理,虽然不阻塞主线程,但是主线程新建bgsave时,会影响主线程,如果每秒新建一次,有可能会阻塞主线程的。
全量备份不行的话,增量备份是否可以用RDB做持久化呢?
增量备份与全量备份的区别就是,增量备份只备份修改的数据。如果是这样的话,我们就需要对每一个数据都加一个记录,这样开销是十分大的。如果为了增量备份牺牲了宝贵的内存资源,这就有点得不偿失了。
实战应用
上述我们介绍了AOF与RDB的区别,流程,优缺点。我们可以发现,如果只依靠某一种方式进行持久化都无法有效的达到数据一致性。
如果只用RDB,快照的频率不好把握,如果使用AOF,文件持续变大也是吃不消的。
最优的策略就是 RDB + AOF 假如每小时备份一次RDB,我们就可以利用RDB文件恢复那一刻的所有数据,然后再用AOF日志恢复这一小时的数据。