• redis集群客户端JedisCluster优化


    Redis在3.0版正式引入了集群这个特性,扩展变得非常简单。然而当你开心的升级到3.0后,却发现有些很好用的功能现在工作不了了, 比如我们今天要聊的pipeline功能。
    1
    我们知道,普通的情况下,redis client与server之间采用的是请求应答的模式,即:
    
    Client: command1 
    Server: response1 
    Client: command2 
    Server: response2 
    …
    
    在这种情况下,如果要完成10个命令,则需要20次交互才能完成。因此,即使redis处理能力很强,仍然会受到网络传输影响,导致吞吐上不去。而在管道模式下,多个请求变成这样:
    
    Client: command1,command2… 
    Server: response1,response2…
    
    在这种情况下,完成命令只需要2次交互。这样网络传输上能够更加高效,加上redis本身强劲的处理能力,是不是有一种飞一样的感觉。听到这里有没有去优化应用的冲动? 然而到了cluster模式下,这样的功能并不支持。 下面我们先来分析下,是什么原因导致redis cluter没办法支持管道模式。首先需要了解集群下的几个特性:
    
    1、集群将空间分拆为16384个槽位(slot),每一个节点负责其中一些槽位。迁移时对整个slot迁移
    2、节点添加,或宕机或不可达的情况下可以正常使用
    3、不存在中心或者代理节点, 每个节点中都包含集群所有的节点信息
    4、集群中的节点不会代理请求:即如果client将命令发送到错误的节点上,操作会失败,但会返回”-MOVED”或”-ASK”,供client进行永久或临时的节点切换
    以上信息中第3、4点信息比较重要。
    
    我们先来看第3点,由于每个节点都包含所有的节点信息,因此client连接任一节点都可以获取整个集群的信息,这样我们在配置JedisCluster时只需要配置其中一部分节点的信息就可以(配置多个是为了高可用)。对应的获取集群命令为:cluster nodes
    
    127.0.0.1:9380> cluster nodes 
    b6d0cfe64dbae9590e6fc4c5a8e309debcbe0529 127.0.0.1:9380 myself,master - 0 0 2 connected 5461-10922 
    b9e5592558aae0f28c79c3750b264d5b2530f6a4 127.0.0.1:9381 master - 0 1466758609932 3 connected 10923-16383 
    b40095eb2023653eaea5b7b4e242a77a7817889a 127.0.0.1:9379 master - 0 1466758608932 1 connected 0-5460
    
    每一行代表一个节点的信息,这里共三个节点(测试用,没有建slave节点),依次的信息为:
    
    {id} {ip:port} {flags如master/slave} {master id} {ping-sent} {pong-recv} {config-epoch} {link-state} {slot} {slot} … {slot} 
    参考: http://redis.io/commands/cluster-nodes
    
    可以看到每个节点对应的slot信息都在这里,{slot}格式一般是{begin}-{end}(如0-5460),表示从{begin}到{end}的所有slot都在当前节点中。因此我们可以通过slot找到对应机器的ip:port。 注意,新版client中使用cluster slots获取对应数据 , 参考: http://redis.io/commands/cluster-slots 。
    
    再来看第4点,由第3点可以知道client可以通过获取所有节点信息,根据key计算得到对应的slot后可以找到对应的节点。所以说在节点稳定(没有增减)的情况下,客户端可以一直用缓存的集群信息来发起各种命令。然而,如果节点发生变更客户端是否能够立即感知? 目前的client JedisCluster是无法感知的,他是通过执行命令后, 服务端返回的“-MOVED”信息感知节点的变化,并以此来刷新缓存信息。
    
    了解以上信息以后,JedisCluster为什么不支持pipeline就比较清晰了。 因为pipeline模式下命令将被缓存到对应的连接(OutputStream)上,而在真正向服务端发送数据时,节点可能发生了改变,数据就可能发向了错误的节点,这导致批量操作失败,而要处理这种失败是非常复杂的。至少目前JedisCluster并未提供这样的机制。(对于单key来说,在发生这种情况的时候,进行简单的节点数据刷新+重新发送当前命令来重试)。
    
    看到这里,你可能会感到沮丧(我猿类如此不易,且行且珍惜)。这里提供一个简单的思路,你可以根据单key的逻辑,如果某些key遇到”-MOVE”或”-ASK”则重试。 根据这个思路,你需要按顺序记录所有的命令,每次执行完成后找出异常的数据,刷新节点信息后重试,最终将重试(可能有多次)获取到的结果根据顺序信息插入返回列表。对于重试多次依然失败的数据,交由业务处理。思路很简单,然而redis命令太多了,要对PipelineBase的每个方法都这样改造,我不想(因为我懒呀),而且估计坑也很多,所以这个只能靠你自己去搞了。
    
    下面我说下针对我们的业务做的一个JedisCluster pipeline实现。对应的业务有以下特点: 
    - 数据为每隔一段时间全量导入redis集群,数据量约xx万(xx较大) 
    - 导入任务为后台执行,可重试,最终如果有部分失败可接受 
    - 集群相对较稳定,不会频繁的加减机器 
    - 在线业务不使用该api
    
    下面是该类的源码(jdk1.7),如在以上这几个条件下有问题,可以一起交流: 
    (github地址: https://github.com/youaremoon/jedis-ext)
    
    
    package com.yam.common.redis;
    
    import java.io.Closeable;
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import java.util.Queue;
    import java.util.Set;
    
    import javax.annotation.concurrent.NotThreadSafe;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import redis.clients.jedis.BinaryJedisCluster;
    import redis.clients.jedis.Client;
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisCluster;
    import redis.clients.jedis.JedisClusterConnectionHandler;
    import redis.clients.jedis.JedisClusterInfoCache;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisSlotBasedConnectionHandler;
    import redis.clients.jedis.PipelineBase;
    import redis.clients.jedis.exceptions.JedisMovedDataException;
    import redis.clients.jedis.exceptions.JedisRedirectionException;
    import redis.clients.util.JedisClusterCRC16;
    import redis.clients.util.SafeEncoder;
    
    /**
     * 在集群模式下提供批量操作的功能。 <br/>
     * 由于集群模式存在节点的动态添加删除,且client不能实时感知(只有在执行命令时才可能知道集群发生变更),
     * 因此,该实现不保证一定成功,建议在批量操作之前调用 refreshCluster() 方法重新获取集群信息。<br />
     * 应用需要保证不论成功还是失败都会调用close() 方法,否则可能会造成泄露。<br/>
     * 如果失败需要应用自己去重试,因此每个批次执行的命令数量需要控制。防止失败后重试的数量过多。<br />
     * 基于以上说明,建议在集群环境较稳定(增减节点不会过于频繁)的情况下使用,且允许失败或有对应的重试策略。<br />
     * 
     * 
     * @author youaremoon
     * @version
     * @since Ver 1.1
     */
    @NotThreadSafe
    public class JedisClusterPipeline extends PipelineBase implements Closeable {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(JedisClusterPipeline.class);
    
        // 部分字段没有对应的获取方法,只能采用反射来做
        // 你也可以去继承JedisCluster和JedisSlotBasedConnectionHandler来提供访问接口
        private static final Field FIELD_CONNECTION_HANDLER;
        private static final Field FIELD_CACHE; 
        static {
            FIELD_CONNECTION_HANDLER = getField(BinaryJedisCluster.class, "connectionHandler");
            FIELD_CACHE = getField(JedisClusterConnectionHandler.class, "cache");
        }
    
        private JedisSlotBasedConnectionHandler connectionHandler;
        private JedisClusterInfoCache clusterInfoCache;
        private Queue<Client> clients = new LinkedList<Client>();   // 根据顺序存储每个命令对应的Client
        private Map<JedisPool, Jedis> jedisMap = new HashMap<>();   // 用于缓存连接
        private boolean hasDataInBuf = false;   // 是否有数据在缓存区
    
        /**
         * 根据jedisCluster实例生成对应的JedisClusterPipeline
         * @param 
         * @return
         */
        public static JedisClusterPipeline pipelined(JedisCluster jedisCluster) {
            JedisClusterPipeline pipeline = new JedisClusterPipeline();
            pipeline.setJedisCluster(jedisCluster);
            return pipeline;
        }
    
        public JedisClusterPipeline() {
        }
    
        public void setJedisCluster(JedisCluster jedis) {
            connectionHandler = getValue(jedis, FIELD_CONNECTION_HANDLER);
            clusterInfoCache = getValue(connectionHandler, FIELD_CACHE);
        }
    
        /**
         * 刷新集群信息,当集群信息发生变更时调用
         * @param 
         * @return
         */
        public void refreshCluster() {
            connectionHandler.renewSlotCache();
        }
    
        /**
         * 同步读取所有数据. 与syncAndReturnAll()相比,sync()只是没有对数据做反序列化
         */
        public void sync() {
            innerSync(null);
        }
    
        /**
         * 同步读取所有数据 并按命令顺序返回一个列表
         * 
         * @return 按照命令的顺序返回所有的数据
         */
        public List<Object> syncAndReturnAll() {
            List<Object> responseList = new ArrayList<Object>();
    
            innerSync(responseList);
    
            return responseList;
        }
    
        private void innerSync(List<Object> formatted) {
            HashSet<Client> clientSet = new HashSet<Client>();
    
            try {
                for (Client client : clients) {
                    // 在sync()调用时其实是不需要解析结果数据的,但是如果不调用get方法,发生了JedisMovedDataException这样的错误应用是不知道的,因此需要调用get()来触发错误。
                    // 其实如果Response的data属性可以直接获取,可以省掉解析数据的时间,然而它并没有提供对应方法,要获取data属性就得用反射,不想再反射了,所以就这样了
                    Object data = generateResponse(client.getOne()).get();
                    if (null != formatted) {
                        formatted.add(data);
                    }
    
                    // size相同说明所有的client都已经添加,就不用再调用add方法了
                    if (clientSet.size() != jedisMap.size()) {
                        clientSet.add(client);
                    }
                }
            } catch (JedisRedirectionException jre) {
                if (jre instanceof JedisMovedDataException) {
                    // if MOVED redirection occurred, rebuilds cluster's slot cache,
                    // recommended by Redis cluster specification
                    refreshCluster();
                }
    
                throw jre;
            } finally {
                if (clientSet.size() != jedisMap.size()) {
                    // 所有还没有执行过的client要保证执行(flush),防止放回连接池后后面的命令被污染
                    for (Jedis jedis : jedisMap.values()) {
                        if (clientSet.contains(jedis.getClient())) {
                            continue;
                        }
    
                        flushCachedData(jedis);
                    }
                }
    
                hasDataInBuf = false;
                close();
            }
        }
    
        @Override
        public void close() {
            clean();
    
            clients.clear();
    
            for (Jedis jedis : jedisMap.values()) {
                if (hasDataInBuf) {
                    flushCachedData(jedis);
                }
    
                jedis.close();
            }
    
            jedisMap.clear();
    
            hasDataInBuf = false;
        }
    
        private void flushCachedData(Jedis jedis) {
            try {
                jedis.getClient().getAll();
            } catch (RuntimeException ex) {
            }
        }
    
        @Override
        protected Client getClient(String key) {
            byte[] bKey = SafeEncoder.encode(key);
    
            return getClient(bKey);
        }
    
        @Override
        protected Client getClient(byte[] key) {
            Jedis jedis = getJedis(JedisClusterCRC16.getSlot(key));
    
            Client client = jedis.getClient();
            clients.add(client);
    
            return client;
        }
    
        private Jedis getJedis(int slot) {
            JedisPool pool = clusterInfoCache.getSlotPool(slot);
    
            // 根据pool从缓存中获取Jedis
            Jedis jedis = jedisMap.get(pool);
            if (null == jedis) {
                jedis = pool.getResource();
                jedisMap.put(pool, jedis);
            }
    
            hasDataInBuf = true;
            return jedis;
        }
    
        private static Field getField(Class<?> cls, String fieldName) {
            try {
                Field field = cls.getDeclaredField(fieldName);
                field.setAccessible(true);
    
                return field;
            } catch (NoSuchFieldException | SecurityException e) {
                throw new RuntimeException("cannot find or access field '" + fieldName + "' from " + cls.getName(), e);
            }
        }
    
        @SuppressWarnings({"unchecked" })
        private static <T> T getValue(Object obj, Field field) {
            try {
                return (T)field.get(obj);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                LOGGER.error("get value fail", e);
    
                throw new RuntimeException(e);
            }
        }   
    
        public static void main(String[] args) throws IOException {
            Set<HostAndPort> nodes = new HashSet<HostAndPort>();
            nodes.add(new HostAndPort("127.0.0.1", 9379));
            nodes.add(new HostAndPort("127.0.0.1", 9380));
    
            JedisCluster jc = new JedisCluster(nodes);
    
            long s = System.currentTimeMillis();
    
            JedisClusterPipeline jcp = JedisClusterPipeline.pipelined(jc);
            jcp.refreshCluster();
            List<Object> batchResult = null;
            try {
                // batch write
                for (int i = 0; i < 10000; i++) {
                    jcp.set("k" + i, "v1" + i);
                }
                jcp.sync();
    
                // batch read
                for (int i = 0; i < 10000; i++) {
                    jcp.get("k" + i);
                }
                batchResult = jcp.syncAndReturnAll();
            } finally {
                jcp.close();
            }
    
            // output time 
            long t = System.currentTimeMillis() - s;
            System.out.println(t);
    
            System.out.println(batchResult.size());
    
            // 实际业务代码中,close要在finally中调,这里之所以没这么写,是因为懒
            jc.close();
        }
    }
  • 相关阅读:
    解决Select2控件不能在jQuery UI Dialog中不能搜索的bug
    Markdown编辑器入门
    使用Ubuntu 12.04作为日常电脑环境
    DIV元素水平和垂直居中
    xocodebulid 自动化打包 解决提示 ld: library not found for -lPods 问题
    IOS 项目的瘦身工具
    Core Animation系列之CADisplayLink
    IOS7 新特性
    objc中国 和 翻译团队博客 (不错的学习ios 资源)
    IOS6 IOS7 Mapkit draw Rout(地图划线)
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13317448.html
Copyright © 2020-2023  润新知