这篇文章的主要目的是因为redis的C语言客户端hiredis不支持集群功能,所以想借鉴jedis的集群功能自己实现。


由于项目中使用Redis,所以使用它的Java客户端Jedis也有大半年的时间(后续会分享经验)。

 最近看了一下源码,源码清晰、流畅、简洁,学到了不少东西,在此分享一下。


(源码地址:https://github.com/xetorthio/jedis

 

协议

 

和Redis Server通信的协议规则都在redis.clients.jedis.Protocol这个类中,主要是通过对RedisInputStream和RedisOutputStream对读写操作来完成。


命令的发送都是通过redis.clients.jedis.Protocol的sendCommand来完成的,就是对RedisOutputStream写入字节流 




Java代码  

javayml redis配置 java redis client_jedis


从这里可以看出redis的命令格式

1. private void sendCommand(final RedisOutputStream os, final byte[] command,  
2. final byte[]... args) {  
3. try
4.             os.write(ASTERISK_BYTE);  
5. 1);  
6.             os.write(DOLLAR_BYTE);  
7.             os.writeIntCrLf(command.length);  
8.             os.write(command);  
9.             os.writeCrLf();  
10.   
11. for (final byte[] arg : args) {  
12.                 os.write(DOLLAR_BYTE);  
13.                 os.writeIntCrLf(arg.length);  
14.                 os.write(arg);  
15.                 os.writeCrLf();  
16.             }  
17. catch
18. throw new
19.         }  
20.     }</span></span>


[*号][消息元素个数]\r\n              ( 消息元素个数 = 参数个数 + 1个命令)

[$号][命令字节个数]\r\n

[命令内容]\r\n

[$号][参数字节个数]\r\n

[参数内容]\r\n

[$号][参数字节个数]\r\n

[参数内容]\r\n 

 

返回的数据是通过读取RedisInputStream  进行解析处理后得到的

 


Java代码  

javayml redis配置 java redis client_jedis



    1. private Object process(final
    2. try
    3. byte
    4. if
    5.             processError(is);  
    6. else if
    7. return
    8. else if
    9. return
    10. else if
    11. return
    12. else if
    13. return
    14. else
    15. throw new JedisConnectionException("Unknown reply: " + (char) b);  
    16.         }  
    17. catch
    18. throw new
    19.     }  
    20. return null;  
    21. }


    通过返回数据的第一个字节来判断返回的数据类型,调用不同的处理函数

    [-号] 错误信息

    [*号] 多个数据         结构和发送命令的结构一样

    [:号] 一个整数

    [$号] 一个数据         结构和发送命令的结构一样

    [+号] 一个状态码

     

    连接

     

    和Redis Sever的Socket通信是由 redis.clients.jedis.Connection 实现的

     

    Connection 中维护了一个底层Socket连接和自己的I/O Stream 还有Protocol

    I/O Stream是在Connection中Socket建立连接后获取并在使用时传给Protocol的

    Connection还实现了各种返回消息由byte转为String的操作

     


    Java代码  

    javayml redis配置 java redis client_jedis



      1. private
      2. private int
      3. private
      4. private Protocol protocol = new
      5. private
      6. private
      7. private int pipelinedCommands = 0;  
      8. private int


       

       

       


      Java代码  

      javayml redis配置 java redis client_jedis



        1. public void
        2. if
        3. try
        4. new
        5. new
        6.             socket.setSoTimeout(timeout);  
        7. new
        8. new
        9. catch
        10. throw new
        11.         }  
        12.     }  
        13. }


         可以看到,就是一个基本的Socket

         这里分享个经验,timeout这个参数默认是2000,我做的项目中有部分是离线运算的,如果读取比较大的数据(大Set 大List之类的)有可能会超过这个时间,可以在JedisPool的构造参数中增大这个值。在线服务一般不要修改。

         

        原生客户端

         

        redis.clients.jedis.BinaryClient 继承 Connection, 封装了Redis的所有命令(http://redis.io/commands)

        从名子可以看出 BinaryClient  是Redis客户端的二进制版本,参数都是byte[]的

        BinaryClient 是通过Connection的sendCommand 调用Protocol的sendCommand 向Redis Server发送命令


        Java代码  

        javayml redis配置 java redis client_jedis


        1. public void get(final byte[] key) {  
        2.     sendCommand(Command.GET, key);  
        3. }

         

        redis.clients.jedis.Client可以看成是BinaryClient 的高级版本,函数的参数都是String int long 这类的,并由redis.clients.util.SafeEncoder 转成byte后 再调用BinaryClient 对应的函数

         


        Java代码  

        javayml redis配置 java redis client_jedis



        1. public void get(final
        2.     get(SafeEncoder.encode(key));  
        3. }


         

        这二个client只完成了发送命令的封装,并没有处理返回数据

         

        Jedis客户端

         

        我们平时用的基本都是由redis.clients.jedis.Jedis类封装的客户端

        Jedis是通过对Client的调用, 完成命令发送和返回数据  这个完整过程的

         

        以GET命令为例,其它命令类似 

        Jedis中的get函数如下

         


        Java代码  

        javayml redis配置 java redis client_jedis


        1. public String get(final
        2.     checkIsInMulti();  
        3.     client.sendCommand(Protocol.Command.GET, key);  
        4. return
        5. }

         checkIsInMulti();

        是进行无事务检查 Jedis不能进行有事务的操作  带事务的连接要用redis.clients.jedis.Transaction类

         

        client.sendCommand(Protocol.Command.GET, key);

        调用Client发送命令

         

        return client.getBulkReply();

        处理返回值    

         

        分析到这里   一个Jedis客户端的基本实现原理应该很清楚了

         

        连接池

         

        在实现项目中,要使用连接池来管理Jedis的生命周期,满足多线程的需求,并对资源合理使用。

         

        jedis有两个连接池类型, 一个是管理 Jedis, 一个是管理ShardedJedis(jedis通过java实现的 多Redis实例的自动分片功能,后面会分析)

         

        他们都是Pool<T>的不同实现 

         

         


        Java代码  

        javayml redis配置 java redis client_jedis



          1. public abstract class
          2. private final
          3.   
          4. public Pool(final
          5.             PoolableObjectFactory factory) {  
          6. this.internalPool = new
          7.     }  
          8.   
          9. @SuppressWarnings("unchecked")  
          10. public
          11. try
          12. return
          13. catch
          14. throw new
          15. "Could not get a resource from the pool", e);  
          16.         }  
          17.     }  
          18. ......  
          19. ......


           

           从代码中可以看出,Pool<T>是通过 Apache Commons Pool 中的GenericObjectPool这个对象池来实现的

          (Apache Commons Pool内容可参考http://phil-xzh.iteye.com/blog/320983 )

           

          在JedisPool中,实现了一个符合 Apache Commons Pool 相应接口的JedisFactory,GenericObjectPool就是通过这个JedisFactory来产生Jedis对你的

           

          其实JedisPoolConfig也是对Apache Commons Pool 中的Config进行的一个封装

           

          当你在调用 getResource 获取Jedis时, 实际上是Pool<T>内部的internalPool调用borrowObject()借给你了一个实例 

          而internalPool 这个 GenericObjectPool 又调用了 JedisFactory 的 makeObject() 来完成实例的生成 (在Pool中资源不够的时候)

           

           


          Java代码  

          javayml redis配置 java redis client_jedis



            1. public Object makeObject() throws
            2. final
            3. if (timeout > 0) {  
            4. new Jedis(this.host, this.port, this.timeout);  
            5. else
            6. new Jedis(this.host, this.port);  
            7.     }  
            8.   
            9.     jedis.connect();  
            10. if (null != this.password) {  
            11. this.password);  
            12.     }  
            13. return
            14. }


             

             

            客户端的自动分片

             

            从这个结构图上可以看出 ShardedJedis 和 BinaryShardedJedis 正好是  Jedis 和  BinaryJedis  的分片版本

             

            其实它们都是 先获取hash(key)后对应的 Jedis 再有这个Jedis进行操作

             

             


            Java代码  

            javayml redis配置 java redis client_jedis



              1. public
              2.     Jedis j = getShard(key);  
              3. return
              4. }



               

              分片逻辑都是在 Sharded<R, S extends ShardInfo<R>> 中实现的

               

              它的构造函数如下 


              Java代码  

              javayml redis配置 java redis client_jedis



              1. public
              2. this.algo = algo;  
              3. this.tagPattern = tagPattern;  
              4.     initialize(shards);  
              5. }


              shards是一组ShardInfo, 具体实现是JedisShardInfo, 每个里面记录分片信息和权重,并负责完成分片对应Jedis实例创建

               

              Sharded的初始化和一致性哈希(Consistent Hashing)的思想是一样的,但这个并不能实现节点的动态变更,只能体现出节点的 权重分配

               


              Java代码  

              javayml redis配置 java redis client_jedis



              1. nodes = new


              这个nodes就是一个虚拟的结点分布环,由TreeMap实现,保证按Key有序,Value就是对应的ShardInfo

               

               


              Java代码  

              javayml redis配置 java redis client_jedis



              1. 160


              根据每个shard的weight值,默认是1,生成160倍的虚拟节点,hash后放到nodes中,也就是分布到环上

               

               


              Java代码  

              javayml redis配置 java redis client_jedis



                1. resources.put(shardInfo, shardInfo.createResource());


                每个shardInfo对应的jedis,也就是真正的操作节点,放到resources中

                 

                 


                Java代码  

                javayml redis配置 java redis client_jedis



                  1. private void
                  2. new
                  3.   
                  4. for (int i = 0; i != shards.size(); ++i) {  
                  5. final
                  6. if (shardInfo.getName() == null)  
                  7. for (int n = 0; n < 160
                  8. this.algo.hash("SHARD-" + i + "-NODE-"
                  9.             }  
                  10. else
                  11. for (int n = 0; n < 160
                  12. this.algo.hash(shardInfo.getName() + "*"
                  13.             }  
                  14.         resources.put(shardInfo, shardInfo.createResource());  
                  15.     }  
                  16. }


                   

                  通过Key获取对应的jedis时,先对key进行hash,和前面初始化节点环时,使用相同的算法

                  再从nodes这个虚拟的环中取出 大于等于 这个hash值的第一个节点(shardinfo),没有就取nodes中第一个节点(所谓的环 其实是逻辑上实现的)

                   

                  最后从resources中取出jedis来

                   

                   


                  Java代码  

                  javayml redis配置 java redis client_jedis



                  1. public S getShardInfo(byte[] key) {  
                  2.     SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));  
                  3. if (tail.size() == 0) {  
                  4. return
                  5.     }  
                  6. return
                  7. }


                  Java代码  

                  javayml redis配置 java redis client_jedis



                    1. public
                    2. return
                    3. }