redis集群客户端JedisCluster优化

来源:互联网 发布:ubuntu 16.04 17.10 编辑:程序博客网 时间:2024/04/29 20:54

原文:http://www.w2bc.com/article/152559

我们知道,普通的情况下,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。

再来看第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),如在以上这几个条件下有问题,可以一起交流:

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 */@NotThreadSafepublic 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 Queue<Client> clients = new LinkedList<Client>();   // 根据顺序存储每个命令对应的Client    private Map<JedisPool, Jedis> jedisMap = new HashMap<>();   // 用于缓存连接    /**     * 根据jedisCluster实例生成对应的JedisClusterPipeline     * @param      * @return     */    public static JedisClusterPipeline pipelined(JedisCluster jedisCluster) {        JedisClusterPipeline pipeline = new JedisClusterPipeline();        pipeline.setJedisCluster(jedisCluster);        return pipeline;    }    public void setJedisCluster(JedisCluster jedis) {        JedisSlotBasedConnectionHandler ch = getValue(jedis, FIELD_CONNECTION_HANDLER);        if (null == ch) {            throw new RuntimeException("cannot get JedisSlotBasedConnectionHandler from JedisCluster");        }        connectionHandler = ch;    }    /**     * 刷新集群信息,当集群信息发生变更时调用     * @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;                    }                    try {                        jedis.getClient().getAll();                    } catch (RuntimeException ex) {                        // 其中一个client出问题,后面出问题的几率较大                    }                }            }            close();        }    }    @Override    public void close() {        clean();        clients.clear();        for (Jedis jedis : jedisMap.values()) {            jedis.close();        }        jedisMap.clear();    }    @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) {        JedisClusterInfoCache cache = getValue(connectionHandler, FIELD_CACHE);        JedisPool pool = cache.getSlotPool(slot);        // 根据pool从缓存中获取Jedis        Jedis jedis = jedisMap.get(pool);        if (null == jedis) {            jedis = pool.getResource();            jedisMap.put(pool, jedis);        }        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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267

0 0
原创粉丝点击