13、Redis集群
13.1、问题
- 容量不够,redis如何进行扩容?
- 并发写操作,redis如何分摊?
- 主从模式,薪火相传,主机宕机,导致ip发生变化,需要修改很多配置,相当繁琐,但是
redis3.0
中提供了解决方案,就是无中心化集群
配置。
13.2、什么是集群
Redis集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N
Redis集群通过分区(partiition)来提供一定程度的可用性(availablity):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
13.3、准备开始搭建集群
1、创建/myredis-sluster文件夹来存放我们的相关文件
mkdir myredis-cluster
2、复制一份redis.conf放进去(跟主从那块一样)
[root@VM-4-12-centos /]# cd myredis-cluster/ [root@VM-4-12-centos myredis-cluster]# cp /usr/local/bin/redis.conf /myredis-cluster/redis.conf
3、创建6个配置文件,一个主一个从,也就是三组主从关系
redis6390.conf , redis6391.conf
redis6392.conf , redis6393.conf
redis6394.conf , redis6395.conf
先创建一个文件,其他的进行复制修改端口即可,执行命令:
vi redis6390.conf
然后在文件中加入以下内容
include /myredis-cluster/redis.conf pidfile "/var/run/redis_6390.pid" port 6390 dbfilename "dump6390.rdb" cluster-enabled yes cluster-config-file nodes-6390.conf cluster-node-timeout 15000
配置解释:
cluster-enabled yes
:打开集群模式
cluster-config-file
:设定节点配置文件名
cluster-node-timeout
: 节点失联时间,超过后集群自动进行主从切换
然后复制5份不同端口的文件,只许改动端口号即可。完成后如下所示:
[root@VM-4-12-centos myredis-cluster]# ls redis6390.conf redis6392.conf redis6394.conf redis.conf redis6391.conf redis6393.conf redis6395.conf
4、启动这6个端口
redis-server redis6390.conf redis-server redis6391.conf redis-server redis6392.conf redis-server redis6393.conf redis-server redis6394.conf redis-server redis6395.conf
5、将6个节点合成一个集群
进入你的src目录下,也就是redis安装位置下面的src文件夹
cd redis-6.2.6/src
执行以下命令即可:
redis-cli --cluster create --cluster-replicas 1 10.0.4.12:6390 10.0.4.12:6392 10.0.4.12:6394 10.0.4.12:6391 10.0.4.12:6393 10.0.4.12:6395
**注意:**此处不要用127.0.0.1,请求真实ip地址。
replicas 1
:代表每台主机下面从机的个数,一台主机一台从机,正好三组
执行命令后出现以下提示:
会给你分配好主机从机的对应关系,输入yes
即可。
注意这里:[ok] All 16384 slots covered
,稍后在slots进行分析
13.4、什么是slots
上面我们将6个节点合并为集群后,最后的结果有一句话
``[ok] All 16384 slots covered`
解释:
一个Redis集群包含16384个插槽(hash slot),数据中的每个键都属于这16384个插槽的其中一个。
集群使用公式CRC16(key)% 16384
来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。
集群中的每一部分负责处理一部分插槽。
举例:
比如有A、B、C三个节点,A负责【0-5460】,B负责【5461-10922】,C负责【10923-16383】
13.5、-c采用集群策略连接,设置的数据会自动切换到相应的写主机
如果想设置多个值呢
不在一个slot下的键值,是不能使用mget,mset等多键操作
可以通过{}来定义组的概念,从而使key中{}内相同内容的键值放到一个slot中去
13.6、查询集群中的值
查询某个键的插槽值
10.0.4.12:6390> cluster keyslot k2 (integer) 449
查看某个插槽中有几个key(注意:只能在自己的范围插槽内进行查询)
10.0.4.12:6390> set city beijing -> Redirected to slot [11479] located at 10.0.4.12:6394 OK 10.0.4.12:6394> cluster countkeysinslot 11479 (integer) 1
在指定插槽里面返回指定的个数的key
10.0.4.12:6390> set city beijing -> Redirected to slot [11479] located at 10.0.4.12:6394 OK 10.0.4.12:6394> cluster getkeysinslot 11479 1 1) "city"
13.7、故障修复
shutdown一个主节点,从节点会自动升为主节点
1、将6394主节点shutdown掉,然后重新集群策略连接一个主节点
2、cluster nodes
:查看集群主从机相关信息
结论:从节点自动升为主节点,原来的主节点fail掉
主节点恢复后,主从关系如何?主节点变回从机
1、新开一个窗口重新启动6394
redis-server redis6394.conf
2、集群信息变化
结论:主节点恢复后,主节点会变成从机
如果某个节点的主从节点全部宕掉,redis服务是否可以继续
结论:
与redis.conf中的参数 cluster-require-full-coverage
有关
如果cluster-require-full-coverage为yes,那么所有集群都挂掉
反之,其他插槽依然可以使用,该插槽全部数据不能使用,也无法存储。
13.8、优缺点
优点:
- 实现扩容
- 分摊压力
- 无中心配置
缺点:
- 多键操作不被支持
- 多键的Redis事务不被支持
14、Redis应用问题
14.1、缓存穿透
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,全部请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
缓存空值
如果一个查询返回的数据为空,我们仍然对这个空结果进行缓存,设置空结果的过期时间会很短,最长不超过5分钟
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。
14.2、缓存击穿
概念
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这个点进行访问,当在这个key失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在屏障上凿开一个洞
解决方案
设置热点时间永不过期
加互斥锁
分布式锁:保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可
14.3、缓存雪崩
概念
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
解决方案
redis高可用
就是多搭建redis集群
限流
缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
15、分布式锁
15.1、概念
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同的机器上,这将使原单机部署的情况并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
15.2、分布式锁的主流实现方案
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于Zookeeper
优缺点
- 性能:redis最高
- 可靠性:zookeeper最高
15.3、使用redis实现分布式锁
15.3.1、设置锁和过期时间
setnx key value`:设置key后,不可修改,除非删除后(或者过期后),也就是释放锁后才能再次操作
127.0.0.1:6380> setnx k1 v1 (integer) 1 # 不能修改 127.0.0.1:6380> setnx k1 v2 (integer) 0 127.0.0.1:6380> del k1 (integer) 1 127.0.0.1:6380> setnx k1 v1 (integer) 1
**问题一:**如果刚上完锁突然断点了,那过期时间无法设置了,那锁岂不是这个时间段一直没释放?
解决:我们可以在上锁的时候同时设置过期时间
set key value nx ex 12
:即上锁又设置过期时间
127.0.0.1:6380> set k2 v2 nx ex 12 OK
15.3.2、UUID防止误删
问题:
在redis分布式锁的时候,会去设置一个key,如果可以设置成功(也就是这个key不存在)则说明拿到锁,反之没有拿到锁(这个key已经存在,代表已经被上锁),但是会不会有这么一种情况?一个没有拿到锁的线程,把这个key给删掉,也就是释放了别人的锁,这种情况是肯定不能出现的。
解决:
在拿锁的时候,key对应的value值可以用UUID生成,每次在释放锁的时候都要先进行判断,如果对应的value是你自己设置的value,就成功释放,防止误删操作。
15.3.3、LUA保证删除原子性
问题描述
a拿到锁—>执行具体操作—>比较uuid(一样)—>准备删除锁,但是还没有删除的时候锁到了过期时间自动释放—>这个时候b拿到锁—>a继续删除操作—>最终a释放了b的锁
解决方案
使用LUA脚本,类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
15.3.4、分布式锁的几个必要条件
- 互斥性:任意时刻,只有一个客户端能持有锁
- 不会发生死锁,即使一个客户端持有锁期间崩溃而没有主动解锁,也能保证后续客户端可以加锁
- 加锁解锁必须是同一个客户端
- 加锁和解锁必须具有原子性