Redis(分布式篇)

分布式理论

Redis Cluster

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),就是将 数据集 按照一定规则,分散存储在 各个节点上。这里涉及两个问题:

  1. 分片规则是什么?
  2. 如何存储在各个节点上?

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迁移。

Move 和 Asking

  • Move (永久重定向)

当客户端向某个节点发出指令,该节点发现指令的key对应的slot不在当前节点上,
这时Redis会向客户端发送一个MOVED指令,告诉它正确的节点,
然后客户端去连这个正确的节点并进行再次操作。如下,15495为key a所在的slot id。

172.16.190.78:7001> get a
(error) MOVED 15495 172.16.190.77:7000
  • Asking(临时重定向)

在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

shell脚本1:创建 各个 redis 的 分布式 配置文件

通过 脚本文件 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

shell脚本2:启动6个容器

通过脚本文件 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.
  • 启动redis集群客户端(-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

运行测试

  • 加入一个数据
    会返回所添加的对应的节点,如这里返回的是12的节点
set name Stephen

-> Redirected to slot [5798] located at 172.28.0.12:6379
OK
  • 停止redis-2容器
    停止刚添加缓存的对应的redis节点
docker stop redis-2
  • 获取数据
    当主节点断开之后,会自动将从节点切换为主节点,比如这里将16切换为了主节点,
    如果能取到值,表示集群搭建成功,如下,从16这个节点中取到了值
get name

-> Redirected to slot [5798] located at 172.28.0.16:6379
"stephen"

redis cluster 常用命令

  • 集群(cluster)
cluster info       打印集群的信息
cluster nodes      列出集群当前已知的所有节点(node),以及这些节点的相关信息  
  • 节点(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 
  • 槽(slot)
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) 
  • 键(key)
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

Go 操作 redis cluster

这里采用 “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
}

你可能感兴趣的:(redis,分布式,docker)