Redis知识点总结

什么是Redis?
Redis(Remote Dictionary Server)是使用C语言编写,基于内存的K-V键值对数据库,它支持数据的持久化以及多种数据类型(字符串、列表、哈希、集合、有序集合)单线程处理请求,QPS可达10W+,也支持简单的事务、持久化、LUA脚本以及多种集群方案。
五大数据类型
String(字符串)
Redis没有使用C语言传统的字符串表示(以空字符串结尾的字符数组,C字符串),而是自己构建了一种简单动态字符串(Simple Dynamic String,SDS字符串)的抽象类型。
存储结构
参数说明
- free:记录buf数组中未使用的字节的数量(0表示这个SDS没有分配任何未使用空间)
- len:记录buf数组中已使用字节的长度(5表示这个SDS保存了一个5字节长的字符串)
- buf:字节数组,用于保存字符串(该属性是一个char类型的数组,该数组分别保存了’R’、‘e’、‘d’、‘i’、‘s’五个字符,最后一个字节则保存了空字符’\0’)
使用场景
做简单的键值对缓存
C字符串与SDS之间的区别
C字符串 | SDS字符串 |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然要执行N次内存重分配 | 修改字符串长度N次最多需要N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h>库中的函数 | 只能使用部分<string.h>库中的函数 |
①常数时间获取字符串的长度
SDS中len字段保存着字符串的长度,所以总能在常数时间内获取字符串长度
②避免缓冲区溢出
假设在内存中有两个紧挨着的两个字符串,s1=“1234”,s2=“abcd”。
由于内存上紧紧相连,当我们对s1进行扩充时,将s1="12345678"后,由于没有进行相应的内存重分配,导致s1把s2覆盖掉,导致s2被莫名的修改。
但SDS的API对字符串修改时首先会检查空间是否足够,若不充足则会分配新空间,避免了缓冲区溢出问题。
③减少字符串修改时内存重分配次数
在C中当我们频繁对一个字符串进行修改(append或trim)操作的时候,需要频繁的进行内存重分配操作,十分影响性能。
对于Redis来说,本身就会频繁的修改字符串,所以使用C字符串并不合适。而SDS实现了空间预分配和惰性空间释放两种优化策略。
-
空间预分配:当SDS的API对一个SDS修改后,并且对SDS空间扩充时,程序不仅会为SDS分配 所需要的必须空间,还会分配额外的未使用空间。
分配规则如下:如果对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。举个例子,如果 len=10,重新分配后,buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。如果对 SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。
- 惰性空间释放:当对SDS进行缩短操作时,程序并不会回收多余的内存空间,而是使用free字段将这些字节数量记录下来,后面如果需要append操作,则直接使用free中未使用空间。
④二进制安全
在Redis中不仅可以存储String类型的数据,而且也能存储一些二进制数据。
二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如’\0’,在C中遇到’\0’则表示字符串结束,但在SDS中,标志字符串结束是len属性。
List(链表)
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表地长度。
由于Redis使用的C语言没有内置这种数据结构,所以Redis构建了自己的链表实现。
存储结构
节点的定义
链表的定义
示意图
使用场景
链表被广泛的用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等
Hash(哈希/字典)
在哈希表中,一个键(Key)可以和一个值(Value)进行关联,这些关联的键和值就称为键值对。
字典中的每个键都是独一无二的,可以在字典中根据键查找与之关联的值,或者通过键更新值。
由于Redis所使用的C语言并没有内置这种数据结构,所以Redis构建了自己的字典实现。
存储结构
字典的定义
参数说明
- table :是一个数组,数组中的每个元素都指向dictEntry结构,每个dictEntry结构保存着一个键值对
- size:记录了哈希表的大小
- used:记录哈希表目前已有节点(键值对)的数量
示意图
使用场景
Redis持久化
Redis是将数据存储在内存中的,要是服务宕机,所有数据将会丢失,为了防止数据丢失,Redis支持两种策略将内存中的数据写到磁盘中来防止数据丢失。
Redis提供两种持久化方式:RDB(Redis DataBase)和AOF(Append Only File)
RDB
该方式服务器进程会fork一个子进程,由子进程去做持久化操作,子进程会先将Redis所有非空数据库的数据进行拷贝到自己的内存空间,然后将这些数据写到一个临时文件,写入完毕,会使用临时文件替换掉dump.rdb文件,线程销毁
示意图
触发时机
使用相关命令保存
- SAVE命令:该命令执行时,Redis服务器会被阻塞,所以当SAVE命令正在执行时,客户端发送的所有命令请求都会被拒绝。
- BGSAVE命令:该命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器任然可以继续处理客户端的命令请求。
自动间隔性保存
在redis.conf文件中已经默认配置了3种:
配置 | 描述 |
---|---|
save 900 1 | 服务器在900s(15min)内,对数据库进行了至少1次修改 |
save 300 10 | 服务器在300s(5min)内,对数据库进行了至少10次修改 |
save 60 10000 | 服务器在60s(1min)内,对数据库进行了至少有10000修改 |
AOF
该方式是以日志的形式记录Redis每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件不可以改写文件,Redis启动之后会读取appendonly.aof文件来实现重新恢复数据。默认不开启,需要将redis.conf种的appendonly no改为yes来启动。
要开启Redis的AOF持久化模式必须将
appendonly
配置项改为yes
appendfsync选项的值 | 对持久化行为的影响 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 |
everysec | 将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的 |
no | 将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统来决定 |
AOF文件的载入与数据还原
因为AOF文件包含了重建数据库状态的所有写命令,所以只要服务器载入AOF文件重新执行一遍保存的写命令,就可以还原数据库关闭之前的状态。(由于Redis命令只能在客户端上下文中执行,所以这块创建了一个伪客户端)
重写机制
上面介绍了AOF方式是通过记录对Redis每次的写操作,这样的话就会存在一个问题,一直添加最终会导致文件过于庞大。因此,为了避免这种状况,Redis提供了重写机制,当AOF文件得大小超过指定的阈值时,Redis会自动启用AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令
bgrewriteaof
。
重写原理:AOF文件持续增长过大时,会fork出一条新进程来将文件重写(也是生成临时文件再rename),遍历服务进程的内存中的数据,每条记录会对应生成一条set语句写入到临时文件。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了到一个新的aof文件,类似快照。
触发时机:Redis会记录上一次重写时的aof大小,默认配置是当AOF文件大小是上一次的一倍并且大于64M时,会触发重写机制。
数据不一致问题:Redis是通过创建一个子进程去做重写操作的,所以就有可能出现下面这种数据不一致情况
当子进程开始重写时,数据库中只有K1这一个键,但是当子进程完成AOF文件重写之后,服务器进程的数据库已经新增了K2、K3、K4三个键,因此,重写AOF文件和服务器当前的数据库状态并不一致。
解决方案:为了解决上面的数据不一致问题,Redis服务器引入了一个AOF重写缓冲区
如上图,这个缓冲区在服务器创建子进程的时候开始使用,当Redis执行完一个写命令之后,它会同时将这个命令发送给AOF缓冲区和AOF重写缓冲区。
RDB与AOF对比
RDB的优点:
- 如果要进行大规模数据的恢复,RDB方式要比AOF方式恢复速度要快
- RDB可以最大化Redis性能,父进程做的就是fork子进程,然后继续接受客户端请求,让子进程负责持久化操作,父进程无需进程IO操作。
- RDB保存了某一时刻的数据集,非常适合用作备份,同时也非常适合灾难性恢复。
RDB的缺点:
- RDB不太适用对数据完整性要求严格的情况,尽管我们可以通过修改快照持久化的频率,但是要持久化的数据时一段时间内的整个数据集的状态,如果在还没有触发快照时,本机就宕机了,那么对数据库所做的写操作就随之消失。
- 每次进行RDB时,父进程会fork一个子进程,由子进程来进行实际的持久化操作,如果数据集庞大,那么fork子进程这个过程将非常耗时,就会出现服务器暂停客户端请求,将内存中的数据复制一份给子进程,让子进程进程持久化操作。
AOF的优点:
- AOF支持多种持久化策略
AOF的缺点:
- 对于相同的数据集来说,AOF文件要比RDB文件大。
- 根据持久化策略来说,AOF的速度要慢于RDB
RDB与AOF如何选择
- 要想做到足够高的数据安全性,应该同时使用两种持久化方式。
- 如果可以接受分钟级别内的数据丢失,可以只使用RDB持久化(比如:只用做缓存)。
数据恢复机制
上面介绍了数据的持久化,而持久化的最终目录就是为了尽可能的避免数据丢失,当重启Redis服务的时候能够重新恢复数据到内存。
重启Redis时,如果dump.rdb与appendfsync.aof同时存在时,Redis会优先读取appendfsync.aof文件进行数据恢复。
key过期机制
设置键的生存时间
EXPIRE <key> <ttl> # 将键key的生存时间设置为ttl秒
PEXPIRE <key> <ttl> # 将键key的生存时间设置为ttl毫秒
EXPIREAT <key> <timestamp> # 将键key的过期时间设置为timestamp所指定的秒数时间戳
PEXPIREAT <key> <timestamp> # 将键key的过期时间设置为timestamp所指定的毫秒数时间戳
为给定key设置生存时间,当key过期时,它会被自动删除。在Redis中带有生存时间的key被称为『易失的』(volatile)
查看剩余生存时间
TTL <key> #以秒为单位返回键的剩余生存时间
PTTL <key> #以毫秒为单位返回键的剩余生存时间
保存过期键的结构
如下图redisDb(代表Redis的数据库对象)结构的expires字段保存了数据库中所有键的过期时间,我们称为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象
- 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳
过期键删除策略
实际上Redis使用懒惰删除+定期删除相结合的方式处理过期的key。
懒惰删除
所谓懒惰删除就是在客户端访问该key的时候,Redis会对key的过期时间进行检查,如果过期了就立即删除。
这种方式看似很完美,在访问的时候检查key的过期时间,不会占用太多的额外CPU资源。但是如果一个key已经过期了,如果长时间没有被访问,那么这个key就会一直存留在内存中,严重消耗内存资源。
定期删除
Redis会将所有设置了过期时间的key放入到一个字典中,然后每隔一段时间从字典中随机找出一些key检查过期时间并删除已过期的key。
Redis默认每秒进行10次过期扫描:
- 从过期字典中随机20个key
- 删除这20个key中已过期的
- 如果超过25%的key过期,则重复第一步
同时,为了保证不出现循环过度的情况,Redis还设置了扫描的时间上限,默认不会超过25ms。
淘汰策略
一般缓存都是通过内存实现的,而内存的空间又非常的珍贵,可能我们一些数据偶尔读取一次就被放入缓存,有的数据经常被访问。但是一般我们会设置缓存的上限,那么如何保证热点数据最终会保留下来呢?抛开Redis,常见的缓存淘汰机制有:随机、最近最少使用(lru)、先进先出等。那我们看看Redis都提供了哪些淘汰机制:
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中选择任意数据淘汰
- allkeys-lru:从数据集中选择最近最少使用的数据淘汰
- allkeys-random:从数据集中任意选择数据淘汰
- no-enviction:禁止驱逐数据
Redis多机实现
主从模式(master-slave)
可以通过执行
SLAVEOF
命令或者设置slaveof选项,让一个服务器去复制(replicate)另一台服务器,这种工作模式我们称为主从模式。其中被复制的服务器为主服务器(master),对主服务器进行复制的服务器称为从服务器(slave)。
示意图
相关命令
# 设置主服务器 host:主服务器ip port:主服务器端口
SLAVEOF host port
实现原理
旧版复制功能的实现
Reids的复制功能分为同步(sync)和命令传播(command propagate)两个操作。
- 同步操作用于将从服务器的状态更新至主服务器当前所处的数据库状态
- 命令传播操作当主服务器的状态被修改时,导致主从服务器的数据库状态不一致时,让主从服务器的数据库重新回到一致状态
同步操作是从服务器向主服务器发送
SYNC
命令来完成的,示意图如下:
- 从服务器向主服务器发送
命令SYNC
- 主服务器收到命令后执行
命令,在后台生成RDB文件,同时使用一个缓冲区记录从现在开始执行的所有写操作。BGSAVE
- 主服务器命令执行完毕,将生成的RDB文件发送给从服务器,从服务器接受并载入,然后将更新数据库状态更新至主服务器执行
命令时的数据库状态BGSAVE
命令传播上面同步操作解决了初次的主从服务器数据的一致性,如果后续对主服务器进行的写操作,又会导致主从服务器不一致问题,为了保证再次回到一致状态,主服务器会将自己执行的写命令发送给从服务器执行,从服务器执行发来的命令后会再次回到一致状态。
新版复制功能的实现
Redis从2.8版本开始,使用
PSYNC
命令代替
SYNC
命令来执行复制时的同步操作。
PSYNC
命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式
完整重同步用于处理初次复制的情况,完整重同步执行步骤和
SYNC
命令的执行步骤基本一样,他们都是通过让主服务器创建并发送RDB文件,以及向服务器发送保存在缓冲区里面的写命令来进行同步
部分重同步
PSYNC命令的实现
哨兵模式(Sentinel)
哨兵模式是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以同时监控任意多个主服务器,以及每个主服务器下的所有从服务器。在监视到主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
示意图
- 双环代表主服务器server1
- 单环代表三个从服务器server2、server3、server4
- server2、server3、server4三个从服务器正在复制主服务器server1,而sentinel系统正在监听所有四个服务器
可以看出哨兵模式就是在主从复制模式之上添加了哨兵系统,从而实现故障的自动转移
故障转移
如上图16-3所示,主服务server1挂掉了,处于下线状态,那么server2、server3、server4对主服务器的复制操作将被终止,并且隔一段时间sentinel系统也会察觉到server1的下线,下面先说一下故障转移的流程:
- Sentinel系统会挑选server1属下的其中一个从服务器,并选中的从服务器升级为新的主服务器
- Sentinel系统会向server1属下的所有从服务器发送新的复制命令,让他们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕
- Sentinel系统还会继续监听已下线的server1,如果它重新上线时,会将它设置为新的主服务器的从服务器
Sentinel系统与各个节点的通讯
Sentinel如何判断主服务器下线
主观下线
在默认情况下,Sentinel系统会以每秒一次的频率向所有与它创建了命令连接的实例发送
PING
命令,并通过实例返回的结果来判断实例是否在线。
如果一个实例在
down-after-milliseconds
毫秒内(默认30s),连续向Sentinel返回无效回复,那该Sentinel就会将其标记为主观下线状态。
Sentinal配置文件中的
down-after-milliseconds
选项指定了Sentinel判断实例进入主观下线所需的时间长度。
客观下线
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经是下线状态(可以是主观下线或客观下线),当Sentinel从其他Sentinel那里接受到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器开始执行故障转移操作。
客观下线状态的判断条件:当认为主服务器已经进入下线状态的Sentinel的数量,超过Sentinel配置中设置的quorum参数的值,那么该Sentinel就会认为主服务器已经进入客观下线状态。
上面配置的含义:包括当前Sentinel在内,只要总共有两个Sentinel认为主服务器已经下线,那么当前Sentinel就将主服务器判断为客观下线。
当Sentinel将一个主服务器判断为主观下线后,它会向同样监视该主服务器的其他Sentinel进行询问,看它们是否同意这个主服务器已经进入主观下线状态。
故障转移
选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。
选举领头Sentinel的规则:
- 所有在线的Sentinel都有被选为领头Sentinel的资格
- 每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元的值都会自增一次。
- 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里就不能再更改
- 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel
- 如果某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel
- 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举
新主服务器的选举&故障转移
选举好领头Sentinel之后,领头Sentinel将对已下线的服务器执行故障转移操作。
第一步:首先会从已下线主服务器(server1)属下所有的从服务器中挑选一个状态良好、数据完整的从服务器,并发送
SLAVE no one
命令。
第二步:此时领头Sentinel会以每秒一次的频率(平时十秒一次)向被升级的从服务器(server2)发送INFO命令并观察返回的role是否已经变成了master,变成master说明升级成功。
第三步:领头Sentinel向已下线主服务器(server1)的两个从服务器(server3、server4)发送
SLAVEOF
命令,让他们复制新的主服务器(server2)。
所有从服务器改变主服务器后如下图
第四步:当server1重新上线时,Sentinel就会向它发送
SLAVEOF
命令,让它成为新主服务器(server2)的从服务器。
至此,整个故障转移就完成了。
Redis高可用集群
缓存穿透、击穿、雪崩现象及解决方案
持续更新,未完待续。。。。
参考资料:Redis设计与实现[黄健宏]