1. moved重定向
1. 客户端读写(get/set)操作执行过程:如果是一个普通的客户端连接到redis cluster中的任意一个节点,然后向该节点发送一条get/set命令,接收的节点首先会依据该key计算对应槽位,然后再找到槽位所在的节点,判断找到的节点是否是自身,如果是则在当前节点执行该命令,否则回复客户端moved异常,异常中包含真正执行命令的节点的信息,客户端需要使用获取到节点信息,重新连接获取到的节点并发送命令,但是该行为不是自动的,需要主动操作(如下图中第4步,该步骤需要在客户端通过专门编写逻辑代码执行);客户端依据返回的moved异常中的节点信息,进行的转移连接操作就是moved重定向
2. moved异常演示:首先启动集群,然后以普通模式的客户端连接到任意一个节点上,进行set/get操作,linux中普通模式的客户端对应Java中的Jedis客户端
2. 可自动进行moved重定向的客户端:
redis-cli -h host -p port -c:linux系统中redis自带的客户端,该客户端可以自动进行moved重定向操作,主要在于-c命令参数,该参数表示以集群模式启动客户端并连接到到集群中的某个节点上,如下图所示,客户端会自动进行连接转移并执行命令
3. ask重定向:类似于moved重定向,但该转移通常与集群伸缩有关,ask重定向发生在两个节点间进行槽位迁移时,当两个节点正在进行槽位转移时(转移未结束),如果此时客户端向源节点发送一条get/set命令,如果key对应的槽位还在源节点,但槽位中的key已经转移到新节点中,此时就会返回ask转向,然后客户端需要依据返回的ask中的信息执行一个asking命令,然后发送要执行的get/set命令,新节点会返回执行结果。
4. moved和ask重定向带来的问题:以集群模式客户端(redis-cli -h host -p port -c)连接任意一个节点后,进行大量的get/set命令,如果执行这些命令时发生了很多的moved和ask重定向,那么就会极大影响系统性能
2. smart客户端——JedisCluster
1. smart客户端:该客户端就是为了改善集群模式客户端(redis-cli -h host -p port -c)可能会因为频繁的moved和ask重定向而导致的性能浪费
2. smart客户端JedisCluster简单原理介绍:
- 从集群中选取一个可执行的节点,使用cluster slots命令获取到每个节点和其负责的槽位的关系映射,比如下图,JedisCluster也会通过类似方式,在JedisCluster创建并初始化时,会自动进行该步骤,并且将每个节点和其负责的槽位的关系映射保存在缓存中
- 每个节点和其负责的槽位的关系映射后,为每一个节点创建一个对应的JedisPool,每当有命令传来时就从该连接池中获取一个Jedis对象,执行完命令后在将该Jedis对象还给JedisPool
- 准备执行命令,JedisCluster中保存着节点和其负责的槽位的关系映射map,在执行命令时,通过key可以获取到其所在的槽位slot,然后依据map就可以获取到对应的节点,然后找到该节点的JedisPool获取一个Jedis对象,发送并执行命令
2. 命令执行时的问题:如果说服务端手动进行了一些改动,比如节点迁移,而JedisCluster却未刷新缓存中的每个节点和其负责的槽位的关系映射,那么当再次执行命令时,就有可能出现连接错误,就会发生并返回ask或moved(一般都是moved)异常,此时JedisCluster客户端就会自动的刷新缓存中的节点和其负责的槽位的关系映射,然后重新发送命令执行,但命令发送次数有限制,多次失败之后就会抛出错误
3. JedisCluster的使用
1. 基本使用:建议使用单例模式管理使用JedisCluster对象
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
public class TestJedisCluster {
public static void main(String[] args) {
//保存集群中每个节点中的IP地址和端口
HashSet<HostAndPort> set=new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.10.128", 6380));
set.add(new HostAndPort("192.168.10.128", 6381));
set.add(new HostAndPort("192.168.10.128", 6382));
set.add(new HostAndPort("192.168.10.128", 6383));
set.add(new HostAndPort("192.168.10.128", 6384));
set.add(new HostAndPort("192.168.10.128", 6385));
//创建JedisCluster对象,该对象的构造方法有多个重载版本
JedisCluster cluster=new JedisCluster(set);
//执行命令
cluster.set("hello","world");
//使用完后,如果确定不在使用该对象,则关闭JedisCluster
try {
cluster.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
2. 多节点执行命令:有一个命令需要集群中的每一个节点都执行一遍,比如生成AOF文件,伪代码过程如下
//获取到集群中的每一个节点的连接池对象
Map<String, JedisPool> nodes=cluster.getClusterNodes();
//遍历每一个节点的连接池对象,获取jedis客户端对象,执行命令
for(Entry<String, JedisPool> set:nodes.entrySet()){
JedisPool pool=set.getValue();
Jedis j=pool.getResource();
j.bgrewriteaof();//生成AOF文件
j.close();
}
3. 批量执行命令:以类似mget或mset这类批量命令,mget或mset中所有的key值都必须在一个槽位中,执行这类命令有以下几种优化
(1)串行化执行:也就是遍历批量命令中操作的每一个key值,遍历执行这些命令即可,问题就是耗时长,因为每一个命令都要在网络中传输
(2)依据key值聚簇分类在串行执行:遍历批量命令中操作的key值,计算出每一个key值所在的槽位,然后依据槽位找到所在的节点,找到每个命令执行所在的节点后就可以对这些命令进行分组,同一个节点的命令分为一组,然后使用每个节点客户端的Pipeline对象一次性执行每个组中的所有命令(使用Jedis客户端中的流水线执行功能),串行执行执行每一个Pipeline对象,耗时比上面更少,取决于多个Pipeline对象执行时间之和
(3)并行化执行方法2:使用多线程,并行执行每个节点客户端的Pipeline对象,性能更好,耗时取决于执行时间最长的那个Pipeline对象
(4)hash_tag:可以在key值前添加一个{tag},tag即为一个标记值,用大括号括起来并写在key值前面可以保证批量命令只会在同一个节点中执行。该方法耗时最短,但也会引起数据倾斜