Redis集群

redis集群所要解决的问题:

  高可用问题,删除添加节点,只需要重新分配槽就可以了,redis可以将数据自动切分(split)到多个节点,当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。

  并发性能问题,首先我们集群的每一个master节点都设置了1到N个从节点,然后它自身也是多节点,每个节点控制一部分槽。

  容量问题

       随着业务增加,我们需要对redis进行扩容,垂直扩容看似最便捷的扩容,但是受到机器的限制,一个机器的内存是有限的,所以垂直扩容到一定阶段不可避免的要进行水平扩容,如果预留出很多节点感觉又是对资源的一种浪费因为对业务的发展趋势很快预测。Redis Sentinel 水平扩容一直都是程序猿心中的痛点,因为水平扩容牵涉到数据的迁移(只有一个master节点,并发写无法解决)。迁移过程一方面要保证自己的业务是可用的,一方面要保证尽量不丢失数据所以数据能不迁移就尽量不迁移。redis集群通过去掉节点或者增加节点,移动分槽等来解决容量问题,并不需要,关闭我们整个业务(因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞(另外开一个线程), 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。)。

 

redis集群架构

    Redis Sentinel 集群模式可以增强整个Redis集群的稳定性与可靠性,但是当某个节点的master节点挂了要重新选取出新的master节点时,Redis Sentinel的集群模式选取的复杂度显然高于单点的Redis Sentinel 模式。

     Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。分布式集群首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集。Redis Cluster采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383,计算公式:slot = CRC16(key)%16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。下图展现一个五个节点构成的集群,每个节点平均大约负责3276个槽,以及通过计算公式映射到对应节点的对应槽的过程

  RedisCluster节点之间的关系:redis cluster的节点之间数据是共享的,如果不是这个节点的数据,它会帮你redirect到其他节点。

  RedisCluster节点之间有 meet操作(用客户端命令自己指派的时候用),指派槽,复制(replicate)

Redis一致性保证

Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作:第一个原因是因为集群是用了异步复制. 写操作过程

     客户端向主节点B写入一条命令

     主节点B向客户端回复命令状态

     主节点将写操作复制给他得从节点 B1, B2 和 B3

           主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。

           注意:Redis 集群可能会在将来提供同步写的方法。 Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区, 并且一个客户端与至少包括一个主节点在内的少数实例被孤立。举个例子 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1 ,小部分的一方则包含节点 B 和客户端 Z1 。

           Z1仍然能够向主节点B中写入, 如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了。注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。

 

一些基本命令:

  创建一个集群:(选项 --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。)

  redis-trib create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

  连接到一个集群

  ./redis-cli -c -p 7000

  集群加入新的主节点

  redis-trib add-node 127.0.0.1:7006 127.0.0.1:7000

  迁移槽(127.0.0.1:7000表示是哪个集群),然后它会询问移多少个槽给某个节点

  redis-trib reshard 127.0.0.1:7000

  移除一个节点

  redis-trib del-node 127.0.0.1:7000 `<node-id>`

  jedis中使用:

public class RedisClusterClient {
    private static JedisCluster jedisCluster = null;
    private static String redisHosts = "127.0.0.1:6378;127.0.0.1:6379;127.0.0.1:6380"; //如:127.0.0.1:26379;127.0.0.1:26378
    private static String password = "";//密码,可选
    private static final int CONNECT_TIMEOUT = 1000;//连接超时时间
    private static final int SO_TIMEOUT = 1000;//响应超时时间
    private static final int MAX_ATTEMPTS = 5;//最大尝试次数
    private static final int MAX_IDLE = 200;//最大空闲数
    private static final int MAX_TOTAL = 400;//最大连接数
    private static final int MIN_IDLE = 200;//最小空闲数
 
    static {
        //连接池配置
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(MAX_IDLE);
        poolConfig.setMaxTotal(MAX_TOTAL);
        poolConfig.setMinIdle(MIN_IDLE);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        //Redis Cluster 初始化
        Set<String> hosts = new HashSet<String>(Arrays.asList(redisHosts.split(";")));
        Set<HostAndPort> nodes = new LinkedHashSet<HostAndPort>();
        for (String host : hosts) {
            HostAndPort hostAndPort = new HostAndPort(host.split(":")[0], Integer.parseInt(host.split(":")[1]));
            nodes.add(hostAndPort);
        }
 
        if (StringUtils.isBlank(password)) {
            jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, poolConfig);
        } else {
            jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, password, poolConfig);
        }
    }
 
    /**
     * @param key
     * @return
     * @throws JedisConnectionException
     */
    public String get(String key) throws JedisConnectionException {
        try {
            return jedisCluster.get(key);
        } catch (JedisConnectionException e) {
            throw e;
        }
    }
 
    /**
     * @param key
     * @param value
     * @return
     * @throws JedisConnectionException
     */
    public String set(String key, String value) throws JedisConnectionException {
 
        try {
            return jedisCluster.set(key, value);
        } catch (JedisConnectionException e) {
            throw e;
        }
    }
}

 

posted @ 2019-05-06 00:30  LeeJuly  阅读(149)  评论(0)    收藏  举报