Redis进阶学习07--分布式缓存--下
- Redis分片集群
- 搭建分片集群
- 集群结构
- 准备实例和配置
- 启动
- 创建集群
- docker-compose快速搭建分片集群
- 注意
- 散列插槽
- 插槽原理
- 小结
- 集群伸缩
- 需求分析
- 创建新的redis实例
- 添加新节点到redis
- 转移插槽
- 需求分析
- 故障转移
- 自动故障转移
- 手动故障转移
- RedisTemplate访问分片集群
- 搭建分片集群
Redis分片集群
搭建分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,如图:
分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
集群结构
分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:
这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:
IP | PORT | 角色 |
---|---|---|
192.168.150.101 | 7001 | master |
192.168.150.101 | 7002 | master |
192.168.150.101 | 7003 | master |
192.168.150.101 | 8001 | slave |
192.168.150.101 | 8002 | slave |
192.168.150.101 | 8003 | slave |
准备实例和配置
删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:
代码语言:javascript复制# 进入/tmp目录
cd /tmp
# 删除旧的,避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003
在/tmp下准备一个新的redis.conf文件,内容如下:
代码语言:javascript复制port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /tmp/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /tmp/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.150.101
# 保护模式--不开启意味着不需要密码验证
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /tmp/6379/run.log
Protected-mode 是为了禁止公网访问redis cache,加强redis安全的。
它启用的条件,有两个: 1) 没有bind IP 2) 没有设置访问密码
如果启用了,则只能够通过lookback ip(127.0.0.1)访问Redis cache,如果从外网访问,则会返回相应的错误信息:
代码语言:javascript复制(error) DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the lookback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the --portected-mode no option. 4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.
将这个文件拷贝到每个目录下:
代码语言:javascript复制# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf
修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:
代码语言:javascript复制# 进入/tmp目录
cd /tmp
# 修改配置文件
printf '%sn' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf
启动
因为已经配置了后台启动模式,所以可以直接启动服务:
代码语言:javascript复制# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%sn' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf
通过ps查看状态:
代码语言:javascript复制ps -ef | grep redis
发现服务都已经正常启动:
如果要关闭所有进程,可以执行命令:
代码语言:javascript复制ps -ef | grep redis | awk '{print $2}' | xargs kill
或者(推荐这种方式):
代码语言:javascript复制printf '%sn' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown
创建集群
虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。
我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。
1)Redis5.0之前
Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。
代码语言:javascript复制# 安装依赖
yum -y install zlib ruby rubygems
gem install redis
然后通过命令来管理集群:
代码语言:javascript复制# 进入redis的src目录
cd /tmp/redis-6.2.4/src
# 创建集群
./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
2)Redis5.0以后
我们使用的是Redis6.2.4版本,集群管理以及集成到了redis-cli中,格式如下:
代码语言:javascript复制redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
命令说明:
redis-cli --cluster
或者./redis-trib.rb
:代表集群操作命令create
:代表是创建集群--replicas 1
或者--cluster-replicas 1
:指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas 1)
得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
运行后的样子:
这里输入yes,则集群开始创建:
通过命令可以查看集群状态:
代码语言:javascript复制redis-cli -p 7001 cluster nodes
docker-compose快速搭建分片集群
这里为了不和之前的哨兵集群端口冲突,我们改为9001-9006
- 准备一个配置文件
port 9001
#redis 访问密码
requirepass 123456
#redis 访问Master节点密码
masterauth 123456
#关闭保护模式
protected-mode no
#开启集群
cluster-enabled yes
#集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file nodes.conf
#节点心跳失败的超时时间
cluster-node-timeout 5000
# 集群节点IP host模式为宿主机IP
cluster-announce-ip 192.168.25.129
# 集群节点端口 9001
cluster-announce-port 9001
#bus-port是redis集群总线端口为redis客户端端口加上10000
cluster-announce-bus-port 19001
# 持久化文件存放目录---工作目录
#dir /tmp
# 绑定地址
bind 0.0.0.0
# 让redis后台运行--docker这里必须设置为no
daemonize no
# 开启 appendonly 备份模式
appendonly yes
# 数据库数量
databases 1
# 日志配置
# debug:会打印生成大量信息,适用于开发/测试阶段
# verbose:包含很多不太有用的信息,但是不像debug级别那么混乱
# notice:适度冗长,适用于生产环境
# warning:仅记录非常重要、关键的警告消息
loglevel notice
# 日志文件路径
logfile "/data/redis.log"
检查配置文件是否设置了daemonize yes,如果是,就要改为daemonize no
因为该选项让redis成为在后台运行的守护进程而docker容器必须要有一个前台进程才能留存否则容器会自动退出
- docker-compose文件准备
version: "3.3"
services:
'9001':
image: "${REDIS_VERSION}"
container_name: '9001'
ports:
- "9001:9001"
- "19001:19001"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9001/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9001/data:/data"
'9002':
image: "${REDIS_VERSION}"
container_name: '9002'
ports:
- "9002:9002"
- "19002:19002"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9002/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9002/data:/data"
'9003':
image: "${REDIS_VERSION}"
container_name: '9003'
ports:
- "9003:9003"
- "19003:19003"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9003/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9003/data:/data"
'9004':
image: "${REDIS_VERSION}"
container_name: '9004'
ports:
- "9004:9004"
- "19004:19004"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9004/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9004/data:/data"
'9005':
image: "${REDIS_VERSION}"
container_name: '9005'
ports:
- "9005:9005"
- "19005:19005"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9005/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9005/data:/data"
'9006':
image: "${REDIS_VERSION}"
container_name: '9006'
ports:
- "9006:9006"
- "19006:19006"
command: redis-server /etc/redis/redis.conf
volumes:
- "/dhy/redis/cluster/9006/redis.conf:/etc/redis/redis.conf"
- "/dhy/redis/cluster/9006/data:/data"
还需要在当前目录准备一个.env文件,里面写入REDIS_VERSION=redis:5.0.5
或者将REDIS_VERSION加入环境变量
注意
在部署集群时,一直卡在 Waiting for the cluster to join …
由于没有打开集群总线的端口,集群总线是在集群端口 10000
首先需要理清一个概念,就是redis集群总线:
redis集群总线端口为redis客户端端口加上10000,比如说你的redis 6379端口为客户端通讯端口,那么16379端口为集群总线端口。若集群总线端口7001,7002,7003…,则应开启的端口是17001,17002,17003…
若搭建的 redis 集群端口为:7001~7006,其中 7001、7002、7003为主节点,7004、7005、7006为从节点,故应该打开的总线端口为:17001、17002、17003
使用云服务器需要开放上面这些端口号
- 进入其中任意一个redis实例,执行集群创建命令
docker-compose exec 9001 bash
代码语言:javascript复制redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
-a pwd
-a指定密码
通过命令可以查看集群状态:
代码语言:javascript复制redis-cli -p 9001 -a pwd cluster nodes
散列插槽
插槽原理
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
- key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{dhy}num,则根据dhy计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
如图,在7001这个节点执行set a 1时,对a做hash运算,对16384取余,得到的结果是15495,因此要存储到103节点。
到了7003后,执行get num
时,对num做hash运算,对16384取余,得到的结果是2765,因此需要切换到7001节点
小结
连接某个集群实例节点时: redis-cli后面必须加上-c参数,表示是集群模式连接,-p指定连接端口,如果cluster集群中每个节点都设置了密码,那么切换的时候,会因为需要密码验证,而导致某个key设置失败,怎么办呢?
redis-cli连接的时候通过-a指定密码即可,这样切换到其他节点时,也会使用这个密码,这也说明,集群中每个节点的密码要相同才可以
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384取余
- 余数作为插槽,寻找插槽所在实例即可
如何将同一类数据固定的保存在同一个Redis实例?
- 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
集群伸缩
redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:
比如,添加节点的命令:
需求分析
需求:向集群中添加一个新的master节点,并向其中存储 num = 10
- 启动一个新的redis实例,端口为9007
- 添加9007到之前的集群,并作为一个master节点
- 给9007节点分配插槽,使得num这个key可以存储到9007实例
这里需要两个新的功能:
- 添加一个节点到集群中
- 将部分插槽分配到新插槽
创建新的redis实例
记得开放9007和19007端口
代码语言:javascript复制docker run -p 9007:9007 -p 19007:19007 -v $PWD/data:/data -v $PWD/redis.conf:/etc/redis/redis.conf --privileged=true --name 9007 -d redis:5.0.5 redis-server /etc/redis/redis.conf
添加新节点到redis
添加节点的语法如下:
执行命令:
代码语言:javascript复制redis-cli --cluster add-node 192.168.150.101:9007 192.168.150.101:9001
-a 密码
可以通过–cluster-slave直接让该节点成为某个–cluster-master-id 节点id对应的从节点
通过命令查看集群状态:
代码语言:javascript复制redis-cli -p 9007 cluster nodes
如图,9007加入了集群,并且默认是一个master节点:
但是,可以看到7007节点的插槽数量为0,因此没有任何数据可以存储到7007上
转移插槽
我们要将num存储到9007节点,因此需要先看看num的插槽是多少:
如上图所示,num的插槽为2765.
我们可以将0~3000的插槽从7001转移到7004,命令格式如下:
具体命令如下:
建立连接:
得到下面的反馈:
询问要移动多少个插槽,我们计划是3000个:
新的问题来了:
哪个node来接收这些插槽??
显然是9007,那么9007节点的id是多少呢?
复制这个id,然后拷贝到刚才的控制台后:
这里询问,你的插槽是从哪里移动过来的?
- all:代表全部,也就是三个节点各转移一部分
- 具体的id:目标节点的id
- done:没有了
这里我们要从7001获取,因此填写7001的id:
填完后,点击done,这样插槽转移就准备好了:
确认要转移吗?输入yes:
然后,通过命令查看结果:
代码语言:javascript复制redis-cli -p 9007 -a 126433zdh cluster nodes
目的达成。
故障转移
集群初识状态是这样的:
其中9001、9002、9003都是master,我们计划让9002宕机。
自动故障转移
当集群中有一个master宕机会发生什么呢?
直接停止一个redis实例,例如9002:
代码语言:javascript复制docker stop 9002
1)首先是该实例与其它实例失去连接
2)然后是疑似宕机:
3)最后是确定下线,自动提升一个slave为新的master:
4)当9002再次启动,就会变为一个slave节点了:
手动故障转移
利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:
这种failover命令可以指定三种模式:
- 缺省:默认的流程,如图1~6歩
- force:省略了对offset的一致性校验
- takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
案例需求:在9002这个slave节点执行手动故障转移,重新夺回master地位
步骤如下:
1)利用redis-cli连接9002这个节点
2)执行cluster failover命令
如图:
效果:
RedisTemplate访问分片集群
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
1)引入redis的starter依赖
2)配置分片集群地址
3)配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
代码语言:javascript复制spring:
redis:
cluster:
nodes:
- 192.168.150.101:9001
- 192.168.150.101:9002
- 192.168.150.101:9003
- 192.168.150.101:9004
- 192.168.150.101:9005
- 192.168.150.101:9006
password: 密码
配置读写分离:
代码语言:javascript复制 @Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
正常使用即可:
代码语言:javascript复制@RestController
public class HelloController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/get/{key}")
public String hi(@PathVariable String key) {
String val = redisTemplate.opsForValue().get(key);
return val;
}
@GetMapping("/set/{key}/{value}")
public String hi(@PathVariable String key, @PathVariable String value) {
redisTemplate.opsForValue().set(key, value);
return "success";
}
}
通过测试发现,读是优先走从节点,写走对应的主节点