Redis-概述
# Redis-概述
# 1、数据结构
# 1.1、String
字符串对象的编码可以是int、raw或者embstr
如果保存的是整数,并且整数可以被long表示,那么底层存储的字符串编码就是int
# 设置num值为100 > set num 100 OK # 查看num编码 > OBJECT ENCODING num "int"
1
2
3
4
5
6如果保存的是字符串,小于等于32字节,那么字符串对象编码使用embstr来保存
> set msg "hello" OK > OBJECT ENCODING msg "embstr"
1
2
3
4如果保存的是字符串,大于32字节,那么字符串使用简单动态字符串(SDS)来保存,编码设置为raw
> set bigmsg "long long ago......." OK # 查询字符串大小 > STRLEN bigmsg (integer)40 > OBJECT ENCODING bigmsg "raw"
1
2
3
4
5
6
7
# 1.2、List
列表对象的编码可以是ziplist或者linkedlist
列表对象保存的所有字符串元素的长度都小于64字节 并且 列表对象保存的元素数量小于512个,使用ziplist编码
# 所有元素的长度都小于64字节 > RPUSH blah "hello" "world" "again" (integer)3 > OBJECT ENCODING blah "ziplist" #将一个65字节长的元素推入列表对象中 > RPUSH blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww" (integer) 4 #编码已改变 > OBJECT ENCODING blah "linkedlist"
1
2
3
4
5
6
7
8
9
10
11否则 使用linkedlist编码
# 列表对象包含512个元素 > EVAL "for i=1, 512 do redis.call('RPUSH', KEYS[1],i)end" 1 "integers" (nil) > LLEN integers (integer) 512 > OBJECT ENCODING integers "ziplist" # 再向列表对象推入一个新元素,使得对象保存的元素数量达到513个 > RPUSH integers 513 (integer) 513 # 编码已改变 > OBJECT ENCODING integers "linkedlist"
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.3、Hash
哈希对象的编码可以是ziplist或者hashtable
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节 并且 哈希对象保存的键值对数量小于512个,哈希对象使用ziplist编码
# 哈希对象只包含一个键和值都不超过64个字节的键值对 > HSET book name "Mastering C++ in 21 days" (integer) 1 > OBJECT ENCODING book "ziplist" # 向哈希对象添加一个新的键值对,键的长度为66字节 > HSET book long_long_long_long_long_long_long_long_long_long_long_description "content" (integer) 1 # 编码已改变 > OBJECT ENCODING book "hashtable"
1
2
3
4
5
6
7
8
9
10
11否则 使用hashtable编码
# 创建一个包含512个键值对的哈希对象
> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i)end" 1 "numbers"
(nil)
> HLEN numbers
(integer) 512
> OBJECT ENCODING numbers
"ziplist"
# 再向哈希对象添加一个新的键值对,使得键值对的数量变成513个
> HMSET numbers "key" "value"
OK
> HLEN numbers
(integer) 513
# 编码改变
> OBJECT ENCODING numbers
"hashtable"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.4、Set
集合对象的编码可以是intset或者hashtable
集合对象保存的所有元素都是整数值 并且 集合对象保存的元素数量不超过512个,使用intset编码
> SADD numbers 1 3 5 (integer) 3 > OBJECT ENCODING numbers "intset"
1
2
3
4否则使用hashtable编码
> SADD numbers "seven" (integer) 1 > OBJECT ENCODING numbers "hashtable"
1
2
3
4
# 1.5、ZSet
有序集合的编码可以是ziplist或者skiplist
有序集合保存的元素数量小于128个 并且 有序集合保存的所有元素成员的长度都小于64字节,使用ziplist编码
# 对象包含了128个元素 > EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers (nil) > ZCARD numbers (integer) 128 > OBJECT ENCODING numbers "ziplist" # 再添加一个新元素 > ZADD numbers 3.14 pi (integer) 1 # 对象包含的元素数量变为129个 > ZCARD numbers (integer) 129 # 编码已改变 > OBJECT ENCODING numbers "skiplist"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16否则使用skiplist编码
# 向有序集合添加一个成员只有三字节长的元素 > ZADD blah 1.0 www (integer) 1 > OBJECT ENCODING blah "ziplist" # 向有序集合添加一个成员为66字节长的元素 > ZADD blah 2.0 ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo (integer) 1 # 编码已改变 > OBJECT ENCODING blah "skiplist"
1
2
3
4
5
6
7
8
9
10
11
# 2、过期键删除策略
如果一个键过期了,那么它什么时候会被删除呢?
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作 内存友好,CPU不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间,在内存不紧张但是CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键 内存不友好,CPU友好,如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放,可以理解为一种内存泄漏
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定;定期删除策略是前两种策略的一种整合和折中
- 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
- 通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费
Redis使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
过期键的定期删除策略由redis.c/activeExpireCycle函数实现:
- 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键
- 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作
# 3、AOF、RDB对于过期键的影响
# 3.1、RDB
生成RDB文件对于过期键的影响:
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中
载入RDB文件对于过期键的影响:
- 如果是主服务器:在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略。
- 如果是从服务器:在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空。
# 3.2、AOF
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除
AOF重写过程:
在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中
集群复制:
- 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键(
从服务器对于过期键也会返回给客户端
) - 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
# 4、Redis持久化
# 4.1、RDB
RDB文件的生成:
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求
自动BGSAVE:
save 900 1
save 300 10
save 60 10000
2
3
RDB文件的载入:
RDB文件的载入工作是在服务器启动时自动执行的,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。(因为AOF文件的更新频率通常比RDB更频繁,所以如果服务器同时开启了AOF持久化,那么服务器会优先使用AOF文件来还原数据库状态)
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
# 4.2、AOF
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤
- 命令追加:当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
- 文件写入:服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数
- 文件同步:flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面(系统调用的fsync函数,会把内存缓冲区的数据刷入磁盘)。flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,
默认everysec
AOF重写:随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多
AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的
AOF重写的实现:
使用子进程进行数据重写,避免主进程阻塞,但是这样会出现主进程依旧在进行数据存储,和子进程数据不一致的情况,那么如何处理?
AOF重写缓冲区:
为了解决这种不一致问题,Redis设置了AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,也就是说当创建子进程之后,Redis主进程接收到一个客户端命令之后会进行三步操作
1、执行客户端命令
2、把执行后的命令写入AOF缓冲区
3、把执行的后的命令写入AOF重写缓冲区
这样能保证什么呢?首先能保证AOF缓冲区拥有最新的内容,也就是说现有的AOF文件数据都是最新的,然后重写缓冲区能存储从创建子进程开始之后的所有的命令。然后子进程重写AOF完成之后,会给主进程发送一个信号量,主进程接收之后,开始进入阻塞状态(不接受任何客户端命令),然后主进程做了两件事:一个是将AOF重写缓冲区的数据都加入到新的AOF文件中,这就保证了新的AOF文件内容和原有AOF保持一致,第二是对新的AOF文件进行改名,并原子的把旧AOF进行覆盖,完成新旧AOF文件的替换
# 5、主从复制
Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式
# 5.1、同步
完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步
部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态
部分重同步功能由以下三个部分构成:
- 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。
- 主服务器的复制积压缓冲区(replication backlog)。
- 服务器的运行ID(run ID)
# 5.2、命令传播
当完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令
REPLCONF ACK <replication_offset>
发送REPLCONF ACK命令对于主从服务器有三个作用:
- 检测主从服务器的网络连接状态。
- 辅助实现min-slaves选项。
- 检测命令丢失。
这里主要说一下检测命令丢失:
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器
# 6、Sentinel
Sentinel(哨岗、哨兵)是Redis的高可用性(high availability)解决方案:由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
哨兵工作的整体步骤,以及每个步骤发生的过程:
1、启动Sentinel
$ redis-sentinel /path/to/your/sentinel.conf
启动最后一步,会创建指向主服务器的两个连接,命令连接和订阅连接
2、获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息;信息中返回的除了主服务器信息之外,还有当前主服务器下属的从服务器列表信息,Sentinel获得信息之后会维护从服务器的信息,并和从服务器建立两个连接(命令连接和订阅连接)
3、建立主从、以及Sentinel的连接
当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络
4、判断主节点下线
Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
实例对PING命令的回复可以分为以下两种情况:
- 有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。
- 无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复
Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度,如果在参数时间范围内,没有返回有效回复,则当前Sentinel会认为次服务处于主观下线状态
注意:多个Sentinel设置的主观下线时长可能不同
由主观下线判定是否处于客观下线,需要多个Sentinel一起判断,具体过程如下:
发送is-master-down-by-addr命令询问其他Sentinel是否同意主服务器已下线,Sentinel将统计其他Sentinel同意主服务器已下线的数量,当这一数量达到配置(quorum参数)
指定的判断客观下线所需的数量时,当前服务器标记为客观下线。
5、选主节点
选举主节点之前,还需要先在多个Sentinel中,选出一个主Sentinel,选举依赖于Raft算法(参照网站中的Raft文章
https://zhaoyb-coder.github.io/dcs/3512ff (opens new window))
选举出主Sentinel节点之后,Sentinel开始在主节点的所有从节点中选出一个当做主节点,具体筛选条件如下:
- 删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的从服务器都是正常在线的。
- 删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。
- 删除所有与已下线主服务器连接断开超过down-after-milliseconds10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。
- 领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。
- 如果有多个具有相同最高优先级的从服务器,那么领头Sentinel将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器(复制偏移量最大的从服务器就是保存着最新数据的从服务器)
- 最后,如果有多个优先级最高、复制偏移量最大的从服务器,那么领头Sentinel将按照运行ID对这些从服务器进行排序,并选出其中运行ID最小的从服务器
选中一个Server之后,会往这个Server发送SLAVEOF no one,此命令会是使当前从服务器变为主服务器
发送之后,主Sentinel节点会以1秒/次的频率(平时是每十秒一次)向选中的Server发送INFO命令,一直监控到当前server变成master。
变成master之后,需要让下线所属的从服务器去复制新的主服务器,向从服务器发送SLAVEOF命令
最后将已下线的主服务器设置为新的主服务器的从服务器,因为旧的主服务器已经下线,所以这种设置是保存在server1对应的实例结构里面的,当server1重新上线时,Sentinel就会向它发送SLAVEOF命令,让它成为server2的从服务器
# 7、集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
# 7.1、创建集群
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式(节点只能使用0号数据库)
把其余节点加入到当前集群中:
CLUSTER MEET <ip> <port> # 加入集群
CLUSTER NOTES # 查询集群中的节点信息
2
3
分配分片槽:
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)
CLUSTER ADDSLOTS <slot> [slot ...]
# 7.2、集群处理命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己
# 7.3、重新分片(新加机器)
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点
重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求