hello,我又回来啦,消失的这一年时间发生了好多事呀,换了工作,当了房奴,在这里祝天下太平,苍生幸福~
最近项目当中遇到需要对数据落地到redis,并且集群使用了最新的redis3.0,满心欢喜的使用新功能,压测的时候却发现吞吐只有几万,达不到要求怎么办,嘿,多次相同操作pipeline有巨大优势,于是,管道加上,什么?redis3.0不支持管道,没办法,只有自己造轮子了。。。
首先我们要知道为什么现在redis3.0不支持管道,现redis-cluster是采用slot方式写入数据(查找这个数据对应的槽位的方法就是对数据的key取模,即 CRC16(key) mod 16384,得到的结果就是写入数据存放的slot位置 ),导致想批量进行操作得数据位于不同slot上,从而操作失败。
而由于key是我们设置的,所以我们可以通过预先计算key所对应的slot(写了半天crc发现jedis自带计算方法。。。),然后再将数据进行一个归纳,将属于相同redis分片上的key放入相同的一个pipeline当中,然后在整体对所有pipeline进行一个sync同步即可。
1、对redis-cluster进行预加载,将每个分片对应的slot-jedisPool关系保存到本地缓存当中
2、从一批数据当中依次获得相应的key
3、通过key计算出对应的slot值
4、通过slot值获得相应的pipeline,并缓存产生的pipeline
5、将redis操作放入相应的pipeline
6、对缓存当中的所有pipeline依次进行一个sync
7、关闭对应的pipeline和jedis
8、新一批数据来了之后重复以上2~7过程
因为pipeline不支持多线程操作,并且在进行sync之后,管道并没有消失,仍然持有jedis的client对象,如果对象池归还了jedis,但是这个jedis对象的client被pipeline持有,会导致线程不安全报错。所以我这里在每次使用完一个pipeline之后都会进行一个close,保证了每次操作的client和pipeline都是新的对象。
由于项目需要,我的代码做了相应的改动,这里只列出相关核心代码,读者需要的话可以自行调试改动:
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10);
config.setMaxTotal(50);
config.setMaxWaitMillis(10000);
config.setTestOnBorrow(true);
Set<HostAndPort> hostSets = new HashSet<>();
hostSets.add(new HostAndPort("127.0.0.1", 6379));
JedisCluster jedisCluster = new JedisCluster(hostSets, 2000, 5000, 5, "password", config);
//建立jediscluster,并得到host-jedisPool的映射关系
Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
//得到jedis
Jedis jedis = new Jedis(hostAndPorts.get(0).getHost(), hostAndPorts.get(0).getPort());
jedis.auth(cluster.getPassword());
//得到redis-cluster的分片信息、host信息、端口信息等
List<Object> list = jedis.clusterSlots();
TreeMap map = new TreeMap<>();
//进行解析
for (Object object : list) {
//list1结构:[2730, 5459, [[B@6973bf95, 6379, [B@2ddc8ecb], [[B@229d10bd, 6379, [B@47542153]]
List<Object> list1 = (List<Object>) object;
List<Object> master = (List<Object>) list1.get(2);
//得到相应的host信息
String hostAndPort = new String((byte[]) master.get(0)) + ":" + master.get(1);
//通过host信息在上面建立的host-jedisPool的map当中找到相应的jedisPool
JedisPool jedisPool = clusterNodes.get(hostAndPort);
//将slot信息和对应的jedisPool保存进treeMap
map.put((Long) list1.get(0), jedisPool);
map.put((Long) list1.get(1), jedisPool);
}
list的数据结构如下:
1.[2730, 5459, [[B@6973bf95, 6379, [B@2ddc8ecb], [[B@229d10bd, 6379, [B@47542153]]
2.[slot开始的位置,slot结束的位置,[master节点的[host,port,节点ID],slave节点的[host,port,节点ID]]
通过有顺序的tree-map依次保存相应的映射关系,数据结构为:
{{slot开始值,相应jedis Pool},{slot结束值,相应jedisPool},{第二个slot开始值,第二个jedisPool}…}
//计算key对应的slot值
long slot = JedisClusterCRC16.getSlot(key);
//ceilingEntry是返回大于或等于指定key的最小map键相关联的map键值对
JedisPool jedisPool = treeMap.ceilingEntry(slot).getValue();
//建立本地缓存
private static Map poolPipelineMap = new ConcurrentHashMap<>();
private static Map pipelineJedisMap = new ConcurrentHashMap<>();
synchronized (LOCK) {
if (!poolPipelineMap.containsKey(jedisPool)) {
Jedis jedis = jedisPool.getResource();
Pipeline pipeline = jedis.pipelined();
poolPipelineMap.put(jedisPool, pipeline);
pipelineJedisMap.put(pipeline, jedis);
}
}
通过之前的操作可以让每个key找到属于他的jedisPool,然后获得相应的pipeline,这里注意维护jedisPool和pipeline之间一一对应的关系,不要生成多个pipeline,那就相当于没有使用到管道的便利性还增加了管道这个负担。,ps:由于我的服务这块使用了多线程,所以进行了一个同步操作,读者请根据自己的情况修改
String key = xxxxx;
byte[] field = xxxxxxx;
Pipeline pipeline = RedisBroker.getPipeline(key);
//pipeline不支持多线程,添加的时候加上同步
synchronized (LOCK) {
pipeline.hset(key.getBytes(), field, valueBytes);
}
RedisBroker.getPipeline()就是上面描述的通过key找到对应pipeline的方法,读者请自己进行相应的封装,通过这样的操作,我们就把这条数据放入了他自己的pipeline当中,之后的数据会依次保存近他自己对应的pipeline,ps:由于我的服务这块使用了多线程,所以进行了一个同步操作,读者请根据自己的情况修改
for (Map.Entry entry : poolPipelineMap.entrySet()) {
Pipeline pipeline = entry.getValue();
pipeline.sync();
try {
pipeline.close();
poolPipelineMap.remove(entry.getKey());
//关闭相应jedis
Jedis jedis = pipelineJedisMap.get(pipeline);
jedis.close();
pipelineJedisMap.remove(pipeline);
} catch (IOException e) {
e.printStackTrace();
}
}
至此,redis3.0的管道支持就成功了,运行了一个多月没有发现什么异常,写的过程中还是遇到大大小小的坑,慢慢挣扎还是出来了,于是写了这篇博客,算是在这里做一个总结吧^_^