Redis踩坑指南1、概述1.1 简介1.2 特性1.3 应用场景2、架构介绍2.1 集群2.1.1 哨兵模式2.1.2 集群模式2.1.2.1 架构图2.1.2.2 特性2.1.2.3 分区2.1.2.4 故障转移2.1.2.5 重定向2.1.2.6 复制迁移2.1.2.7 可用性3、主从复制3.1 旧版本复制3.1.1 同步3.1.2 命令传播3.1.3 缺陷3.2 新版本复制3.2.1 部分重同步3.2.2 复制偏移量3.2.3 复制积压缓存区3.2.4 服务器运行ID4、数据持久化4.1 快照模式4.2 追加模式5、数据清理5.1 最大内存配置5.2 回收策略5.3 过期时间6、动态扩容6.1 增加节点6.1.1 增加主节点6.1.2 增加从节点6.2 删除节点6.2.1 删除从节点6.3.2 删除主节点7、坑7.1 集群查询所有key7.2 集群节点个数7.3 集群选主
Redis(Remote Dictionary Server)是一个基于key-value键值对的数据库存储系统。
a. Redis读取的速度是110000次/s,写的速度是81000次/s
b. 原子性,Redis所有操作都是原子性的,同时Redis还支持对几个操作合并后原子性执行
c. 支持多种数据结构:String、List、Hash、Set、ZSet
d. 持久化,主从复制(集群)
e. 支持过期时间、支持事务、支持消息订阅
数据缓存、会话缓存、排行榜/计数器、消息队列
在Redis 3.0版本之前,只支持单例模式,在3.0版本之后才支持了集群模式。那么Redis 3.0之前版本如何保证服务高可用呢?在3.0之前使用的是哨兵模式部署Redis服务。
为了对主服务器进行监控,Redis在2.6版本之后提供了一个哨兵的机制。顾名思义,哨兵的含义就是监控Redis服务的运行状态。可以启动多个哨兵,去监控Redis数据库的运行状态,其主要功能有两点:
a. 监控所有的节点数据库是否正常运行
b. master节点出现故障时,自动通过投票机制,从slave节点中选举一个新的master。
一主多从的Redis系统中,可以使用多个哨兵执行监控任务以保证系统足够稳健。哨兵之间也会相互监控,一般部署不少于3的奇数个哨兵。
哨兵模式存在以下缺点:
a. 主从服务器的数据要经常进行主从复制,这样造成性能下降
b. 当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不可用的
哨兵模式不是本文介绍重点,详细的业务逻辑请自行查阅。
Redis在3.0版本正式引入了集群这个特性。
Redis集群几个比较重要的特性:
a. Redis集群的分片特性在于将键空间分拆了16384个槽位,每一个节点负责其中一些槽位
b. Redis集群提供一定程度的可用性,可以在某个节点宕机或不可达时继续处理命令
c. Redis集群中不存在中心节点或者代理节点,集群其中一个主要设计目的是达到线性可扩展性
Redis集群其他特点:
a. 所有节点相互连接,集群部署成功后,每个节点会定期向其他节点发送ping消息
b. 集群消息通过集群总线通信,集群总线端口为服务端口+10000,这个10000是固定值
c. 节点与节点之间通过二进制协议进行通信
d. 集群节点不会代理查询
Redis Cluster如何知道哪些槽由哪个master负责呢?某个master怎么知道自己拥有的槽呢?
master维护着一个16384/8字节的位序列,master用一个bit是否为1来标识某个槽自己是否拥有。如图:
Redis Cluster维护一个长度为16384的数组,每个下标代表一个槽,当前下标的内容代表此槽所在master的信息。
(1)心跳
Redis Cluster之间持续发送PING消息和回复PONG消息,统称为心跳包。
每个节点在每一秒钟都会向一定数量的其他节点发送PING消息,这些节点应该向发送PING的节点回复一个PONG消息。节点会尽可能确保拥有每个其他节点在NOTE_TIMEOUT/2秒时间内的最新信息,否则会发送一个PING消息,以确定与该节点的连接是否正常。
假定一个Cluster有301和节点,NOTE_TIMEOUT为60秒,那么每30秒每个节点至少发过300个PING,即每秒10个PING,整个Cluster每秒发送10x301=3010个PING,这个数量级的流量不会造成网络负担。
(2)故障检测
当一个节点A向节点B发送了PING消息后,在NODE_TIMEOUT内没有收到PONG应答,则节点A认为节点B异常,节点A将节点B标记为PFAIL(主观认为失败)。
在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT的时间内,当Cluster中大多数节点认为节点B异常(即设置了PFAIL标记)时,Cluster认为节点B真的失效了,此时节点A将节点B标记为FAIL,并向所有其他节点发送FAIL消息。
Slave发现自己的Master变为FAIL,将记录的集群当前纪元加1,并广播故障转移请求信息,其他节点收到该信息,只有Master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个纪元只发送一次ACK,发送故障转移请求的Slave收集FAILOVER_AUTH_ACK,超过半数后变成新Master,广播PONG通知其他集群节点。
因为集群节点不会代理查询,且每个master保存部分槽,如果客户端访问一个并不存在要查询key的节点,这个节点怎么处理呢?
比如客户端查询key为msg的值,msg计算得到的槽编号是254,当前节点正好不负责编号为254的槽,那么返回客户端以下信息:
GET msg
-MOVED 254 127.0.0.1:6381
当Cluster中某个master节点失效时,会进行复制关系的重新配置,即复制迁移。
假定Cluster中有三个master节点:A、B、C,且A有一个slave节点A1,B有一个slave节点B1,C有两个个slave节点C1、C2,如果节点A失效了。
在不考虑自动复制迁移时,则将唯一的slave节点A1提升为master,但是如果A1又失效了,则节点A负责的槽将失效,因此导致整个Cluster不能工作。
在考虑自动复制迁移时,将唯一的slave节点A1提升为master,由于A1没有slave,则将有两个slave的节点C的一个子节点配置成A1的子节点,这样就提高了整个集群的可用性。
情况一:集群中任意没有slave的master失效,则整个集群不可用(映射槽不完整)
情况二:集群中半数以上的master失效,则整个集群不可用(无论是否有slave)
在Redis中,用户可以通过执行SLAVEOF命令或设置slaveof选项,让一个服务器去复制另一个服务器。
旧版本复制功能分为同步(SYNC)和命令传播两个操作:
a. 同步操作用于将从服务器的数据库状态更新至主服务器所处的状态
b. 命令传播是用于在主服务器状态被修改时,主服务器会向其从服务器发送更新命令
从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成, 以下时SYNC命令的执行步骤:
a. 从服务器向主服务器发送SYNC命令
b. 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓存区记录从现在开始执行的所有写命令
c. 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的状态更新至主服务器执行BGSAVE时的状态
d. 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器当前所处的状态
命令传播即主服务器每次修改之后,会将自己执行的写命令发送给从服务器执行,以保证主从一致性。
在Redis中,主从复制可分为两种情况:
a. 初次复制:从服务器第一次复制此主服务器
b. 断线后重复制:从服务器断线重连后,继续复制原主服务器
对于初次复制来讲,旧版本复制功能可以很好完成功能,但是对于断线后重复制来讲,旧版本复制效率会比较低。
假如在运行的某个时刻,从服务器宕机一段时间,在重启后向主服务器发送SYNC请求,主服务器先要生成RDB文件,然后将RDB文件发送给从服务器,从服务器又要重新加载这个RDB文件。由于从服务器宕机这段时间可能主服务器修改很少,但为了弥补一小部分数据的丢失,复制整个主节点的数据,无疑是非常低效的。
SYNC命令是个非常耗费资源的操作:
a. 主服务器执行BGSAVE生成RDB文件,会耗费大量的CPU、内存、磁盘I/O资源
b. 主服务器需要将生成的RDB文件发送给从服务器,会占用大量的带宽
c. 接收到RDB文件的从服务器需要将RDB文件载入,载入期间从服务器会阻塞
为了解决旧版本复制功能在处理断线重复制时的低效问题,Redis从2.8开始,使用PSYNC命令代替SYNC命令来进行同步。
PSYNC命令具有完整同步和部分同步两种模式:
a. 完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步
b. 当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以更新至主服务器当前所处的状态
部分重同步功能由三部分组成:
a. 主服务器的复制偏移量和从服务器的复制偏移量
b. 主服务器的复制积压缓存区
c. 服务器运行ID
执行复制双方(主服务器和从服务器)分别维护一个复制偏移量。主服务器每次向从服务器发送N个字节的数据,则主服务器将偏移量的值加N。从服务器每次接收到N个字节的数据时,将偏移量的值加N。
复制积压缓存区是由主服务器维护的一个固定长度、先进先出的队列,默认为1MB。
复制积压缓存区长度固定,如果入队的元素数量大于队列长度,最先入队的元素会被弹出,而新元素会被放入队列。
当主服务器进行命令传播时,不仅将命令发送给所有从服务器,还会将写命令入队到复制积压缓存区内,如图:
主服务器的复制积压缓冲区里会保存着一部分最近传播的写命令,并且复制积压缓存区会为队列中每个字节记录相应的复制偏移量。
当主服务器收到PSYNC命令时,会根据命令所携带的从服务器的复制偏移量来决定对从服务器执行何种同步操作:
a. 如果复制偏移量之后的数据仍然存在于复制积压缓冲区中,则对从服务器执行部分重同步操作
b. 如果复制偏移量之后的数据已经不存在于复制积压缓存区中,则对从服务器执行完整重同步操作
复制积压缓存区大小是多少呢?是否可以调整?
Redis为复制积压缓存区设置的默认大小是1MB,如果主服务器需要执行大量的写请求,或者从服务器断线时间比较久,那么这个默认大小并不合适。如果复制积压缓存区大小不合适,那么PSYNC命令就起不到应有的作用,因此正确估算和设置复制积压缓冲区的大小也很重要。
一般复制积压缓存区的大小可以根据公式 second*write_size_per_second 来估算:
second:从服务器断线重连的平均花费时间
write_size_per_second:主服务器平均每秒写命令数据量
复制积压缓存区修改可通过修改配置文件中 repl-backlog-size 参数实现。
除了复制偏移量和复制积压缓存区外,实现部分重同步还需要用到服务器运行ID:
a. 每个Redis服务器,无论主从都有自己的服务器ID
b. 服务器ID在启动时自动生成,格式为40个随机十六进制字符
当初次复制时,主服务器将自己的ID发送给从服务器,从服务器将这个服务器ID保存起来。当从服务器断线重连时,从服务器会将之前保存的服务器ID发送给主服务器:
a. 如果主服务器发现此ID与自己的ID一致,则尝试进行部分重同步操作
b. 如果主服务器发现此ID与自己的ID不一致,则直接进行完整重同步操作
默认Redis会以快照方式将数据持久化到磁盘(dump.rdb),在配置文件中格式为save N M,表示N秒之内,Redis至少发生M次修改则Redis抓快照到磁盘,配置文件如下:
save 900 1
save 300 10
save 60 10000
上面三组命令也是非常好理解的,就是说900指的是“秒数”,1指的是“change次数”,接下来如果在“900s“内至少有1次更改,那么就执行save保存,同样的道理,如果300s内至少有10次change,60s内至少有1w次change,那么也会执行save操作。
也可以由用户执行save或者bgsave指令执行快照抓取。
192.168.101.19:7000> save
OK
192.168.101.19:7000> bgsave
Background saving started
192.168.101.19:7000>
RDB优点:
a. RDB 文件保存了 Redis 在某个时间点上的数据集,内容都非常紧凑,这种文件非常适合用于进行备份
b. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
RDB缺点:
a. 保存数据不完整,因为RDB文件时定期保存,所以总有最后几分钟数据未持久化的问题
b. 在数据集比较大时,持久化工作消耗CPU较高,且可能造成服务阻塞
配置文件中appendonly改为yes则启动AOF持久化,Redis每执行一个修改数据的命令,都会把他添加到AOF文件中,当redis重启时,会读取AOF文件将Redis恢复到关闭前的状态。
AOF三种模式,对应配置文件如下:
a. appendfsync always //保存每个命令,非常慢,但非常安全
b. appendfsync everysec //每秒保存一次,很快, 但会丢失1秒内的数据
c. appendfsync no //依靠OS进行刷新, 最快, 但安全性差
默认每秒刷新, 兼顾效率和安全。
AOF优点:
a. 持久化过程中可以保持良好性能
b. 默认每秒fsync一次,最多只会丢失1秒的数据
c. AOF是个只进行追加操作的日志文件
AOF缺点:
a. AOF文件体积要大于RDB文件体积
b. AOF的速度可能会慢于 RDB
maxmemory配置选项用来配置Redis的存储数据所能使用的最大内存限制,例如可配置内存上限100M:
//设置maxmemory为0表示无限制,64bit系统默认是0,32bit系统默认3GB
maxmemory 100mb
当内存达到限制时,Redis具体的回收策略通过maxmemory-policy配置项决定。
noenviction:不清除数据,只是返回错误,导致浪费更多的内存
allkeys-lru:从所有的数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,以供新数据使用
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰,以供新数据使用
allkeys-random:从所有数据集(server.db[i].dict)中任意选择数据淘汰,以供新数据使用
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰,以供新数据使用
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,以供新数据使用
当 cache 中没有符合清除条件的 key 时,回收策略 volatile-lru, volatile-random,volatile-ttl 将会和 策略 noeviction 一样返回错误。
选择正确的回收策略很重要,可以使用 INFO 输出来监控缓存命中和错过的次数,以调优Redis的设置,普适经验规则:
a. 如果部分子集元素被访问得远比其他元素多,可以使用allkeys-lru策略
b. 如果请求符合平均分布,可以使用allkeys-random策略
c. 如果创建缓存对象时设置了TTL值,可以使用volatile-ttl策略
//expire key second:设置key的过期时间
192.168.101.21:7000> expire name 30
-> Redirected to slot [5798] located at 192.168.101.19:7000
(integer) 1
?
//ttl key:查看key的有效期
192.168.101.19:7000> ttl name
(integer) 25
192.168.101.19:7000> ttl name
(integer) 19
?
//persist key:清除key的过期时间, 持久化
192.168.101.19:7000> persist name
(integer) 1
192.168.101.19:7000> ttl name
(integer) -1
192.168.101.19:7000>
本节主要介绍如何在不停掉集群服务的情况下,动态的往集群环境中增加主、从节点,以及动态的从集群删除主、从节点。这些操作都是通过redis-trib.rb脚本完成的,脚本说明如下:
root@ubuntu:/usr/local/redis_cluster# ./redis-trib.rb
Usage: redis-trib <command> <options> <arguments ...>
?
create host1:port1 ... hostN:portN
--replicas <arg>
check host:port
info host:port
fix host:port
--timeout <arg>
reshard host:port
--from <arg>
--to <arg>
--slots <arg>
--yes
--timeout <arg>
--pipeline <arg>
rebalance host:port
--weight <arg>
--auto-weights
--use-empty-masters
--timeout <arg>
--simulate
--pipeline <arg>
--threshold <arg>
add-node new_host:new_port existing_host:existing_port
--slave
--master-id <arg>
del-node host:port node_id
set-timeout host:port milliseconds
call host:port command arg arg .. arg
import host:port
--from <arg>
--copy
--replace
help (show this help)
?
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.
?
=============参数解释==================
create:创建集群
check:集群验证
fix:单点修复
reshard:重新分片
add-node:增加节点
del-node:删除节点
call:调用redis的命令
=============参数解释==================
首先创建节点目录,以及拷贝配置文件,以Redis部署文档环境为例,如增加节点7006。
//创建节点目录,并拷贝怕配置文件
root@ubuntu:/usr/local/redis_cluster# mkdir 7006
?
//修改配置文件
root@ubuntu:/usr/local/redis_cluster# cd 7006
oot@ubuntu:/usr/local/redis_cluster/7006# vi redis.conf
?
//启动新节点
root@ubuntu:/usr/local/redis/bin# ./redis-server ../../redis_cluster/7006/redis.conf
?
//查看节点状态,可以发现7006已经启动成功
root@ubuntu:/usr/local/redis/bin# ps -ef|grep redis
root 31166 1 0 May30 ? 00:03:55 ./redis-server *:7000 [cluster]
root 31171 1 0 May30 ? 00:04:03 ./redis-server *:7001 [cluster]
root 31178 1 0 May30 ? 00:03:52 ./redis-server *:7002 [cluster]
root 31183 1 0 May30 ? 00:03:54 ./redis-server *:7003 [cluster]
root 31188 1 0 May30 ? 00:03:56 ./redis-server *:7004 [cluster]
root 31195 1 0 May30 ? 00:03:54 ./redis-server *:7005 [cluster]
root 34114 1 0 04:33 ? 00:00:00 ./redis-server *:7006 [cluster]
root 34119 33604 0 04:34 pts/0 00:00:00 grep --color=auto redis
?
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7006 cluster nodes
b97e80c3814710f1055a06e261549b518ef49e7e :7006@17006 myself,master - 0 0 0 connected
?
//将新节点加入集群
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000
?
//查看节点7006已经成功加入集群,但是没有分配任何数据,需要下一步进行分槽(slot)
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7006 cluster nodes
?
//分槽
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb reshard 127.0.0.1:7000
?
//输入想要分的槽数,比如3000
How many slots do you want to move (from 1 to 16384)? 3000
?
//输入哪个节点接收这些槽,比如新节点 b97e80c3814710f1055a06e261549b518ef49e7e
What is the receiving node ID? b97e80c3814710f1055a06e261549b518ef49e7e
?
//输入从哪个节点分,比如节点 f1117abb5b5074101c35bbee6155f489f4006629(必须是master)
//可以输入多个节点,以done结束
Source node #1:f1117abb5b5074101c35bbee6155f489f4006629
Source node #2:done
?
//输入yes
Do you want to proceed with the proposed reshard plan (yes/no)? yes
?
//查看集群状态,发现新节点7006被分配了 0~2999 的槽
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7006 cluster nodes
增加从节点与增加主节点添加的部分操作一致,但是添加之后不重新分配槽。
//前几步与主节点一致。。。
?
//将新节点加入集群
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb add-node 127.0.0.1:7007 127.0.0.1:7000
?
//查看节点7007已经成功加入集群
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7007 cluster nodes
?
//登录7007,并将其指定为7006的slave
root@ubuntu:/usr/local/redis/bin# ./redis-cli -c -p 7007
127.0.0.1:7007> cluster replicate b97e80c3814710f1055a06e261549b518ef49e7e
OK
?
//查看节点7007已经是7006的slave了
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7007 cluster nodes
删除节点时要先删除从节点,再删除主节点。
//从节点可以直接执行脚本删除,格式:ruby redis-trib.rb del-node IP:PORT NODE_ID
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb del-node 127.0.0.1:7007 8a053e6ec8051aa95ae623aa3d6079a2c3285b0b
?
//查看集群,发现已经没有了7007节点
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7000 cluster nodes
删除主节点时,要先将主节点管理的槽迁移到其他主节点上,防止数据丢失。
//分槽
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb reshard 127.0.0.1:7000
?
//输入想要分的槽数,这里要把所有的槽都迁移到其他主节点
How many slots do you want to move (from 1 to 16384)? 3000
?
//输入哪个节点接收这些槽,比如节点 f1117abb5b5074101c35bbee6155f489f4006629
What is the receiving node ID? f1117abb5b5074101c35bbee6155f489f4006629
?
//输入从哪个节点分,比如节点 b97e80c3814710f1055a06e261549b518ef49e7e(必须是master)
//可以输入多个节点,以done结束
Source node #1:b97e80c3814710f1055a06e261549b518ef49e7e
Source node #2:done
?
//输入yes
Do you want to proceed with the proposed reshard plan (yes/no)? yes
?
//查看集群状态,发现7006已没有了数据槽
root@ubuntu:/usr/local/redis/bin# ./redis-cli -p 7006 cluster nodes
?
//删除节点7006
root@ubuntu:/usr/local/redis_cluster# ruby redis-trib.rb del-node 127.0.0.1:7006 b97e80c3814710f1055a06e261549b518ef49e7e
在Redis集群中取所有key是取不到的,只能取到当前查询的服务器上的所有的key。
因为Redis集群无中心节点,数据分槽存储,而key存储在哪个槽需要计算获取,因此不指定key而查询所有key仅能显示当前节点所管理的槽中所有的key。
//切换到槽5798, 数据在192.168.101.19:7000节点
192.168.101.15:7000> get name
-> Redirected to slot [5798] located at 192.168.101.19:7000
"wjq"
192.168.101.19:7000> keys *
1) "name"
?
//切换到槽741, 数据在192.168.101.15:7000节点
192.168.101.19:7000> get age
-> Redirected to slot [741] located at 192.168.101.15:7000
"10"
192.168.101.15:7000> keys *
1) "age"
192.168.101.15:7000>
Redis集群要求至少3个master节点,要保证高可用性,每个master节点最少要配一个slave节点,因此推荐部署至少6个节点。
Redis集群是多主多从模式,一般来讲,创建集群时,前三个节点是master,后三个节点是slave。
Q:然而slave与master的匹配关系如何确定?
?
A:关于主从节点的选择及槽的分配,其算法如下:
?
1> 把节点按照host分类,这样保证master节点能分配到更多的主机中。
2> 遍历host列表,从每个host列表中弹出一个节点,放入interleaved数组。直到所有的节点都弹出为止。
3> 将interleaved数组中前master个数量的节点保存到masters数组中。
4> 计算每个master节点负责的slot数量,16384除以master数量取整,这里记为N。
5> 遍历masters数组,每个master分配N个slot,最后一个master,分配剩下的slot。
6> 接下来为master分配slave,分配算法会尽量保证master和slave节点不在同一台主机上。对于分配完指定slave数量的节点,还有多余的节点,也会为这些节点寻找master。分配算法会遍历两次masters数组。
7> 第一次遍历master数组,在余下的节点列表找到replicas数量个slave。每个slave为第一个和master节点host不一样的节点,如果没有不一样的节点,则直接取出余下列表的第一个节点。
8> 第二次遍历是分配节点数除以replicas不为整数而多出的一部分节点。
原文:https://www.cnblogs.com/wlxj/p/14886144.html