Redis Client Jedis 源码分析

Redis模型:

所采用的是Redis集群(直连型)模型。

模型介绍:

从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
特点:
1、无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
2、数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3、可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
4、高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
5、实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
6、单线程处理队列中读写请求。
缺点:
1、资源隔离性较差,容易出现相互影响的情况。
2、数据通过异步复制,不保证数据的强一致性

Master: 集群中的写库
Slaver: 集群中的读库
每个master和slaver都有主线程,主线程负责接受请求,重定向请求到对应的服务器节点,同时对没有重定向的请求,放到队列中由子线程处理。

redis cluster常用集群命令:

集群(cluster)
cluster info 打印集群的信息
cluster nodes 列出集群当前已知的所有节点(node),以及这些节点的相关信息
节点(node)
cluster meet “ip” “port” 将ip和port所指定的节点添加到集群当中,让它成为集群的一份子
cluster forget “node_id” 从集群中移除node_id指定的节点
cluster replicate “node_id” 将当前节点设置为node_id指定的节点的从节点
cluster saveconfig 将节点的配置文件保存到硬盘里面
cluster slaves “node_id” 列出该slave节点的master节点
cluster set-config-epoch 强制设置configEpoch
槽(slot)
cluster addslots “slot” [slot …] 将一个或多个槽(slot)指派(assign)给当前节点
cluster delslots “slot” [slot …] 移除一个或多个槽对当前节点的指派
cluster flushslots 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点
cluster setslot “slot” node “node_id” 将槽slot指派给node_id指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派
cluster setslot “slot” migrating “node_id” 将本节点的槽slot迁移到node_id指定的节点中
cluster setslot “slot” importing “node_id” 从node_id 指定的节点中导入槽slot到本节点
cluster setslot “slot” stable 取消对槽slot的导入(import)或者迁移(migrate)
键(key)
cluster keyslot “key” 计算键key应该被放置在哪个槽上
cluster countkeysinslot “slot” 返回槽slot目前包含的键值对数量
cluster getkeysinslot “slot” “count” 返回count个slot槽中的键
其它
cluster myid 返回节点的ID
cluster slots 返回节点负责的slot
cluster reset 重置集群,慎用

Redis的三个客户端通信框架

Jedis Redisson Lettuce
  下面的源码分析基于Jedis客户端
  
概念:
  Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持,
  Redisson:实现了分布式和可扩展的Java数据结构。
  Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:
  Jedis:比较全面的提供了Redis的操作特性
  Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列
  Lettuce:主要在一些分布式缓存框架上使用比较多

可伸缩:
  Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
  Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作
  Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作
  
附-BIO NIO AIO  
  Java BIO (blocking I/O): 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  Java NIO (non-blocking I/O): 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  Java AIO(NIO.2) (Asynchronous I/O) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

Redis Client Jedis 源码分析

主要包括Jedis初始化和Jedis连接请求两部分。

1. Jedis初始化

源码版本:jedis-2.9.0
Jedis初始化,每一个节点都包括节点插槽信息和集群信息,插槽取值范围:0-16383。
Jedis根据任何一个HostAndPort节点发送请求查询得集群和插槽分配信息。

        poolConfig.setMaxTotal(10);
        poolConfig.setMaxIdle(3);
        poolConfig.setMinIdle(1);
        poolConfig.setMaxWaitMillis(1000L);
        String property = "xxx.xxx.xxx.xxx:22400;xxx.xxx.xxx.xxx:22400";
        String[] jedisClusterNodes = property.split(",");
        nodes = new HashSet(jedisClusterNodes.length);
        String[] var5 = jedisClusterNodes;
        int var4 = jedisClusterNodes.length;
        
        for (int var3 = 0; var3 < var4; ++var3)
        {
            String jedisClusterNode = var5[var3];
            int index = jedisClusterNode.indexOf(":");
            nodes.add(new HostAndPort(jedisClusterNode.substring(0, index),
                Integer.valueOf(jedisClusterNode.substring(index + 1))));
        }
        jedisCluster = new JedisCluster(nodes, poolConfig);

JedisCluster 继承 BinaryJedisCluster 持有 JedisSlotBasedConnectionHandler(extends JedisClusterConnectionHandler), JedisClusterConnectionHandler 持有 JedisClusterInfoCache(保存集群节点和插槽信息)。

JedisClusterConnectionHandler 中初始化集群信息

    private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password) {
        Iterator var4 = startNodes.iterator();

        while(var4.hasNext()) {
            HostAndPort hostAndPort = (HostAndPort)var4.next();
            Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
            if(password != null) {
                jedis.auth(password);
            }

            try {
                this.cache.discoverClusterNodesAndSlots(jedis);
                break;
            } catch (JedisConnectionException var11) {
                ;
            } finally {
                if(jedis != null) {
                    jedis.close();
                }

            }
        }

    }

可以看到过程简单是:遍历Set,直到成功discoverClusterNodesAndSlots获取集群信息。

其中JedisClusterInfoCache中discoverClusterNodesAndSlots的实现:

    private final Map<String, JedisPool> nodes;
    private final Map<Integer, JedisPool> slots;
    private final ReentrantReadWriteLock rwl;
    private final Lock r;
    private final Lock w;
    private volatile boolean rediscovering;
    private final GenericObjectPoolConfig poolConfig;
    private int connectionTimeout;
    private int soTimeout;
    private static final int MASTER_NODE_INDEX = 2;
    
    public void discoverClusterNodesAndSlots(Jedis jedis) {
        this.w.lock();

        try {
            this.reset();
            List<Object> slots = jedis.clusterSlots();
            Iterator var3 = slots.iterator();

            while(true) {
                List slotInfo;
                do {
                    if(!var3.hasNext()) {
                        return;
                    }

                    Object slotInfoObj = var3.next();
                    slotInfo = (List)slotInfoObj;
                } while(slotInfo.size() <= 2);

                List<Integer> slotNums = this.getAssignedSlotArray(slotInfo);
                int size = slotInfo.size();

                for(int i = 2; i < size; ++i) {
                    List<Object> hostInfos = (List)slotInfo.get(i);
                    if(hostInfos.size() > 0) {
                        HostAndPort targetNode = this.generateHostAndPort(hostInfos);
                        this.setupNodeIfNotExist(targetNode);
                        if(i == 2) {
                            this.assignSlotsToNode(slotNums, targetNode);
                        }
                    }
                }
            }
        } finally {
            this.w.unlock();
        }
    }

jedis.clusterSlots()实际是向节点发送集群信息查询请求的过程,取得slots后进行初始化。
在这里插入图片描述

2. Jedis连接请求

下面再看下jedis.get(“key”)的过程。
首先是JedisCluster的直接调用,由一个实现execute方法的JedisClusterCommand类去运行run方法。

    public String get(final String key) {
        return (String)(new JedisClusterCommand<String>(this.connectionHandler, this.maxAttempts) {
            public String execute(Jedis connection) {
                return connection.get(key);
            }
        }).run(key);
    }

JedisClusterCommand.run方法调用的是runWithRetries,runWithRetries是主要过程为获取connection,然后调用connection.get获得返回。

    public T run(String key) {
        if(key == null) {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        } else {
            return this.runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
        }
    }
    private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
        if(attempts <= 0) {
            throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
        } else {
            Jedis connection = null;

            Object var7;
            try {
                if(asking) {
                    connection = (Jedis)this.askConnection.get();
                    connection.asking();
                    asking = false;
                } else if(tryRandomNode) {
                    connection = this.connectionHandler.getConnection();
                } else {
                    connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
                }

                Object var6 = this.execute(connection);
                return var6;
            } catch (JedisNoReachableClusterNodeException var13) {
                throw var13;
            } catch (JedisConnectionException var14) {
                this.releaseConnection(connection);
                connection = null;
                if(attempts <= 1) {
                    this.connectionHandler.renewSlotCache();
                    throw var14;
                }

                var7 = this.runWithRetries(key, attempts - 1, tryRandomNode, asking);
                return var7;
            } catch (JedisRedirectionException var15) {
                if(var15 instanceof JedisMovedDataException) {
                    this.connectionHandler.renewSlotCache(connection);
                }

                this.releaseConnection(connection);
                connection = null;
                if(var15 instanceof JedisAskDataException) {
                    asking = true;
                    this.askConnection.set(this.connectionHandler.getConnectionFromNode(var15.getTargetNode()));
                } else if(!(var15 instanceof JedisMovedDataException)) {
                    throw new JedisClusterException(var15);
                }

                var7 = this.runWithRetries(key, attempts - 1, false, asking);
            } finally {
                this.releaseConnection(connection);
            }

            return var7;
        }
    }

JedisSlotBasedConnectionHandler根据插槽获取JedisPool连接的实现为(根据slot号在JedisClusterInfoCache的slots哈希表中获取JedisPool):
在这里插入图片描述
至于怎样在JedisPool调用getResource()获得Jedis这里不展开介绍。

Jedis持有Client(extends BinaryClient(extends Connection))。主要的socket请求和协议在Connection和Protocol这两个类里,下面只截取关键部分,后面补充具体实现或先参考:
Redis Java Client Jedis 源码分析

描述直接引自 Redis Java Client Jedis 源码分析
和Redis Server通信的协议规则都在redis.clients.jedis.Protocol这个类中,主要是通过对RedisInputStream和RedisOutputStream对读写操作来完成。
命令的发送都是通过redis.clients.jedis.Protocol的sendCommand来完成的,就是对RedisOutputStream写入字节流
和Redis Sever的Socket通信是由 redis.clients.jedis.Connection 实现的
Connection 中维护了一个底层Socket连接和自己的I/O Stream 还有Protocol
I/O Stream是在Connection中Socket建立连接后获取并在使用时传给Protocol的
Connection还实现了各种返回消息由byte转为String的操作。

    protected Connection sendCommand(Command cmd, byte[]... args) {
        try {
            this.connect();
            Protocol.sendCommand(this.outputStream, cmd, args);
            ++this.pipelinedCommands;
            return this;
        } catch (JedisConnectionException var6) {
            JedisConnectionException ex = var6;

            try {
                String errorMessage = Protocol.readErrorLineIfPossible(this.inputStream);
                if(errorMessage != null && errorMessage.length() > 0) {
                    ex = new JedisConnectionException(errorMessage, ex.getCause());
                }
            } catch (Exception var5) {
                ;
            }

            this.broken = true;
            throw ex;
        }
    }

    public void connect() {
        try {
            this.needAuth = false;
            this.pureConnect();
            this.sendCommand(Command.ECHO, new String[]{" "});
            this.getBinaryBulkReply();
        } catch (JedisDataException var6) {
            if(!var6.getMessage().contains("NOAUTH")) {
                throw var6;
            }

            this.needAuth = true;
        }

        if(this.needAuth && !this.authed) {
            try {
                boolean authSuccess = false;
                int tryedTimes = 0;

                while(!authSuccess) {
                    try {
                        this.authext();
                        authSuccess = true;
                    } catch (JedisConnectionException var4) {
                        if(tryedTimes > 2) {
                            throw var4;
                        }

                        ++tryedTimes;
                        this.disconnect();
                        this.pureConnect();
                    }
                }
            } catch (RuntimeException var5) {
                this.broken = true;
                throw var5;
            }
        }

    }
    public void pureConnect() {
        if(!this.isConnected()) {
            try {
                this.socket = new Socket();
                this.socket.setReuseAddress(true);
                this.socket.setKeepAlive(true);
                this.socket.setTcpNoDelay(true);
                this.socket.setSoLinger(true, 0);
                this.socket.connect(new InetSocketAddress(this.host, this.port), this.connectionTimeout);
                this.socket.setSoTimeout(this.soTimeout);
                this.outputStream = new RedisOutputStream(this.socket.getOutputStream());
                this.inputStream = new RedisInputStream(this.socket.getInputStream());
            } catch (IOException var2) {
                this.broken = true;
                throw new JedisConnectionException(var2);
            }
        }

    }

Jedis/JedisPool/JedisFactory(连接池)的相关代码分析待补充。

集群信息查询:

8.96.143.20:22400> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:10
cluster_my_epoch:10
cluster_stats_messages_sent:2883354
cluster_stats_messages_received:2882816
8.96.143.20:22400>
8.96.143.20:22400>
8.96.143.20:22400> cluster keyslot hqw
(integer) 2002
8.96.143.20:22400>
8.96.143.20:22400> cluster nodes
e59088d7208f2015fe15f46082c3d5c0028d8d96 8.96.143.22:22401 slave 7a4b7ffe2e577b4cd37b66ed98653e4577202c76 0 1557138320121 6 connected
9078cd6391adb1b6577bd7282f78e95e0088c936 8.96.143.20:22401 slave 875d77a9dea9700289ba648f872ecd63a7c804c0 0 1557138319112 5 connected
db13d949a0a750e72bc36f267dcceb95f8973786 8.96.143.21:22401 slave d7fdad5d6dd63804bdbe2b29ddf9a3e7baffc53e 0 1557138321126 10 connected
7a4b7ffe2e577b4cd37b66ed98653e4577202c76 8.96.143.21:22400 master - 0 1557138322138 3 connected 5461-10921
875d77a9dea9700289ba648f872ecd63a7c804c0 8.96.143.22:22400 master - 0 1557138323138 5 connected 10922-16383
d7fdad5d6dd63804bdbe2b29ddf9a3e7baffc53e 8.96.143.20:22400 myself,master - 0 0 10 connected 0-5460
8.96.143.20:22400>
8.96.143.20:22400>
8.96.143.20:22400> cluster slots
1) 1) (integer) 5461
   2) (integer) 10921
   3) 1) "8.96.143.21"
      2) (integer) 22400
   4) 1) "8.96.143.22"
      2) (integer) 22401
2) 1) (integer) 10922
   2) (integer) 16383
   3) 1) "8.96.143.22"
      2) (integer) 22400
   4) 1) "8.96.143.20"
      2) (integer) 22401
3) 1) (integer) 0
   2) (integer) 5460
   3) 1) "8.96.143.20"
      2) (integer) 22400
   4) 1) "8.96.143.21"
      2) (integer) 22401
8.96.143.20:22400>
8.96.143.20:22400>

Redis使用PipeLine

待补充。Pipeline的源码分析待补充。

pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。性能提升的原因主要是TCP连接中减少了“交互往返”的时间。

            HashMap<String, String> map = new HashMap<>();
            map.put("1", "1");
            map.put("2", "2");
            map.put("3", "3");
            map.put("4", "4");
            map.put("5", "5");
            map.put("6", "6");
            
            ClusterBatch pipe = jedis.getPipeline();
            
            HashMap<String, Response<String>> newMap = new HashMap<>();
            
            for (Map.Entry<String, String> entry : map.entrySet())
            {
                newMap.put(entry.getKey(), pipe.get(entry.getKey()));
            }
            
            pipe.sync();
            
            for (Map.Entry<String, Response<String>> entry : newMap.entrySet())
            {
                Response<String> sResponse = entry.getValue();
                long temp = Long.parseLong(map.get(entry.getKey())) + Long.parseLong(sResponse.get());
                map.put(entry.getKey(), Long.toString(temp));
            }
            
            for (Map.Entry<String, String> entry : map.entrySet())
            {
                pipe.set(entry.getKey(), entry.getValue());
            }
            pipe.sync();

参考博客:
Redis Java Client Jedis 源码分析
Redis 3.2.8 源码剖析注释

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值