Redis从入门到放弃系列(十) Cluster

Stella981
• 阅读 729

Redis从入门到放弃系列(十) Cluster

本文例子基于:5.0.4

Redis Cluster集群高可用方案,去中心化,最基本三主多从,主从切换类似Sentinel,关于Sentinel内容可以查看编者另外一篇【Redis从入门到放弃系列(九) Sentinel】.

在Redis Cluster中,只存在index为0的数据库,而且其实Redis作为单线程,如果在同一个实例上创建多个库的话,也是需要上下文切换的.

slot

由于Redis Cluster是采用16384个slot来划分数据的,也就是说你当前插入的数据会存在不同的节点上,简而言之不支持比较复杂的多建操作(可以对key打上hash tags来解决).

我们说Cluster是按照16384个slot来划分数据的,那么是如何来确定一个key落在那个节点上呢?

//计算slot
HASH_SLOT = CRC16(key) mod 16384

每个节点会拥有一部分的slot,通过上述获取到具体key的slot即知道应该去哪儿找对应的节点啦.可是在网络中,一切都会有不存稳定因素,网络抖动.

当在Cluster中存在网络抖动的时候,当时间过长,有可能产生下线,其实原理跟Sentinel里面讲的很相似,因为都是依赖Gossip协议来实现的.可以通过以下配置来设置确定下线的时间.

//节点持续timeout的时间,才认定该节点出现故障,需要进行主从切换,
cluster-node-timeout
//作为上面timeout的系数来放大时间
cluster-replica-validity-factor

由于数据是按照16384个slot去划分的,那么当我们在请求某个key到错误的节点,这时候key不在该节点上,Redis会向我们发送一个错误

-MOVED 3999 127.0.0.1:6381

该消息是提示我们该key应该是存在127.0.0.1这台服务器上面的3999slot,这时候就需要我们的redis客户端去纠正本地的slot映射表,然后请求对应的地址.

增删集群节点

当我们在增加或者删除某个节点的时候,其实就只是将slot从某个节点移动到另外一个节点.可以使用一下命令来完成这一件事

  • CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node 有时候运维需要对redis节点的某些数据做迁移,官方提供了redis-trib工具来完成这件事情。

在迁移的时候,redis节点会存在两种状态,一种是MIGRATING和IMPORTING,用于将slot从一个节点迁移到另外一个节点.

  • 节点状态设置为MIGRATING时,将接受与此散列槽有关的所有查询,但仅当有问题的key存在时才能接受,否则将使用-Ask重定向将查询转发到作为迁移目标的节点。
  • 节点状态设置为IMPORTING时,节点将接受与此哈希槽有关的所有查询,但前提是请求前面有ASKING命令。如果客户端没有发出ASKING命令,查询将通过-MOVED重定向错误重定向到真正的散列槽所有者

多线程批量获取/删除

public class RedisUtils {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    private final ThreadLocal<String> requestId = new ThreadLocal<>();

    private final static ExecutorService executorService = new ThreadPoolExecutor(
            //核心线程数量
            1,
            //最大线程数量
            8,
            //当线程空闲时,保持活跃的时间
            1000,
            //时间单元 ,毫秒级
            TimeUnit.MILLISECONDS,
            //线程任务队列
            new LinkedBlockingQueue<>(1024),
            //创建线程的工厂
            new RedisTreadFactory("redis-batch"));

    @Autowired
    private JedisCluster jedisCluster;

    public String set(String key, String value) {
        return jedisCluster.set(key, value);
    }

    public String get(String key) {
        return jedisCluster.get(key);
    }

    public Map<String, String> getBatchKey(List<String> keys) {
        Map<Jedis, List<String>> nodeKeyListMap = jedisKeys(keys);
        //结果集
        Map<String, String> resultMap = new HashMap<>();
        CompletionService<Map<String,String>> batchService = new ExecutorCompletionService(executorService);
        nodeKeyListMap.forEach((k,v)->{
            batchService.submit(new BatchGetTask(k,v));
        });
        nodeKeyListMap.forEach((k,v)->{
            try {
                resultMap.putAll(batchService.take().get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        return resultMap;
    }

    public boolean lock(String lockKey, long expireTime){
        String uuid = UUID.randomUUID().toString();
        requestId.set(uuid);
        String result = jedisCluster.set(lockKey, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        return LOCK_SUCCESS.equals(result);
    }

    public boolean unLock(String lockKey){
        String uuid = requestId.get();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedisCluster.eval(script, Collections.singletonList(lockKey), Collections.singletonList(uuid));
        requestId.remove();
        return RELEASE_SUCCESS.equals(result);
    }

    private Map<Jedis, List<String>> jedisKeys(List<String> keys){
        Map<Jedis, List<String>> nodeKeyListMap = new HashMap<>();
        for (String key : keys) {
            //计算slot
            int slot = JedisClusterCRC16.getSlot(key);
            Jedis jedis = jedisCluster.getConnectionFromSlot(slot);
            if (nodeKeyListMap.containsKey(jedis)) {
                nodeKeyListMap.get(jedis).add(key);
            } else {
                nodeKeyListMap.put(jedis, Arrays.asList(key));
            }
        }
        return nodeKeyListMap;
    }

    public long delBatchKey(List<String> keys){
        Map<Jedis, List<String>> nodeKeyListMap = jedisKeys(keys);
        CompletionService<Long> batchService = new ExecutorCompletionService(executorService);
        nodeKeyListMap.forEach((k,v)->{
            batchService.submit(new BatchDelTask(k,v));
        });
        Long result = 0L;
        for (int i=0;i<nodeKeyListMap.size();i++){
            try {
                result += batchService.take().get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    class BatchGetTask implements Callable<Map<String,String>>{

        private Jedis jedis;

        private List<String> keys;

        private BatchGetTask(Jedis jedis, List<String> keys) {
            this.jedis = jedis;
            this.keys = keys;
        }

        @Override
        public Map<String, String> call() throws Exception {
            Map<String, String> resultMap = new HashMap<>();
            String[] keyArray = keys.toArray(new String[]{});
            try {
                List<String> nodeValueList = jedis.mget(keyArray);
                for (int i = 0; i < keys.size(); i++) {
                    resultMap.put(keys.get(i),nodeValueList.get(i));
                }
            }finally {
                jedis.close();
            }
            return resultMap;
        }
    }

    class BatchDelTask implements Callable<Long>{

        private Jedis jedis;

        private List<String> keys;

        private BatchDelTask(Jedis jedis, List<String> keys) {
            this.jedis = jedis;
            this.keys = keys;
        }

        @Override
        public Long call() throws Exception {
            String[] keyArray = keys.toArray(new String[]{});
            try {
                return jedis.del(keyArray);
            }finally {
                jedis.close();
            }
        }
    }

     static class RedisTreadFactory implements ThreadFactory{

        private final AtomicInteger threadNumber = new AtomicInteger(0);

        private final String namePredix;

        public RedisTreadFactory(String namePredix) {
            this.namePredix = namePredix +"-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread( r,namePredix + threadNumber.getAndIncrement());
            if (t.isDaemon())
                t.setDaemon(true);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
}

写在最后

Redis从入门到放弃系列终于完结啦!!!!!!!!!!!

写博客,真的是非常耗时间,真的,本来星期六日要写的,然而因为某些问题而没有写出来(PS:纯粹是因为打游戏.hhhh),终于在今天痛定思痛,顶着脖子酸的压力(PS:贴着狗皮膏药在撸码),终于完结了.

感谢各位看官那么辛苦看我码字,真心感谢.

希望写的东西对各位看官有启发.

Redis从入门到放弃系列(十) Cluster

点赞
收藏
评论区
推荐文章
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Peter20 Peter20
3年前
Redis集群详解
Redis集群详解Redis有三种集群模式,分别是:主从模式Sentinel模式Cluster模式三种集群模式各有特点,关于Redis介绍可以参考这里:Redis官网:https://redis.io/,最新版本5.0.4主从模式主从模式介绍主从模式是三种模式中最简单的,在主从复制中,数据库分为两类:主数据库(master)和从数据库(sl
Stella981 Stella981
3年前
Redis从入门到放弃系列(二) Hash
Redis从入门到放弃系列(二)Hash本文例子基于:5.0.4Hash是Redis中一种比较常见的数据结构,其实现为hashtable/ziplist,默认创建时为ziplist,当到达一定量级时,redis会将ziplist转化为hashtableRedis从入门到放弃系列(一)String
Stella981 Stella981
3年前
Redis从入门到放弃系列(四) Set
Redis从入门到放弃系列(四)Set本文例子基于:5.0.4Set是Redis中一种比较常见的数据结构,当存储的member为十进制64位有符号整数范围内的整数的字符串的时候其实现为intset,其他为hashtableRedis从入门到放弃系列(三)List(https://www.osch
Stella981 Stella981
3年前
Redis从入门到放弃系列(一) String
Redis从入门到放弃系列(一)String本文例子基于:5.0.4字符串是Redis中最常见的数据结构,底层是采用SDS,是可以修改的字符串,类似ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。首先让我们来看一下该如何在redis里面使用字符串这种类型//将字符
Stella981 Stella981
3年前
Redis从入门到放弃系列(九) Sentinel
Redis从入门到放弃系列(九)Sentinel本文例子基于:5.0.4RedisSentinel作为Redis高可用方案,具有监听,通知,自动故障转移等功能.这一切都是依赖主备同步的大前提(参考上一节:Redis从入门到放弃系列(八)主备同步).监听:Sentinel会不断
Stella981 Stella981
3年前
Redis从入门到放弃系列(三) List
Redis从入门到放弃系列(三)List本文例子基于:5.0.4List是Redis中一种比较常见的数据结构,其实现为quicklist,quicklist是一个ziplist的双向链表Redis从入门到放弃系列(一)String(https://www.oschina.net/action/
Stella981 Stella981
3年前
Redis从入门到放弃系列(七) 过期、内存淘汰策略
Redis从入门到放弃系列(七)过期、内存淘汰策略本文例子基于:5.0.4过期策略Redis对于设置了过期时间的key的过期策略有两种惰性删除定时随机删除惰性删除惰性删除的时机在于当你要获取该key的时候再去做判断.这里我以String类型
Stella981 Stella981
3年前
Redis从入门到放弃系列(五) ZSet
Redis从入门到放弃系列(五)ZSet本文例子基于:5.0.4ZSet是Redis中一种比较复杂的数据结构,当存储大小在128之内且member得长度在64以下,其实现为zipList,超过为SkipList忽然发现,到现在第五篇文章,还没有讲到zipList,然而前面例如Hash,List的篇章
Stella981 Stella981
3年前
Redis从入门到放弃系列(八) 主备同步
Redis从入门到放弃系列(八)主备同步本文例子基于:5.0.4在现在不管啥应用都谈分布式的阶段下(真的有必要???),我们的redis都会做一下主备,唔,如果redis存的数据不重要的话,其实也可以不用做\手动狗头\.为了能进一步上分布式,先让我们来了解一下CAP原理吧~Consis