Redis哈希槽实战


Redis哈希槽实战

面试题:在高并发的互联网公司中,有1亿条数据需要缓存,请问如何设计存储这批数据? 单台服务器肯定存储不了这么大的数据,一般是分布式存储,就像数据库的分库分表一样存储,那针对缓存redis如何分布式存储这么大的数据?

业界的做法一般有3种:

1.哈希取余分区

针对redis来说1亿条数据,一般是对应1亿个key value,我们把他分别存储在N个节点,如上图N=3,然后用户每次读写操作,根据节点N使用公式hash (key) %N计算出哈希值, 用来决定数据映射到哪一个节点上。 这种方案的优缺点:

优点:简单粗暴,只要提前预估好数据量,然后规划好节点,例如3台、30台、300台节点,就能保证未来一段时间内的数据支撑

缺点:节点扩容或收缩节点的时候就麻烦了,因为每次节点有变动数据节点映射关系需要重新计算,会导致数据的重新迁移,例如原先是3台,后面要新增到8台,要把所有的历史数据按hash (key) %8,重新洗一遍,非常麻烦。

2.一致性哈希分区

3.一致性哈希算法中首先有一个哈希函数,哈希函数产生hash值,所有可能的哈希值构成一个哈希空间,哈希空间为[0,2^32-1],这本来是一个”线性”的空间,但是在算法中通过恰当逻辑控制,使其首尾相衔接,也即是0-2^32,这样就构造一个逻辑上的环形空间。

4.节点映射将集群中的各IP节点映射到环上的某个一位置。假设有四个节点Node A, B, C、D,经过ip地址的哈希函数计算(例如:ash(192.168.1.13)) ,它们的位置如下:

5.路由规则包括存储(setx)和取值(getx)规则。当需要存储一个对时,首先计算键key的hash值: hash(key),这个hash值必然对应于一致性hash环上的某个位置,然后沿着这个值按顺时针找到第一个节点,并将该键值对存储在该节点上. - 例如有4个存储对象Object A,B、 C,D,经过对Key的哈希计算后,它们的位置如图3) - 对于各个Object,它所真正的存储位置是按顺时针找到的第一个存储节点 - 例如Object A顺时针找到的第一个节点是Node A,所以Node A负责存储Object A, Object B存储在Node B.

  • 一致性哈希如何实现容错性和扩展性?(图4)
    • 容错性

    假设Node C节点挂掉了, Object C的存储丢失,如果要重新把数据补回来时, Object C就会顺时针找到的最新节点是Node D也就是说Node C挂掉了,受影响仅仅包括Node B到Node C区间的数据,并且这些数据会转移到Node D进行存储。

    • 扩展性

    假设现在数据量大了,需要增加一台节点Node x, Node x的位置在Node A到Node B之间,那么受到影响的仅仅是Node A到Node x间的数据,重新把A到x之间的数据洗到Node x上即可

优点:

与哈希取余分区相比,容错性和扩展性更灵活,例如node c瘫痪,只影响Node B到Node C区间的数据,影响面小;再例如增加一台节点Node x,只影响到Node A到Node B之间的数据,不会导致哈希取余全部数据重洗。

缺点:数据倾斜不一致性:

如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如(图5) ,大部分数据都在A上了, B的数据比较少

6.哈希槽分区 由于一致性哈希分区存在数据倾斜不一致性的问题,故引入了槽的概念

  • 哈希槽的概念 把哈希槽均匀分段,分配给redis节点(图2)
    1. redis节点1负责存储5461个哈希槽的数据,编号0号至5460号哈希槽.
    2. redis节点2负责存储5462个哈希槽的数据,编号5461号至10922号哈希槽。
    3. redis节点3负责存储5461个哈希槽的数据,编号10923号至16383号哈希槽

redis节点3,负责存储5461个哈希槽的数据,编号10923号至16383号哈希槽。

  • 计算每条数据的slot空间位置如(图3)

将数据key进行哈希取值,映射已经固定大小的hash slot空间上,例如可以采用spring redis的API

 <dependency>
 <groupld>org.springframework.boot</groupld>
 <artifactid>spring-boot-starter-data-redis </artifactld>
 </dependency>

例如、我们往redis设置3条数据

redisTemplate.opsForValue).set(“A”, “agan1””)
redisTemplate.opsForValue0.set(“B”,”agan2”)
redisTemplate.opsForValue0.set(“C”,”agan3”)

然后计算key ABC的的slot槽位置

io.lettuce.core.cluster.SlotHash.getSlot(A”)-6373
io.lettuce.core.cluster.SlotHash.getSlot(“B”)= 10374
io.lettuce.core.cluster.SlotHash.getSlot(C)=14503

故,

key AB落在slot空间的5461至10922区间上,并最终存储在Node2上
key C落在slot空间的10923至16383区间上,并最终存储在Node3上

  • Redis哈希槽分区的特点:
    1. 解耦数据和节点之间的关系,例如数据的读写只要计算出槽号就可以,节点的扩容和收缩只要重新均衡分配槽区间即可;故简化了节点扩容和收缩难度
    2. 节点自身维护槽的映射关系,不需要客户端(spring)或者代理服务维护槽分区和数据。
    3. 支持节点、槽、键之间的映射意询,用于数据路由、在线伸缩等场景。


哈希槽案例实战:Redis集群哈希槽安装部署

1. 利用docker创建容器,创建6个redis实例

docker create --name redis-node-1 --net host --privileged=true -v /data/redis/share/redis-node-1:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6381
docker create --name redis-node-2 --net host --privileged=true -v /data/redis/share/redis-node-2:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6382
docker create --name redis-node-3 --net host --privileged=true -v /data/redis/share/redis-node-3:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6383
docker create --name redis-node-4 --net host --privileged=true -v /data/redis/share/redis-node-4:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6384
docker create --name redis-node-5 --net host --privileged=true -v /data/redis/share/redis-node-5:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6385
docker create --name redis-node-6 --net host --privileged=true -v /data/redis/share/redis-node-6:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6386

docker create //创建容器的命令
–name redis-node-1 //容器的名字 例如redis-node-1
–net host //使用宿主机的IP和i口
–privileged=true //docker容器获取宿主机root权限
-v /data/redis/share/redis-node-4:/data //容器的data目录映射到宿机/data/redis/share/redis-node-1
redis:5.0.7 //redis镜像名称和版本号
–cluster-enabled yes //redis.conf的配置:开启redis集群
–appendonly yes //redis.conf的配置:开启数据持久化
–port 6381 //redis.conf的配置: redis端口号

2. 启动容器

docker start redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5 redis-node-6

3. 查看容器(6台容器)

docker ps

4. 进入节点redis-node-1容器中

docker exec -it redis-node-1 /bin/bash

参数create表示创建一个新的集群, –replisas 1表示为每个master创建一个slave。

redis-cli --cluster create 192.168.1.138:6381 192.168.1.138:6382 192.168.1.138:6383 192.168.1.138:6384 192.168.1.138:6385 192.168.1.138:6386 --cluster-replicas 1

执行结果:

5. 使用cluster info命令查看集群状态

  • 先进入容器
redis-cli -h 192.168.1.138 -p 6381 -c
  • 查看info
cluster info

6. 使用cluster nodes查看节点的状态

cluster nodes

哈希槽案例实战:Redis主从切换

1. 客户端验证

set user:100 agan
set user:200 alext

2. 查看集群信息

exit

redis-cli --cluster check 192.168.1.138:6381 

3. 主从切换

cluster nodes

从上可以看出6381是主,6384是从,我们先停掉6381查看效果

  • 查看镜像
docker ps
  • 停掉
docker stop redis-node-1
  • 进入容器,进入集群
docker exec -it redis-node-2 /bin/bash

redis-cli -h 192.168.1.138 -p 6382 -c
  • 查看目前的集群效果,查看节点信息
cluster nodes

哈希槽案例实战:集群扩容演练

由于之前把6381变成fail,6386变成master
我们先进行还原,6381重新变成master,6386变成slave

redis-cli -h 192.168.1.138 -p 6382 -c 
cluster nodes
  • 先把6381启动,然后停止6386
docker start redis-node-1

docker stop redis-node-6
  • 再启动6386,成为slave
docker start redis-node-6

目前架构情况

1. 查看集群信息

exit

redis-cli --cluster check 192.168.1.138:6381

2. 增加2个redis 节点7和8

利用docker,创建2个redis集群

docker create --name redis-node-7 --net host --privileged=true -v /data/redis/share/redis-node-7:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6387

docker create --name redis-node-8 --net host --privileged=true -v /data/redis/share/redis-node-8:/data redis:5.0.7 --cluster-enabled yes --appendonly yes --port 6388

3. 启动容器

docker start redis-node-7 redis-node-8

4. 先进入节点redis-node-7容器中

docker exec -it redis-node-7 /bin/bash

5. 添加master节点

192.168.1.138:6387代表新增的,192.168.1.138:6381代表原集群的任意一个节点

redis-cli --cluster add-node 192.168.1.138:6387 192.168.1.138:6381

再一次检查集群

redis-cli --cluster check 192.168.1.138:6381 

哈希槽案例实战:分配哈希槽

1. 进入集群

新的节点加入集群,就需要重新分配槽,用redis-cli-cluster reshard ip:port命令来重新分配槽;

ip: port指集群中任意一个节点就行,如下:

redis-cli --cluster reshard 192.168.1.138:6381 

2. 重新查看集群

redis-cli --cluster check 192.168.1.138:6381 

3. 为master4(node7)配置slave4节点

add-node:后面的分别跟着新加入的slave和slave对应的master
cluster-slave:表示加入的是slave节点
–cluster-master-id:表示slave对应的master的node ID

redis-cli --cluster add-node 192.168.1.138:6388 192.168.1.138:6387 --cluster-slave --cluster-master-id  55ec78fb6b6e501e717ad88643b64

4. 查看集群情况

redis-cli --cluster check 192.168.1.138:6387

哈希槽案例实战:哈希槽下线,收缩

删除集群中的2个节点,node7与node8

1. 查看集群节点信息

redis-cli --cluster check 192.168.1.138:6381

2. 把6387master对呀的slave6388删除

del-node后面跟着slave节点的ip:port 和node ID

redis-cli --cluster del-node 192.168.1.138:6388   84acba0a54a5ac8ee0541eb973ef2ecdbbfa0e16

3. 查看集群情况

redis-cli --cluster check 192.168.1.138:6381

4. 查看容器,发现node8没了

docker ps

5. 重新分配master的slot

redis-cli --cluster reshard 192.168.1.138:6381 
  • 查看集群节点情况
redis-cli --cluster check 192.168.1.138:6381

6. 删除master节点node7

del-node后面跟着slave节点的ip:port 和node ID

redis-cli --cluster del-node 192.168.1.138:6387     0a925be15fcb5f749b9014e86ebf84d26797306b

7.继续查看集群情况

exit

docker exec -it redis-node-2 /bin/bash

redis-cli --cluster check 192.168.1.138:6381

哈希槽案例实战:spring集成redis集群

1.redis的序列化

2.配置文件

##Redis 配置
##Redis数据库索引(默认为0
spring.redis.database=1
##Redis服务器地址
#spring.redis.host=127.0.0.1
##Redis服务器连接端口
spring.redis.port=4637
##Redis服务器连接密码(默认为空)
#spring.redis.password=agan
##连接池最大连接罚(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
#带连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
##连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1ms
#带连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#spring.redis.sentinel.master=# Name of Redis server.
#spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
Spring. redis. timeout=1m
spring.redis.cluster.nodes=192.168.1.138:6381,192.168.1.138:6382

3.往redis加入10条数据

@GetMapping(value = "/init")
public void refreshData(){
	for(int i-8;1<10;1++){
		String key="user:"+i:
		this.redisTemplate.opsForvalue().set(key,i);
		log.debug("set key={}, value={}",key, i); .
	}
}

4.测试一 所有的数据会被分成3份,存到3个master里面,进入6381与6382查看里面的数据是否不一样

docker exec -it redis-node-1 /bin/bash

redis-cli -h 192.168.1.138 -p 6381 -c
keys *

redis-cli -h 192.168.1.138 -p 6382 -c
keys *

redis-cli -h 192.168.1.138 -p 6383 -c
keys *

5.测试二 验证主从的数据是否一致

查看192.168.1.138:6386和1192.168.1.138:6381的数据是否一样

redis-cli -h 192.168.1.138 -p 6381 -c
keys *

redis-cli -h 192.168.1.138 -p 6386 -c
keys *

数据一致,只是顺序不同

结论

  1. redis的集群读写操作,数据都会被切成3份,放在不同的master里面
  2. redis的主从,里面的数据一定是一样的



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • 2379. Minimum Recolors to Get K Consecutive Black Blocks
  • 2471. Minimum Number of Operations to Sort a Binary Tree by Level
  • 1387. Sort Integers by The Power Value
  • 2090. K Radius Subarray Averages
  • 2545. Sort the Students by Their Kth Score