Jedis是Redis官方推荐的Java连接工具。
Jedis通过Tcp协议来连接Redis,并有一套特有的解析协议,Jedis通过socket连接Redis服务,每个连接服务称为Jedis(类名),Jedis类又包装了Client,Transaction和pipeline,每个Jedis实例都支持三种操作方式:普通命令操作,事务操作,管道操作。其中普通命令操作和事务操作是Redis服务实现的,Jedis将对应的命令发送给服务即可,而管道操作是由客户端实现的。
按照Redis的部署方式,Jedis的连接方式可以又分为单机模式,集群模式和分片模式,其中单机模式和集群模式也是Redis服务实现的,分片模式是Jedis客户端实现的
一. 普通命令操作,核心类:Client.class
- Connection类
Connection类用于支持单个Redis服务连接,并且支持SSL连接。Connection实现了Closeable,是因为他包装了Socket,其成员变量引用了Socket的输出流和输入流。其成员变量如下
private
connect()方法用于连接单个Redis服务,调用这个方法用于初始化socket和inputStream和outPutStream,他会与Redis服务建立一个长连接,用RedisOutputStream和RedisInputStream对socket的TelnetOutputStream和TelnetInputStream做引用包装,并配置一块缓存,默认大小为8M。
这个方法并不是在实例构造时调用,而是在需要发送命令时才调用,相当于懒加载。如下:
public
Connection还提供了三个基础的命令发送方法,子类可以基于这三个方法丰富扩展更多的方法。这三个命令的区别是入参不同,Connection会把他们处理成标准的输入格式(byte数组),然后通过协议类Protocol将命令写入到outStream中,但是不会立即推流,等待调用flush()方法或者缓存区满才推流。
//cmd是需要发送的命令,例如:get/set等,args是命令的入参
flush()的作用是推流,Redis服务响应的结果会缓存在inputstream中,用户去inputstream取结果就可以了。
protected
2. BinaryClient类
BinaryClient继承了Connection类,相比父类,它提供了密码鉴权功能,并且丰富了命令方法
//重写了父类的connect()方法,并添加了密码鉴权服务
//这些方法都是通过父类的sendCommand扩展的,特点是入参都是byte数组
3. Client类
Client类继承了BinaryClient类并实现了Commands接口。它的作用是基于父类的方法上进一步拓展,与父类有很大不同的是,父类的拓展方法入参都是byte数组,而Client的的拓展方法都是常用的封装数据类型。
//子类对封装数据类型进行了编码,然后调用父类的方法
二. 管道操作,核心类Pipeline.class
Redis本身并不支持管道操作,Jedis是通过客户端实现的。Jedis的管道和Linux的管道不同的是,上一次命令的执行结果不能作为下一次命令的入参,因为他是将增量累积的命令一次性发送给Redis客户端,由Redis按照顺序执行后统一返回结果,然后再读取,相当于一个有序的操作集。
Jedis的管道实现是基于一个有序先进先出的有序队列,Pipeline每次新增命令时,Pipeline就将命令编码后的byte数组增量写入输出流,并在队列中增加一个Reponse类,用于标记此次命令的返回类型和获取最终的结果。
- 管道类Queable
Queable使用了队列,Queue的类型为LinkedList
/**
管道内每次新增一条命令,都会调用getResponse()方法,pipelinedResponses中就会多一个Response<T>实例,这个T就是这次命令执行成功后返回的数据类型。
//每次新增命令后都会调用这个方法
Reponse类的设计是比较有特点的,他有三个重要的成员变量:
//build前的数据
data是推流后从inputStream中截取的数据,response则是调用builder(),将data处理成特定的数据类型,而builder就是处理器,Builder是一个抽象类有且只有一个抽象方法build(),这个方法由子类去实现,builder通过Response的构造方法入参得到。
2. PipelineBase
PipelineBase是一个抽象类,继承自Queable类。
上面说到,每次新增命令都要调用Queable的getResponse,这是两次方法调用,于是PipelineBase就对这个逻辑进行了封装,PipelineBase封装后的方法如下:
@Override
重点来了,PipelineBase是需要先获取Client,然后才能写入命令,Client从哪里来?答案是进一步抽象,由抽象方法获得,让这个类拥有更高的拓展性,于是,PipelineBase不仅仅可以用于队列,也可以用于事务,还可以用于分片模式!
PipelineBase获取Client的抽象方法
//交给子类去实现
3. MultiKeyPipelineBase
MultiKeyPipelineBase也是一个抽象类,继承自PipelineBase,它相比PipelineBase的不同是多了一个成员变量,但是不提供其的set方法,其他大体无异。
//看到protected就应该警觉,大概率是让子类去操作的
4. Pipeline
Pipeline就是最终类了,Pipeline算是MultiKeyPipelineBase的包装类了,他继承了MultiKeyPipelineBase的同时也有MultiKeyPipelineBase的成员变量
Pipeline提供了Client的set方法,并实现了get方法
public
当用户完成了所有命令的写入操作,如何获取返回结果呢?
通过Connection的getMany()方法:
public
输入流是一整块,里面包含了多次命令的返回结果,如何区分开它们呢?协议通过关键字划分拆开:
private
以处理无返回值的processStatusCodeReply(is)为例:
public
这里以一个简单的例子来看,Jedis给Redis服务发送了什么,Jedis又从Redis接收到了什么内容:
向管道中插入两条命令,设置“foo”的value为“bar”,然后获取“foo”的值,如下:
Pipeline
发送出去的命令如下:
*
接收到的命令如下:
+OK
$3
bar
其实管道操作也是支持事务的,因为他也提供了开启事务、取消事务和结束事务的方法,通过给Redis发送对应的命令实现:
//开启事务
//结束事务
//取消事务
三. 事务操作,核心类Transaction.class
Transaction继承自MultiKeyPipelineBase,MultiKeyPipelineBase的子类一共有两个,一个是它,还有一个是Pipeline,Transaction和Pipeline的实现如出一辙,Transaction有一个标志事务是否已经结束的标志,在实例构造时设置为true
//标志事务是否已经结束
Transaction只提供对inTransaction设置为false的方法,一旦设置为false了就不能再变为true了,所以,一个Transaction只能执行一个事务,执行完就没用了,而Pipeline是可以复用的。
//Transaction的exec方法
四. 分片模式,核心类ShardedJedis.class
上面说了,单个连接可以做普通命令操作,管道操作,事务操作,在实际使用场景中,开发人员并不会单一使用某一种操作,大多数情况下都是混着用,所以需要有一个实例能够通知支持这三种使用操作,这样就会方便很多,Jedis类就封装了这三种操作,它的原理是每种操作都有一个对应的成员变量,Jedis类是一个非常重要的类,分片模式和集群模式都对它有依赖。
Jedis类的三个成员变量如下:
//普通命令
Redis分片模式是由客户端Jedis来实现的,Redis本身并不支持这种功能。
ShardedJedis在初始化时,会基于每个Redis服务的信息计算一遍hash(这个hash是一致性hash),然后将计算出来的hash和服务信息放在一个TreeMap中,hash为key,服务信息为value,这是第一个容器,然后每个服务信息都会生成一个Jedis实例,放在一个LinkedHashMap中,服务信息为key,Jedis实例为value,这是第二个容器。当我们需要调用命令时,ShardedJedis会取这个命令的key,例如set("a","b"),就会取a,然后对key做一次hash,用key去第一个容器里面拿服务信息,然后根据服务信息去第二个容器里面拿Jedis实例。
- Sharded
切片模式下Sharded主要提供基础服务支持,hash计算,上面说的两个容器也都是它提供的,这些都体现在它的成员变量中:
//k是ShardInfo的哈希值,v是对应的ShardInfo
Sharded在实例构造时,就会初始化两个容器,一旦完成初始化,容器大小不再变化,不提供动态拓展的方法。初始化方法如下:
private
计算key获取服务信息的方法如下:
public
根据服务信息获取Jedis实例的方法如下:
//获取key对应的服务信息,如果未匹配到,就取第一个
2. BinaryShardedJedis
BinaryShardedJedis主要是提供扩展的服务,什么get,set这类方法,这个类会做一层封装,特点是入参都是byte数组,例如:
@Override
3. ShardedJedis
ShardedJedis继承自BinaryShardedJedis,上面说了BinaryShardedJedis的入参都是byte数组,而ShardedJedis的入参都是包装数据类型,其他区别不大,例如:
@Override
五. 集群模式,核心类JedisCluster.class
Redis集群内一共有16384个solt,中文名是插槽,这些插槽均匀分布在所有的Node上,每个Node都有数量相等的Slot,当向Redis中set一个k-v时,Redis会通过crc16算法计算一次结果,然后取16384的余,确保key不论是什么结果都能落在0-16384上,通过slot再去对应的Node进行命令操作,有了这个基本思想之后看源码就会简单很多。
- JedisClusterInfoCache
JedisClusterInfoCache类是用来存储Node信息,Slot信息,和连接池的,通过两个类型为HashMap的成员变量来缓存,如下:
//key是node计算出来的hash值,value是这个node的连接池
那么这些信息是怎么被放进去的呢,来看看他的初始化方法:
private
从上面可以看到,只循环了一次,调用一次discoverClusterNodesAndSlots方法就可以拿到所有的信息了,深入一点,discoverClusterNodesAndSlots方法如下:
public
上面的代码表示,它向Redis服务发送了命令请求到了Slot信息,所以循环一遍就可以拿到所有的信息了
2. JedisClusterConnectionHandler
JedisClusterConnectionHandler是JedisClusterInfoCache的包装类,它的主要功能是获取connection,其他没有特殊的了
abstract
3. BinaryJedisCluster
BinaryJedisCluster又是JedisClusterConnectionHandler的包装类,看这么名字也大概知道是干嘛的了,提供以byte数组为入参的Redis命令,如下:
@Override
注意上面用了一个特殊的设计模式,每次调用get,set等这些操作,都会new 一个JedisClusterCommand实例,重写它的execute方法,然后调用runBinary方法,为什么呢?每个命令都new一下,不吃资源吗,来看一下它的runBinary方法:
public
看到这里就恍然大悟了,JedisClusterCommand这个类是带着重试机制的,它不感知命令的内容,只知道如果execute方法出错了,就要重试,所以它的execute方法给子类去实现,这样还有一个好处,由于每条命令都是new了一个实例,就可以隔离每次操作了,这是很特别的重试实现机制。
4. JedisCluster
JedisCluster是BinaryJedisCluster的子类,这两个最大的区别就是,一个方法的入参是byte数组,一个是包装数据类型,其他的没区别了。
@Override
总结
- connection是线程不安全的,如果向connection添加了命令而没有及时推流,就可能会存在多个命令混在一起的情况。
- Jedis是如何反序列化Map的:Jedis接收一个byte[][],然后可以转成List<String>,其中偶数为key,奇数为value,例如,list.get(0)为key,list.get(1)为value。
- Jedis有监控吗?有,但是没有什么功能,JedisMonitor只在BinaryJedis.class中用了一下,也仅仅是用来查询连接状态。
- Jedis有哨兵模式吗?有,类为JedisSentinelPool.class,但是整个项目都没有用到这个类,只有测试用例中有使用。
- Jedis有连接池吗?有,Jedis使用apache-common-pool2来管理连接对象,在集群模式下才会使用连接池,其他模式下不会使用。