项目组最近准备将Redis由哨兵模式组网切换到集群组网,切换后应用访问redis时报错,“Pipeline is currently not supported for JedisClusterConnection.”。
初步定为Jedis在集群模式下不支持pipeline。
org.springframework.data.redis.connection.jedis.JedisClusterConnection#openPipeline
@Override
public void openPipeline() {
throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection.");
}
pipeline解析
pipeline并不是redis的设计,只要是基于TCP的长连接,而且访问协议为简单请求/应答,都能去实现pipeline。
比如HTTP中加入keepalive后,就能实现pipeline。而且浏览器也是这么优化的。
如上图所示,客户端一次发送一批命令给服务端,客户端一次解析多个响应结果。
因为有定义应用层协议,无论是请求还是响应,尽管多个报文的字节流挤在一起发送或者接收,可以通过应用层协议去将一帧帧报文解析出来,而不影响各自的处理。
上图为pipeline通用处理过程,具体实现起来,并不严格按照上述流程。比如:
- openPipeline、closePileline这两个动作并不是必须的
对于应用层协议来讲,它并不感知这两个动作。这个仅仅只是客户端代码为了更好的去复用单条指令执行的逻辑而已。openPipeline可以让sdk知道此时是pipeline模式,后续所有的命令都不要发送,等调用closePipeline后一次性发送。 - 客户端并不一定是在closePipeline的时候一次性发送所有的指令,这个得看具体的客户端实现,比如Jedis就不是,可以看下面的Jedis是如何实现Pipeline的。
List源码解析
org.springframework.data.redis.core.RedisTemplate#executePipelined(org.springframework.data.redis.core.RedisCallback>, org.springframework.data.redis.serializer.RedisSerializer>)
代码有省略
public Listconnection.openPipeline()executePipelined(RedisCallback> action, @Nullable RedisSerializer> resultSerializer) { return execute((RedisCallback >) connection -> { connection.openPipeline(); boolean pipelinedClosed = false; try { Object result = action.doInRedis(connection); List
closePipeline = connection.closePipeline(); pipelinedClosed = true; return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer); } finally { if (!pipelinedClosed) { connection.closePipeline(); } } }); }
因为JedisClusterConnection不支持openPipeline,因此先分析一下JedisConnection的源码,可以看到仅仅只是创建了一个Pipeline,用于容纳后续响应。
org.springframework.data.redis.connection.jedis.JedisConnection#openPipeline
public void openPipeline() {
if (pipeline == null) {
pipeline = jedis.pipelined();
}
}
redis.clients.jedis.BinaryJedis#pipelined
public Pipeline pipelined() {
pipeline = new Pipeline();
pipeline.setClient(client);
return pipeline;
}
action.doInRedis
action为示例代码中的RedisCallback,我们以最简单的connection.get(“aaa”.getBytes());为例进行分析。
可以看到代码最终走到了org.springframework.data.redis.connection.jedis.JedisStringCommands#get
可以看到简单的get命令,会根据是否开启pipeline来决定不同的逻辑。
// 代码有省略
public byte[] get(byte[] key) {
if (isPipelined()) {
pipeline(connection.newJedisResult(connection.getRequiredPipeline().get(key)));
return null;
}
return connection.getJedis().get(key);
}
redis.clients.jedis.PipelineBase#get(byte[])
public Responseget(byte[] key) { getClient(key).get(key); return getResponse(BuilderFactory.BYTE_ARRAY); }
注意
可以看到BinaryClient#get方法仅仅只是发送命令,并不会去读取相应。
另外如前面所说,jedis并不是命令集中起来一次性发送,因为在RedisCallback中调用connection.get(“aaa”.getBytes())时就会走到这里将命令发送出去
redis.clients.jedis.BinaryClient#get
public void get(final byte[] key) {
sendCommand(Command.GET, key);
}
可以看到getResponse并不是真正的去读取socket里面的数据去获取响应结果,仅仅只是在pipelinedResponses中放了一个占位符,这样调用connection.closePipeline()时,就知道要获取多少个响应结果。
redis.clients.jedis.Queable#getResponse
protectedconnection.closePipeline()解析Response getResponse(Builder builder) { Response lr = new Response (builder); pipelinedResponses.add(lr); return lr; }
org.springframework.data.redis.connection.jedis.JedisConnection#closePipeline
该方法比较简单,不做详细介绍,主要逻辑在convertPipelineResults中
public ListclosePipeline() { if (pipeline != null) { try { return convertPipelineResults(); } finally { pipeline = null; pipelinedResults.clear(); } } return Collections.emptyList(); }
org.springframework.data.redis.connection.jedis.JedisConnection#convertPipelineResults
代码有删减,重点在getRequiredPipeline().sync()这一行;这一行代码会去根据pipeline中的命令数去服务端读取对应数量的响应。其他的逻辑仅仅只是判断是否需要对内容做转换不,比如将字符串类型的结果反序列化为对象。
private ListconvertPipelineResults() { List results = new ArrayList<>(); getRequiredPipeline().sync(); Exception cause = null; for (JedisResult result : pipelinedResults) { try { Object data = result.get(); if (!result.isStatus()) { results.add(result.conversionRequired() ? result.convert(data) : data); } } } return results; }
redis.clients.jedis.Pipeline#sync
重点内容在client.getAll();
public void sync() {
if (getPipelinedResponseLength() > 0) {
List unformatted = client.getAll();
for (Object o : unformatted) {
generateResponse(o);
}
}
}
redis.clients.jedis.Connection#getAll()
从下面源码来看,跟前面写的内容有些出入,调用connection.closePipeline()时,是根据之前那执行sendCommand函数对pipelinedCommands进行的计数,才知道要读取多少个响应结果。
public ListgetAll() { return getAll(0); } public List getAll(int except) { List all = new ArrayList (); flush(); while (pipelinedCommands > except) { // readProtocolWithCheckingBroken就是根据redis设计的报文格式去读取一次响应的结果 all.add(readProtocolWithCheckingBroken()); pipelinedCommands--; } return all; } // 发送命令的时候会对pipelinedCommands进行计数 protected Connection sendCommand(final Command cmd, final byte[]... args) { try { connect(); Protocol.sendCommand(outputStream, cmd, args); pipelinedCommands++; return this; } }
至此jedis对pipeline的实现的主要逻辑已经分析清楚,由上可以看到整个pipeline分三步
- 执行connection.openPipeline开启pipeline,主要就是创建Pipeline这个类,用于后面存放响应的占位符。
- 执行回调,回调里面执行的所有命令都会直接发送到服务端,并不会集中起来一起发送。只是发送完命令后,不会立即读取服务端的响应。
- 执行connection.closePipeline,根据执行执行的命令计数,去读取n次命令的结果。
待续



