「Redis开发与运维」读书笔记

第一章 初识Redis

请添加图片描述

Redis的8个重要特性

  1. 速度快

    • 内存存储数据
    • C语言实现
    • 单线程架构,避免多线程竞争问题
    • Redis源码优化好
  2. 基于键值对的数据结构服务器

    Redis 值可支持多种数据结构,如字符串、哈希、列表、集合、有序集合

  3. 丰富的功能

    • 键过期功能,可用来实现缓存
    • 发布订阅功能,可用来实现消息系统
    • 支持Lua脚本,可以利用Lua创造出新的Redis命令
    • 提供流水线功能,客户端能将一批命令一次性传到Redis,减少网络开销
  4. 简单稳定

    • Redis代码量少,能够较容易掌握
    • 使用单线程模型
    • 不需要依赖OS中的类库
  5. 客户端语言多

  6. 持久化

  7. 主从复制

  8. 高可用和分布式

Redis使用场景

Redis可以做什么

  1. 缓存
  2. 排行榜系统
  3. 计数器应用
  4. 社交网络
  5. 消息队列系统

Redis不适合做什么

  • 数据规模角度:Redis使用内存存储数据,成本较高,不适合存储大规模数据
  • 数据冷热角度:Redis适合存储热点数据,加速读写,提升系统性能

Redis使用建议

  1. 切勿当做黑盒使用,开发与运维同样重要。
  2. 阅读源码

Redis安装启动

安装(原书中的安装方式):

$ wget http://download.redis.io/releases/redis-3.0.7.tar.gz
$ tar xzf redis-3.0.7.tar.gz
$ ln -s redis-3.0.7 redis
$ cd redis
$ make
$ make install

查看Redis版本(本人安装的是6.x版本Redis):

$ redis-cli -v
redis-cli 6.0.9
  • 启动Redis Server:
  1. 默认配置
  2. 运行启动
  3. 配置文件启动
  • 启动Redis Client:
  1. 交互式方式
$ redis-cli -h {host} -p {port}
  1. 命令方式
$ redis-cli -h ip {host} -p {port} {command}
  • 停止Redis服务
$ redis-cli shutdown

66947:M 29 May 2021 15:25:54.059 # User requested shutdown...  #客户端发出的shutdown命令
66947:M 29 May 2021 15:25:54.059 * Saving the final RDB snapshot before exiting. #保存RDB持久化文件
66947:M 29 May 2021 15:25:54.059 * DB saved on disk #将RDB文件保存在磁盘上
66947:M 29 May 2021 15:25:54.059 # Redis is now ready to exit, bye bye... #关闭

停止Redis服务注意点:

  1. Redis关闭的过程:断开与客户端的连接、持久化文件生成,是一种相对优雅的关闭方式
  2. 除了可以通过shutdown命令关闭Redis服务以外,还可以通过kill进程号的方式关闭掉Redis,但是不要粗暴地使用kill -9强制杀死Redis服务,不但不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造成AOF和复制丢失数据的情况。
  3. shutdown还有一个参数,代表是否在关闭Redis前,生成持久化文件:
$ redis-cli shutdown nosave|save

第二章 API的理解和使用

预备

全局命令

  1. 查看所有键
keys *
  1. 键总数
dbsize

dbsize返回当前数据库中键的总数。dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以时间复杂度为O(1)。而keys命令会遍历所有键,时间复杂度为O(n).

  1. 检查键是否存在
exists key
  1. 删除键
del key [key ...]
  1. 键过期
expire key seconds
  1. 键的数据类型
type key

数据结构和内部编码

在这里插入图片描述

在这里插入图片描述

单线程架构

Redis使用单线程结构和IO多路复用模型来实现高性能的内存数据库服务。

单线程快的原因
  1. 纯内存访问
  2. 非阻塞IO,Redis使用epoll作为IO多路复用技术的实现,同时Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络IO上浪费过多的时间。
  3. 单线程避免线程切换和竞态产生的消耗

字符串

字符串类型是Redis最基础的数据结构,类型可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字,甚至是二进制(图片、音频、视频),但是值最大不能超过512M

命令

常用命令
  1. 设置值
set key value [ex seconds] [px milliseconds] [nx|xx]
  • ex seconds:为键设置秒级别过期时间
  • px millseconds:为键设置毫秒级过期时间
  • nx:键必须不存在才可以设置成功,用于添加
  • xx:键必须存在才可以设置成功,用于更新

redis还提供setnx和setxx两个命令:

setex key seconds value
setnx key value
  1. 获取值
get key
  1. 批量设置值
mset key value [key value]
  1. 批量获取值
mget key [key ...]
  1. 计数
incr key

incr命令用于对值做自增操作,返回结果分为三种情况:

  • 值不是整数,返回错误。
  • 值是整数,返回自增后的结果。
  • 键不存在,按照值为0自增,返回结果为1。
不常用命令
  1. 追加值
append key value

向字符串尾部追加值

  1. 字符串长度
strlen key
  1. 设置并返回旧值
getset key value
  1. 设置指定位置的字符
setrange key offset value
  1. 获取部分字符串
getrange key start end

start和end分别是开始和结束的偏移量,偏移量从0开始计算

在这里插入图片描述

内部编码

字符串类型的内部编码有3种:

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

典型使用场景

  1. 缓存功能
  2. 计数
  3. 共享session
  4. 限速

哈希

命令

  1. 设置值
hset key field value
  1. 获取值
hget key field
  1. 删除field
hdel key field [field ...]
  1. 计算field个数
hlen key
  1. 批量设置或获取field-value
hmset key field value [field value ...]
hmget key field [field ...]
  1. 判断field是否存在
hexists key field
  1. 获取所有field
hkeys key
  1. 获取所有value
hvals key
  1. 获取所有的field-value
hgetall key

在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。 如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部 field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型.

  1. hincrby hincrbyfloat
hincrby key field
hincrbyfloat key field
  1. 计算value的字符串长度(需要Redis3.2以上)
hstrlen key field

内部编码

哈希类型的内部编码有两种:

  • ziplist(压缩列表):当 哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节) 时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
  • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而 hashtable的读写时间复杂度为O(1)。

使用场景

哈希类型和关系型数据库不同之处:

  1. 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)
  2. 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。

缓存用户信息的方法:

  • 原生字符串类型:每个属性一个键
set user:1:name tom
set user:1:age 24
set user:1:city shanghai

优点:简单直观,每个属性都支持更新操作
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性较差,一般不用于生产环境

  • 序列化字符串类型:将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)

优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。

  • 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存
hmset user:1 name tomage 23 city beijing

优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会 消耗更多内存。

列表

在这里插入图片描述

命令

在这里插入图片描述
在这里插入图片描述

内部编码

列表类型的内部编码有两种:

  • ziplist(压缩列表):当 列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节) ,Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。
  • linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。

使用场景

  1. 消息队列
  2. 文章列表

集合

集合类型也是用来保存多个元素的,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。

命令

在这里插入图片描述

内部编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max- intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实 现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使 用hashtable作为集合的内部实现。

使用场景

  1. 标签

有序集合

有序集合保留了集合不能有重复成员的特性,但是有序集合中的元素可以排序。它给每个元素设置一个分数作为排序的依据。有序集合中的元素不能重复,但是score可以重复。

在这里插入图片描述

命令

有序集合命令的时间复杂度:
在这里插入图片描述

内部编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当 有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节) 时,Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。

使用场景

  1. 添加用户赞数
  2. 取消用户赞数
  3. 展示获取赞数最多的十个用户
  4. 展示用户信息以及用户分数

键管理

单个键管理

  1. 键重命名
rename key newkey # 如果newkey已经存在,则值会发生覆盖
renamenx key newkey # 如果newkey已经存在,则rename失败

注意点:

  • 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性
  • 如果rename和renamenx中的key和newkey如果是相同的,在Redis3.2和之
    前版本返回结果略有不同。

Redis3.2中会返回OK:

127.0.0.1:6379> rename key key
OK

Redis3.2之前的版本会提示错误:

127.0.0.1:6379> rename key key
(error) ERR source and destination objects are the same
  1. 随机返回一个键
127.0.0.1:6379> randomkey
"hello"
127.0.0.1:6379> randomkey
"jedis"
  1. 键过期
expire key seconds
expireat key timestamp
  1. 迁移键
  • move:把指定的键从源数据库移动到目标数据库中
  • dump+restore:在不同Redis实例之间进行数据迁移
  • migrate:用于在Redis实例间进行数据迁移,命令具有原子性

遍历键

  1. 全量遍历键
keys pattern
  1. 渐进式遍历
scan cursor [match pattern] [count number]
  • cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
  • match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。
  • count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。

每次scan命令的时间复杂度是O(1)。

如果scan的过程中如果有键的变化(增加、删除、修改),那么可能会遇到以下问题:新增的键可能没有遍历到,遍历出了重复的键等

数据库管理

  1. 切换数据库
select dbIndex
  1. flushdb/flushall
    flushdb和flushall命令用于清除数据库,两者的区别在于flushdb只清除当前数据库,flushall会清除所有数据库。

第三章 小功能大用处

慢查询分析

Redis客户端执行命令的生命周期:
在这里插入图片描述
慢查询只会统计步骤3执行命令的时间,所以没有慢查询并不代表客户端没有超时问题。

慢查询配置参数设置

  • slowlog-log-slower-than指定慢查询的阈值,超过这个值即为慢查询。它的单位是微秒,默认值是10000(如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slower- than<0对于任何命令都不会进行记录。)
  • slowlog-max-len表示慢查询日志最多存储多少条。Redis使用一个列表来存储慢查询日志,slowlog-max-len是列表的最大长度。

如果要Redis将配置持久化到本地配置文件,需要执行config rewrite命 令

(1)获取慢查询日志

slowlog get [n] 
127.0.0.1:6379> slowlog get 
1)  1) (integer) 666
	2) (integer) 1456786500
	3) (integer) 11615
	4)  1) "BGREWRITEAOF"
2)  1) (integer) 665
	2) (integer) 1456718400
	3) (integer) 12006
	4)  1) "SETEX"
		2) "video_info_200" 
		3) "300"
		4) "2"
...

每个慢查询日志有4个属性组成,分别是慢查询日志的标识 id、发生时间戳、命令耗时、执行命令和参数

在这里插入图片描述

(2)获取慢查询日志列表当前的长度

slowlog len

(3) 慢查询日志重置

slowlog reset

最佳实践

  • slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时 Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
  • slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询, 需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到 1000。因此对于高OPS场景的Redis建议设置为1毫秒。
  • 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。
  • 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多 的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL),然后 可以制作可视化界面进行查询

Redis Shell

redis-cli命令详解

  1. -r
    -r(repeat)选项代表将命令执行多次。

  2. -i
    -i(interval)选项代表每隔几秒执行一次命令,-i必须和-r一起使用

-i的单位是秒,不支持毫秒为单位,但是如果想以每隔10毫秒执行 一次,可以用-i0.01

  1. -x
    -x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数

  2. -c
    -c(cluster)选项是连接Redis Cluster节点时需要使用的,-c选项可以防 止moved和ask异常

  3. -a
    如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要 手动输入auth命令。

  4. –scan和–pattern
    –scan选项和–pattern选项用于扫描指定模式的键,相当于使用scan命令。

  5. –slave
    –slave选项是把当前客户端模拟成当前Redis节点的从节点,可以用来 获取当前Redis节点的更新操作

  6. –rdb
    –rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。可使用它做持久化文件的定期备份。

  7. –pipe
    –pipe选项用于将命令封装成Redis通信协议定义的数据格式,批量发送 给Redis执行

  8. –bigkeys
    –bigkeys选项使用scan命令对Redis的键进行采样,从中找到内存占用比 较大的键值,这些键可能是系统的瓶颈。

  9. –eval
    –eval选项用于执行指定Lua脚本

  10. –latency
    latency有三个选项,分别是–latency、–latency-history、–latency-dist。 它们都可以检测网络延迟

  11. –stat
    –stat选项可以实时获取Redis的重要统计信息

  12. –raw和–no-raw
    –no-raw选项是要求命令的返回结果必须是原始的格式,–raw恰恰相反,返回格式化后的结果。

redis-server详解

检测当前操作系统能否提供1G的内存给Redis:

redis-server --test-memory 1024

redis-benchmark详解

redis-benchmark可以为Redis做基准性能测试

-c(clients)选项代表客户端的并发数量(默认是50)。
-n(num)选项代表客户端请求总量(默认是100000)
-q选项仅仅显示redis-benchmark的requests per second信息
-r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四 位做随机处理(-r不是随机数的个数)
-P选项代表每个请求pipeline的数据量(默认为1)
-k选项代表客户端是否使用keepalive,1为使用,0为不使用,默认值为 1
-t选项可以对指定命令进行基准测试
–csv选项会将结果按照csv格式输出

Pipeline

Pipeline(流水线)能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。

  • Pipeline执行速度一般比逐条执行要快。
  • 客户端和服务端的网络延时越大,Pipeline的效果越明显。
    在这里插入图片描述

原生批量命令与Pipeline对比

  • 原生批量命令是原子的,Pipeline是非原子的
  • 原生批量命令是一个命令对应多个key,Pipeline支持多个命令
  • 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现

每次Pipeline组装的命令个数不能太多,否则一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。Pipeline只能操作一个Redis实例。

事物与Lua

事物

Redis提供了简单的事物功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事物开始,exec命令代表事物结束,它们之间的命令是原子顺序执行的。如果要停止事物的执行,可以执行discard命令。

如果事物中的命令出现错误,Redis的处理机制也不一样。

  1. 命令错误
    语法错误会造成整个事物无法执行。
  2. 运行时错误
    例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的:

有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修 改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来 解决这类问题。

Redis不支持事物中的回滚特性,同时无法实现命令之间的逻辑计算关系

Lua用法简述

  1. 数据类型及其逻辑处理
    Lua提供的数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)
    (1)字符串
local strings val = "world"

local代表val是一个局部变量,如果没有local代表是全局变量。
(2)数组
在Lua中可使用tables类型实现类似数组的功能,Lua的数组下标从1开始计算:

local tables myArray = {"redis", "jedis", true, 88.0}
-- 打印结果:true
print(myArray[3])

遍历整个数组可使用for和while

Redis 与 Lua

  1. 在Redis中使用Lua
    在Redis中执行Lua脚本有两种方法:eval和evalsha

(1)eval

eval 脚本内容 key个数 key列表 参数列表
127.0.0.1:6379> eval 'return "hello "..KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"

此时KEYS[1]=“redis”,ARGV[1]=“world”

如果Lua脚本较长,还可以使用redis-cli–eval直接执行文件。eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端
在这里插入图片描述

(2)evalsha
首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和, evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送 Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻 在服务端,脚本功能得到了复用。
在这里插入图片描述

  • 加载脚本:script load命令可以将脚本内容加载到Redis内存中:
$ redis-cli script load "$(cat lua_get.lua)" 
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
  • 执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和 eval一致。
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world 
"hello redisworld"

Bitmaps

数据结构类型

  • Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作
  • 在Redis中使用Bitmaps和使用字符串的方法不太一样。可以把BItmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。

命令

本节将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记作1,没有访问的用户记作0,用偏移量作为用户的id。

  1. 设置值
setbit key offset value

设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps的值如下所示:
在这里插入图片描述

具体操作过程如下,unique:users:2016-04-05代表2016-04-05这天的独立访问用户的Bitmaps:

127.0.0.1:6379> setbit unique:users:2016-04-05 0 1 
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1 
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1 
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1 
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1 
(integer) 0

如果此时有一个userid=50的用户访问了网站,那么Bitmaps的结构变成 了下图,第20位~49位都是0。
在这里插入图片描述
很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id 和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit 操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移 量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。

  1. 获取值
getbit key offset

获取键的第offset的值(从0开始计算)

  1. 获取Bitmaps指定范围值为1的个数
bitcount [start] [end]
  1. Bitmaps间的运算
bitop op destkey key [key ...]

bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。

  1. 计算Bitmaps中第一个值为targetBit的偏移量
bitpos key targetBit [start] [end]

Bitmaps分析

(个人总结:适合密集型的数据存储,稀疏性数据存储会造成较大的空间浪费)
在这里插入图片描述

HyperLogLog

HyperLogLog是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。HyperLogLog提供三个命令:pfadd、pfcount、pfmerge。

  1. 添加
pfadd key element [element ...]

pfadd用于向HyperLogLog添加元素,如果添加成功则返回1

  1. 计算独立用户数
pfcount key [key ...]

pfcount用于计算一个或多个HyperLogLog的独立总数

  1. 合并
pfmerge destkey sourcekey [sourcekey ...]

pfmerge可以求出多个HyperLogLog的并集并赋值给destkey。

HyperLogLog内存占用量很小,但是存在错误率,开发者在进行数据结构选型时只需要确认下面两条即可:

  • 只为了计算独立总数,不需要获取单条数据
  • 可以容忍一定误差率

发布订阅

在这里插入图片描述

命令

Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。

  1. 发布消息
publish channel message
  1. 订阅消息
subscribe channel [channel ...]
  1. 取消订阅
unsubscribe [channel [channel ...]]
  1. 按照模式订阅和取消订阅
psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]
  1. 查询订阅
    (1)查看活跃的频道
pubsub channels [pattern]

所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式

(2)查看频道订阅数

pubsub numsub [channel ...]

(3)查看模式订阅数

pubsub numpat

GEO

  1. 增加地理位置信息
geoadd key longitude latitude member [longitude latitude memeber ....]

longitude、latitude、member分别是该地理位置的经度、纬度、成员

  1. 获取地理位置信息
geopos key member [member ...]
  1. 获取地理位置的距离
geodist key member1 member2 [unit]

其中unit代表返回结果的单位,包含以下四种:

  • m(meters)代表米。
  • km(kilometers)代表公里。
  • mi(miles)代表英里。
  • ft(feet)代表尺。
  1. 获取指定位置范围内的地理信息位置集合
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
  1. 获取geohash
geohash key member [member ...]

geohash有如下特点:

  • GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
  • 字符串越长,表示的位置更精确
  • 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
  • geohash编码和经纬度是可以相互转换的。
  1. 删除地理位置信息
zrem key member

GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。

第四章 客户端

在这里插入图片描述

客户端通信协议

  • 客户端与服务端之间的通信协议是在TCP协议之上构建的
  • Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互
  1. 发送命令格式
    RESP规定一条命令的格式如下,CRLF代表"\r\n"。
*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF

命令set hello word对应的格式如下:

*3 # 参数数量有三个
$3 # set的字节数为3
SET
$5 # hello的字节数为5
hello
$5
world

在这里插入图片描述

  1. 返回结果格式
    Redis的返回结果类型分为以下五种:
  • 状态回复:在RESP中第一个字节为"+"
  • 错误回复:在RESP中第一个字节为"-"
  • 整数回复:在RESP中第一个字节为":"
  • 字符串回复:在RESP中第一个字节为"$"
  • 多条字符串回复:在RESP中第一个字节为"*"
    在这里插入图片描述
    无论是字符串回复还是多条字符串回复,如果有nil 值,那么会返回$-1
$ nc localhost 6379
set hello world
+OK
sethx
-ERR unknown command `sethx`, with args beginning with:
incr counter
:1
get hello
$5
world
mset java jedis python redis-py
+OK
mget java python
*2
$5
jedis
$8
redis-py
get not_exist_key
$-1
mget hello not_exist_key java
*3
$5
world
$-1
$5
jedis

客户端管理

客户端API

  1. client list
    client list命令能列出与Redis服务端相连的所有客户端连接信息。
127.0.0.1:6379> client list
id=795 addr=172.17.0.1:13410 fd=12 name= age=347 idle=261 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=mget
id=796 addr=172.17.0.1:14408 fd=16 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

输出结果的每一行代表一个客户端的信息

(1)标识:id、addr、fd、name

  • id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启 Redis后会重置为0。
  • addr:客户端连接的ip和端口
  • fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1 代表当前客户端不是外部客户端,而是Redis内部的伪装客户端
  • name:客户端的名字

(2) 输入缓冲区:qbuf、qbuf-free

Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓冲区拉取命令并执行。

client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。

输入缓冲使用不当会产生两个问题:

  • 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭
  • 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了 maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了 3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等 情况

造成输入缓冲区过大的原因:

  • Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况
  • Redis发生了阻塞,短期内不能处理命令,造成了客户端输入的命令积压在输入缓冲区,造成输入缓冲区过大。

监控输入缓冲区异常的方法:

  • 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找出可能出问题的客户端。
  • 通过info命令的info clients模块,找到最大的输入缓冲区

在这里插入图片描述

(3)输出缓冲区:obl、oll、omem
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。

输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端。

配置命令:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
  • <class>:客户端类型,分为三种。a)normal:普通客户端;b) slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。
  • <hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。
  • <soft limit>和<soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft seconds>秒,客户端会被立即关闭。

输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲 区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结 果,例如大的字符串、hgetall、smembers命令的结果等.

固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。

client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。

(4)客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间

(5)客户端的限制maxclients和timeout
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数。Redis提供timeout(单位为秒)参数来限制连接的最大空闲时间,一 旦客户端连接的idle时间超过了timeout,连接将会被关闭。Redis默认的timeout是0,也就是不会检测客户端的空闲。

(6)客户端类型
在这里插入图片描述

在这里插入图片描述

  1. client setName和client getName
    client setName用于给客户端设置名字,这样比较容易标识出客户端的来源

  2. client kill

client kill ip:port

此命令用于杀掉指定IP地址和端口的客户端

  1. client pause
client pause timeout(毫秒)

client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。

  • client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。
  • client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
  1. monitor
    monitro命令用于监控Redis正在执行的命令。

客户端相关配置

  • timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。
  • maxclients:客户端最大连接数,这个参数会受到操作系统设置的限制
  • tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。
  • tcp-backlog:TCP三次握手之后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,默认值为511。

客户端统计片段

  1. info clients
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0

1)connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
2)client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
3)client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
4)blocked_clients:正在执行阻塞命令(例如blpop、brpop、 brpoplpush)的客户端个数。

  1. info stats
127.0.0.1:6379> info stats
# Stats
total_connections_received:80
...
rejected_connections:0
  • total_connections_received:Redis自启动以来处理的客户端连接数总数。
  • rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控。

客户端常见异常

  1. 无法从连接池获取到连接
  2. 客户端读写超时
  3. 客户端连接超时
  4. 客户端缓冲区异常
  5. Lua脚本正在执行
  6. Redis正在加载持久化文件
  7. Redis使用的内存超过maxmemory配置
  8. 客户端连接数过大

第五章 持久化

RDB

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。

触发机制

手动触发对应save和bgsave命令:

  • save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
  • bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。

自动触发 RDB持久化:
(1)使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
(2)如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
(3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
(4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。

流程说明

在这里插入图片描述
1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项。

RDB文件的处理

保存: RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir {newDir}config set dbfilename {newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。

RDB的优缺点

优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
  • Redis加载RDB恢复数据远远快于AOF的方式。

缺点:

  • RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
  • RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

AOF

AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。

使用AOF

开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF工作流程:
在这里插入图片描述

(1)所有的写入命令会追加到aof_buf(缓冲区)中。
(2)AOF缓冲区根据对应的策略向硬盘做同步操作。
(3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
(4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。

命令写入

AOF命令写入的内容直接是文本协议格式。例如set hello world这条命令,在AOF缓冲区会追加如下文本:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

AOF为什么直接采用文本格式?

  • 文本协议兼容性好
  • 开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免二次处理开销。
  • 文本协议可读性好,方便直接修改和处理。

文件同步

在这里插入图片描述

系统调用write和fsync说明:

  • write操作会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高硬盘IO性能,write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
  • fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化。

重写机制

AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。

为什么重写后的AOF文件体积变小?

  • 进程内已经超时的数据不再写入文件。
  • 旧的AOF文件含有无效命令,如del key1、hdel key2等。重写使用进程内数据直接生成,新的AOF文件只保留最终数据的写入命令。
  • 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢 出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。

AOF重写过程可以手动触发和自动触发:

  • 手动触发:直接调用bgrewriteaof命令。
  • 自动触发:根据auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage参数确定自动触发时机。

auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比 值。

自动触发时机=aof_current_size>auto-aof-rewrite-min-size&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite- percentage

AOF重写运作流程:

在这里插入图片描述

流程说明:
(1)执行AOF重写请求
如果当前进程正在执行AOF重写,请求不执行并返回如下响应:ERR Background append only file rewriting already in progress

如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再 执行,返回如下响应:Background append only file rewriting scheduled

(2)父进程执行fork创建子进程,开销等同于bgsave过程。
(3.1)主进程fork操作完成之后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到磁盘,保证原有AOF机制正确性。
(3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内 存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部 分新数据,防止新AOF文件生成期间丢失这部分数据。
(4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每 次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为 32MB,防止单次刷盘数据过多造成硬盘阻塞。
(5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新 统计信息,具体见info persistence下的aof_*相关统计。
(5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件。
(5.3)使用新AOF文件替换老文件,完成AOF重写。

重启加载

Redis持久化文件加载流程:

在这里插入图片描述

(1)AOF持久化开启且存在AOF文件时,优先加载AOF文件
(2)AOF关闭或者AOF文件不存在时,加载RDB文件
(3)加载AOF/RDB文件成功后,Redis启动成功。
(4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

文件校验

加载损坏的AOF文件时会拒绝启动,并打印如下日志:
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>

对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命 令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可 以人工修改补全。

问题定位与优化

fork操作

Redis做RDB或AOF重写时,都会进行fork操作,对于大多数操作系统来说fork是个重量级错误。fork操作会复制父进程的空间内存页表。

fork耗时问题定位 :可以在info stats统 计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。
改善fork耗时问题
(1)优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用Xen虚拟机。
(2)控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB以内。
(3)合理配置Linux内存分配策略,避免物理内存不足虑导致fork失败
(4)降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制。

AOF追加阻塞

当开启AOF持久化时,常用的同步硬盘的策略是everysec,用于平衡性 能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步 硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞。
在这里插入图片描述
阻塞流程分析:
(1)主线程负责写入AOF缓冲区。
(2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
(3)主线程负责对比上次AOF同步时间:

  • 如果距上次同步成功时间在2秒内,主线程直接返回
  • 如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。

通过对AOF阻塞流程可以发现两个问题:

  • everysec配置最多可能丢失2秒数据,而不是1秒。
  • 如果系统fsync缓慢,将会导致Redis主线程阻塞影响效率。

AOF阻塞问题定位:
(1)发生AOF阻塞时,Redis输出如下日志,用于记录AOF fsync阻塞导致拖慢Redis服务的行为:
Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis
(2)每当发生AOF追加阻塞事件发生时,在info Persistence统计中, aof_delayed_fsync指标会累加,查看这个指标方便定位AOF阻塞问题。
(3)AOF同步最多允许2秒的延迟,当延迟发生时说明硬盘存在高负载问 题,可以通过监控工具如iotop,定位消耗硬盘IO资源的进程。

第六章 复制

配置

建立复制

参与复制的Redis实例划分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能从主节点复制到从节点。配置复制的方式有以下三种:
(1)在配置文件中加入slaveof {masterHost} {masterPort}随Redis启动生效。
(2)在redis-server启动命令后加入--slaveof {masterHost} {masterPort}生效。
(3)直接使用命令:slaveof {masterHost} {masterPort}生效。

slaveof命令在使用时,可以运行期动态配置,也可以提前写到配置文件中。

slaveof配置都是在从节点发起。

slaveof本身是异步命令,执行slaveof命令时,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行。

断开复制

执行slaveof no one来断开与主节点的复制关系。

断开复制主要流程:

  1. 断开与主节点复制关系。
  2. 从节点晋升为主节点。

从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。

通过slaveof命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。执行slaveof {newMasterIp} {newMasterPort}命令即可。

切主操作流程如下:

  1. 断开与旧主节点复制关系。
  2. 与新主节点建立复制关系。
  3. 删除从节点当前所有数据。
  4. 对新主节点进行复制操作。

安全性

主节点可以设置requirepass参数进行密码验证,客户端访问时必须使用auth命令进行校验。从节点与主节点 的复制连接是通过一个特殊标识的客户端来完成,因此需要配置从节点的 masterauth参数与主节点密码保持一致,这样从节点才可以正确地连接到主 节点并发起复制流程。

只读

默认情况下,从节点使用slave-read-only=yes配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式。

传输延迟

主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭 TCP_NODELAY,默认关闭,说明如下:

  • 当关闭时,主节点产生的命令数据无论大小都会及时地发送给从节 点,这样主从之间延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景,如同机架或同机房部署。
  • 当开启时,主节点会合并较小的TCP数据包从而节省带宽。默认发送 时间间隔取决于Linux的内核,一般默认为40毫秒。这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂或带宽紧张的场景,如跨机房部署。

拓扑

Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性
可以分为以下三种:一主一从、一主多从、树状主从结构:

  1. 一主一从结构
    一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时, 如果主节点脱机要避免自动重启操作。因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失了持久化的意义。安全的做法是在从节点上执行 slaveof no one断开与主节点的复制关系,再重启主节点从而避免这一问题。

  2. 一主多从
    一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点 实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的 读命令,如:keyssort等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加 重了主节点的负载影响服务稳定性。

  3. 树状主从
    树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点 数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。

原理

复制过程

在这里插入图片描述
(1)保存主节点(master)信息。执行slaveof命令后,从节点只保存主节点的地址信息便直接返回,这时建立复制流程还没有开始。
(2)从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。从节点会建立一个socket套接字,专门用于接受主节点发送的复制命令。如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one取消复制。
(3)发送ping命令。连接建立成功之后从节点发送ping请求进行首次通信。ping请求主要目的如下:

  • 检测主从之间网络套接字是否可用。
  • 检测主节点当前是否可接受处理命令。

如果发送ping命令后,从节点没有收到主节点的pong回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连。

(4)权限验证。如果主节点设置了requirepass参数,则需要密码验证, 从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。
(5)同步数据集。主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。Redis在2.8版本以后采用新复制命令psync进行数据同步,原来的sync命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量同步和部分同步
(6)命令持续复制。当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

数据同步

  • 全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会 对主从节点和网络造成很大的开销。
  • 部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

psync命令运行需要以下组件支持:

  • 主从节点各自复制偏移量。
  • 主节点复制积压缓冲区。
  • 主节点运行id。

全量复制

在这里插入图片描述

流程说明:

  1. 发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1。
  2. 主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应。
  3. 从节点接收主节点的响应数据保存运行ID和偏移量offset,执行到当前步骤时从节点打印如下日志:
Partial resynchronization not possible (no cached master)
Full resync from master: 92d1cb14ff7ba97816216f7beb839efe036775b2:216789
  1. 主节点执行bgsave保存RDB文件到本地
  2. 主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志
  3. 对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为client-output-buffer-limit slave256MB 64MB 60,如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。
  4. 从节点接收完主节点传送来的全部数据后会清空自身旧数据
  5. 从节点清空数据后开始加载RDB文件
  6. 从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能, 它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。

部分复制

在这里插入图片描述

流程说明:

  1. 当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接
  2. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。
  3. 当主从节点网络恢复后,从节点会再次连上主节点
  4. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。
  5. 主节点接到psync命令后首先核对参数runId是否与自身一致,如果一 致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。
  6. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

心跳

主从节点在建立复制之后,它们之间维护着长连接并彼此发送心跳命令。

在这里插入图片描述
主从心跳判断机制:

  1. 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为 flags=M,从节点连接状态为flags=S。
  2. 主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period控制发送频率。
  3. 从节点在主线程中每隔1秒发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:
  • 实时监测主从节点网络状态。
  • 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
  • 实现保证从节点的数量和延迟性功能,通过min-slaves-to-writemin-slaves-max-lag参数配置定义。

异步复制

主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户端,并不等待从节点复制完成。

在这里插入图片描述

由于主从复制过程是异步的,就会造成从节点的数据相对主节点存在延迟。

开发与运维中的问题

读写分离

对于读占比较高的场景,可以通过把一部分读流量分摊到从节点 (slave)来减轻主节点(master)压力,同时需要注意永远只对主节点执行写操作。

当使用从节点响应读请求时,业务端可能会遇到如下问题:

  • 复制数据延迟
  • 读到过期数据
  • 从节点故障
  1. 数据延迟
    Redis复制数据的延迟由于异步复制特性是无法避免的,延迟取决于网络带宽和命令阻塞情况,比如刚在主节点写入数据后立刻在从节点上读取可能获取不到。
  2. 读到过期数据
  • 惰性删除:主节点每次处理读取命令时,都会检查键是否超时,如果超时则执行del命令删除键对象,之后del命令也会异步发送给从节点。为了保证复制的一致性,从节点自身永远不会主动删除超时数据。
  • 定时删除:Redis主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行del命令,之后再同步给从节点。
  1. 从节点故障问题
    对于从节点的故障问题,需要在客户端维护可用从节点列表,当从节点故障时立刻切换到其他从节点或主节点上。

主从配置不一致

对于有些配置主从之间是可以不一致,比如:主节点关闭AOF但是在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。当配置的 maxmemory从节点小于主节点,如果复制的数据量超过从节点maxmemory 时,它会根据maxmemory-policy策略进行内存溢出控制,此时从节点数据已 经丢失,但主从复制流程依然正常进行,复制偏移量也正常。修复这类问题 也只能手动进行全量复制。当压缩列表相关参数不一致时,虽然主从节点存 储的数据一致但实际内存占用情况差异会比较大。

规避全量复制

  • 第一次建立复制:当对数据量较大且流量较高的主节点添加从节点时,建议在低峰时进行操作,或者尽量规避使用大数据量的Redis节点。
  • 节点运行ID不匹配:当主从复制关系建立后,从节点会保存主节点的 运行ID,如果此时主节点因故障重启,那么它的运行ID会改变,从节点发现 主节点运行ID不匹配时,会认为自己复制的是一个新的主节点从而进行全量 复制。对于这种情况应该从架构上规避,比如提供故障转移功能。当主节点 发生故障后,手动提升从节点为主节点或者采用支持自动故障转移的哨兵或 集群方案。
  • 复制积压缓冲区不足:当主从节点网络中断后,从节点再次连上主节 点时会发送psync{offset}{runId}命令请求部分复制,如果请求的偏移量不在 主节点的积压缓冲区内,则无法提供给从节点数据,因此部分复制会退化为 全量复制。针对这种情况需要根据网络中断时长,写命令数据量分析出合理 的积压缓冲区大小。网络中断一般有闪断、机房割接、网络分区等情况。这 时网络中断的时长一般在分钟级(net_break_time)。写命令数据量可以统 计高峰期主节点每秒info replication的master_repl_offset差值获取(write_size_per_minute)。积压缓冲区默认为1MB,对于大流量场景显然 不够,这时需要增大积压缓冲区,保证 repl_backlog_size>net_break_time*write_size_per_minute,从而避免因复制积 压缓冲区不足造成的全量复制。

规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点 短时间内发起全量复制的过程。复制风暴对发起复制的主节点或者机器造成 大量开销,导致CPU、内存、带宽消耗。

  1. 单主节点复制风暴
    单主节点复制风暴一般发生在主节点挂载多个从节点的场景。当主节点重启恢复后,从节点会发起全量复制流程,这时主节点就会为从节点创建 RDB快照,如果在快照创建完毕之前,有多个从节点都尝试与主节点进行全 量同步,那么其他从节点将共享这份RDB快照。但同时向多个从节点发送RDB快照,可能使主节 点的网络带宽消耗严重,造成主节点的延迟变大,极端情况会发生主从节点 连接断开,导致复制失败。
    解决方案首先可以减少主节点(master)挂载从节点(slave)的数量, 或者采用树状复制结构,加入中间层从节点用来保护主节点
  2. 单机器复制风暴
    由于Redis的单线程架构,通常单台机器会部署多个Redis实例。当一台机器(machine)上同时部署多个主节点(master)时,如果这台机器出现故障或网络长时间中断,当它重启恢复后,会有大量 从节点(slave)针对这台机器的主节点进行全量复制,会造成当前机器网 络带宽耗尽。
    解决方案:
  • 应该把主节点尽量分散在多台机器上,避免在单台机器上部署过多的 主节点。
  • 当主节点所在机器故障后提供故障转移机制,避免机器恢复后进行密 集的全量复制。

第七章 Redis阻塞

阻塞原因:

  • 内在原因:不合理地使用API或数据结构、CPU饱和、持久化阻塞等。
  • 外在原因:CPU竞争、内存交换、网络问题等。

内在原因

API或数据结构使用不合理

如何发现慢查询

执行slowlog get {n}命令获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。慢查询队列长度默认128,可适当调大。

慢查询优化方案:

  1. 修改为低算法度的命令,如hgetall改为hmget等,禁用keys、sort等命令。
  2. 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
如何发现大对象

执行命令rediscli -h {ip} -p {port} bigkeys可扫描大对象,内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化。

CPU饱和

单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis把单核CPU使用率跑到接近100%。对于这种情况,首先判断当前Redis的并发量是否达到极限,建议使用redis-cli -h {ip} -p {port} --stat获取当前Redis使用情况,该命令每秒输出一行统计信息:

(base) ➜  ~ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections
0          1.02M    1       0       1 (+0)              2
0          1.02M    1       0       2 (+1)              2
0          1.02M    1       0       3 (+1)              2
0          1.02M    1       0       4 (+1)              2
0          1.02M    1       0       5 (+1)              2
0          1.02M    1       0       6 (+1)              2
0          1.02M    1       0       7 (+1)              2

还有些情况可以通过info commandstats统计信息分析出命令不合理开销时间。

持久化阻塞

持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。

  1. fork阻塞
    fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时 过长,必然会导致主线程的阻塞。可以执行info stats命令获取到latest_fork_usec指标,表示Redis最近一次 fork操作耗时,如果耗时很大,比如超过1秒,则需要做出优化调整,如避 免使用过大的内存实例和规避fork缓慢的操作系统等。
  2. AOF刷盘阻塞
    当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后 台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。这种阻塞行为主要 是硬盘压力引起
  3. HugePage写操作阻塞
    子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只 有写操作时Redis才复制要修改的内存页。对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。

外在原因 TODO

第八章 理解内存 TODO

内存消耗

第九章 哨兵

基本概念

在这里插入图片描述

主从复制的问题

Redis的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用:第一,作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备“顶”上来,并且保证数据尽量不丢 失(主从复制是最终一致性)。第二,从节点可以扩展主节点的读能力,一 旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力。

主从复制的问题:

  • 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
  • 主节点的性能受到单机的限制
  • 主节点的存储能力受到单机的限制

Redis Sentinel的高可用性

当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移并通知应用方,从而实现真正的高可用。

Redis Sentinel与Redis主从复制模式只是多了若干Sentinel节点,所以Redis Sentinel并没有针对Redis节点做特殊处理:
在这里插入图片描述
从逻辑架构上看,Sentinel节点集合会定期对所有节点进行监控,特别是对主节点的故障实现自动转移。

下面以1个主节点、2个从节点、3个Sentinel节点组成的Redis Sentinel为例子进行说明:

在这里插入图片描述
整个故障转移的处理逻辑有下面4个步骤:

  1. 主节点出现故障,此时两个从节点与主节点失去连接,主从复制失败。
    在这里插入图片描述

  2. 每个Sentinel节点通过定期监控发现主节点出现了故障。
    在这里插入图片描述

  3. 多个Sentinel节点对主节点的故障达成一致,选举出Sentinel-3节点作为领导者负责故障转移。
    在这里插入图片描述

  4. Sentinel领导者节点执行了故障转移。
    在这里插入图片描述

  5. 故障转移后整个Redis Sentinel的拓扑结构图如下所示:
    在这里插入图片描述

Redis Sentinel具有以下几个功能:

  • 监控:Sentinel节点会定期检测Redis数据节点、其余Sentinel节点是否可达。
  • 通知:Sentinel节点会将故障转移的结果通知给应用方。
  • 主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系。
  • 配置提供者:在Redis Sentinel结构中,客户端在初始化的时候链接的是Sentinel节点集合,从中获取主节点信息。

Redis Sentinel包含若干个Sentinel节点:

  • 对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判。
  • Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel 节点不可用,整个Sentinel节点集合依然是健壮的。

安装与部署 TODO

API TODO

实现原理

三个定时任务

Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控:

  1. 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
    在这里插入图片描述
    定时任务的作用体现在:
  • 通过向主节点执行info命令,获取从节点的信息,这也是为什么 Sentinel节点不需要显式配置监控从节点。
  • 当有新的从节点加入时都可以立刻感知出来。
  • 节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息。
  1. 每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:
  • 发现新的Sentinel节点:通过订阅主节点的__sentinel__:hello了解其他 的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该Sentinel节点创建连接。
  • Sentinel节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据。
    在这里插入图片描述
  1. 每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。通过上面的定时任务,Sentinel节点对主节点、从节点、其余 Sentinel节点都建立起连接,实现了对每个节点的监控,这个定时任务是节点失败判定的重要依据。
    在这里插入图片描述

主观下线和客观下线

  1. 主观下线
    每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过 down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
    在这里插入图片描述

  2. 客观下线
    当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is-master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过 <quorum>个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定。

从节点、Sentinel节点在主观下线后,没有后续的故障转移操作。

领导者Sentinel节点选举

主节点被客观下线,这时只需要一个Sentinel节点来完成故障转移的工作,所以Sentinel节点之间会选举一个领导者进行故障转移的工作。Redis使用Raft算法实现领导者选举。

Redis Sentinel领导者选举的大致思路:

  1. 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送sentinel is-master-down-by addr命令,要求将自己设置为领导者。
  2. 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。
  3. 如果该Sentinel节点发现自己的票数已经大于等于max(quorum, num(sentinels)/2+1),那么它将成为领导者。
  4. 如果此过程没有选举出领导者,将进入下一次选举。

故障转移

领导者选举出的Sentinel节点负责故障转移,具体步骤如下:

  1. 在从节点列表中选出一个节点作为新的主节点,选择方法如下:

    1. 过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节点ping响应、与主节点失联超过down-after-milliseconds*10秒
    2. 选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
    3. 选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
    4. 选择runid最小的从节点。
      在这里插入图片描述
  2. Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。

  3. Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。

  4. Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

第十章 集群

数据分布

数据分布理论

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。常见的分区规则有哈希分区和顺序分区两种。

在这里插入图片描述

哈希分区规则:

  1. 节点取余分区
    使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式: hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。
  • 优点:简单,常用于DB的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。
  • 缺点:当节点数量变化时,数据节点映射关系需要重新计算,导致数据的重新迁移。
  1. 一致性哈希分区
    一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,范围一般在0~ 2 23 2^{23} 223,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。
    在这里插入图片描述
  • 优点:加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
  • 缺点:1. 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。 2. 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。 3. 普通的一致性哈希分区在增减节点时需要增加一倍或者减去一半节点才能保证数据和负载的均衡。
  1. 虚拟槽分区
    虚拟槽使用哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。

Redis数据分区

Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
(CRC16(key)%16384可以转换为CRC16(key)&16383,详见使用位运算替代模运算

在这里插入图片描述

Redis虚拟槽分区的特点:

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
  • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

集群功能限制

Redis集群相对单机在功能上存在一些限制:

  1. key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上因此不被支持。
  2. key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
  3. key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
  4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
  5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

搭建集群 TODO

节点通信

通信流程

在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。

通信过程说明:

  1. 集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
  2. 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
  3. 接收到ping消息的节点用pong消息作为响应。

集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

Gossip消息

Gossip协议的主要职责就是信息交换。常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,它们的通信模式如下:
在这里插入图片描述

  • meet消息:用于通知新节点接入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据。
  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

所有的消息格式划分为:消息头和消息体。消息头包含发送节点自身状态数据,接收节点根据消息头就可以获取到发送节点的相关数据,结构如下:

typedef struct {
	char sig[4]; /* 信号标示 */
	uint32_t totlen; /* 消息总长度 */
	uint16_t ver; /* 协议版本*/
	uint16_t type; /* 消息类型,用于区分meet,ping,pong等消息 */
	uint16_t count; /* 消息体包含的节点数量,仅用于meet,ping,pong消息类型*/
	uint64_t currentEpoch; /* 当前发送节点的配置纪元 */
	uint64_t configEpoch; /* 主节点/从节点的主节点配置纪元 */
	uint64_t offset; /* 复制偏移量 */
	char sender[CLUSTER_NAMELEN]; /* 发送节点的nodeId */
	unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点负责的槽信息 */
	char slaveof[CLUSTER_NAMELEN]; /* 如果发送节点是从节点,记录对应主节点的nodeId */ 
	uint16_t port; /* 端口号 */
	uint16_t flags; /* 发送节点标识,区分主从角色,是否下线等 */
	unsigned char state; /* 发送节点所处的集群状态 */
	unsigned char mflags[3]; /* 消息标识 */
	union clusterMsgData data /* 消息正文 */;
} clusterMsg;

集群内所有的消息都采用相同的消息头结构clusterMsg,它包含了发送节点关键信息,如节点id、槽映射、节点标识(主从角色,是否下线)等。 消息体在Redis内部采用clusterMsgData结构声明,结构如下:

union clusterMsgData {
	/* ping,meet,pong消息体*/ 
	struct {
		/* gossip消息结构数组 */
		clusterMsgDataGossip gossip[1];
	} ping;
	/* FAIL 消息体 */
	struct {
		clusterMsgDataFail about;
	} fail;
// ...
};

在这里插入图片描述
接收节点收到ping/meet消息时,执行解析消息头和消息体流程:

  • 解析消息头过程:消息头包含了发送节点的信息,如果发送节点是新节点且消息是meet类型,则加入到本地节点列表;如果是已知节点,则尝试更新发送节点的状态,如槽映射关系、主从角色等状态。
  • 解析消息体过程:如果消息体的clusterMsgDataGossip数组包含的节点是新节点,则尝试发起与新节点的meet握手流程;如果是已知节点,则根据cluster MsgDataGossip中的flags字段判断该节点是否下线,用于故障转移。

消息处理完后回复pong消息,内容同样包含消息头和消息体,发送节点接收到回复的pong消息后,采用类似的流程解析处理消息并更新与接收节点最后通信时间,完成一次消息通信。

节点选择

通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销,通信节点选择的规则如下:

在这里插入图片描述
根据通信节点选择的流程可以看出消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。

  1. 选择发送消息的节点数量
    集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。根据以上规则得出每个节点每秒需要发送ping消息的数量=1+10*num(node.pong_received>cluster_node_timeout/2),因此 cluster_node_timeout参数对消息发送的节点数量影响非常大。当我们的带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。过度调大cluster_node_timeout会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。
  2. 消息数据量
    每个ping消息的数据量体现在消息头和消息体中,其中消息头主要占用空间的字段是myslots[CLUSTER_SLOTS/8],占用2KB,这块空间占用相对固定。消息体会携带一定数量的其他节点信息用于信息交换。具体数量见以下伪代码:
def get_wanted():
	int total_size = size(cluster.nodes) 
	# 默认包含节点总量的1/10
	int wanted = floor(total_size/10); 
	if wanted < 3:
		# 至少携带3个其他节点信息
		wanted = 3;
	if wanted > total_size -2 :
		# 最多包含total_size - 2个
		wanted = total_size - 2; 
return wanted;

根据伪代码可以看出消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是大而全的集群更好。

集群伸缩

伸缩原理

Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。其中原理可以抽象为槽和对应数据在不同节点之间灵活移动。

在这里插入图片描述
三个节点分别维护自己负责的槽和对应的数据,如果希望加入一个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点。
在这里插入图片描述

扩容集群

Redis集群扩容操作分为如下几步:
(1) 准备新节点
(2) 加入集群
(3) 迁移槽和数据

  1. 准备新节点
    提前准备好新节点并运行在集群模式下,新节点建议跟集群内的节点配置保持一致,便于统一管理。
  2. 加入集群
    新节点依然采用cluster meet命令加入到现有集群中。在集群内任意节点执行cluster meet命令让新节点加入进来。
    对于新节点的后续操作一般有两种选择:
  • 为它迁移槽和数据实现扩容
  • 作为其他主节点的从节点负责故障转移
  1. 迁移槽和数据
    加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务。
    (1)槽迁移计划
    槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。
    (2)迁移数据
    数据迁移过程是逐个槽进行的,每个槽数据迁移过程如下所示:
    在这里插入图片描述
    流程说明:
    a. 对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据。
    b. 对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
    c. 源节点循环执行cluster getkeysinslot {slot} {count}命令,获取count个属于{slot}的键。
    d. 在源节点上执行migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO 次数。
    e. 重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
    f. 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。

(3)添加从节点
扩容加入的新节点迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力。使用cluster replicate{masterNodeId}命令为主节点添加对应从节点。

收缩集群

在这里插入图片描述
流程说明:
a. 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
b. 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。

  1. 下线迁移槽
    下线节点需要把自己负责的槽迁移到其他节点,原理与之前节点扩容的迁移槽过程一致。
  2. 忘记节点
    由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种简单的机制让集群内所有节点忘记下线的节点。也就是说让其他节点不再与要下线节点进行Gossip消息交换。Redis提供了cluster forget {downNodeId}命令实现该功能。当节点接收到cluster forget {down NodeId}命令后,会把nodeId指定的节点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。

当下线主节点具有从节点时需要把该从节点指向到其他主节点,因此对于主从节点都下线的情况,建议先下线从节点再下线主节点,防止不必要的全量复制。

请求路由

Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。

请求重定向

在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向。

在这里插入图片描述
重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。

使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作。redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发。节点对于不属于它的键命令只回复重定向响应,并不负责转发。

键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。

  1. 计算槽
    Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16384的余数,使每个键都可以映射到0~16383槽范围内。伪代码如下:
def key_hash_slot(key):
	int keylen = key.length(); 
	for (s = 0; s < keylen; s++):
		if (key[s] == '{'):
			break;
	if (s == keylen) 
		return crc16(key,keylen) & 16383; 
	for (e = s+1; e < keylen; e++):
		if (key[e] == '}'): 
			break;
	if (e == keylen || e == s+1) 
		return crc16(key,keylen) & 16383; /* 使用{}之间的有效部分计算槽 */
	return crc16(key+s+1,e-s-1) & 16383;

根据伪代码,如果键内容包含{和}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。

键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。

例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。

Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。

  1. 查找槽所对应的节点
    Redis计算得到键对应的槽之后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息,内部保存在clusterState结构中,结构所示:
typedef struct clusterState {
	clusterNode *myself; /* 自身节点,clusterNode代表节点结构体 */
	clusterNode *slots[CLUSTER_SLOTS]; /* 16384个槽和节点映射数组,数组下标代表对应的槽 */
...
} clusterState;

slots数组表示槽和节点对应关系,实现请求重定向伪代码如下:

def execute_or_redirect(key):
	int slot = key_hash_slot(key);
	ClusterNode node = slots[slot];
	if(node == clusterState.myself):
		return executeCommand(key);
	else:
		return '(error) MOVED {slot} {node.ip}:{node.port}';

根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销, 这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一 种实现:Smart(智能)客户端。

Smart客户端

  1. Smart客户端原理
    Smart客户端通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。

ASK重定向

  1. 客户端ASK重定向流程
    当slot对应的数据从源节点到目标节点迁移过程中,可能出现一部分数据在源节点,而另一部分数据在目标节点,当出现上述情况时,客户端键命令执行流程将发生变化:
    (1) 客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
    (2) 如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下: (error) ASK {slot} {targetIP}: {targetPort}
    (3) 客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。

在这里插入图片描述
在这里插入图片描述
ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。

  1. 节点内部处理
    为了支持ASK重定向,源节点和目标节点在内部的clusterState结构中维护当前正在迁移的槽信息,用于识别槽迁移情况,结构如下:
typedef struct clusterState {
	clusterNode *myself; /*自身节点*/
	clusterNode *slots[CLUSTER_SLOTS]; /* 槽和节点映射数组 */ 
	clusterNode *migrating_slots_to[CLUSTER_SLOTS];/* 正在迁出的槽节点数组 */
	clusterNode *importing_slots_from[CLUSTER_SLOTS];/* 正在迁入的槽节点数组*/
	...
} clusterState;

节点每次接收到键命令时,都会根据clusterState内的迁移属性进行命令处理,如下所示:

  • 如果键所在的槽由当前节点负责,但键不存在则查找migrating_slots_to数组查看槽是否正在迁出,如果是返回ASK重定向。
  • 如果客户端发送asking命令打开了CLIENT_ASKING标识,则该客户端下次发送键命令时查找importing_slots_from数组获取clusterNode,如果指向自身则执行命令。
  • 需要注意的是,asking命令是一次性命令,每次执行完后客户端标识都会修改回原状态,因此每次客户端接收到ASK重定向后都需要发送asking命令。
  • 批量操作。ASK重定向对单键命令支持得很完善,但是,在开发中我们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响。

故障转移

Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线 (pfail)和客观下线(fail)。

  • 主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
  • 客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
  1. 主观下线
    集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
    在这里插入图片描述
    1)节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点a更新最近一次与节点b的通信时间。
    2)如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
    3)节点a内的定时任务检测到与节点b最后通信时间超高cluster-node-timeout时,更新本地对节点b的状态为主观下线(pfail)。

  2. 客观下线
    当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。

通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流 程。这里有两个问题:
1)为什么必须是负责槽的主节点参与故障发现决策?因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
2)为什么半数以上处理槽的主节点?必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。

在这里插入图片描述
流程说明:
(1)当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
(2)找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
(3)根据更新后的下线报告链表告尝试进行客观下线。这里针对维护下线报告和尝试客观下线逻辑进行详细说明:
a. 维护下线报告链表
每个节点ClusterNode结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告,结构如下:

typedef struct clusterNodeFailReport {
	struct clusterNode *node; /* 报告该节点为主观下线的节点 */
	mstime_t time; /* 最近收到下线报告的时间 */
} clusterNodeFailReport;

下线报告中保存了报告故障的节点结构和最近收到下线报告的时间,当接收到fail状态时,会维护对应节点的下线上报链表,伪代码如下:

def clusterNodeAddFailureReport(clusterNode failNode, clusterNode senderNode) : 
	// 获取故障节点的下线报告链表
	list report_list = failNode.fail_reports;
	// 查找发送节点的下线报告是否存在
	for(clusterNodeFailReport report : report_list): 
		// 存在发送节点的下线报告上报
		if(senderNode == report.node): 
		// 更新下线报告时间
			report.time = now();
			return 0;
		// 如果下线报告不存在,插入新的下线报告
		report_list.add(new clusterNodeFailReport(senderNode,now()));
	return 1;

每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下 线报告是否过期,对于过期的下线报告将被删除。如果在cluster-node-time*2 的时间内该下线报告没有得到更新则过期并删除。

如果在cluster-node-time*2时间内无法收集到一半以上槽节点的下线报 告,那么之前的下线报告将会过期,也就是说主观下线上报的速度追赶不上 下线报告过期的速度,那么故障节点将永远无法被标记为客观下线从而导致 故障转移失败。因此不建议将cluster-node-time设置得过小。

b. 尝试客观下线
集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线:

在这里插入图片描述

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
在这里插入图片描述

  1. 资格检查
    每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time*cluster-slave-validity-factor,则当前从节点不具备故障转移资格。参数cluster-slave-validity-factor用于从节点的有效因子,默认为10。
  2. 准备选举时间
    当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。故障选举时间相关字段如下:
struct clusterState {
	...
	mstime_t failover_auth_time; /* 记录之前或者下次将要执行故障选举时间 */
	int failover_auth_rank; /* 记录当前从节点排名 */ 
}

这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。所有的从节点中复制偏移量最大的将提前触发故障选举流程。

  1. 发起选举
    当从节点定时任务检测到达故障选举时间(failover_auth_time)后,发起选举流程如下:
    (1)更新配置纪元
    配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元 (clusterNode.configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元(clusterState.current Epoch),用于记录集群内所有主节点配置纪元的最大版本。

配置纪元会跟随ping/pong消息在集群内传播,当发送方与接收方都是主节点且配置纪元相等时代表出现了冲突,nodeId更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突。

配置纪元的主要作用:

  • 标示集群内每个主节点的不同版本和当前集群最大的版本。
  • 每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。
  • 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。
  1. 选举投票
    只有持有槽的主节点才会处理故障选举消息 (FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复 FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。

投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从 节点。

Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够 N 2 + 1 \frac{N}{2}+1 2N+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作。

故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。

投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。

  1. 替换主节点
    当从节点收集到足够的选票之后,触发替换主节点操作:
    1)当前从节点取消复制变为主节点。
    2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
    3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

故障转移时间

(1)主观下线识别时间=cluster-node-timeout
(2)主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
(3)从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

根据以上分析可以预估出故障转移时间,如下:
failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000

因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。 配置时可以根据业务容忍度做出适当调整,但不是越小越好。

集群运维

集群完整性

为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回(error) CLUSTERDOWN Hash slot not served错误。这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。

带宽消耗

集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000以内,因此单集群不适合部署超大规模的节点。集群内所有节点通过ping/pong消息彼此交换信息,节点间消息通信对宽带的消耗体现在以下几个方面:

  • 消息发送频率:跟cluster-node-timeout密切相关,当节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息。
  • 消息数据量:每个消息主要的数据占用包含:slots槽数组(2KB空间)和整个集群1/10的状态数据(10个节点状态数据约1KB)。
  • 节点部署的机器规模:机器带宽的上限是固定的,因此相同规模的集群分布的机器越多每台机器划分的节点越均匀,则集群内整体的可用带宽越高。

Pub/Sub广播问题

在集群模式下内部实现对所有的publish命令都会向所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担。
在这里插入图片描述

集群倾斜

集群倾斜指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。

  1. 数据倾斜
    数据倾斜主要分为以下几种:
  • 节点和槽分配严重不均。
  • 不同槽对应键数量差异过大。
  • 集合对象包含大量元素。
  • 内存相关配置不一致。
  1. 请求倾斜
    集群内特定节点请求量/流量过大将导致节点之间负载不均,影响集群均衡和运维成本。常出现在热点键场景,当键命令消耗较低时如小对象的 get、set、incr等,即使请求量差异较大一般也不会产生负载严重不均。但是当热点键对应高算法复杂度的命令或者是大对象操作如hgetall、smembers等,会导致对应节点负载过高的情况。避免方式如下:
    (1)合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
    (2)不要使用热键作为hash_tag,避免映射到同一槽。
    (3)对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用。

集群读写分离

  1. 只读连接
    集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到 负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点 读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配 置slave-read-only在集群模式下无效。当开启只读状态时,从节点接收读命 令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命 令,否则返回重定向信息。

readonly命令是连接级别生效,因此每次新建连接时都需要执行readonly 开启只读状态。执行readwrite命令可以关闭连接只读状态。

  1. 读写分离
    集群模式下的读写分离,也会遇到:复制延迟,读取过期数据,从节点故障等问题。
    集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。

手动故障迁移

Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从 节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为 它的从节点。

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值