Redis(5.0)集群分析
1、为了达到redis数据库的高可用,所以需要在单机的基础上建立集群,首先了解下它的集群模式,大概有以下几种:
- 主从复制
- 哨兵模式
- Redis官方提供的Cluster集群模式(服务端)
- Jedis sharding集群(客户端sharding)
- 利用中间件代理,比如豌豆荚的codis等
2、简单分析一下原理以及优缺点
2.1、主从复制(Master-Slave Replication):
主从复制原理:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;(从服务器初始化完成)
- 主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)
- 一个master可以拥有多个slave,但是一个slave只能对应一个master
优点:
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
- 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成;
- Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力;
- Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求;
- Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据
缺点:
- Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复;
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;
2.2、哨兵模式:
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。
- 监控主服务器和从服务器是否正常运行。
- 主服务器出现故障时自动将从服务器转换为主服务器。
哨兵的工作方式:
- 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个PING 命令;
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
- 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
- 当有足够数量的Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN),则Master主服务器会被标记为客观下线(ODOWN)
- 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。;
- 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次;
- 若没有足够数量的 Sentinel(哨兵)进程同意Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
优点:
- 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
- 主从可以自动切换,系统更健壮,可用性更高。
缺点:
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
2.3、Redis官方 Cluster集群模式
redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容;
Redis-Cluster采用无中心结构,它的特点如下:
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽;
- 节点的fail是通过集群中超过半数的节点检测失效时才生效;
- 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可;
工作方式:
在redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
2.4、Jedis sharding集群
Redis Sharding可以说是在Redis cluster出来之前业界普遍的采用方式,其主要思想是采用hash算法将存储数据的key进行hash散列,这样特定的key会被定为到特定的节点上。
庆幸的是,Java Redis客户端驱动Jedis已支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool
Jedis的Redis Sharding实现具有如下特点:
- 采用一致性哈希算法,将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
- 为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
- ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协。比如扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移。作为轻量级客户端sharding,处理Redis键值迁移是不现实的,这就要求应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。
2.5、利用中间件代理
中间件的作用是将我们需要存入redis中的数据的key通过一套算法计算得出一个值。然后根据这个值找到对应的redis节点,将这些数据存在这个redis的节点中。
常用的中间件有这几种
- Twemproxy
- Codis
- nginx
3、几种集群模式大概有了了解,现在搭建开始集群
由于我使用了redis5.0, 搭建过程中也是遇到了一个坑,redis5.0使用redis-cli作为创建集群的命令,使用c语言实现,不再使用ruby语言,redis3.0使用redis-trib.rb创建集群,且需要安装ruby。
gcc的环境之前已经安装过了,现在不需要处理,直接下一步。
3.1、将之前安装好的redis文件,复制到一个新的文件夹redis-cluster下 ,并在新文件夹下复制成6份。
注意要将.rdb和.aof后缀的持久化文件删除,如果有的话。
这里就不考虑3.0 的搭建方式了
3.2、将六个节点的redis.conf配置文件按照如下进行修改
如果是同一台主机的话,端口必须不同。不同主机可以相同。 我这里使用同一台主机,端口:7001-7006。
daemonize yes
port **** #配置自己的节点端口
cluster-enabled yes # 开启集群
3.3启动、关闭脚本
启动脚本:
cd redis1
./src/redis-server redis.conf
cd ..
cd redis2
./src/redis-server redis.conf
cd ..
cd redis3
./src/redis-server redis.conf
cd ..
cd redis4
./src/redis-server redis.conf
cd ..
cd redis5
./src/redis-server redis.conf
cd ..
cd redis6
./src/redis-server redis.conf
cd ..
关闭脚本:
./redis1/redis-cli -p 7001 shutdown
./redis1/redis-cli -p 7002 shutdown
./redis1/redis-cli -p 7003 shutdown
./redis1/redis-cli -p 7004 shutdown
./redis1/redis-cli -p 7005 shutdown
./redis1/redis-cli -p 7006 shutdown
3.4、修改权限
chmod 777 start-all.sh stop-all.sh
3.5、启动节点
./start-all.sh
# 查看redis进程
ps -aux | grep redis
3.6、创建集群
redis-cli --cluster create 192.0.0.179:7001 192.0.0.179:7002 192.0.0.179:7003 192.0.0.179:7004 192.0.0.179:7005 192.0.0.179:7006 --cluster-replicas 1
可以看到6个节点配分配成3个主节点,3个从节点。
3.7、查询集群信息
集群创建成功登陆任意redis结点查询集群中的节点情况。
客户端以集群方式登陆:
redis-cli -c -h 192.0.0.179 -p 7001
#其中-c表示以集群方式连接redis,-h指定ip地址,-p指定端口号
查询集群结点信息:
cluster nodes
3.8、测试
192.0.0.179:7001> set a 123456
-> Redirected to slot [15495] located at 192.0.0.179:7003
OK
192.0.0.179:7003> get a
"123456"
192.0.0.179:7003> get a
"123456"
192.0.0.179:7003> set b 44444444
-> Redirected to slot [3300] located at 192.0.0.179:7001
OK
192.0.0.179:7001> get b
"44444444"
192.0.0.179:7001>
可以看到redis 集群会根据计算为我们分配hash槽,以分配不同的节点
3.9、使用jedis连接
package com.pc.jedis.test;
import java.util.HashSet;
import java.util.Set;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
public class JedisClusterTest {
public static void main(String[] args) {
// 创建并填充节点信息
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.0.0.179", 7001));
nodes.add(new HostAndPort("192.0.0.179", 7002));
nodes.add(new HostAndPort("192.0.0.179", 7003));
nodes.add(new HostAndPort("192.0.0.179", 7004));
nodes.add(new HostAndPort("192.0.0.179", 7005));
nodes.add(new HostAndPort("192.0.0.179", 7006));
// 创建JedisCluster对象
JedisCluster jedisCluster = new JedisCluster(nodes);
// 使用jedisCluster操作redis
String key = "clusterTest";
String setResult = jedisCluster.set(key, "redis集群搭建");
System.out.println(setResult);
String getResult = jedisCluster.get(key);
System.out.println(getResult);
// 关闭jedisCluster(程序执行完后才能关闭,内部封装了连接池)
jedisCluster.close();
}
}