- 一、基本简介
- 二、前期准备
- 三、通用命令(针对key的操作)
- 四、内部数据结构
- 五、Redis对象
- 六、持久化
- 七、复制
- 八、Sentinel
- 此时的auth是product_auth
- 此时的auth是product_auth
- 此处监听的端口可随意指定
- 监视一个名为market,地址为172.28.171.111,端口为6379的Master;2表示当2个Sentinel都判定Master失效了才会触发自动故障转移
- 指定Sentinel判定Master主观下线的时间,单位毫秒
- 指定故障转移的超时时间,单位毫秒
- 故障转移时最多可以有多少个实例在同步新的主实例,取值越大,整体完成同步的时间越短,但是可能会造成数据读取失败
- 十三、缓存
- 一、基本简介
- 二、前期准备
- 三、通用命令(针对key的操作)
- 四、内部数据结构
- 五、Redis对象
- 六、持久化
- 七、复制
- 八、Sentinel
- 此时的auth是product_auth
- 此时的auth是product_auth
- 此处监听的端口可随意指定
- 监视一个名为market,地址为172.28.171.111,端口为6379的Master;2表示当2个Sentinel都判定Master失效了才会触发自动故障转移
- 指定Sentinel判定Master主观下线的时间,单位毫秒
- 指定故障转移的超时时间,单位毫秒
- 故障转移时最多可以有多少个实例在同步新的主实例,取值越大,整体完成同步的时间越短,但是可能会造成数据读取失败
- 十三、缓存
一、基本简介
Redis时一个开源的、高性能的、基于键值对的缓存与存储系统,通过提供多种键值对数据类型来适应不同应用场景下的缓存与存储需求。同时Redis的诸多高层级功能使其可以胜任消息队列、任务队列等不同角色。特性:
- 基础数据结构:字符串类型(String)、散列类型(Hash)、列表类型(List)、集合类型(Set)、有序集合类型(ZSet)
- 拓展数据结构:基数统计(HyperLogLog)、地理位置(Geo)、发布/订阅(Pub/Sub)
- 内存存储与持久化:Redis中所有数据否存储在内存中,一秒内读写可超过10万个键值;同时提供数据持久化将内存的数据异步写入到硬盘中
- 功能丰富:作为缓存系统,同时可指定占用的最大内存空间,达到空间限制后按一定规则自动淘汰不需要的键;List可用来实现队列,并支持阻塞式读取,可很容易实现一个高性能的优先级队列;支持“发布/订阅”消息模式
- 简单稳定、语法简单、二次开发更容易(Redis是基于C语言开发的)
二、前期准备
1、版本说明:Redis约定次版本号(即第一个小数点后的数字)为偶数的版本是稳定版,奇数是非稳定版本。
2、安装:根据自己电脑配置前往Redis官网下载对应版本。
- Windows
- 安装版安装(.msi):一直“下一步”就OK
- 解压缩版安装(.zip):直接解压到指定目录即可
- Linux
- 源码包安装(.tar.gz):使用命令
tar -zxvf 包名
解压缩后再用make
编译即可 - rpm包安装(.rpm):使用命令
rpm -ivh 包名
安装即可 - yum源安装:使用命令
yum install redis
一键安装 - Docker安装:先使用
docker pull redis
拉取redis镜像;再用docker run --name myredis -d -p6379:6379 redis
运行redis镜像;再通过docker exec -it myredis redis-cli
执行redis容器中的redis-cli以进行 客户端操作
- 源码包安装(.tar.gz):使用命令
3、可执行文件:这些命令都可使用后接 --help
的方式来查看参数。
文件名 | 作用 |
---|---|
redis-server | 启动Redis服务,可指定配置文件,置空使用默认配置文件 |
redis-cli | 启用Redis自带的命令行客户端 |
redis-benchmark | Redis性能测试工具 |
redis-check-aof | AOF文件检查修复工具,用于持久化 |
redis-check-rdb | RDB文件检查修复工具,用于持久化 |
redis-sentinel | 用于管理多个Redis服务器,实现高可用 |
4、配置:Redis所有配置都可在redis.conf配置文件中进行,此时的配置为全局配置,因为每次启动Redis服务,该服务的配置参数都是从配置文件中读取;也可在命令行客户端中通过命令 CONFIG SET 参数名 值
进行局部设置,只对当前服务有效,关闭当前客户端对应的Redis服务后即失效;可通过命令 CONFIG GET *
查看可通过命令修改的所有参数,其中奇数号为参数名,紧接着的偶数号为对应的取值。
5、拾遗:Redis默认支持16个数据库,可通过配置参数database修改;数据库名称是从0开始递增的数字,客户端建立连接后自动选择0号数据库,可通过 SELECT
命令随时切换数据库,Redis不允许自定义数据库名字;Redis不支持为每个数据库设置密码,要么全访问,要么一个访问不了;FLUSHALL
命令会清空一个Redis实例中所有数据库中的数据;不同的应用应该使用不同的Redis实例来存储数据。Redis6.0之后,开始提供ACL功能,实现细粒度的权限控制。
三、通用命令(针对key的操作)
3.1 常用
DEL key [key ...]
:删除一个或多个key,返回删除的个数;若key不存在,返回0。
时间复杂度为O(N),N为被删除的key的数量;
删除单个字符串类型的key,时间复杂度为O(1);
删除单个List、Set、SortedSet或Hash类型的key,时间复杂度为O(M),M为以上数据结构内的元素数量。EXISTS key
:检查指定key是否存在,存在返回1,否则返回0。KEYS pattern
:返回所有匹配成功的key到一个列表中,匹配模式【*、?、[ ]、\】,如果在较大的数据库中使用,可能会造成性能问题。
时间复杂度为O(N),N为数据库中key的数量。RANDOMKEY
:从当前db中随机返回一个key(不删除),当数据库为空时,返回nil。RENAMR key newkey
:重命名key,当newkey存在时会覆盖旧值;若key不存在,返回错误。RENAMENX key newkey
:当且仅当newkey不存在时,将key重命名为newkey;若key不存在,返回错误;若newkey存在,返回0;修改成功,返回1。TYPE key
:返回key所存储的值的类型,若key不存在,返回none。
127.0.0.1:6379> MSET test1 one test2 two test3 three #同时设置多个键值对
OK
127.0.0.1:6379> KEYS test* #返回匹配的所有键,结果为一个列表
1) "test3"
2) "test1"
3) "test2"
127.0.0.1:6379> DEL test1 test2 test3
(integer) 3 #返回3,代表删除了3个键
127.0.0.1:6379> KEYS test*
(empty list or set) #此时没有匹配的键
127.0.0.1:6379> EXISTS gree #检查gree键是否存在
(integer) 0
127.0.0.1:6379> DEL gree
(integer) 0 #删除不存在的key,返回0
127.0.0.1:6379> MSET test1 one test2 two test3 three
OK
127.0.0.1:6379> RANDOMKEY #随机返回一个key
"test2"
127.0.0.1:6379> KEYS *
1) "test3"
2) "test1" #并没有键被删除
3) "test2"
127.0.0.1:6379> MGET test1 test2 test3 #获取多个键的值
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> RENAME test1 test2 #将test1重命名为test2,此时test1丢失,test2的值被覆盖
OK
127.0.0.1:6379> MGET test1 test2 test3
1) (nil)
2) "one"
3) "three"
127.0.0.1:6379> RENAME gree test4 #重命名不存在的键,报错
(error) ERR no such key
127.0.0.1:6379> RENAMENX test2 test3 #test3已存在,重命名失败
(integer) 0
127.0.0.1:6379> MGET test1 test2 test3
1) (nil)
2) "one"
3) "three"
127.0.0.1:6379> RENAMENX test2 test4 #test4不存在,重命名成功,test2丢失
(integer) 1
127.0.0.1:6379> MGET test1 test2 test3 test4
1) (nil)
2) (nil)
3) "three"
4) "one"
127.0.0.1:6379> RENAMENX gree test5 #重命名不存在的键,报错
(error) ERR no such key
127.0.0.1:6379> TYPE test4
string
127.0.0.1:6379> TYPE test5
none
3.2 生存时间
前四个命令都是用来设置键的生存时间或过期时间,实际上前三种命令最终都会转换成第四种命令去执行。Redis中的数据库结构(redisDb)中有一个字典(expires),专门用于保存键的过期时间;字典中的键是一个指针,指向某个键对象,值是一个long类型的整数(13为的时间戳),保存键的过期时间。
EXPIRE key time
:为指定key设置生存时间,接收时间参数为“秒”,到期后自动删除。EXPIREAT key time
:为指定key设置过期时间,接收时间参数为时间戳(10位),到期后自动删除。PEXPIRE key time
:为指定key设置生存时间,接收时间参数为“毫秒”,到期后自动删除。PEXPIREAT key time
:为指定key设置过期时间,接收时间参数为时间戳(13位),到期后自动删除。PTTL key
:以毫秒为单位返回key的剩余生存时间;若key不存在,返回-2;若key存在但未设置过期时间,返回-1。TTL key
:以秒为单位返回key的剩余生存时间;若key不存在,返回-2;若key存在但未设置过期时间,返回-1。PERSIST key
:移除key的生存时间,将其变为永生的。
127.0.0.1:6379> MSET gree1 123 gree2 456
OK
127.0.0.1:6379> PTTL gree1 #gree1未设置生存时间
(integer) -1
127.0.0.1:6379> TTL gree2 #gree2未设置生存时间
(integer) -1
127.0.0.1:6379> EXPIRE gree1 10 #设置gree1 10秒后过期
(integer) 1
127.0.0.1:6379> TTL gree1 #此时生存时间还剩5秒
(integer) 5
127.0.0.1:6379> TTL gree1 #10秒后,gree1被删除
(integer) -2
127.0.0.1:6379> PEXPIRE gree2 5000 #设置gree2 5秒后过期
(integer) 1
127.0.0.1:6379> PTTL gree2 #此时生存时间还剩2261毫秒
(integer) 2261
127.0.0.1:6379> PTTL gree2 #5秒后,gree2被删除
(integer) -2
127.0.0.1:6379> MGET gree1 gree2 #验证,两个键都已被删除
1) (nil)
2) (nil)
127.0.0.1:6379> SET gree 789 EX 10 #设置键的同时指定生存时间为10秒
OK
127.0.0.1:6379> TTL gree #此时生存时间还剩6秒
(integer) 6
127.0.0.1:6379> PERSIST gree #移除gree的生存时间
(integer) 1
127.0.0.1:6379> TTL gree #此时gree已无生存时间
(integer) -1
过期键三种删除策略:
- 定时删除:使用定时器,保证过期键会尽快被删除,对内存友好(即时释放内存),对CPU时间不友好(过期键较多时占用CPU时间)影响服务器的响应时间和吞吐量
- 惰性删除:只有当键被使用时才对键进行过期检查并进行删除操作,对CPU时间友好,对内存不友好(过期键较多又恰好没被访问到会占用大量内存)有内存泄露的危险
- 定期删除:上述两种策略的整合和折中,每隔一段时间执行一次删除过期键操作,不过该操作的执行时长和频率难以确定
Redis使用惰性删除和定期删除两种策略作为过期键删除策略。新的RDB文件不会包含已过期的键,重写AOF文件也不会包含已过期的键;当一个过期键被删除后,服务器会追加一条DEL命令到现有AOF文件末尾,显示删除过期键;当主服务器删除一个过期键后,会向所有从服务器发送一条DEL命令,显示删除过期键;从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
3.3 序列化、反序列化
DUMP key
:序列化指定key,并返回被序列化的值;若key不存在,返回nil。RESTORE key time value [REPLACE]
:反序列化指定的序列化值,并将它和给定的key关联;time以毫秒设置key的生存时间,若取值为0,代表不设置;value为序列化值;若指定REPLACE选项,反序列化的值可替代key原有的值,若不指定且key已存在,则返回错误。
127.0.0.1:6379> GET test
"hello"
127.0.0.1:6379> DUMP test #序列化test键
"\x00\x05hello\b\x00\xda_3\xc9\xcc-\xaa2"
127.0.0.1:6379> RESTORE test_copy 0 "\x00\x05hello\b\x00\xda_3\xc9\xcc-\xaa2"
OK
127.0.0.1:6379> GET test_copy
"hello"
127.0.0.1:6379> TTL test_copy
(integer) -1
127.0.0.1:6379> RESTORE test 0 "\x00\x05hello\b\x00\xda_3\xc9\xcc-\xaa2"
(error) BUSYKEY Target key name already exists. #反序列化到已存在的键,报错
127.0.0.1:6379> GET haha
"123456789"
127.0.0.1:6379> RESTORE haha 100000 "\x00\x05hello\b\x00\xda_3\xc9\xcc-\xaa2" REPLACE
OK
127.0.0.1:6379> GET haha
"hello"
127.0.0.1:6379> TTL haha #生存时间还剩92秒
(integer) 92
3.4 迁移
MOVE key db
:将当前db的key移动到指定的db(数据库索引)中,成功返回1,失败返回0;若目标数据库中有同名的key或当前db中key不存在,该操作无效。MIGRATE host port key db timeout [COPY|REPLACE]
:将key原子性地从当前实例传送到目标实例(host + port)的指定数据库(db代表数据库号)上,同时数据传送时间不能超过timeout毫秒;COPY代表不移除源实例上的key,REPLACE代表替换目标实例上已存在的key。
127.0.0.1:6379> GET test
"hello"
127.0.0.1:6379> MOVE test 1
(integer) 1
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> KEYS *
1) "test"
127.0.0.1:6379[1]> GET test
"hello"
127.0.0.1:6379[1]> SELECT 0
OK
127.0.0.1:6379> GET test
(nil)
3.5 内部结构
OBJECT subcommand args
:查看指定key对应的Redis对象的信息(引用次数、内部编码方式、空闲时间)。
分别对应子命令:REFCOUNT
、ENCODING
、IDLETIME
127.0.0.1:6379> SET test 123 #int编码的字符串
OK
127.0.0.1:6379> OBJECT REFCOUNT test
(integer) 2147483647
127.0.0.1:6379> OBJECT ENCODING test
"int"
127.0.0.1:6379> OBJECT IDLETIME test
(integer) 61940
127.0.0.1:6379> SET test hello #embstr编码的字符串(长度小于等于44)
OK
127.0.0.1:6379> OBJECT REFCOUNT test
(integer) 1
127.0.0.1:6379> OBJECT ENCODING test
"embstr"
127.0.0.1:6379> OBJECT IDLETIME test
(integer) 10
127.0.0.1:6379> SET test "helloworld!helloredis!helloworld!helloworld!h" #raw编码的字符串(长度大于44)
OK
127.0.0.1:6379> OBJECT REFCOUNT test
(integer) 1
127.0.0.1:6379> OBJECT ENCODING test
"raw"
127.0.0.1:6379> OBJECT IDLETIME test
(integer) 17
3.6 排序、迭代
SORT key [BY pattern][LIMIT offset count] [GET pattern [GET pattern ...]][ASC | DESC] [ALPHA][STORE destination]
:对给定的列表、集合、有序集合进行排序。默认以数字作为对象,值被解释为双精度浮点数,然后进行排序。- ASC和DESC分别为升序(默认)和降序。
- 指定ALPHA后可对字符串进行排序。
- 使用STORE可将排序的结果保存到指定的键上,如果键已存在,值将被覆盖。
- LIMIT表示获取跳过offset个元素后的count个元素。
- 使用BY选项可以用外部key的数据作为权重来进行排序,先取出被排序key的所有值,然后获取pattern模式对应的各个键的值作为权重再进行排序。
- 使用GET选项可以根据排序出来的结果取出相应的键值,取出的具体键由pattern决定。若指定pattern为#,可获取被排序键的值。
该排序操作是由快速排序算法实现。选项执行顺序:(ASC或DESC、ALPHA、BY)、LIMIT、GET、STORE。
SCAN cursor [MATCH pattern][COUNT count]
:用于迭代当前数据库中的所有键。
3.7 数据过期策略
Redis 中数据过期策略采用定期删除+惰性删除策略
- 定期删除策略:Redis 启用一个定时器定时监视所有的 key,判断key是否过期,过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。
- 惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
- 这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制,当内存不够用时,内存淘汰机制就会上场。淘汰策略分为:
- noeviction:直接返回错误,当内存限制达到,并且客户端尝试执行会让更多内存被使用的命令(默认)。
- allkeys-lru:对所有键采用lru算法进行淘汰。
- volatile-lru:在拥有过期时间的键集合中,使用lru算法进行淘汰。
- allkeys-random:在所有键中随机回收一个键。
- volatile-random:在拥有过期时间的键集合中,随机进行淘汰。
- volatile-ttl:在拥有过期时间的键集合中,优先回收存活时间较短的键
- allkeys-lfu:对所有键采用lfu算法进行淘汰。
- volatile-lfu:在拥有过期时间的键集合中,使用lfu算法进行淘汰。
常见缓存算法
- LRU:最近最少使用,衡量的是访问时间,可以通过LinkedHashMap实现(构造函数中设置accessOrder为true即可)。新数据插入链表头部;每当缓存命中,将数据移到链表头部;当链表满的时候将尾部数据丢弃。
- LFU:最不经常使用,衡量的是在最近一段时间内的使用频次。短时间内对某些数据的访问频次很高,这些数据会立刻晋升为热点数据,保证不会淘汰,但是后续很长时间内访问频次都不高,这样就会造成新加入的缓存很容易被淘汰,即使是热点数据,因为它的访问频次没有之前的高。用于统计频次的时间段不好确定。redis在实现上维护一个计数器(并不是简单的增加计数器),并通过两个参数 lfu-log-factor 和 lfu-decay-time 控制计数器的增长和减少速度,前者越大计数器增加越慢,随着时间的推移,计数器会减小。新对象计数器默认值为5.
- FIFO:先进先出,也可以通过LinkedHashMap实现,默认的构造函数就是FIFO。
3.8 帮助
通过命令 help @group
查看某个命令组下的命令使用帮助,group的取值可以是:
generic
string
list
set
sorted_set
hash
pubsub
transactions
connection
server
scripting
hyperloglog
cluster
geo
stream
四、内部数据结构
Redis中主要有以下几种底层数据结构:简单动态字符串(SDS)、链表(linkedlist)、字典(dict)、整数集合(intset)、跳跃列表(skiplist)、压缩列表(ziplist)、快速列表(quicklist);而Redis的基础数据结构都是由这些内部数据结构来实现的。
4.1 简单动态字符串(SDS)
Redis使用SDS(简单动态字符串)作为String的默认表示,而不是C语言传统的字符串。C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符“\0”。而SDS内部有三个属性:
- len:表示此SDS保存的字符串字节长度,即buf中已使用的字节数量
- free:表示此SDS尚未被分配使用的空间,即buf中为使用的字节数量
- buf:字节数组,用于保存字符串
SDS的空间分配策略(杜绝缓冲区溢出):
- 空间预分配:用于优化SDS的字符串增长操作;当执行增长操作时,在扩展必要空间的同时也会为SDS分配额外的未使用空间(free属性的值);若修改后SDS长度(len属性的值)小于1MB,将free和len置等;若修改后SDS长度大于1MB,将分配1MB的未使用空间给SDS。如果第二次执行字符串增长操作时,将要增长的长度小于free,则无须进行内存重分配。
- 惰性空间释放:用于优化SDS的字符串缩短操作;当执行缩短操作时,并不立即回收内存,而是将多出来的字节用free属性进行记录,等待将来使用;当需要内存时,可调用API释放SDS的未使用空间,避免浪费。
SDS相比C字符串优点:
- 获取字符串长度的时间复杂度为O(1),而C字符串为O(N)。
- 通过空间分配策略杜绝缓冲区溢出问题,同时减少了修改字符串时所需的内存重分配次数。
- 能保证数据的二进制安全。
除存储字符串值外,SDS还被用作AOF模块中的缓冲区和客户端状态中的输入缓冲区。
4.2 链表(linkedlist)
每个链表节点(listNode)有三个指针:prev(前置节点)、next(后置节点)、value(该节点存储的值)。
通过prev和next指针将多个链表节点组成双端链表,除此之外,Redis为链表提供了表头指针head、表尾指针tail、链表长度计数器len,同时提供了dup、free和match成员用于实现多态链表所需的类型特定函数:
- dup函数用于复制链表节点所保存的值;
- free函数用于释放链表节点所保存的值;
- match函数用于对比链表节点所保存的值和另一个输入值是否相等。
总结:
- 除实现List外,发布与订阅、慢查询、监视器等功能的实现也用到了链表。
- 每个链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
- 通过为链表设置不同的类型特定函数,链表可用于保存各种不同类型的值。
- 获取某个节点的前置节点和后置节点、链表的表头节点和表尾节点以及链表中节点数量的复杂度均为O(1)。
4.3 字典(dict)
字典使用哈希表作为底层实现,一个哈希表中可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
每个哈希表由四个属性:
- table:一个数组,数组中每个元素都是指向一个哈希表节点的指针
- size:记录哈希表的大小(即table数组的大小)
- used:记录哈希表目前已有的哈希表节点的数量
- sizemask:该属性的值总是等于size - 1,该值和哈希值一起决定一个键应该被放到table属性的哪个索引上
每个哈希表节点由三个属性:
- key:保存键值对中的键
- value:保存键值对中的值(指针、uint64_t整数、int64_t整数)
- next:指向另一个哈希表节点的指针,将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题
每个字典由四个属性(前两个用于创建多态字典):
- type:指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数
- privdata:保存需要传给那些类型特定函数的可选参数
- ht:长度为2的数组,每一项都是一个哈希表,一般字典只使用ht[0]哈希表,ht[1]只会在对ht[0]进行rehash时使用
- rehashidx:记录rehash目前的进度,如果目前没有在进行rehash,取值为-1
哈希算法:当要将一个新的键值对添加到字典里时,程序首先根据键值对的键通过哈希算法计算出哈希值,然后将哈希值和哈希表的sizemask进行“按位与”操作得到索引值,最后根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
解决键冲突:当有两个或以上的数量的键被分到了哈希表数组的以同一个索引上时,哈希表使用链地址法解决键冲突,即使用哈希表节点的next属性将的冲突的多个哈希表节点构成一个单向链表(新节点总是添加到链表的表头位置)。
rehash(重新散列):随着操作的不断执行,为了让哈希表的负载因子(used/size)维持在一个合理的范围,需要对哈希表进行相应的扩展或收缩,具体步骤:
- 为字典的ht[1]哈希表分配空间,空间的大小取决于要执行的操作和ht[0]的used属性值:
- 如果执行扩展操作,ht[1]的大小为第一个大于等于ht[0].used * 2 的 2^n
- 如果执行收缩操作,ht[1]的大小为第一个大于等于ht[0].used 的 2^n
- 对ht[0]的所有键值对重新计算哈希值和索引值,并将键值对放到ht[1]的指定索引上
- 当ht[0]的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备
自动执行扩展操作:
- 服务器目前没有执行BGSAVE或BGREWRITEAOF,且负载因子大于等于1
- 服务器目前正在执行BGSAVE或BGREWRITEAOF,且负载因子大于等于5
自动执行收缩操作:
- 哈希表的负载因子小于0.1
渐进式rehash:为避免一次性将过多的键值对rehash到ht[1]中对服务器性能的影响,Redis服务器分多次、渐进式地将ht[0]中的键值对慢慢地rehash到ht[1]中。具体步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 将字典的rehashidx属性设为0,表示rehash正式开始
- 在rehash进行期间,每次对字典执行操作的同时,还会顺带将ht[0]在rehashidx索引上的键值对rehash到ht[1]上,并在完成后将rehashidx属性的值加1
- 随着字典操作的不断执行,当ht[0]的所有键值对都被rehash到ht[1]后,将rehashidx置0,代表rehash完成
拾遗:Redis的数据库就是用字典作为底层实现的。在渐进式rehash执行期间,针对键的操作都会先在ht[0]中进行查找,如果没找到就会到ht[1]中查找;新添加的键值对一律保存到ht[1]中。
4.4 跳跃列表(skiplist)
跳跃列表是一种有序数据结构。由多个跳跃表节点组成。
每个跳跃表节点由四个属性:
- level:数组,表示该节点所持有的所有层,每创建一个新节点,程序根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小(即层的“高度”)。数组中每个元素(层)都有两个属性:
- forward:前进指针,指向表尾方向,用于从表头向表尾方向访问节点,当碰到NULL时,结束此次遍历
- span:跨度,用于记录两个节点之间的距离,指向NULL的所有前进指针的跨度都为0;同时跨度也用来计算节点在跳跃表中的排名(在查找某节点的过程中,将沿途访问过的所有层的跨度累计起来即为排名)
- backward:后退指针,指向位于当前节点的前一个节点,用BW字样表示;用于从表尾向表头访问节点
- score:该节点的分值,一个double类型的浮点数,跳跃表中所有节点按分值从小到大排序
- obj:该节点所保存的成员对象,一个指向字符串对象的指针,字符串中保存一个SDS值
每个跳跃表由四个属性:
- header:指向跳跃表表头结点的指针
- tail:指向跳跃表表尾节点的指针
- level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
- length:记录跳跃表的长度,即节点的数量(表头结点不计算在内)
拾遗:在集群节点中用作内部数据结构。表头节点也有后退指针、分值和成员对象,不过不会被使用到,所以图中省略了这些部分。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但保存的分值可以是相同的;分支相同的节点按成员对象在字典序中的大小进行排序(从小到大)。
4.5 整数集合(intset)
整数集合可保存类型为int16_t、int32_t或int64_t的整数值,并且保证集合中不会出现重复元素。每个整数集合有三个属性:
- encoding:编码方式:INT_16(-215-1)、INT_32(-231-1)、INT_64(-263-1)
- length:记录整数集合所包含的元素数量(即contents数组的长度)
- contents:保存元素的数组,各个数组项从小到大有序排列,且不重复
升级:当添加数据类型比整数集合现有所有元素的类型都要长的新元素到集合时,需先对集合进行升级后才可添加,升级分三步进行:
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,操作过程中需保持底层数组的有序性不变
- 将新元素添加到底层数组中
升级之后新元素的位置要么在底层数组的最开头,要么在底层数组的最末尾;因为新元素的值要么大于所有现有元素,要么小于所有现有元素。
拾遗:整数集合不支持降级操作。一旦对数组进行了升级,编码就会一直保持升级后的状态。
4.6 压缩列表(ziplist)
压缩列表时Redis为节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可包含任意多个节点,每个节点可以保存一个字节数组或一个整数。主要组成部分如下:
具体实例如下:
zlbytes值为80,表示压缩列表总长为80字节;zltail值为60,表示如果我们由一个指向压缩列表起始地址的指针p,那么p+60则为表尾节点的地址;zllen值为3,表示压缩列表包含3个节点。
每个压缩列表节点可以保存一个字节数组或一个整数值,其中,字节数组可以是以下三种的一种:
- 长度小于等于63(2^6-1)字节的字节数组
- 长度小于等于16383(2^14-1)字节的字节数组
- 长度小于等于4294967295(2^32-1)字节的字节数组
整数值可以是以下六种的一种:
- 4位长,介于0~12之间的无符号整数
- 1字节长的有符号整数
- 3字节长的有符号整数
- int16_t类型整数
- int32_t类型整数
- int64_t类型整数
每个压缩列表节点都由previous_entry-length、encoding、content三部分组成,具体介绍如下:
- previous_entry-length:以字节为单位,记录前一个节点的长度,取值可以是1字节或5字节。如果前一节点的长度小于254字节,该属性取值为1字节,保存前一节点的长度;如果前一节点的长度大于等于254字节,该属性取值为5字节,其中第一字节置为0xFE,之后的四个字节用于保存前一节点的长度。可以根据当前节点的起始地址和该属性,通过指针运算,计算出前一节点的起始地址;压缩列表从表尾向表头的遍历就是使用这一原理实现的。
- encoding:记录节点的content属性所保存数据的类型以及长度,分字节数组编码和整数编码
- content:负责保存节点的值,可以是一个字节数组或整数,值的类型和长度由encoding属性决定。
连锁更新:当压缩列表中有多个连续的、长度介于250字节至253字节之间的节点时,如果在第一个节点之前添加一个长度大于等于254字节的新节点,那么后续所有节点的previous_entry-length属性都需从原来的1字节扩展为5字节,需要程序不断执行空间重分配操作。同样,删除节点也可能会引发连锁更新,例:在多个连续的、长度介于250字节至253字节之间的节点之前是一个长度小于254字节的节点(即为small),而在small之前是一个长度大于等于254字节的节点,那么在删除small节点是也会触发连锁更新。连锁更新最坏复杂度为O(N^2),平均复杂度为O(N)。
4.7 快速列表(quicklist)
快速列表是一个由ziplist组成的双向链表。链表中的每一个quicklistNode节点都以ziplist结构保存数据,而ziplist用多个entry节点保存数据。每个quicklist的结构如下:
- head:指向表头quicklistNode节点的指针
- tail:指向表尾quicklistNode节点的指针
- count:记录所有ziplist中entry节点的数量
- len:记录quicklistNode节点的数量
- fill:记录每个quicklistNode节点的大小,由配置文件(list-max-ziplist-size)设定,只占16位,默认-2
- -1表示每个quicklistNode节点的ziplist字节大小不能超过4kb
- -2表示每个quicklistNode节点的ziplist字节大小不能超过8kb
- -3表示每个quicklistNode节点的ziplist字节大小不能超过16kb
- -4表示每个quicklistNode节点的ziplist字节大小不能超过32kb
- -5表示每个quicklistNode节点的ziplist字节大小不能超过64kb
- 正数表示ziplist所最多包含的entry个数,最大值为2^15
- comoress:记录quicklist的压缩程度值,由配置文件(list-compress-depth)设定,只占16位,默认0
- 0表示不压缩
- 1表示quicklist列表的两端各有1个quicklistNode节点不压缩,中间的节点压缩
- 2表示quicklist列表的两端各有2个quicklistNode节点不压缩,中间的节点压缩
- 3表示quicklist列表的两端各有3个quicklistNode节点不压缩,中间的节点压缩
- 以此类推,最大值为2^16
每个quicklistNode节点的结构如下:
- prev:指向链表前一个quicklistNode节点的指针
- next:指向链表后一个quicklistNode节点的指针
- zl:不设置压缩参数时指向一个ziplist结构;设置压缩参数时指向quicklistLZF结构
- sz:表示ziplist的总长度(包括zlbytes、zltail、zllen、zlend);如果ziplist被压缩,仍表示压缩前的大小
- count:ziplist中包含的entry节点数,占16位
- encoding:表示是否采用LZF算法压缩ziplist,占2位,1表示没有压缩,2表示压缩
- container:表示一个quicklistNode节点是否采用ziplist结构保存数据,占2位,默认2(表示是)
- recompress:访问压缩数据时,需暂时解压,此时将该属性置为1,等机会再把数据重新压缩,占1位
被压缩过的ziplist用quicklist结构表示,具体如下:
- sz:表示压缩后的ziplist大小
- compressed:是个柔性数组,用于存放压缩后的ziplist字节数组
五、Redis对象
Redis基于上述的数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。使用对象我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。除此之外,对象系统还实现了基于引用计数技术的内存回收机制,以自动释放不再使用的对象所占用的内存;另外,Redis还通过引用计数技术实现了对象共享机制,以此来节约内存;最后,对象还可记录访问时间,用于计算key的空转时长,在服务器启用maxmemory功能的情况下,空转时长较大的key可能会优先被服务器删除。
5.1 对象基本结构
每当在Redis数据库中新创建一个键值对时,至少会创建两个对象,一个键对象,一个值对象。每个对象有五个属性分别为:
- type:记录对象的类型,占4位,可使用
TYPE
命令查看key的类型
- encoding:记录对象所使用的编码(即底层实现方式),占4位,可通过
OBJECT ENCODING
命令查看key的编码方式
**注:Redis3.2版本后使用quicklist作为列表的底层实现**
- ptr:指向对象的底层实现数据结构的指针
- refcount:记录该对象的引用计数信息;创建新对象时该属性值被初始化为1;每次被新程序调用则加1;调用结束则减1;当该属性值变为0时,会释放该对象所占用的内存。可通过
OBJECT REFCOUNT
命令查看key保存的值的引用次数。 - lru:记录对象最后一次被命令程序访问的时间。可通过
OBJECT IDLETIME
命令查看key的空转时长(用当前时间减去该属性的值即为空转时间。单位:秒)。在服务器启用maxmemory功能的情况下,通过修改配置文件中的maxmemory
和maxmemory-policy
选项配置maxmemory的上限和达到上限后的内存回收策略。
5.2 字符串(String)
- 字符串对象的编码可以是int、raw或者embstr。字符串对象是唯一一种会被其他四种对象嵌套的对象。
- int:保存整数值,用long类型表示
- raw:保存长度大于44字节的字符串,会调用两次内存分配函数分别创建对象结构和SDS结构,且释放时也需要调用两次内存释放函数
- embstr:保存长度小于等于44字节的字符串,只需调用一次内存分配函数在一块连续的空间内依次创建对象结构和SDS结构,释放时只需要调用一次内存释放函数
- 用long double类型表示的浮点数在Redis中也作为字符串保存,且在有需要的时候,会先将字串串值转换回浮点数,执行操作后,将转为字符串值并保存。
- 编码的转换:当int编码的字符串经过一些操作后保存的不再是整数值,而是一个字符串值,将编码转换为raw;当对embstr编码的字符串执行修改命令后,编码转换为raw。
- 拾遗:字符串类型可存储的最大长度为512M。如果value值时一个整数,还可对其进行自增和自减操作,范围:(-2^63) ~ (2^63 - 1)。如果在redis中使用中文,redis会显示对应的Unicode编码;如果想正常显示,只需在启动redis-cli时在其后面加上—raw参数即可。
- 相关命令
| 命令 | 作用 |
| —- | —- |
| SET | 将字符串值关联到key,若key已持有其他值将被覆写 |
| SETEX | 以秒为单位设置key的生存时间,若key已存在,该命令将覆写旧值 |
| SETNX | 当且仅当key不存在时,将key的值设为value |
| MSET | 同时设置一个或多个key - value对 |
| PSETEX | 以毫秒为单位设置key的生存时间,若key已存在,该命令将覆写旧值 |
| MSETNX | 当且仅当所有给定的key都不存在时,同时设置一个或多个key - value对 |
| APPAND | 将value追加到key原来的值的末尾,返回最终value的长度 |
| DECR | 将key中存储的数字值减1 |
| DECRBY | 将key中存储的数字值减去给定的减量 |
| GET | 返回key所关联的字符串值,不存在返回nil |
| GETRANGE | 返回key中字符串值的子字符串,可指定截取范围,偏移量可正可负,代表方向 |
| GETSET | 将给定key设置新值,并返回旧值 |
| INCR | 将key中存储的数字值加1 |
| INCRBY | 将key中存储的数字值加上给定的增量 |
| INCRBYFLOAT | 将key中存储的数字值加上给定的浮点数增量 |
| MGET | 返回一个或多个指定key的值 |
| SETRANGE | 从指定偏移量位置开始覆写指定key所存储的字符串值 |
| STRLEN | 返回key所存储的字符串长度 |
5.3 二进制位数组
Redis使用SDS逆序来保存位数组。使用 STRLEN
命令可以获取位数组的字节数。
命令 | 作用 |
---|---|
SETBIT | 用于为位数组指定偏移量上的二进制位设置值,偏移量从0开始,值只能是0或1 |
GETBIT | 获取位数组指定偏移量上的二进制位的值,key不存在或偏移量大于字符串的长度返回0 |
BITCOUNT | 用于统计位数组里值为1的二进制位的数量 |
BITFIELD | 对字符串的二进制位组成的数组进行访问 |
BITOP | 对一个或多个二进制位数组进行位操作(and、or、xor、not),并保存其运算结果 |
5.4 哈希表(Hash)
- 哈希对象的编码可以是ziplist或hashtable。
- ziplist:当Hash中包含的所有键值对的键和值的字读串长度都小于64字节且保存的键值对的数量小于512个时,使用压缩列表实现。针对这两个数值的限制,可通过配置文件中的
hash-max-ziplist-value
和hash-max-ziplist-entries
进行修改。
- ziplist:当Hash中包含的所有键值对的键和值的字读串长度都小于64字节且保存的键值对的数量小于512个时,使用压缩列表实现。针对这两个数值的限制,可通过配置文件中的
- dict:当Hash不能同时满足上述两个条件中任意一个时,使用字典实现。
- 编码转换:当使用ziplist编码的列表对象不能同时满足上述两个条件时,将编码转换为hashtable。
- 相关命令
| 命令 | 作用 |
| —- | —- |
| HSET | 为哈希表key中的域field设置value,若域已存在,则旧值将被覆盖 |
| HSETNX | 当且仅当指定域不存在时,为哈希表key中的域field设置value |
| HGET | 返回哈希表 key中给定域 field的值 |
| HEXISTS | 判断哈希表 key中给定域 field是否存在 |
| HDEL | 删除哈希表key中的一个或多个指定域(字段),不存在的将被忽略 |
| HMSET | 同时将多个field-value(域-值)对设置到哈希表key中,此命令会覆盖已存在的域 |
| HMGET | 返回哈希表key中一个或多个指定域的值,不存在的返回nil |
| HGETALL | 返回哈希表 key中所有的域和值,奇偶交替 |
| HKEYS | 返回哈希表key中的所有域 |
| HLEN | 返回哈希表key中域的数量 |
| HVALS | 返回哈希表key中所有域的值 |
| HSTRLEN | 返回哈希表key中指定域所关联值的字符串长度 |
| HINCRBY | 为哈希表 key中指定域 field的值加上给定的增量,增量为负数时执行减操作 |
| HINCRFLOAT | 为哈希表 key中指定域 field的值加上给定的浮点数增量 |
| HSCAN | 基于游标的迭代器,用于增量式迭代 |
5.5 列表(List)
- 列表对象的编码可以是linkedlist(3.2版本前)和ziplist(3.2版本前)或者quicklist(3.2版本后)。
- linkedlist:当List中保存的元素数量大于512个或有元素的长度大于64字节时,使用双端链表实现。
- ziplist:当List中保存的元素数量小于512个且每个元素的长度都小于64字节时,使用压缩列表实现。
- quicklist:3.2版本后作为列表的底层实现。
- 编码转换:当使用ziplist编码的对象不能同时满足上述两个条件时,就会被转换为linkedlist(3.2版本前)。
- 相关命令
| 命令 | 作用 |
| —- | —- |
| LPUSH | 将一个或多个值插入到列表key的表头 |
| LPOP | 移除并返回列表key的头元素 |
| BLPOP | LPOP命令的阻塞版本 |
| LPUSHX | 当且仅当key存在并且是一个列表时,将值插入到列表key的表头 |
| RPUSH | 将一个或多个值插入到列表key的表尾 |
| RPOP | 移除并返回列表key的尾元素 |
| BRPOP | RPOP命令的阻塞版本 |
| RPUSHX | 当且仅当key存在并且是一个列表时,将值插入到列表key的表尾 |
| RPOPLPUSH | 返回第一个列表的尾元素,同时插入第二个列表的表头 |
| BRPOPLPUSH | RPOPLPUSH命令的阻塞版本 |
| LINDEX | 返回列表key中指定下标的元素 |
| LINSERT | 在列表key的指定位置之前或之后插入给定的值 |
| LLEN | 返回列表key的长度 |
| LRANGE | 返回列表key中指定区间内的元素,偏移量可正可负,代表方向 |
| LREM | 移除列表中与value相等的元素(count = 0:移除所有与value相等的值;count > 0:从表头开始,移除数量为count的与value相等的值;count < 0:从表头开始,移除数量为count绝对值的与value相等的值) |
| LSET | 为列表key中指定下标的元素设置值 |
| LTRIM | 保留指定区间内的元素,不在指定区间之内的元素将被删除 |
5.6 集合(Set)
- 集合对象的编码可以是intset或者hashtable。
- intset:当Set中保存的所有元素都是整数值且元素数量不超过512个时,使用整数集合实现。针对元素数量的限制,可通过配置文件中的
set-max-intset-entries
进行修改。
- intset:当Set中保存的所有元素都是整数值且元素数量不超过512个时,使用整数集合实现。针对元素数量的限制,可通过配置文件中的
- hashtable:当Set不能同时满足上述两个条件时,使用hashtable实现。
- 编码转换:当intset编码的集合对象不能同时满足上述两个条件时,就转换编码为hashtable。
- 相关命令
| 命令 | 作用 |
| —- | —- |
| SADD | 将一个或多个元素加入到集合key中,已存在的将被忽略 |
| SCARD | 返回集合key中元素的数量(基数) |
| SDIFF | 返回第一个集合与后续所有集合的差集 |
| SDIFFSTORE | 将所有给定集合的差集保存到指定集合中,若指定集合已存在,则将其覆盖 |
| SINTER | 返回所有给定集合的交集 |
| SINTERSTORE | 将所有给定集合的交集保存到指定集合中,若指定集合已存在,则将其覆盖 |
| SISMEMBER | 判断某元素是否是集合key的成员 |
| SMEMBERS | 返回集合key中的所有成员 |
| SMOVE | 将元素从一集合移动到另一集合 |
| SPOP | 移除并返回集合中的一个随机元素 |
| SRANDMEMBER | 返回集合中一个或多个元素(若count为正数且小于集合长度,返回一个包含count个元素的数组,其中元素各不相同,若count大于集合长度,返回整个集合;若count为负数,返回元素个数为count绝对值的数组,其中元素可重复出现多次) |
| SREM | 移除集合key中的一个或多个元素,不存在的将被忽略 |
| SUNION | 返回所有给定集合的并集 |
| SUNIONSTORE | 将所有给定集合的并集保存到指定集合中,若指定集合已存在,则将其覆盖 |
| SSCAN | 基于游标的迭代器,用于增量式迭代 |
5.7 有序集合(ZSet)
- 有序集合的编码可以是ziplist或者skiplist。
- ziplist:当ZSet保存的所有元素长度都小于64字节且元素数量小于128个时,使用压缩列表实现。针对两个条件的限制值,可通过配置文件中的
zset-max-ziplist-entries
和zset-max-ziplist-value
进行修改。
- ziplist:当ZSet保存的所有元素长度都小于64字节且元素数量小于128个时,使用压缩列表实现。针对两个条件的限制值,可通过配置文件中的
- skiplist:当ZSet不能同时满足上述两个条件时,同时使用跳跃列表和字典实现。
- 编码转换:当用ziplist编码的有序集合不能同时满足上述两个条件时,转换编码为skiplist。
- 相关命令
| 命令 | 作用 |
| —- | —- |
| ZADD | 将一个或多个元素及其score值加入到有序集key中 |
| ZCARD | 返回有序集key中元素的数量(基数) |
| ZCOUNT | 返回有序集key中score值在指定范围内的元素的数量 |
| ZINCRBY | 为有序集key中指定元素的score值加上给定的增量,增量为负数时执行减操作 |
| ZRANGE | 返回有序集key中指定位置区间内的成员,成员按score值递增排列 |
| ZRANGEBYSCORE | 返回有序集key中所有score值介于指定区间内的成员,成员按score值递增排列 |
| ZRANK | 返回有序集key中指定成员的排名,成员按score值递增排列 |
| ZREM | 移除有序集key中的一个或多个成员,不存在的成员将被忽略 |
| ZREMRANGEBYRANK | 移除有序集key中处于排名区间内的所有成员 |
| ZREMRANGEBYSCORE | 移除有序集key中score值介于指定区间内的所有成员 |
| ZREVRANGE | 返回有序集key中指定位置区间内的成员,成员按score值递减排列 |
| ZREVRANGEBYSCORE | 返回有序集key中所有score值介于指定区间内的成员,成员按score值递减排列 |
| ZREVRANK | 返回有序集key中指定成员的排名,成员按score值递减排列 |
| ZSCORE | 返回有序集key中指定成员的score值,若key不存在返回nil |
| ZUNIONSTORE | 将一个或多个有序集的并集存储到指定结果集,可指定乘法因子和聚合方式来计算结果集中成员的score值 |
| ZINTERSTORE | 将一个或多个有序集的交集存储到指定结果集,可指定乘法因子和聚合方式来计算结果集中成员的score值 |
| ZRANGEBULEN | 返回所有成员score都相同的有序集key中成员介于指定范围内的成员组成的列表 |
| ZLENCOUNT | 返回所有成员score都相同的有序集key中成员介于指定范围内的成员数量 |
| ZREMRANGEBYLEN | 移除所有成员score都相同的有序集key中成员介于指定范围内的所有成员 |
| ZSCAN | 基于游标的迭代器,用于增量式迭代 |
六、持久化
6.1 RDB持久化
RDB持久化功能是通过保存数据库中的键值对来记录数据库状态,其生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。
- Redis有两个命令用于生成RDB文件:
SAVE
和BGSAVE
。SAVE
命令会阻塞Redis服务进程,直到RDB文件创建完成才能处理命令请求;BGSAVE
命令会派生出一个子进程负责RDB文件的创建并在完成之后向父进程发送信号,服务器进程(父进程)继续处理命令请求,当父进程接收到子进程处理完成的信号后用新的RDB文件替代旧的RDB文件。 - 可以手动执行上述两命令,也可通过配置文件中的
save
配置执行策略,当达到策略的条件时,执行BGSAVE
自动持久化数据(针对当前数据库所有数据),但是在BGSAVE
开始后针对数据库的更改无法写入到RDB文件中。 - 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态;只有在AOF持久化功能处于关闭状态时,服务器才用RDB文件来还原数据库状态。在服务器载入RDB文件期间,会一直处于阻塞状态。
- 在
BGSAVE
执行期间,客户端发送的SAVE
命令和BGSAVE
命令会被直接拒绝、BGREWRITEAOF
命令会被延迟到BGSAVE
命令执行完毕之后执行;而在BGREWRITEAOF
命令执行期间,客户端发送的BGSAVE
命令会被拒绝。 - Redis针对RDB文件都有其特定的组成部分,可以通过Linux的
od
命令查看RDB文件的内部组成。 - 优点:完全备份;紧凑的单一文件,方便网络传输,时和灾难恢复;恢复大数据集速度较AOF快。
- 缺点:会丢失最近写入、修改的而未能持久化的数据;fork过程非常耗时。
6.2 AOF持久化
AOF持久化功能是通过保存Redis服务器所执行的写命令来记录数据库状态。AOF文件中的所有命令都以Redis命令请求协议(是纯文本协议)的格式保存,可以直接打开一个AOF文件查看内部结构。
- 当AOF持久化功能处于打开状态时,每当服务器执行完一个写命令后,会以协议格式将写命令追加到aof_buf缓冲区(此时并未写入到硬盘文件,只是在缓存中)的末尾,至于何时将缓冲区的内容同步到AOF文件(即写入硬盘文件)中,则由配置项
appendfsync
决定,该配置项取值有三选择:- always:每写入一个命令到缓冲区就立即写入硬盘,效率低,不会丢失已成功执行的数据
- everysec:(默认)每一秒钟执行一次fdatasync,将缓冲区命令写入硬盘文件,效率高,最多丢失一秒的数据
- no:由操作系统决定何时同步数据到硬盘(基本是在缓冲区填满后才会同步),效率高,丢失数据的风险很大
- 通过建立一个不带网络连接的伪客户端,遍历AOF文件中的所有命令,在伪客户端中执行,就可还原之前的数据库状态。
- AOF重写:为避免AOF文件体积过于膨胀,Redis提供了AOF文件重写功能简化AOF文件中的冗余命令以节约空间(通过读取服务器当前数据库状态新建一个AOF文件后替代原来的AOF文件实现),但是在重写AOF文件期间,会阻塞Redis服务进程。
- AOF后台重写:为避免AOF重写期间服务的阻塞,将AOF重写程序放入子进程执行,同时为了保证数据一致性,在执行后台重写时,服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,保证新旧两个AOF文件保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,至此完成AOF文件的后台重写操作。
- 优点:默认每秒同步一次AOF文件,性能好不阻塞服务,最多丢失一秒的数据;后台重写,优化AOF文件。
- 缺点:相同数据集,AOF文件体积较RDB大;恢复数据库速度较RDB慢。
6.3 相关命令
命令 | 作用 |
---|---|
SAVE | 执行一个同步保存操作,将当前Redis实例的所有数据快照以RDB文件保存到硬盘 |
BGSAVE | 在后台异步保存当前数据库的所有数据以RDB文件格式到磁盘 |
BGREWRITAOF | 执行一个AOF文件重写操作 |
七、复制
7.1 旧版复制(SYNC命令)
通过 SLAVEOF
命令可以让一个服务器去复制另一个服务器,以让两者的数据库状态达到一致状态,执行该命令的服务器为从服务器,被复制的服务器为主服务器。复制功能的实现分为同步和命令传播两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
- 命令传播操作用于将主服务器执行成功的写命令传播给从服务器执行,以保证两者数据库状态的一致。
上图为Redis2.8以前的版本所使用的复制原理,不过在每次从服务器断线后需要重新复制主服务器的所有数据,效率非常低。
7.2 新版复制(PSYNC命令)
从2.8版本开始,Redis使用 PSYNC
命令代替 SYNC
命令来执行复制时的同步操作,PSYNC
命令具有完整重同步和部分重同步两种模式。前者用于首次复制(与SYNC
命令一样);后者用于断线后重复制(可显著提升从服务器断线后的恢复速度),过程如下:
部分重同步功能由以下三个部分构成:
- 主服务器的复制偏移量和从服务器的复制偏移量。用于判断从服务器断线并重连后是执行部分重同步还是执行完整重同步。
- 主服务器的复制积压缓冲区。一个固定长度的先进先出队列,默认大小1MB,可通过配置文件中的配置项
repl-backlog-size
进行修改。用于保存最近传播的部分写命令。 - 服务器的运行ID。由40个随机的十六进制字符组成,用于判断从服务器断线并重连后是执行部分重同步还是执行完整重同步。
如果从服务器是第一次执行复制,直接执行完整重同步即可。如果从服务器是断线后又重新连接到主服务器,为保持两者状态一致,则会向主服务器发送 PSYNC
命令的同时带上运行ID和复制偏移量。如果传来的运行ID和主服务器保存的不相同,则直接执行完整重同步;如果运行ID一致,且复制偏移量之后的数据在复制积压缓冲区内,则执行部分重同步(将复制偏移量之后的所有数据发送给从服务器),实现同步;如果运行ID一致,但复制偏移量之后的数据不在复制积压缓冲区内,则执行完整重同步。流程图如下所示:
复制功能(即从服务器向主服务器发送 SLAVEOF
命令)的实现过程:
- 从服务器设置主服务器的IP地址和端口。
- 从服务器通过上述地址和端口创建连向主服务器的套接字连接(从服务器是主服务器的客户端)。
- 从服务器发送
PING
命令,判断套接字是否正常、主服务器能否正常处理命令请求。如果返回“PONG”表示正常;如果返回超时或错误,则断开并重新连接主服务器。 - 身份验证(通过
AUTH
命令),只有两者都没有密码或两者密码一致时才能验证通过,否则中止复制工作。 - 向主服务器发送从服务器的监听端口号,执行命令
REPLCONF listening-port 端口号
。 - 向主服务器发送
PSYNC
命令,执行同步操作。同步操作前,只有从服务器时主服务器的客户端;但在执行同步操作后,主服务器也会成为从服务器的客户端。 - 命令传播,主服务器将自己的写命令发送给从服务器,从服务器执行后即可和主服务器保持一致状态。
在命令转播阶段,从服务器默认会以每秒一次的频率,向主服务器发送 REPLCONF ACK 复制偏移量
命令,用于检测主从服务器的网络连接状态、辅助实现min-slaves选项、检测命令丢失。
- 如果主服务器超过一秒钟没有接收到从服务器发来的
REPLCONF ACK
命令,则认为主从服务器间的连接出现问题。 - 配置文件中的
min-slaves-to-write
和min-slaves-max-lag
配置项可防止主服务器在不安全的情况下执行写命令。当从服务器的数量少于前者的值,或者所有从服务器的延迟值(lag)都大于或等于后者的值时,主服务器将拒绝执行写命令。 - 如果从服务器发来的命令中的复制偏移量和主服务器保存的偏移量不一致时,则认为命令丢失,主服务器将会再次向从服务器发送丢失的命令(即从服务器复制偏移量之后的)。
| 命令 | 作用 |
| —- | —- |
| SYNC | 用于复制功能的内部命令(2.8版本之前的复制功能) |
| PSYNC | 用于复制功能的内部命令(2.8版本之后的复制功能) |
| SLAVEOF | 用于在Redis运行时动态地修改复制功能的行为 |
八、Sentinel
Sentinel是Redis的高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。可通过可执行文件 redis-server
或 redis-sentinel
启动一个Sentinel。例:
- redis-server /path/to/your/sentinel.conf —sentinel
- redis-sentinel /path/to/your/sentinel.conf
8.1 初始化Sentinel
Sentinel本质上是一个运行在特殊模式下的Redis服务器,初始化Sentinel时不会载入RDB文件或AOF文件,同时该模式下有自己的可执行命令表(可在源码中的sentinel.c中查看),目前共有11个(普通服务器的命令可在server.c源码中查看)。除此之外其他的命令都不能被Sentinel模式下的服务器接收并执行 。初始化服务器的同时sentinel.c中的sentinelState结构的会保存和Sentinel功能有关的状态:
- 当前纪元:用于实现故障转移(current_epch)
- 被Sentinel监视的所有主服务器,是一个字典。键为主服务器的名字,值为一个指向sentinelRedisInstance结构的指针(masters)
- 是否进入TILT模式(tilt)
- 目前正在执行的脚本数量(runing_scripts)
- 进入TILT模式的时间(tilt_start_time)
- 最后一次执行时间处理器的时间(previous_time)
- 一个FIFO队列,包含所有需要至性的用户脚本(scripts_queue)
其中masters属性的值从sentinel.conf配置文件中读取,根据配置文件中的每一个主服务器的配置构造sentinelRedisInstance实例,该结构的具体属性可查看源码中的sentinel.c,主要包含:
- flags:标示符,记录实例的类型及当前的状态
- name:实例名字,主服务器名字由用户在配置文件中设置,从服务器及Sentinel的名字由Sentinel自动设置,格式为“ip:port”
- runid:实例的运行ID
- config_epoch:配置纪元,用于实现故障转移
- addr:实例的地址
- down_after_period:实例无响应多少毫秒后才会被判断为主观下线,可配置
- quorum:判断这个实例为客观下线所需的支持投票数量,可配置
- parallel_syncs:在执行故障转移操作时,可同时对新的主服务器进行同步的从服务器数量,可配置
- failovertimeout:刷新故障迁移状态的最大时限,可配置
初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel会创建两个连向主服务器的异步网络连接。一个是命令连接:用于向主服务器发送命令并接收命令回复;一个是订阅连接:用于订阅主服务器的 `_sentinel:hello` 频道。
8.2 获取主从服务器信息并发送信息
Sentinel默认会以每十秒一次的频率,向被监视的主服务器发送 INFO
命令。一方面获取主服务器的当前信息存入sentinelRedisInstance的相关属性中,一方面获取主服务器属下的所有从服务器的信息存入主服务器对应实例结构的salves字典中。主服务器实例结构中的flags属性值为SRI_MASTER,从服务器的为SRI_SLAVE。
当Sentinel发现主服务器有新的从服务器出现时,除了会为这个新的从服务器创建对应的实例结构外,还会创建连接到从服务器的命令连接和订阅连接。
8.3 接收主从服务器的频道信息并更新sentinels字典
Sentinel默认会以每两秒一次的频率通过命令连接向所有被监视的主服务器和从服务器的发送以下格式的命令:PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<s_ip>,<s_port>,<s_runid>,<s_epoch>"
。其中以s开头的是Sentinel本身的信息;以m开头的是主服务器或从服务器的信息。同时Sentinel会通过订阅连接向服务器发送以下命令: SUBSCRIBE __sentinel__:hello
。对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel__:hello
频道发送信息,又通过订阅连接从服务器的__sentinel__:hello
频道接收信息。如果一个服务器被多个Sentinel监视,那么一个Sentinel发送给服务器的信息也会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel会发送信息的Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。
sentinelRedisInstance实例结构中的sentinels字典会保存监视这个服务器的所有Sentinel的信息,通过接收频道信息,多个Sentinel可互相发现对方。当Sentinel通过频道信息发现一个新的Sentinel时,不仅会在sentinels字典中创建对应实例,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一个服务器的多个Sentinel将形成相互连接的网络。
8.4 检测主观下线状态
Sentinel默认会以每秒一次的频率向所有与它创建了命令连接的实例发送 PING
命令,并通过该命令的恢复判断实例是否在线。有效回复:+PONG、-LOADING、-MASTERDOWN。如果一个实例在down_after_period毫秒内,连续向Sentinel返回无效回复,那么Sentinel会打开该实例flags属性中的SRI_S_DOWN标识,表示该实例已进入主观下线状态。
8.5 检测客观下线状态并选举领头Sentinel
当Sentinel将一个主服务器判断为主观下线后,同时会向监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入下线状态。如果从其他Sentinel那里接收到足够数量的已下线判断后,Sentinel就会将主服务器判定为客观下线,并对主服务器执行故障转移操作。操作过程如下:
- 源Sentinel向目标Sentinel发送
SENTINEL is-master-down-by-addr
命令,带上主服务器的ip、port、epoch和runid。此时runid取值为*
(其中runid取值为*
时代表该命令仅用于检测主服务器的客观下线状态,如果为Sentinel的运行ID则用于选举领头Sentinel。epoch只用于选举领头Sentinel)。 - 目标Sentinel接收源Sentinel发来的命令时,目标Sentinel会根据ip和port检查主服务器是否已下线,并向源Sentinel进行命令回复,包括:down_state(主服务器的检查结果,1代表已下线、0代表未下线)、leader_runid(
*
的作用同上、可以是局部领头Sentinel的运行ID)、leader_epoch(若leader_runid为*
,则为0、如果leader_runid不为*
,则为领头Sentinel的配置纪元)。 - 源Sentinel接收到目标Sentinel的关于上述命令的回复,并统计其他Sentinel同意主服务器已下线的数量,当这一数量达到quorum时,源Sentinel会打开该主服务器flags属性中的SRI_O_DOWN标识,表示主服务器已进入客观下线状态。```
当一个主服务器被判定为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并有领头Sentinel对下线主服务器执行故障转移操作。选举过程如下:
4. 每个Sentinel都会向其他Sentinel发送 `SENTINEL is-master-down-by-addr` 命令,此时runid取值为自己的运行ID。
5. 如果接收这个命令的Sentinel还没有设置局部领头Sentinel的话,会将源Sentinel设置为自己的局部领头Sentinel,并进行命令回复(1、运行ID、配置纪元)。
6. 源Sentinel分析命令回复,如果返回的运行ID和配置纪元和自己的相同,则表示目标Sentinel已将自己设置成为领头Sentinel。
7. 当某个Sentinel被半数以上的Sentinel设置成为局部领头Sentinel,那么这个Sentinel就会成会领头Sentinel。
### 8.6 故障转移
在选举出领头Sentinel之后,领头Sentinel将对下线主服务器执行故障转移操作,过程如下:
1. 在已下线主服务器属下的所有从服务器中挑选出一个并将其转换为主服务器。领头Sentinel会先删除该主服务器的所有从服务器列表中处于下线或断线状态的从服务器、再删除最近5秒内没有回复过领头Sentinel的INFO命令的从服务器、再删除与已下线主服务器连接断开超过down_after_period * 10毫秒的从服务器,然后依次选择优先级最高、复制偏移量最的、运行ID最小的从服务器,并发送 `SLAVEOF no one` 命令将其升级为主服务器。
2. 让已下线主服务器属下的所有从服务器改为复制新的主服务器。领头Sentinel向除新主服务器外的所有从服务器发送 `SLAVEOF` 命令,带上新主服务器的ip和port,让他们复制新的主服务器。
3. 将已下线主服务器设置为新的主服务器的从服务器。当已下线主服务器重新上线时,Sentinel会向其发送 `SLAVEOF` 命令,让其成为新主服务器的从服务器。
# 九、集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
### 9.1 节点
一个Redis集群通常由多个节点(node)组成,起初各个节点间都是相互独立的,都处于一个只包含自己的集群当中。如果想构建一个包含多个节点的集群,需要发送 `CLUSTER MEET` 命令到指定ip和port的节点完成节点间的握手,握手成功后,两个节点就会处于同一个集群(发送命令节点所在的集群)中,可使用 `CLUSTER NODES` 命令查看当前集群中的所有节点信息。节点握手过程如下:
![clusternode.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338195697-aa67ccc2-a322-42bd-80f4-92bbb3b9d5c2.png#align=left&display=inline&height=127&margin=%5Bobject%20Object%5D&name=clusternode.png&originHeight=127&originWidth=438&size=34520&status=done&style=none&width=438)
在握手的过程中,节点A和节点B都会为对方创建clusterNode结构,并添加到自己的clusterState.nodes字典中。握手成功后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,节点B会被集群中的所有节点认识。
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器会在启动时根据配置项cluster-enabled是否为yes来决定是否开启服务器的集群模式。如果开启集群模式,每个节点将使用以下三种数据结构:
- clusterNode结构记录每个节点的状态(包括节点的创建时间、名称、标识符、配置纪元、IP、port、连接节点所需要的有关信息【指向clusterLink结构的指针】等,具体结构见源码cluster.h)
- clusterLink结构保存连接节点所需的所有信息(包括连接创建时间、套接字描述符、输出缓冲区、输入缓冲区、与该结点相关联的节点)
- clusterState结构记录当前节点视角下集群的状态(包括指向当前节点的指针、集群是在线还是下线、包含多少节点、配置纪元、集群中至少处理着一个槽的节点数量、节点名单【一个字典】等,具体见源码cluster.h)实现
### 9.2 槽指派
Redis集群通过分片的方式保存数据库中的键值对:集群的整个数据库被分为16384个槽,数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。当数据库中的16384个槽都有节点在处理时,集群处于上线状态;如果数据库中有任何一个槽没有得到处理,集群处于下线状态。
每个节点的clusterNode结构中的slots属性和numslots属性记录该节点负责处理的槽。slots是一个长度为2048字节的二进制位数组,共包含16384个二进制位,若索引i上的二进制位值为1,表示该节点负责处理槽i,若为0,表示不处理;numslots记录节点负责处理的槽数量(即slots数组中值为1的二进制位的数量)。
通过向节点发送 `CLUSTER ADDSLOTS` 命令,可将一个或多个槽指派给该节点负责,在命令执行完毕后,节点会将自己的solts数组通过消息发送给集群中的其他节点,自己目前正在负责处理哪些槽,其他节点会在自己的clusterState.nodes字典中查找源节点对应的clusterNode结构,并对结构中的slots数组进行保存或更新。最终,集群中每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。
每个节点的clusterState结构中的solts数组记录了集群中所有16384个槽的指派信息。如果槽i已经被指派,那么slots[i]指针则会指向负责处理该槽的节点;如果槽i未指派给任何节点,对应指针指向NULL。
### 9.3 集群中执行命令
![clusterruncommand.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338220505-0a55d604-0776-4ed8-8cfe-6f9fdd35f32d.png#align=left&display=inline&height=293&margin=%5Bobject%20Object%5D&name=clusterruncommand.png&originHeight=293&originWidth=570&size=82301&status=done&style=none&width=570)
可通过命令 `CLUSTER KEYSLOT` 查看一个给定键属于哪个槽(记为i)。然后通过检查自己clusterState.solts数组中的项i,判断给定键是否由自己负责。若是,正常处理命令;若不是,返回MOVED错误,并转向负责给定键所处槽的节点,再进行处理。
集群节点保存键值对以及键值对过期时间的方式与单机Redis服务器完全相同(两个字典),唯一的区别是节点只能使用0号数据库,单机没有这一限制。集群中节点还会用clusterState结构中的slots_to_keys基数树来保存槽和键之间的关系,属于同一个槽的键值对将会挂在同一个raxNode下面,这样我们就可以快速遍历具体某个槽位下面的所有键值对。
基数树(radixTree):它是一个有序字典树,按照key的字典序排列,支持快速地定位、插入和删除操作。
### 9.4 重新分片
重新分片操作是将任意数量已指派给某个节点(源节点)的槽改派给另一节点(目标节点),并将相关槽所属的键值对也会从源节点移至目标节点。该操作可以在线进行,由管理软件redis-trib负责执行,具体步骤(针对单个槽;若是多个槽,重复多次即可)如下:
1. redis-trib向目标节点发送 `CLUSTER SETSLOT <slot> IMPORTING <socure_id>` 命令,让目标节点准备好从源节点导入属于槽slot的键值对。
2. redis-trib向源节点发送 `CLUSTER SETSLOT <slot> MIGRATING <target_id>` 命令,让源节点准备好将属于槽slot的键值对迁移至目标节点。
3. redis-trib向源节点发送 `CLUSTER GETKEYSINSLOT <slot> <count>` 命令,获得最多count个属于槽slot的键值对的键名。
4. 针对第3步返回的每个键名,redis-trib向源节点发送 `MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>` 命令,将被选中的键原子地从源节点迁移至目标节点。
5. 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
6. redis-trib向集群中的每一个节点发送 `CLUSTER SETSLOT <slot> NODE <target_id>` 命令,让集群中所有节点都知道槽slot已经指派给了目标节点。
![clustersetslot.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338239654-9b2f5c37-7a3a-4505-9da2-951d4b475270.png#align=left&display=inline&height=344&margin=%5Bobject%20Object%5D&name=clustersetslot.png&originHeight=344&originWidth=355&size=79680&status=done&style=none&width=355)
### 9.5 ASK错误
如果节点的clusterState结构中的importing_slots_from数组的第i项指向一个clusterNode结构,则表示当前节点正在从clusterNode所代表的节点导入槽 i。该数组的填充由重新分片的第1步触发。
如果节点的clusterState结构中的migrating_slots_to数组的第i项指向一个clusterNode结构,则表示当前节点正在将槽i迁移至clusterNode所代表的节点。该数组的填充由重新分片的第2步触发。
在进行重新分片期间,如果源节点接收到客户端的一个与数据库键有关的命令,并且源节点没能在自己的数据库里找到指定的键,那么源节点就会检查自己的clusterState.migrating_slots_to[i],看键所属的槽i是否正在进行迁移,如果是,就向客户端返回一个ASK错误,接收到ASK错误的客户端会根据错误提供的IP地值和端口号,转向正在导入槽的目标节点,然后先向目标节点发送一个 `ASKING` 命令,再重新发送之前想要执行的命令。
命令 `ASKING` 的作用就是打开发送命令客户端的REDIS_ASKING标识,让节点破例执行关于槽i的命令一次,执行一次后,客户端的REDIS_ASKING标识就会被移除。如果不发送 `ASKING` 命令,目标节点将会拒绝执行,并返回MOVED错误。
MOVED错误代表槽的负责权已从一个节点转移到另一个节点,在客户端收到关于槽i的MOVED错误之后,客户端【每次】遇到关于槽i的命令请求时,直接将请求发送至MOVED错误所指向的节点;而ASK错误只是两个节点在迁移槽的过程中使用的临时措施,在客户端收到关于槽i的ASK错误后,客户端只会在接下来的【一次】命令请求中将关于槽i的请求发送至ASK错误所提示的节点。
### 9.6 复制与故障转移
Redis集群中的节点分为主节点和从节点,主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
向一个节点发送命令 `CLUSTER REPLICATE <node_id>` 可以让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制。
接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterNode结构中的slaveof指向这个结构,以此来记录这个节点正在复制的主节点;然后会修改自己clusterNode结构中的flags属性,关闭CLUSTER_NODE_MASTER标识,打开CLUSTER_NODE_SLAVE标识,表示该节点已从主节点变为从节点;最后,从节点调用复制代码(发送 `SLAVEOF` 命令),对主节点进行复制。
一个节点成为从节点,并开始复制某个主节点这一消息会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点。集群中主节点的clusterNode结构的slaves数组会记录正在复制该主节点的从节点(每一项指向从节点的clusterNode结构),numalsves属性记录正在复制该主节点的从节点数量。
集群中的每个节点都会定期向集群中的其他节点发送 `PING` 命令,以此来检测对方是否在线,如果接收PING消息的节点没有在规定时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线状态(打开clusterNode结构中flags属性的CLUSTER_NODE_PFAIL标识)。
集群中的各个节点会通过互相发送消息的方式来交换各个节点的状态信息。当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,A会在自己的clusterState.nodes字典中找到C所对应的clusterNode结构,并将B的下线报告添加到clusterNode结构的fail_reports链表中(记录所有其他节点对该节点的下线报告)。链表中每个下线报告由一个clusterNodeFailReport结构表示,该结构中由两个属性:
- node:一个指针,指向报告目标节点已经下线的节点
- time:最后一次从node节点收到下线报告的时间,用于检查下线报告是否过期
如果在一个集群中,半数以上负责处理槽的主节点都将某个主节点D报告为疑似下线,那么这个主节点D将被标记为已下线(打开clusterNode结构中flags属性的CLUSTER_NODE_FAIL标识),将主节点D标记为已下线的节点会向集群广播“主节点D已下线”的消息,所有收到这条消息的节点都会立即将主节点D标记为已下线。
当一个从节点发现自己正在复制的主节点进入已下线状态时,从节点将开始对下线主节点进行故障转移,执行步骤如下:
1. 从下线主节点的所有从节点中,选择一个从节点,执行 `SLAVEOF no one` 命令,成为新的主节点。<br />在当前配置纪元中,每个主节点都有一次投票的权力,而第一个向主节点要求投票的从节点将获得该主节点的投票,当一个从节点收集到大于等于N/2+1(N为具有投票权的主节点个数)张投票时,当选新的主节点。若当前配置纪元没有从节点收集到足够多的支持票,则进入下一个配置纪元,并再次选举,直到选出新的主节点为止。
1. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
1. 新的主节点向集群广播一条PONG消息,让其他节点立即知道该节点已由从节点变成主节点,且已接管已下线主节点负责处理的所有槽。
1. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
<a name="ba63eb35"></a>
### 9.7 节点间的通信
集群中的各个节点通过发送和接收消息来进行通信,称发送消息的节点为发送者,接收消息的节点为接收者。节点发送的消息(具体结构见clusterMsg结构体)主要有以下五种:
- MEET消息:用于集群中节点集间的握手。
- PING消息:集群中每个节点默认每隔一秒从已知节点列表中随机选出五个节点,然后对其中最长时间没有发送过PING消息的节点发送PING消息。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已超过了节点A的cluster-node-timeout配置项的一半,那么节点A也会向节点B发送PING消息。
- PONG消息:当接收者收到发送者发来的MEET或PING消息时,会向发送者返回一条PONG消息告知其发送的消息已到达。另外,一个节点可通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新对该节点的认知。
- FAIL消息:当主节点A将主节点B标识为FAIL状态时,节点A会向集群广播该消息,所有收到这条消息的节点都会立即将节点B标识为已下线。
- PUBLISH消息:当节点收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
<a name="4bf4185a"></a>
# 十、Codis
<a name="e05dce83"></a>
### 简介
![codis.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338258252-81caa6a0-5329-44e0-a01b-d298d4c7a902.png#align=left&display=inline&height=1302&margin=%5Bobject%20Object%5D&name=codis.png&originHeight=1302&originWidth=2614&size=52316&status=done&style=none&width=2614#align=left&display=inline&height=1302&margin=%5Bobject%20Object%5D&originHeight=1302&originWidth=2614&status=done&style=none&width=2614)
整体架构如上图所示。zookeeper用来存储数据路由表和一些元数据;codis-proxy会监听集群中所有的redis服务,并对外提供Redis服务入口供Client连接;codis-fe和codis-dashboard实现集群的统一管理;codis-group是codis-server的组容器,用于实现水平扩展,内部包括一个master和至少一个slave;redis-sentinel用来实现组内服务的高可用性。
在codis的架构中,将slot分为1024份,采用CRC32的取模算法进行Key的路由【确定Key属于哪个slot】,同时支持将1024个slot分配给不同的codis-group【哪个slot属于哪个group,将会被记录在zookeeper中】,codis-group中的codis-server实例负责存储属于该group的Key,并保障高可用性。具体路由过程见下图:
![key_route.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338283727-d19b4769-cb61-4ee6-8af0-b784e70f8a4c.png#align=left&display=inline&height=387&margin=%5Bobject%20Object%5D&name=key_route.png&originHeight=387&originWidth=577&size=152105&status=done&style=none&width=577#align=left&display=inline&height=387&margin=%5Bobject%20Object%5D&originHeight=387&originWidth=577&status=done&style=none&width=577)
codis-server对原生的redis底层进行了部分修改,新增了一些关于slot上key迁移的命令,[详情见此](https://github.com/CodisLabs/codis/blob/release3.2/doc/redis_change_zh.md);同时对某些命令也不再支持,[详情见此](https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md)。我们可以直接通过codis-fe的管理页面实现key的迁移操作。
<a name="92a1dc61"></a>
### 集群搭建
搭建教程也可参考官网给出的[官方文档](https://github.com/CodisLabs/codis/blob/release3.2/doc/tutorial_zh.md)。首先需要搭建zookeepr集群【此处已搭建完成,分别在2181、2182、2183端口下】,然后按下列步骤进行:
1. 配置 go 语言环境,同时需要配置两个环境变量 GOROOT 和 GOPATH;前者是 go 语言的安装家目录,后者是用来存放 codis 源码的,然后在 GOPATH 目录【此处GOPATH=/home/260205/codis】下执行:```shell<br />mkdir -p src/github.com/CodisLabs/
此目录是必须创建的,codis编译需要,且只能如此【官方规定】。
下载 codis 源码包,然后解压到目录
$GOPATH/src/github.com/CodisLabs
,并重命名为 codis 【此操作必须进行】,同时需要 yum 安装 gcc、make、autoconf、libtool 和 automake。tar xvf codis-3.2.2.tar.gz -C /home/260205/codis/src/github.com/CodisLabs/ cd /home/260205/codis/src/github.com/CodisLabs/ mv codis-3.2.2/ codis cd codis make
进入 codis 目录,使用
make
命令进行编译,编译完成后会生成一个 bin 目录,该目录下是一些可执行程序,主要有:| 文件名 | 作用 |
| —- | —- |
| assets | 存放dashboard服务所需要的前端资源,需要和codis-dashboard在同一目录下 |
| codis-admin | codis集群管理的命令行工具 |
| codis-dashboard | codis集群管理工具,支持proxy、server的添加、删除,以及数据迁移等操作。维护集群下所有proxy状态的一致,对于同一个业务集群而言,同一时刻dashboard只有0或1个 |
| codis-fe | codis集群管理页面 |
| codis-ha | 提供高可用服务,不过3.x版本推荐使用redis-sentinel |
| codis-proxy | 提供连接redis集群服务的入口 |
| codis-server | 提供redis服务,与原生相比,有些命令不再支持,同时添加有新命令 |
| redis-benchmark | 提供redis服务性能测试的工具 |
| redis-cli | 提供redis客户端功能 |
| redis-sentinel | 提供redis dentinel模式,实现高可用 |
目录 config 下主要存放配置文件。
4. 首先启动codis-dashboard,对配置文件dashboard.toml做如下修改:```
coordinator_name = “zookeeper”
coordinator_addr = “127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183”
product_name = “market”
product_auth = “market”
admin_addr = “172.28.171.111:18080”
再用下列命令启动dashboard:
./codis-dashboard —ncpu=1 —config=../config/dashboard.toml —log-level=WARN —log=../logs/dashboard.log &
启动成功后,会在zookeeper集群中注册 `/codis3/market/topom` 节点,可登录zk进行验证。<br />关闭dashboard时,最好使用命令来正确关闭【此时会删除zk中的对应节点】,否则再次启动失败:
此时的auth是product_auth
./codis-admin —dashboard=172.28.171.111:18080 —auth=”market” —shutdown
如果异常退出,可直接进入zk删除对应节点,或者通过命令进行删除:
./codis-admin —remove-lock —product=codis-demo —zookeeper=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
5. 再启动codis-proxy,对配置文件proxy.toml做如下修改:```<br />product_name = "market"<br />product_auth = "market"<br />admin_addr = "172.28.171.111:11080"<br />proxy_addr = "172.28.171.111:19000"
再用下列命令启动proxy:``` ./codis-proxy —ncpu=1 —config=../config/proxy.toml —log=../logs/proxy.log —log-level=WARN &
正常关闭proxy的命令:
此时的auth是product_auth
./codis-admin —proxy=172.28.171.111:11080 —auth=”market” —shutdown
如果proxy异常退出,可用下列命令进行关闭:
./codis-admin —dashboard=172.28.171.111:18080 —remove-proxy —addr=172.28.171.111:11080 —force
添加proxy的方式有两种,一是通过 codis-fe 添加,二是通过下列命令添加:```<br />./codis-admin --dashboard=172.28.171.111:18080 --create-proxy --addr=172.28.171.111:11808
- 然后启动codis-server,其实就是redis服务,对配置文件redis.conf做如下配置:
bind 172.28.171.111 port 6379 pidfile /home/260205/codis/src/github.com/CodisLabs/codis/redis/redis_6379.pid logfile /home/260205/codis/src/github.com/CodisLabs/codis/logs/redis_6379.log dbfilename "dump_01.rdb" dir /home/260205/codis/src/github.com/CodisLabs/codis/bin masterauth "market" requirepass "market"
如果在dashboard和proxy中配置了product_auth ,则需要配置redis 的 requirepass 与其取值相同。
再通过命令启动redis服务:```
./codis-server ../config/redis.conf
7. 最后启动codis-fe,通过命令:
此处监听的端口可随意指定
./codis-fe —ncpu=1 —log=../logs/fe.log —log-level=WARN —zookeeper=172.28.171.111:2181,172.28.171.111:2182,172.28.171.111:2183 —listen=172.28.171.111:8080 &
8. 使用浏览器访问 `[http://172.28.171.111:8080](http://172.28.171.111:8080)` 即可进入 codis 的集群管理界面。
至此,单节点的codis集群已搭建完成,此外可以在其它机器上启动多个codis-proxy和codis-server实例,然后通过codis-fe进行proxy、server的添加和group的创建,以及slot的指派等相关操作,同时也可结合redis-sentinel实现高可用。
<a name="601e3060"></a>
### Sentinel配置
首先修改配置文件sentinel.conf,改动如下:
protected-mode no port 26379 dir “/home/260205/codis/src/github.com/CodisLabs/codis/redis” logfile “/home/260205/codis/src/github.com/CodisLabs/codis/redis/sentinel.log”
监视一个名为market,地址为172.28.171.111,端口为6379的Master;2表示当2个Sentinel都判定Master失效了才会触发自动故障转移
sentinel monitor market 172.28.171.111 6379 2 sentinel auth-pass market market
指定Sentinel判定Master主观下线的时间,单位毫秒
sentinel down-after-milliseconds market 5000
指定故障转移的超时时间,单位毫秒
sentinel failover-timeout market 60000
故障转移时最多可以有多少个实例在同步新的主实例,取值越大,整体完成同步的时间越短,但是可能会造成数据读取失败
sentinel parallel-syncs market 1
然后再使用下列命令启动:
./redis-sentinel ../config/sentinel.conf &
每个codis-server实例上都要开启Sentinel模式,这样才能在发生故障时实现主从的自动切换。
<a name="d6ca825c"></a>
### 密码配置
- 配置文件proxy.toml、dashboar.toml中的product_auth需要和redis.conf中的requirepass保持一致。
- 当客户端通过codis-proxy连接时,使用的密码是proxy.toml中的session_auth;如果通过redis-cli连接时,使用的是redis.conf中的requirepass。
<a name="60df9ab6"></a>
### ZK节点分布
![codis-zk.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338305251-d632d5bb-a128-4396-ac53-ffeeab0d3eb5.png#align=left&display=inline&height=335&margin=%5Bobject%20Object%5D&name=codis-zk.png&originHeight=335&originWidth=466&size=17194&status=done&style=none&width=466#align=left&display=inline&height=335&margin=%5Bobject%20Object%5D&originHeight=335&originWidth=466&status=done&style=none&width=466)
- proxy:所有codis-proxy服务的元信息
- slots:1024个slot中已分配使用的slot的元信息
- sentinel:redis-sentinel服务的元信息
- topom:codis-dashboard服务的元信息
- group:所有codis-group的元信息
<a name="25b26bbf"></a>
# 十一、数据库、客户端和服务器
<a name="68051bf4"></a>
### 数据库
每个Redis服务器的redisServer结构中有一个db数组(每一项指向一个redisDb结构),保存着当前服务器中的所有数据库;dbnum属性保存数据的数量,具体应该创建多少个数据库,由配置项database决定。每个客户端都有自己的目标数据库,默认位0号数据库,可通过 `SELECT` 命令切换目标数据库。
每个数据库都由redisDb结构表示,其中的dict字典(简称:键空间)保存了数据库中的所有键值对,键空间的键就是数据库的键(一个字符串对象),键空间的值就是数据库的值(任意一个Redis对象),针对数据库的操作,实际上都是通过对键空间字典的操作来实现的。
可通过订阅的方式实时获得数据库中某个键的变化(即针对该键执行的所有命令),以及数据库中某个命令的执行情况。
| 命令 | 作用 |
| --- | --- |
| DBSIZE | 返回当前数据库中key的数量 |
| FLUSHALL | 清空整个Redis服务器的数据(所有数据库的所有key) |
| FLUSHDB | 清空当前数据库中的所有key |
| SELECT | 切换到指定数据库,以0作为起始索引值 |
<a name="efc6882b"></a>
### 客户端
对于每个与服务器进行连接的客户端,服务器都会为这些客户端建立相应的client结构,用于保存客户端的当前状态信息,其中包括:
- 套接字描述符(fd):-1代表伪客户端(使用场景:载入AOF文件和执行Lua脚本中的Redis命令)、大于-1的整数代表普通客户端。
- 名字(name):默认没有名字,通过命令 `CLIENT setname` 设置,让客户端身份变清晰。
- 标志(flags):记录客户端的角色及目前所处状态,具体取值见server.h中以CLIENT_开头的常量。
- 输入缓冲区(querybuf):一个SDS字符串,保存客户端发送的命令请求,大小不能超过1GB。
- 正在使用的数据库指针及该数据库的库号、当前要执行的命令及命令的参数和参数的个数及指向命令实现函数的指针、输出缓冲区、复制状态、事务状态、身份验证标志、创建时间、与服务器的最后一次通信时间等<br />| 命令 | 作用 |<br />| --- | --- |<br />| CLIENT GETNAME | 返回当前连接名字 |<br />| CLIENT KILL | 关闭地址为 ip:port 的客户端 |<br />| CLIENT LIST | 返回所有连接到服务器的客户端信息和统计数据 |<br />| CLIENT SETNAME | 为当前连接分配一个名字 |<br />| AUTH | 密码匹配,解锁命令 |
<a name="c566ca59"></a>
### 服务器
一个服务器可以与多个客户端建立网络连接,接收并处理每个客户端发送的命令请求。通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。每个RedisServer结构中的clients链表保存了所有与服务器连接的客户端的状态结构(client)。
服务器启动过程:初始化服务器状态 --> 载入服务器配置 --> 初始化服务器数据结构 --> 还原数据库状态 --> 执行事件循环。
一个命令请求从发送到完成的主要步骤:客户端将命令请求发送到服务器-->服务器读取命令请求,并分析出命令参数-->命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复-->服务器将命令回复返回给客户端。
| 命令 | 作用 |
| --- | --- |
| CONFIG GET | 返回运行中的Redis服务器的配置参数 |
| CONFIG RESETSTAT | 重置INFO命令中的某些统计数据 |
| CONFIG REWRITE | 根据运行时服务器的配置参数对redis.conf文件进行重写 |
| CONFIG SET | 动态调整Redis服务器的配置(无须重启) |
| INFO | 返回关于Redis服务器的各种信息和统计数值 |
| LASTSAVE | 返回最近一次Redis成功将数据保存到磁盘上的时间,以UNIX时间戳格式表示 |
| SHUTDOWN | 停止所有客户端;执行SAVE操作;更新AOF文件;关闭Redis服务器 |
| TIME | 返回当前服务器时间 |
| ECHO | 打印一个特定的信息,测试时使用 |
| PING | 使用客户端向服务器发送一个PING,如果服务器运作正常的话,会返回一个PONG |
| QUIT | 请求服务器关闭与当前客户端的连接 |
| DEBUG OBJECT | 一个调试命令 |
| DEBUG SEGFAULT | 执行一个不合法的内存访问从而让Redis崩溃,仅在开发时用于BUG模拟 |
<a name="b6c9d1cd"></a>
# 十二、扩展功能
<a name="9d6f16cc"></a>
### 基数统计(HyperLogLog)
| 命令 | 作用 |
| --- | --- |
| PFADD | 将任意数量的元素添加到指定的HyperLogLog里面 |
| PFCOUNT | 返回HyperLogLog中包含的唯一元素的近似数量 |
| PFMERGE | 将多个HyperLogLog合并为一个HyperLogLog,并存储到指定key中 |
<a name="52a7038a"></a>
### 地理位置(GEO)
| 命令 | 作用 |
| --- | --- |
| GEOADD | 将给定的空间元素(纬度、经度、名字)添加到指定的键里面 |
| GEOPOS | 从键里面返回所有给定位置元素的位置(经度和纬度) |
| GEODIST | 返回两个给定位置之间的距离 |
| GEORADIUS | 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素 |
| GEORAUDISBYMEM | 以给定的位置元素为中心, 返回键包含的位置元素当中, 与中心元素的距离不超过给定最大距离的所有位置元素 |
| GEOHASH | 返回一个或多个位置元素的Geohash表示 |
<a name="98520548"></a>
### 发布/订阅(Pub/Sub)
| 命令 | 作用 |
| --- | --- |
| SUBSCRIBE | 订阅给定的一个或多个频道的信息 |
| UNSUBSCRIBE | 指示客户端退定给定的一个或多个频道 |
| PSUBSCRIBE | 订阅一个或多个符合给定模式的频道 |
| PUNSUBSCRIBE | 指示客户端退定所有给定模式 |
| PUBLISH | 将信息发送到指定的频道的所有订阅者和与该频道匹配的模式的所有订阅者 |
| PUBSUB | 用于查看订阅与发布系统的状态信息,子命令有3个: CHANNELS [pattern](返回所有被订阅的频道或其中与pattern模式相匹配的频道) NUMSUB [channel-1 ......](返回这些频道的订阅者数量) NUMPAT(返回服务器当前被订阅模式的数量) |
<a name="9f82401d"></a>
### 事务
事务可将多个命令打包执行,在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求。一个事务从开始到结束会经历以下三个阶段:
- 事务开始:执行MULTI命令,打开客户端的CLIENT_MULTI标识
- 命令入队:具体过程加下图
![shiwu.png](https://cdn.nlark.com/yuque/0/2020/png/1617241/1605338334693-da300c7f-ef34-4371-97f2-f0fbf727b2b3.png#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&name=shiwu.png&originHeight=351&originWidth=526&size=88300&status=done&style=none&width=526#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&originHeight=351&originWidth=526&status=done&style=none&width=526)
- 事务执行:执行EXEC命令,执行队列中保存的所有命令```<br />WATCH命令是一个乐观锁,可在EXEC执行前,监视任意数量的数据库键,并在EXEC执行前,检查被监视的所有键,如果至少有一个被修改的话,服务器将拒绝执行事务。<br />在Redis中,事务总是具有原子性、一致性和隔离性,并且当Redis运行在appendfsyna值为always的AOF持久化模式下,事务也具有持久性。<br />Redis的事务功能不支持回滚机制,即使事务队列中的某个命令在执行期间出现了错误,后续命令依然会继续执行,直到整个事务队列中的命令都执行完毕。
命令 | 作用 |
---|---|
MULTI | 标记一个事务块的开始 |
EXEC | 执行所有事务块内的命令 |
DISCARD | 取消事务,放弃执行事务块内的所有命令 |
UNWATCH | 取消WATCH命令对所有key的监视 |
WATCH | 监视一个或多个key,如果在事务执行前这些key被其他命令所改动,事务将被打断 |
Lua脚本
命令 | 作用 |
---|---|
EVAL | 通过内置的Lua解释器,对Lua脚本进行求值 |
EVALSHA | 根据给定的校验码,对缓存在服务器中的脚本进行求值 |
SCRIPT EXISTS | 根据一个或多个脚本的校验码,判断对应的脚本是否已经被保存在缓存中 |
SCRIPT FLUSH | 清除所有Lua脚本缓存 |
SCRIPT KILL | 杀死当前正在运行的Lua脚本,当且仅当这个脚本没有执行过任何写操作时才生效 |
SCRIPT LOAD | 将脚本添加到脚本缓存中,但并不立即执行这个脚本 |
慢查询日志
Redis的慢查询日志用于记录执行时间超过给定时长的命令请求,用户可以通过该日志监视和优化查询速度。配置项 slowlog-log-slower-than
指定执行时间超过多少微秒的命令请求会被记录到日志中(默认10毫秒);配置项 slowlog-max-len
指定服务器最多保存多少条慢查询日志(默认128条),采用FIFO方式保存日志。
命令 | 作用 |
---|---|
SLOWLOG | 用来记录查询执行时间超过给定时长的命令请求的日志系统,用于优化查询速度,子命令有: GET:打印全部的慢查询日志或指定条数的日志 LEN:获取慢查询日志的条目数量 RESET:清除所有慢查询日志 |
监视器
通过执行下方命令,客户端可将自己变为一个监视器(打开客户端的CLIENT_MONITOR标识),实时接受并打印出服务器当前处理的命令请求的相关信息。每当服务器收到一条命令请求时,除处理请求外,还会将关于这条命令请求的信息发送给所有监视器。
命令 | 作用 |
---|---|
MONITOR | 将客户端变为一个监视器,并实时打印出Redis服务器接收到的命令,调试用 |
十三、缓存
缓存机制
缓存穿透
定义:出于容错考虑,如果数据库中查不到数据就不写入缓存,这就导致每次请求不存在的数据时都要到数据库去查询。如果黑客利用不存在的key频繁请求服务器,这些请求就会穿透缓存,直接落在DB上,对DB造成巨大压力。
解决方案:
- 使用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉
- 如果查询结果为空,仍将结果写入缓存,只不过为其设置很短的过期时间(一般为几分钟)
缓存雪崩
定义:几乎在同一时间大量的缓存同时失效,此时所有请求会全部转发到DB,瞬间压力过大雪崩。
解决办法:
- 在设置过期时间的时候增加一个随机值,让缓存的失效时间错开,可有效避免雪崩
- 用加速或者队列的方式保证缓存的单线程写,从而避免失效时大量的并发请求落到DB上
缓存击穿
定义:与缓存雪崩类似,区别是缓存雪崩是集体失效,缓存击穿是单个失效,比如一个热点数据,如果在超高并发访问的时候缓存过期,那么在系统从后端DB加载数据到缓存的这个短暂过程中,会有大量的并发请求落到DB上,可能造成DB崩溃。
解决办法:
- 使用互斥锁。在缓存失效的时候,不直接请求DB,先获取分布式锁,如果加锁成功,再从DB中加载数据并设置缓存;如果加锁失败,说明已经有别的进程在重设缓存,我么秩序等待重试或者让用户手动重试
- 提前更新缓存。在读取缓存时,如果已经失效,就沿用第一种处理办法;如果为失效,则判断其是否快要过期,如果是就需要后台异步请求DB重设缓存并更新过期时间 切换模式 ```