目录

1、redis性能瓶颈在哪里?

2、redis为什么需要高可用?

3、主从复制

3.1、主从复制原理

3.1.1、复制初始化

3.1.2、复制同步阶段

3.1.3、增量复制

3.1.4、无硬盘复制

3.2、异步复制导致主从库数据不一致怎么解决?

3.3、主从复制崩溃恢复

4、哨兵

5、集群

5.1、插槽

5.2、集群的主从复制模型

5.3、集群一致性

5.4、搭建集群

6、redis模板配置文件

7、安装Ruby


1、redis性能瓶颈在哪里?

首先说说redis为什么这么快。主要有以下几点原因:

  • 纯内存操作;
  • 单线程操作,避免频繁上下文切换;
  • 采用IO多路复用模型;
  • 纯ANSI C语言编写;

由于redis是纯内存操作并且采用了IO多路复用模型,因此redis的性能瓶颈不应该是CPU,内存和网络延迟更应该是redis的性能瓶颈。

 

2、redis为什么需要高可用?

需要高可用的原因主要为:

  • 单点故障:因此需要将数据生成多个副本分布在不同的机器上;
  • 内存容易成为瓶颈:因此需要对数据进行分片;

redis有三种高可用方案,分别是:

  • 主从复制
  • 哨兵
  • 集群

 

3、主从复制

主库:可以读写,将数据同步给从库

从库:只能读,如果修改从库数据会报错(将slave-read-only设置为no就不会报错)

一个主库可以有多个从库,但一个从库只能有一个主库。从库也可以作为主库。

主从复制可以同来实现读写分离,比如在电商系统中,读的压力大于写,那么可以配置一主多从的redis高可用架构,主库用来写,多个从库用来读。

由于主从复制不能有多个主库,因此当写压力过大时一主多从不再满足需求,此时需要使用redis集群。

 

如何配置主从复制?两种方法如下:

  • 在从库的redis.conf中配置slaveof host port;
  • 在从库执行slaveof host port命令;

 

info section命令以一种易于理解和阅读的格式,返回关于Redis服务器的各种信息和统计数值。比如查看复制相关信息,可以使用info replication。

在一个主库上执行info replication命令打印出来的信息如下所示。

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=14,lag=0
master_replid:0dca9fd8627630d1058cd3ae740c39edc03f07c5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14

也可以使用role命令查看节点角色。

 

如果当前库已经是某个主库的从库,那么执行slaveofhost port 将使当前库停止对旧主库的同步,丢弃旧数据集,转而开始接收新主库的同步。

另外,对一个从库执行命令slave no one将使得这个从库关闭复制功能,并从从库转变回主库,原来同步所得的数据集不会被丢弃。

利用slave no one不会丢弃同步所得数据集这个特性,可以在主库失败的时候,将从库作为新的主库,从而实现无间断运行。

 

3.1、主从复制原理

3.1.1、复制初始化

从库启动后,向主库发生SYNC命令

主库接收到命令并在后台保存RDB快照(异步),并将保存快照期间接收到的客户端命令缓存起来,然后一起发给从库

从库会将收到的内容写入磁盘临时文件,最后用临时文件替换旧的RDB文件(在写临时文件期间,从库不会阻塞客户端请求,并且用同步前的数据响应客户端,可以在redis.conf中配置slave-serve-stale-data为no来让从库在同步完成前对除了info和slave之外的所有命令响应错误)

 

3.1.2、复制同步阶段

主库每当收到写命令就将命令同步给从库(同步的内容就是redis通信协议的内容)

 

3.1.3、增量复制

主从库断开连接后,从库再次连接主库时,不必再进行一次复制初始化,而是直接增量复制(redis2.8版本开始支持)。

原理:

  1. 在复制同步阶段,主库每接收到一个命令,都会将该命令放到积压队列中,并记录好偏移量,从库同步数据时,也会记录好偏移量;
  2. 增量复制使用psync命令代替sync命令,且psync命令会带两个参数,psync 主库id 断开前最新的命令偏移量;
  3. 主库收到psync命令后,会判断从库传过来的主库id与自身id是否相等;如果相等,则再判断从库传过来的偏移量是否在积压队列中;
  4. 如果上述两个条件有一个不满足,则不满增量复制,而进行一次复制初始化;

 

3.1.4、无硬盘复制

复制初始化时,由于主库会进行快照生成RDB文件,因此会影响性能。redis2.8版本支持无硬盘复制(redis.conf中将repl-diskless-sync设置为yes),主库直接通过网络将数据发生给从库

 

3.2、异步复制导致主从库数据不一致怎么解决?

redis主从复制不保证强一致性,而是保证最终一致性。

另外,可以在主库的redis.conf中可以做如下配置。

# 允许最小连接的从库数
min-slaves-to-write 3 
# 允许从库最长失去连接的时间
min-slaves-max-lag 10 

以上两个配置只要有一个不满足,则主库不可写。

 

3.3、主从复制崩溃恢复

在主从复制中,主库一般是禁用持久化的,因为从库已经做数据备份了,禁用持久化可以提高性能。

从库崩溃,直接重启从库即可

主库崩溃,不能直接重启主库,因为主库禁用持久化,如果重启主库则直接丢失数据,然后同步给从库,从库也相应地丢失数据。

为了避免数据丢失,当主库崩溃时,需要做以下两步操作:

在从库执行slave no one命令将从库升级为主库

重启崩溃的主库,然后使用slave命令将其设置为新主库的从库

 

无论主库崩溃还是从库崩溃,都需要手动操作,手动操作不仅麻烦还容易出错,因此redis提供哨兵这一机制来支持自动操作。

 

4、哨兵

哨兵的功能:

  • 监控主从库是否正常运行;
  • 主库故障时自动将主库转为从库;

可以部署多个哨兵,哨兵之间也会互相监控。

哨兵甚至可以监控多个主从系统,通过在sentinel.conf中配置多行sentinel monitor配置来实现。

哨兵是一个可执行文件,在redis的src目录下面,名称为redis-sentinel。

在sentinel.conf中,做如下配置。

sentinel monitor mymaster 127.0.0.1 6379 1

mymaster是主库名。

127.0.0.1:6379是主库的ip和端口号,sentinel.conf中只需要配置要监控的主库即可,而从库sentinel会根据主库自动发现。

1表示至少需要1个哨兵节点同意。

然后执行以下命令启动sentinel进程

src/redis-sentinel  sentinel.conf

 

下面演示一下哨兵是如何自动化监控主从复制的。

为了简单起见,现有一主一从,主库为127.0.0.1::6379,从库为127.0.0.1::6380。

  1. 启动一个哨兵进程,观察启动日志;
  2. 然后我们刻意停掉主库,观察哨兵的日志变化;
  3. 最后,手动重启旧主库,观察哨兵的日志变化(哨兵只会帮助我们选举新的主库,然后将旧主库变为新主库的从库,并不会为我们重启旧主库);

以上三步哨兵的日志如下图所示。

redis复制、哨兵、集群详细介绍_Redis

 

5、集群

虽然引入了哨兵,但主从复制中每个节点都存储全量数据,因此最大能存储多少数据受限于内存最小的那个节点,形成木桶效应,因此需要对redis做数据分片。

客户端分片:

  • 旧版redis使用使用客户端分片,即由客户端决定每个键交由那个数据库节点存储;
  • 弊端:如果集群增删节点,则需要手动迁移数据,且为了保证迁移过程中的数据一致性需要将集群暂时下线;

redis3.0开始支持集群。

集群特点:

  • 拥有和单机实例同样的性能;
  • 在网络分区后提供一定的可访问性以及对主库故障恢复的支持;
  • redis集群并不支持处理多个key的命令(如mget),这是因为在不同的节点间移动数据会达不到像单机redis那样的性能,在高负载的情况下可能会导致不可预料的错误;
  • 只能使用0号数据库,如果用select切换则报错;
  • 支持数据分片和主从复制;

 

5.1、插槽

redis cluster没有使用一致性hash,而是有一个哈希槽的概念,默认有16384个插槽。

redis将每个键键名的有效部分使用CRC16算法计算出散列值,然后对16384取余。

有效部分:

  • 如果键名中包含{至少一个字符},则有效部分为大括号里面的内容,比如{user001}:username和{user001}:password这两个键的有效部分都是user001,那么它们会被分配到一个结点上,可以使用涉及多键的命令(比如mget)去处理它们;
  • 如果键名中不包含{至少一个字符},则整个键名都是有效部分;
  • 如果有多个大括号则算法会在匹配第一个大括号就停止,然后进行判断。比如键a{b}c{d}的有效部分为b,而a{}c{d}的有效部分为整个键名;

集群中的每个主节点负责分配一部分插槽,cluster slots命令用于查看插槽分配情况,而从节点只负责备份主节点的插槽。

增删节点怎么办?

  • 比如现在有A、B、C三个主节点,假如想增加D节点,那么只要将A、B、C节点中的部分槽迁移到D即可,如果想删除B节点,则只需要将B节点的槽迁移到A、C节点即可。

增删节点或者是修改某个节点槽的数量都不会导致集群不可用。

 

5.2、集群的主从复制模型

redis集群是支持主从复制的,目的是为了提高可用性。

比如当前集群有三个主节点A、B、C和三个从节点A1、B1、C1,则有以下情况:

  1. 如果从节点B1挂掉了,则不影响,整个集群可用;
  2. 如果主节点B挂掉了,集群会选举B1为新的主节点,整个集群仍然是可用的,待B重启后会变为B1的从节点;
  3. 但如果B和B1都挂掉了,那集群就不可用了,比如执行set命令时,会报错:(error) CLUSTERDOWN The cluster is down;

主从节点只要不是都挂,则不需要开启持久化,如果都挂,那必须要开启持久化了。

如果你的redis cluster只用于缓存,那么为了最大程度提升性能,不需要开启持久化。

如果你的redis cluster用来做数据库,那么就要看情况了,如果主从同时挂掉的几率不大且你对数据丢失的容忍度还行的话,就不需要开启持久化,否则需要。

 

5.3、集群一致性

redis集群不保证强一致性,导致不一致的可能原因有两个:

  • 主从节点间是异步复制的;
  • 某个主节点发生了网络分区,比如集群有A、B、C三个主节点以及对应的从节点A1、B1、C1,此时主节点B与集群形成网络分区,在网络分区期间某个客户端往B节点写数据,如果网络分区很短暂,那数据就不会丢失;但如果在网络分区期间集群重新选举了B1节点为主节点,那么数据就会丢失了;

形成网络分区的节点的超时时间是可以通过cluster-node-timeout这个配置项配置的,如果没有超时,则客户端可以正常向分区节点写数据,如果超时则不能写入。

 

5.4、搭建集群

集群至少需要3个节点才能正常运行,下面演示一下在单个centos系统上配置一个3主3从的redis集群。

集群重要配置如下。

cluster-enable yes
cluster-node-timeout 5000
cluster-config-file nodes.conf

集群文件夹结构如下图所示。

redis复制、哨兵、集群详细介绍_数据库_02

6381节点配置文件内容如下,其它节点类似(关于模板配置文件见本文第6节-redis模板配置文件)。

include ../run/redis-cluster.conf
port 6381
pidfile /usr/local/redis-cluster/run/redis-6381.pid
logfile /usr/local/redis-cluster/run/redis-6381.log
dir /usr/local/redis-cluster/6381/
cluster-config-file /usr/local/redis-cluster/run/nodes-6381.conf

启动各个节点,用redis-cli连接任意节点执行info cluster命令可以查看集群状态,cluster_enable为1表示可用。

但此时还不能写数据,比如在任意节点执行set age 10命令时,会报错:(error) CLUSTERDOWN Hash slot not served。

这是因为此时每个节点还是独立状态,下面需要将各节点联系在一起,即初始化集群。

如果是旧版本的redis,可以使用src/redis-trib(ruby语言编写,安装Ruby见本文第7节-安装Ruby)来初始化集群,初始化集群的命令如下。

src/redis-trib.rb --replicas 1 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 127.0.0.1:6386

replicas 1表示每个主节点的副本个数为1。

如果执行上述命令提示WARNING: redis-trib.rb is not longer available!,则表示当前版本的redis已经不支持redis-trib.rb,具体的提示信息如下图所示。

redis复制、哨兵、集群详细介绍_Redis_03

根据该提示,我们知道了应该使用redis-cli来代替redis-trib.rb。

首先在redis-cli中执行cluster help命令,查看与集群cluster相关的命令有哪些,如下所示。

ADDSLOTS <slot> [slot ...] -- 为当前节点分配槽位
BUMPEPOCH -- Advance the cluster config epoch.
COUNT-failure-reports <node-id> -- 根据<node-id>返回失败报告的数量
COUNTKEYSINSLOT <slot> - 根据槽位返回key数量
DELSLOTS <slot> [slot ...] -- 在当前节点删除指定槽位
FAILOVER [force|takeover] -- 将当前复制节点提升为主节点
FORGET <node-id> -- 从集群中移除节点
GETKEYSINSLOT <slot> <count> -- 返回当前节点存储在插槽中的键名
FLUSHSLOTS -- 删除当前节点的槽位信息
INFO - 返回关于集群的信息.
KEYSLOT <key> -- 根据key返回hash槽
MEET <ip> <port> [bus-port] -- 将节点连接到工作集群
MYID -- 返回当前节点id
NODES -- 返回节点看到的集群配置。输出格式:<id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>
REPLICATE <node-id> -- 配置当前节点为<node-id>的复制节点
RESET [hard|soft] -- 重置当前节点 (默认: soft).
SET-config-epoch <epoch> - Set config epoch of current node.
SETSLOT <slot> (importing|migrating|stable|node <node-id>) -- 设置槽位状态.
REPLICAS <node-id> -- 返回指定主节点的副本节点
SLOTS -- 返回关于槽范围映射的信息。每个系列都是由:start, end, master and replicas IP addresses, ports and ids

 

初始化集群步骤:

1、使用cluster meet命令将各个节点联系在一起,如下图所示。

redis复制、哨兵、集群详细介绍_数据库_04

再执行cluster nodes命令,可以看到六个节点联系在一起了,如下图所示。但此时这六个节点都是主库,并且还没有分配插槽。

redis复制、哨兵、集群详细介绍_数据库_05

2、使用cluster addslots命令为所有主库分配插槽。

由于cluster addslots命令不支持批量添加插槽,所以我写了一个shell脚本利用for循环批量分配插槽,shell脚本如下。

for ((i=$3; i<=$4; i ++))
do
  6381/src/redis-cli -h $1 -p $2 cluster addslots $i > /dev/null
done

现在让6381、6382、6383为主库,我们将16384个插槽分配给这三个主库,具体分配情况如下:

6381占0~5460号插槽,共5461个

6382占5461~10920号插槽,共5462个

6383占10921~16383号插槽,共5461个

执行如下命令即可分配插槽:

./cluster-addslots.sh 127.0.0.1 6381 0 5460
./cluster-addslots.sh 127.0.0.1 6382 5461 10920
./cluster-addslots.sh 127.0.0.1 6383 10921 16383

最新使用cluster slots命令查找插槽分配情况。

redis复制、哨兵、集群详细介绍_数据库_06

 

3、使用cluster replicate命令设置从库

执行如下命令,使得:

  • 6384为6381的从库;
  • 6383为6382的从库;
  • 6384为6383的从库;
6381/src/redis-cli -h 127.0.0.1 -p 6384 cluster replicate 6381的节点id
6381/src/redis-cli -h 127.0.0.1 -p 6385 cluster replicate 6382的节点id
6381/src/redis-cli -h 127.0.0.1 -p 6386 cluster replicate 6383的节点id

最后使用cluster nodes命令查看集群节点信息,即可看到有三个主库和三个从库,如下图所示。

redis复制、哨兵、集群详细介绍_数据库_07

 

集群搭建好后,我们使用redis-cli往集群写一些数据试试,但如果不幸运的话写第一个数据就会报错,如下图所示。

redis复制、哨兵、集群详细介绍_Redis_08

这是因为redis-cli连接的是6381这个节点,但user:1这个key会被分配到6382节点,因此写入错误。

解决办法是,redis-cli支持以集群模式连接到某一个节点,并且支持自动重定向,如下图所示。

redis复制、哨兵、集群详细介绍_数据库_09

 

6、redis模板配置文件

redis-5.0.5版本默认配置文件(去除注释)如下。

################################## INCLUDES ###################################
################################## MODULES #####################################
################################## NETWORK #####################################
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
################################# GENERAL #####################################
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo yes
################################ SNAPSHOTTING  ################################
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
################################# REPLICATION #################################
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
################################## SECURITY ###################################
################################### CLIENTS ####################################
############################## MEMORY MANAGEMENT ################################
############################# LAZY FREEING ####################################
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
############################## APPEND ONLY MODE ###############################
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
################################ LUA SCRIPTING  ###############################
lua-time-limit 5000
################################ REDIS CLUSTER  ###############################
########################## CLUSTER DOCKER/NAT support  ########################
################################## SLOW LOG ###################################
slowlog-log-slower-than 10000
slowlog-max-len 128
################################ LATENCY MONITOR ##############################
latency-monitor-threshold 0
############################# EVENT NOTIFICATION ##############################
notify-keyspace-events ""
############################### ADVANCED CONFIG ###############################
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
########################### ACTIVE DEFRAGMENTATION #######################

将默认的配置文件做如下修改(如何修改具体看需求),使其变为所有节点通用的模板配置文件:

  • 注释bind
  • protected-mode改为no
  • 注释port
  • daemonize改为yes
  • 注释pidfile
  • 注释logfile
  • 注释三个save
  • 注释dir

如果是集群模式,则还需要:

  • 新增cluster-enabled yes;

然后再针对单个节点用include指令导入公共模板(注意使用相对路径,否则启动会报错),然后再做一些本节点的特殊配置,如下所示:

include ../run/redis_useful.conf
port 6379
pidfile /var/run/redis_6379.pid
logfile /var/log/redis_6379.log
dir /usr/local/redis-5.0.5/

 

7、安装Ruby
  1. wget https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.1.tar.gz
  2. tar -xvzf ruby-3.0.1.tar.gz 
  3. ./configure
  4. make
  5. make install