Redis官方在3.0版本推出了自己的分布式方案,这就是Redis Cluster。
Redis Cluster 提供了一种去中心化
的分布式方案,该方案可以实现水平拆分,故障转移等需求。
其拓扑结构
通过一个简单的额例子说明:一个Redis Cluster由多个Redis节点组成,这里的节点指的是Redis实例,一台服务器上可能有多个实例。
假设 有 1、2、3、4、5、6、号redis实例,其中 1和2 是一个节点组
, 且 1号是 master 2号是 slave;
同理 3 和 4 是一个节点组
, 且 3号是 master 4号是 slave; 一次类推 一共有 3个 节点组。
也就是全量数据被划分为3份,分别标号1/2/3。
是Redis分布式的基础。Redis中包含两种节点:master节点与slave节点。
同一份数据存放在master与多个slave节点上,master对外提供读写,slave不对外提供写操作。
当master宕机时,slave节点还能继续提供服务。主从复制中最重要的就是 master 和 slave 之间数据如何同步。
主从同步分为 全量同步 和 增量同步
主节点上进将当前内存的数据全部快照写到磁盘文件中,然后将文件同步给从节点。
从节点清空当前内存全部数据后全量加载该文件,这样达到与主节点数据同步的目的。
但是在从节点加载快照文件的过程中,主节点还在 对外提供写服务。
所以当从节点加载完快照后,依旧可能与主节点数据不一致,这时就需要增量同步上场了。
增量同步的不是数据,而是
指令流
, 这个 指令流 类似于 mysql 主从复制 的 binlog。
主节点会将对当前数据状态产生修改的指令记录在内存的一个buffer
中,
然后异步地将buffer同步到从节点,从节点通过执行buffer中的指令,达到与主节点数据一致的目的。
Redis的buffer是一个
定长的环形结构
,当指令流满的时候, 一定会覆盖最前面的内容。
所以当从节点上次增量同步由于各种原因,导致花费时间较长时,
再次同步指令流时,就有可能前面没有同步的指令被覆盖掉了。
这种情况就需要进行全量同步了。
当master宕机时,需要 人工将slave节点切换成master,这 显然是无法接受的。
所以需要有一个机制,当master宕机时能自动进行主从切换,应用程序无感知,继续提供服务。
于是Redis官方提供了一种方案:Redis Sentinel
(哨兵模式)。
简单的说,哨兵模式就是在主从基础上增加了哨兵节点,哨兵节点不存储业务数据,它负责监控主从节点的健康,当主节点宕机时,它能及时发现,并自动选择一个从节点,将其切换成主节点。为了避免哨兵本身成为单点,哨兵一般也由多个节点组成
所谓分片(Sharding),就是将 数据集 按照一定规则,分散存储在 各个节点上。这里涉及两个问题:
- 分片规则是什么?
- 如何存储在各个节点上?
Redis将所有数据分为
16384个hash slot(槽)
,每条数据(key-value)根据key值通过算法映射到其中一个slot上,
这条数据就存储在该slot中。映射算法是:slotId=crc16(key)%16384
Redis的每个key都会基于该分片规则,落到特定的slot上。而在集群部署完成时,slot的分布就已经确定了。对于一个稳定的集群,slot的分布也是固定的。但在一些情况下,slot的分布需要发生改变:
- 新的master加入
- 节点分组退出集群
- slot分布不均匀
这些情况下就需要进行slot的迁移。slot迁移的触发与过程控制都是由外部系统完成,Redis只提供能力,但不自动进行slot迁移。
当客户端向某个节点发出指令,该节点发现指令的key对应的slot不在当前节点上,
这时Redis会向客户端发送一个MOVED指令,告诉它正确的节点,
然后客户端去连这个正确的节点并进行再次操作。如下,15495为key a所在的slot id。172.16.190.78:7001> get a (error) MOVED 15495 172.16.190.77:7000
在slot迁移过程中 的一种错误返回。当某个slot在迁移过程中,
客户端发了一个位于该slot的某个key的操作请求,请求被路由到旧的节点。
此时该key如果在旧节点上存在,则正常操作;如果在旧的节点上找不到,
那么可能该key已被迁移到新的节点上,也可能就没有该key,
此时会返回ASKING,让客户端跳转到新的节点上去执行。
MOVED 是永久重定向,下次对同样的key 进行操作,客户端就将请求发送到正确的节点,
ASKING 是临时重定向,它只对这次操作起作用,不会更新客户端的槽位关系表。
Redis Cluster没有专门用于维护节点状态的节点,而是所有节点通过 Gossip协议相互通信,
广播自己的状态以及自己对整个集群认知的改变。
如果一个节点宕掉了,其他节点和它进行通信时,会发现改节点失联。当某个节点发现其他节点失联时,
会将这个失联节点状态变成PFail(possible fail)
,并广播给其他节点。
当一个节点收到某个节点PFail的数量达到了主节点的大多数
,就标记该节点为Fail,并进行广播,通过这种方式确认节点故障。
当slave发现其master状态为Fail后,它会发起选举,如果其他master节点都同意,则该slave进行从主切换,变成master节点。
同时会将自己的状态广播给其他节点,达到大家信息一致性。
这里采用docker的形式 部署 redis cluster。
使用的redis 是 3.x 之后的版本, 之前的版本 不支持集群。
因为 cluster至少要 3 个节点,这了建立 6个节点,3主3从。
# 获取镜像
docker pull redis:5.0.9-alpine3.11
# 自定义一个网络
docker network create redis --subnet 172.28.0.0/16
通过 脚本文件 generate_redis_config.sh 创建6个 redis 配置
for port in $(seq 1 6); \
do \
mkdir -p ./redis/node-${port}/conf
touch ./redis/node-${port}/conf/redis.conf
cat << EOF > ./redis/node-${port}/conf/redis.conf
port 6379
bind 0.0.0.0
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.28.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
EOF
done
脚本文件输出:
redis/
├── node-1
│ └── conf
│ └── redis.conf
├── node-2
│ └── conf
│ └── redis.conf
├── node-3
│ └── conf
│ └── redis.conf
├── node-4
│ └── conf
│ └── redis.conf
├── node-5
│ └── conf
│ └── redis.conf
└── node-6
└── conf
└── redis.conf
12 directories, 6 files
通过脚本文件 generate_redis_container.sh 生成6个redis容器
# 容器1
docker run -p 6371:6379 -p 16371:16379 --name redis-1 \
-v ${PWD}/redis/node-1/data:/data \
--mount type=bind,src=${PWD}/redis/node-1/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.11 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器2
docker run -p 6372:6379 -p 16372:16379 --name redis-2 \
-v ${PWD}/redis/node-2/data:/data \
--mount type=bind,src=${PWD}/redis/node-2/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.12 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器3
docker run -p 6373:6379 -p 16373:16379 --name redis-3 \
-v ${PWD}/redis/node-3/data:/data \
--mount type=bind,src=${PWD}/redis/node-3/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.13 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器4
docker run -p 6374:6379 -p 16374:16379 --name redis-4 \
-v ${PWD}/redis/node-4/data:/data \
--mount type=bind,src=${PWD}/redis/node-4/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.14 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器5
docker run -p 6375:6379 -p 16375:16379 --name redis-5 \
-v ${PWD}/redis/node-5/data:/data \
--mount type=bind,src=${PWD}/redis/node-5/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.15 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器6
docker run -p 6376:6379 -p 16376:16379 --name redis-6 \
-v ${PWD}/redis/node-6/data:/data \
--mount type=bind,src=${PWD}/redis/node-6/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.16 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker exec -it redis-1 /bin/sh
redis-cli --cluster create 172.28.0.11:6379 172.28.0.12:6379 172.28.0.13:6379 172.28.0.14:6379 172.28.0.15:6379 172.28.0.16:6379 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.28.0.15:6379 to 172.28.0.11:6379
Adding replica 172.28.0.16:6379 to 172.28.0.12:6379
Adding replica 172.28.0.14:6379 to 172.28.0.13:6379
M: cfb09d104f563d5dea870437cf73fac266b69a30 172.28.0.11:6379
slots:[0-5460] (5461 slots) master
M: 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c 172.28.0.12:6379
slots:[5461-10922] (5462 slots) master
M: be4034133574260765165beac5fd1edacb63a2bd 172.28.0.13:6379
slots:[10923-16383] (5461 slots) master
S: 74685c90c5aebdc8a035d288c962e22f98e994e8 172.28.0.14:6379
replicates be4034133574260765165beac5fd1edacb63a2bd
S: 96b4185cc40b532b144b2ac3e2fe7213e7c13a50 172.28.0.15:6379
replicates cfb09d104f563d5dea870437cf73fac266b69a30
S: edd2e12510d921be2193df5b461033fc0f465144 172.28.0.16:6379
replicates 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c
Can I set the above configuration? (type 'yes' to accept): #### 这里输入 yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 172.28.0.11:6379)
M: cfb09d104f563d5dea870437cf73fac266b69a30 172.28.0.11:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 96b4185cc40b532b144b2ac3e2fe7213e7c13a50 172.28.0.15:6379
slots: (0 slots) slave
replicates cfb09d104f563d5dea870437cf73fac266b69a30
S: edd2e12510d921be2193df5b461033fc0f465144 172.28.0.16:6379
slots: (0 slots) slave
replicates 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c
M: be4034133574260765165beac5fd1edacb63a2bd 172.28.0.13:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c 172.28.0.12:6379
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 74685c90c5aebdc8a035d288c962e22f98e994e8 172.28.0.14:6379
slots: (0 slots) slave
replicates be4034133574260765165beac5fd1edacb63a2bd
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
-c表示集群
)redis-cli -c
cluster info
集群创建好之后,11,12,13为主节点,其余为从节点
cluster nodes
# 输出
ec2d5b58fb0572e06ba8eeac62f87c595fa73913 172.28.0.14:6379@16379 slave ad075641de017884f05e9b17365a956994912f77 0 1656504631515 4 connected
3d7137bd841fd90e37880e3fa2dc0eeb6586ea18 172.28.0.12:6379@16379 slave 8dcfe2052c7a278d2de90a1ee9b48d287f11c2a7 0 1656504632018 7 connected
8dcfe2052c7a278d2de90a1ee9b48d287f11c2a7 172.28.0.16:6379@16379 master - 0 1656504630000 7 connected 5461-10922
ad075641de017884f05e9b17365a956994912f77 172.28.0.13:6379@16379 master - 0 1656504630000 3 connected 10923-16383
0e0dcd720fee1821826fb9808ad0d3446afc3fe4 172.28.0.15:6379@16379 slave 172c41d7253e68cb3564f115b21450575f43e5d3 0 1656504630513 5 connected
172c41d7253e68cb3564f115b21450575f43e5d3 172.28.0.11:6379@16379 myself,master - 0 1656504631000 1 connected 0-5460
set name Stephen
-> Redirected to slot [5798] located at 172.28.0.12:6379
OK
docker stop redis-2
get name
-> Redirected to slot [5798] located at 172.28.0.16:6379
"stephen"
cluster info 打印集群的信息
cluster nodes 列出集群当前已知的所有节点(node),以及这些节点的相关信息
cluster meet <ip> <port> 将ip和port所指定的节点添加到集群当中,让它成为集群的一份子
cluster forget <node_id> 从集群中移除node_id指定的节点
cluster replicate <node_id> 将当前节点设置为node_id指定的节点的从节点
cluster saveconfig 将节点的配置文件保存到硬盘里面
cluster slaves <node_id> 列出该slave节点的master节点
cluster set-config-epoch 强制设置configEpoch
cluster addslots <slot> [slot ...] 将一个或多个槽(slot)指派(assign)给当前节点
cluster delslots <slot> [slot ...] 移除一个或多个槽对当前节点的指派
cluster flushslots 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点
cluster setslot <slot> node <node_id> 将槽slot指派给node_id指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派
cluster setslot <slot> migrating <node_id> 将本节点的槽slot迁移到node_id指定的节点中
cluster setslot <slot> importing <node_id> 从node_id 指定的节点中导入槽slot到本节点
cluster setslot <slot> stable 取消对槽slot的导入(import)或者迁移(migrate)
cluster keyslot <key> 计算键key应该被放置在哪个槽上
cluster countkeysinslot <slot> 返回槽slot目前包含的键值对数量
cluster getkeysinslot <slot> <count> 返回count个slot槽中的键
cluster myid 返回节点的ID
cluster slots 返回节点负责的slot
cluster reset 重置集群,慎用
################################ redis 集群 ###############################
port 6379
bind 0.0.0.0
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.28.0.11
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
# 集群开关,如果配置yes则开启集群功能,此redis实例作为集群的一个节点,否则,它是一个普通的单一的redis实例。
cluster-enabled yes
# 保存节点配置,自动创建、自动更新
cluster-config-file nodes.conf
# 这是集群中的 节点 能够 失联的最大时间,超过这个时间,该节点就被 断定为 宕机。
# 主节点 超过这个时间还是不可达,则用它的 从节点 将启动故障迁移,升级成主节点。
cluster-node-timeout 15000
# 存储方式, aof,将写操作记录到日志中
appendonly yes
# 如果设置成0,则无论 从节点 与 主节点 失联多久,从节点 都会尝试升级成主节点。
# 如果设置成 正数,则 cluster-node-timeout 乘以 cluster-slave-validity-factor 得到的时间,
# 是 从节点 与 主节点 失联后,此 从节点 数据有效的最长时间,超过这个时间,从节点 不会启动 故障迁移。
# 假设:cluster-node-timeout=5,cluster-slave-validity-factor=10,
# 则如果 从节点 跟 主节点 失联超过50秒,此 从节点 不能成为主节点,因为 从节点 上的数据 不全。
# 注意,如果此参数配置为非0,将可能出现由于某主节点失联却没有从节点能顶上的情况,从而导致集群不能正常工作,
# 在这种情况下,只有等到原来的 主节点 重新回归到集群,集群才恢复运作。
cluster-replica-validity-factor 10
# 主节点 需要的最小 从节点 数,只有达到这个数,主节点 失败时,它从节点才会进行迁移。
cluster-migration-barrier 1
# 在部分 key 所在的节点不可用时,如果此参数设置为"yes"(默认值), 则整个集群停止接受操作;
# 如果此参数设置为”no”,则 集群 依然为可达节点上的key提供读操作。
cluster-require-full-coverage yes
这里采用 “github.com/go-redis/redis” 库
package main
import (
"fmt"
"time"
"github.com/go-redis/redis"
)
// redis集群连接 配置
var RedisClusterConfig = &redis.ClusterOptions{
//---------------------------------- 集群相关的参数 ------------------------------------------------------
//集群节点地址,理论上 只要填一个可用的节点 客户端就可以自动获取到集群的所有节点信息。
//但是最好多填一些节点以增加容灾能力,因为只填一个节点的话,如果这个节点出现了异常情况,则Go应用程序在启动过程中无法获取到集群信息。
// Addrs: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002", "127.0.0.1:7003", "127.0.0.1:7004", "127.0.0.1:7005"},
Addrs: []string{"172.28.0.16:6379"},
MaxRedirects: 8, // 当遇到 网络错误 或者 MOVED/ASK重定向命令时,最多重试几次,默认8
//只含读操作的命令的"节点选择策略"。默认都是false,即只能在 主节点 上执行。
// 置为true则允许在 从节点 上执行只含读操作的命令
ReadOnly: false,
// 默认false。 置为true则ReadOnly自动置为true,表示在处理只读命令时,可以在一个slot对应的主节点和所有从节点中选取Ping()的响应时长最短的一个节点来读数据
RouteByLatency: false,
// 默认false。置为true则ReadOnly自动置为true,表示在处理只读命令时,可以在一个slot对应的主节点和所有从节点中随机挑选一个节点来读数据
RouteRandomly: false,
//用户可定制读取节点信息的函数,比如在非集群模式下可以从zookeeper读取。
//但如果面向的是redis cluster集群,则客户端自动通过cluster slots命令从集群获取节点信息,不会用到这个函数。
// ClusterSlots: func() ([]ClusterSlot, error) {
// },
//钩子函数,当一个新节点创建时调用,传入的参数是新建的redis.Client
// OnNewNode: func(*Client) {
// },
//------------------------------------------------------------------------------------------------------
//ClusterClient管理着一组redis.Client,下面的参数和非集群模式下的redis.Options参数一致,但默认值有差别。
//初始化时,ClusterClient会把下列参数传递给每一个redis.Client
//钩子函数
//仅当客户端执行命令需要从连接池获取连接时,如果连接池需要新建连接则会调用此钩子函数
OnConnect: func(conn *redis.Conn) error {
fmt.Printf("从连接池获取连接:conn=%v\n", conn)
return nil
},
Password: "",
//每一个redis.Client的连接池容量及闲置连接数量,而不是cluterClient总体的连接池大小。实际上没有总的连接池
//而是由各个redis.Client自行去实现和维护各自的连接池。
PoolSize: 15, // 连接池最大socket连接数,默认为5倍CPU数, 5 * runtime.NumCPU
MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;。
//命令执行失败时的重试策略
MaxRetries: 0, // 命令执行失败时,最多重试多少次,默认为0即不重试
MinRetryBackoff: 8 * time.Millisecond, //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔
MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔
//超时
DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。
ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时
WriteTimeout: 3 * time.Second, //写超时,默认等于读超时,-1表示取消读超时
PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。
//闲置连接检查包括IdleTimeout,MaxConnAge
IdleCheckFrequency: 60 * time.Second, //闲置连接检查的周期,无默认值,由ClusterClient统一对所管理的redis.Client进行闲置连接检查。初始化时传递-1给redis.Client表示redis.Client自己不用做周期性检查,只在客户端获取连接时对闲置连接进行处理。
IdleTimeout: 5 * time.Minute, //闲置超时,默认5分钟,-1表示取消闲置超时检查
MaxConnAge: 0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接
}
var RedisClient *redis.ClusterClient
func init() {
RedisClient = redis.NewClusterClient(RedisClusterConfig)
}
func main() {
// 获取 数据
val := RedisClient.Get("age")
fmt.Println(val)
// 设置数据
RedisClient.Set("age", "13", 0)
val = RedisClient.Get("age")
fmt.Println(val)
}
扩展: “github.com/go-redis/redis” 库 连接 redis 单机
package main
import (
"fmt"
"github.com/go-redis/redis"
)
var RedisConfig = &redis.Options{
Addr: "localhost:6379", // redis地址
Password: "", // redis密码,没有则留空
DB: 0, // 默认数据库,默认是0
}
var RedisClient *redis.Client
func init() {
RedisClient = redis.NewClient(RedisConfig)
}
func main() {
res, err := RedisClient.Ping().Result()
if err != nil {
panic(err)
}
fmt.Println(res) // PONG
}