Jedis分析一-Pipeline is currently not supported for JedisClusterConnection.

背景

项目组最近准备将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。而且浏览器也是这么优化的。
在这里插入图片描述

如上图所示,客户端一次发送一批命令给服务端,客户端一次解析多个响应结果。
因为有定义应用层协议,无论是请求还是响应,尽管多个报文的字节流挤在一起发送或者接收,可以通过应用层协议去将一帧帧报文解析出来,而不影响各自的处理,另外TCP协议能够保证报文的时序,应用层的响应顺序能够与请求保持一致。
上图为pipeline通用处理过程,具体实现起来,并不严格按照上述流程。比如:

  1. openPipeline、closePileline这两个动作并不是必须的
    对于应用层协议来讲,它并不感知这两个动作。这个仅仅只是客户端代码为了更好的去复用单条指令执行的逻辑而已。openPipeline可以让sdk知道此时是pipeline模式,后续所有的命令都不要发送,等调用closePipeline后一次性发送。
  2. 客户端并不一定是在closePipeline的时候一次性发送所有的指令,这个得看具体的客户端实现,比如Jedis就不是,可以看下面的Jedis是如何实现Pipeline的。

jedis是如何实现Pipline的

RedisTemplate中pipeline的用法

List<Object> result = redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) {
        // 这里可以使用connection做任何其支持的操作,但是得到的返回结果都是null
        ...  
        //  doInRedis必须返回null,不然会抛异常,作者估计也是想提醒开发者,这里的返回值没有任何意义   
        return null;
    }
});

源码解析

org.springframework.data.redis.core.RedisTemplate#executePipelined(org.springframework.data.redis.core.RedisCallback<?>, org.springframework.data.redis.serializer.RedisSerializer<?>)
代码有省略

	public List<Object> executePipelined(RedisCallback<?> action, @Nullable RedisSerializer<?> resultSerializer) {

		return execute((RedisCallback<List<Object>>) connection -> {
			connection.openPipeline();
			boolean pipelinedClosed = false;
			try {
				Object result = action.doInRedis(connection);
				List<Object> closePipeline = connection.closePipeline();
				pipelinedClosed = true;
				return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer);
			} finally {
				if (!pipelinedClosed) {
					connection.closePipeline();
				}
			}
		});
	}
connection.openPipeline()

因为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 Response<byte[]> get(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

  protected <T> Response<T> getResponse(Builder<T> builder) {
    Response<T> lr = new Response<T>(builder);
    pipelinedResponses.add(lr);
    return lr;
  }

connection.closePipeline()解析

org.springframework.data.redis.connection.jedis.JedisConnection#closePipeline
该方法比较简单,不做详细介绍,主要逻辑在convertPipelineResults中

	public List<Object> closePipeline() {
		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 List<Object> convertPipelineResults() {
		List<Object> 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<Object> unformatted = client.getAll();
      for (Object o : unformatted) {
        generateResponse(o);
      }
    }
  }

redis.clients.jedis.Connection#getAll()
从下面源码来看,跟前面写的内容有些出入,调用connection.closePipeline()时,是根据之前那执行sendCommand函数对pipelinedCommands进行的计数,才知道要读取多少个响应结果。

public List<Object> getAll() {
    return getAll(0);
}

public List<Object> getAll(int except) {
    List<Object> all = new ArrayList<Object>();
    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分三步

  1. 执行connection.openPipeline开启pipeline,主要就是创建Pipeline这个类,用于后面存放响应的占位符。
  2. 执行回调,回调里面执行的所有命令都会直接发送到服务端,并不会集中起来一起发送。只是发送完命令后,不会立即读取服务端的响应。
  3. 执行connection.closePipeline,根据执行的命令计数,去读取n次命令的结果。

为啥jedis集群模式下不支持pipeline

tcp协议保证对端应用层能够按发送顺序读取数据,即使数据包并不是按顺序到达的。
在pipeline模式下,发送命令和读取命令必须一一对应,不然就会导致客户端读取到错误的结果。
在这里插入图片描述
从上图可以看到线程一,发送了前两条命令,但是读取响应时,被线程二插了队,导致两个线程读取到的响应和自己发送的命令对不上。
不仅如此,如果线程二一次无法从connection中读走完整响应,可能线程一会把单个响应剩余的部分读走,导致畸形报文。
因此要求连接在使用的过程中,被线程独占
集群模式下,当操作的key不属于该节点,服务会发送重定向的响应,让客户端去找正确的节点重新发送请求。
如果pipeline模式下发送了多条命令,而且多个命令的key由redis集群中不同节点所拥有,则要求Jedis客户端在本线程中独占所有的本次用到的connection。不然就会出现读取响应错乱的问题。
在这里插入图片描述
pipeline并不是Redis的特性,Jedis在实现客户端时,并没有将该功能纳入;导致spring-data-redis在基于Jedis进行封装时,无法支持pipeline。

解决方案

  1. redis客户端由Jedis切换到Lettuce
    lettuce为啥支持pipeline待分析
  2. 不使用pipeline
    从原理上来看,pipeline主要是减少了系统调用次数(发送的时候一次性发送,读取的时候延时读取可以一次多读点数据,从而减少系统调用),其提升的性能并不一定很明显,这一点redis的官网也有提及,因此不是极端调优场景,干脆就不使用pipeline。

拓展cluster模式下为啥不支持事务-Multi指令

Multi的语义是一批命令一起执行,redis处理命令是单线程的,因此服务端很容易实现这个语义。

  1. 客户端执行MULTI命令,告诉服务端开启事务
  2. 客户端发送批量命令
  3. 批量操作在发送 EXEC 命令给服务端,告诉服务端执行之前发送的所有命令;或者发送DISCARD告诉服务端取消之前所有的命令。

收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
在事务执行过程,因为redis是单线程的,其他客户端提交的命令请求不会插入到事务执行命令序列中。

在集群模式下,即使客户端提前分析出所有的命令,并对涉及到的节点执行MULTI,也无法保证该语义有效。因为目前服务端执行命令时,节点间并不会做协同。
从下图可以看到“客户端一”向两个服务端发送EXEC提交事务时,存在一个时间差,原子性无法得到保证,因此无法做到事务里面的语句在所有节点上一起同时执行。
既然该语义无法得到保证,那集群模式下执行事务就没有意义了。
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值