JedisClusterPipeLine扩展支持【线程安全版】-转

 

1.为什么要写这篇文章

由于使用redis cluster模式,在操作redis时,有时会有大量的命令,如果每次都进行提交,会非常慢(比如一分钟之内操作几百万次甚至千万次)。通过一次请求提交多个命令,这个在某些场景下,能够提升很多性能。

但是,查看jediscluster的代码,却发现其不支持pipeline。这就很郁闷了。通过在网上查询,我找到了这篇文章:

redis集群客户端JedisCluster优化 - 管道(pipeline)模式支持

于是,抱着试一试的心态,我拿到了这个源码。通过使用,我发现存在线程安全性问题。当然博主也在代码中注释了,not thread safe。我就想着能不能将其改成线程安全的,毕竟性能确实不错。

2.源码分析

下面是截取引用的博主中的segment

JedisClusterPipeLine扩展支持【线程安全版】-转_第1张图片

 

下面是我自己的优化

JedisClusterPipeLine扩展支持【线程安全版】-转_第2张图片

 

3.源码展示

package pipeline;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisMovedDataException;
import redis.clients.jedis.exceptions.JedisRedirectionException;
import redis.clients.util.JedisClusterCRC16;
import redis.clients.util.SafeEncoder;

import java.io.Closeable;
import java.lang.reflect.Field;
import java.util.*;

/**
 * 在集群模式下提供批量操作的功能。 
* 由于集群模式存在节点的动态添加删除,且client不能实时感知(只有在执行命令时才可能知道集群发生变更), * 因此,该实现不保证一定成功,建议在批量操作之前调用 refreshCluster() 方法重新获取集群信息。
* 应用需要保证不论成功还是失败都会调用close() 方法,否则可能会造成泄露。
* 如果失败需要应用自己去重试,因此每个批次执行的命令数量需要控制。防止失败后重试的数量过多。
* 基于以上说明,建议在集群环境较稳定(增减节点不会过于频繁)的情况下使用,且允许失败或有对应的重试策略。
* * @author youaremoon * @since Ver 1.1 */ public class JedisClusterPipeline extends PipelineBase implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(JedisClusterPipeline.class); private static final String SPLIT_WORD = ":"; // 部分字段没有对应的获取方法,只能采用反射来做 // 你也可以去继承JedisCluster和JedisSlotBasedConnectionHandler来提供访问接口 private static final Field FIELD_CONNECTION_HANDLER; private static final Field FIELD_CACHE; static { FIELD_CONNECTION_HANDLER = getField(BinaryJedisCluster.class, "connectionHandler"); FIELD_CACHE = getField(JedisClusterConnectionHandler.class, "cache"); } private JedisSlotBasedConnectionHandler connectionHandler; private JedisClusterInfoCache clusterInfoCache; private Queue clients = new LinkedList(); // 根据顺序存储每个命令对应的Client private Map> jedisMap = new HashMap>(); // 用于缓存连接 private boolean hasDataInBuf = false; // 是否有数据在缓存区 public JedisClusterPipeline(JedisCluster jedisCluster) { setJedisCluster(jedisCluster); } /** * 刷新集群信息,当集群信息发生变更时调用 * * @param * @return */ public void refreshCluster() { connectionHandler.renewSlotCache(); } /** * 同步读取所有数据. 与syncAndReturnAll()相比,sync()只是没有对数据做反序列化 */ public void sync() { innerSync(null); } @Override public void close() { clean(); clients.clear(); for (Map.Entry> poolEntry : jedisMap.entrySet()) { for (Map.Entry jedisEntry : poolEntry.getValue().entrySet()) { if (hasDataInBuf) { flushCachedData(jedisEntry.getValue()); } jedisEntry.getValue().close(); } } jedisMap.clear(); hasDataInBuf = false; } /** * 同步读取所有数据 并按命令顺序返回一个列表 * * @return 按照命令的顺序返回所有的数据 */ public List syncAndReturnAll() { List responseList = new ArrayList(); innerSync(responseList); return responseList; } private void setJedisCluster(JedisCluster jedis) { connectionHandler = getValue(jedis, FIELD_CONNECTION_HANDLER); clusterInfoCache = getValue(connectionHandler, FIELD_CACHE); } private void innerSync(List formatted) { HashSet clientSet = new HashSet(); try { for (Client client : clients) { // 在sync()调用时其实是不需要解析结果数据的,但是如果不调用get方法,发生了JedisMovedDataException这样的错误应用是不知道的,因此需要调用get()来触发错误。 // 其实如果Response的data属性可以直接获取,可以省掉解析数据的时间,然而它并没有提供对应方法,要获取data属性就得用反射,不想再反射了,所以就这样了 Object data = generateResponse(client.getOne()).get(); if (null != formatted) { formatted.add(data); } // size相同说明所有的client都已经添加,就不用再调用add方法了 if (clientSet.size() != jedisMap.size()) { clientSet.add(client); } } } catch (JedisRedirectionException jre) { if (jre instanceof JedisMovedDataException) { // if MOVED redirection occurred, rebuilds cluster's slot cache, // recommended by Redis cluster specification refreshCluster(); } throw jre; } finally { if (clientSet.size() != jedisMap.size()) { // 所有还没有执行过的client要保证执行(flush),防止放回连接池后后面的命令被污染 for (Map.Entry> poolEntry : jedisMap.entrySet()) { for (Map.Entry jedisEntry : poolEntry.getValue().entrySet()) { if (clientSet.contains(jedisEntry.getValue().getClient())) { continue; } flushCachedData(jedisEntry.getValue()); } } } hasDataInBuf = false; close(); } } private void flushCachedData(Jedis jedis) { try { jedis.getClient().getAll(); } catch (RuntimeException ex) { } } @Override protected Client getClient(String key) { byte[] bKey = SafeEncoder.encode(key); return getClient(bKey); } @Override protected Client getClient(byte[] key) { Jedis jedis = getJedis(JedisClusterCRC16.getSlot(key)); Client client = jedis.getClient(); clients.add(client); return client; } private Jedis getJedis(int slot) { // 根据线程id从缓存中获取Jedis Jedis jedis = null; Map tmpMap = null; //获取线程id long id = Thread.currentThread().getId(); //获取jedispool JedisPool pool = clusterInfoCache.getSlotPool(slot); if (jedisMap.containsKey(pool)) { tmpMap = jedisMap.get(pool); if (tmpMap.containsKey(id)) { jedis = tmpMap.get(id); } else { jedis = pool.getResource(); tmpMap.put(id, jedis); } } else { tmpMap = new HashMap(); jedis = pool.getResource(); tmpMap.put(id, jedis); jedisMap.put(pool,tmpMap); } hasDataInBuf = true; return jedis; } private static Field getField(Class cls, String fieldName) { try { Field field = cls.getDeclaredField(fieldName); field.setAccessible(true); return field; } catch (NoSuchFieldException | SecurityException e) { throw new RuntimeException("cannot find or access field '" + fieldName + "' from " + cls.getName(), e); } } @SuppressWarnings({"unchecked"}) private static T getValue(Object obj, Field field) { try { return (T) field.get(obj); } catch (IllegalArgumentException | IllegalAccessException e) { LOGGER.error("get value fail", e); throw new RuntimeException(e); } } }

4.引用

redis集群客户端JedisCluster优化 - 管道(pipeline)模式支持

你可能感兴趣的:(java)