最近在写程序的过程中遇到一个问题,就是需要删除redis中key值以某个字符串开头的数据。因为通过查阅资料以后说如果数据量过大使用keys可能会产生死锁,并且速度会很慢,所以通过查阅各种资料发现使用scan会是比较好的一种方案,但是在真实的开发过程中发现了单节点的redis会支持scan但是集群环境中并不支持scan命令(本人使用的是redistemplate,Jedis是Redis官方推荐的面向Java的操作Redis的客户端,而RedisTemplate是SpringDataRedis中对JedisApi的高度封装。SpringDataRedis相对于Jedis来说可以方便地更换Redis的Java客户端,比Jedis多了自动管理连接池的特性,方便与其他Spring框架进行搭配使用如:SpringCache),不仅如此并且之前所熟知的Pipline管道操作也是不支持集群环境的,通过翻阅各种网上的博客以及redis的官网,自己简单的实现了一下在java中redis集群环境下scan和pipline的使用。
为什么这两种操作在redis集群环境中是不支持的:
因为redis中有一个槽(slot)的概念,如果在单个节点的情况下redis是只有16384个槽,使用pipline只建立一次连接,如果循环操作他会首先将数据放入到缓冲区,然后一次将数据发送并且获取,这样无疑比每一次获取建立一次连接性能要高很多。但是在集集群的模式之下,多个节点共享16384个槽,假设集群有三个节点那么此时就是三个节点平分16384个槽位,当放入一个数据是会对key进行hash然后根据hash以后的值找到对应的槽位放入对应的节点,但是如果此时使用pipline就会发现问题,因为pipline要建立一次连接,但是操作的key可能在不同的分片上,所以此时就需要进行请求的转发,但是这是与pipline的思想相违背的所以集群模式下不支持pipline的操作。scan是同样的道理,不同的key可能在不同的节点上,但是scan只能操作一个节点上的数据。
通过以上的分析那么就知道应该如何解决这个问题了,那就是对所有的key进行hash找出其对应的槽位并且分类。保证同一pipeline内所有的key都对应一个节点就好了,最后通过pipeline执行。当然scan也是同样的道理此外还有mget和mset。但是scan的解决方案就是通过获取每个节点然后通过每个节点的scan的命令来进行寻找key值。
关于pipline和scan使用的代码实现:
(1)Pipline:
public static List<Object> clusterPiplineGetAndSet(List<String> keys) throws Exception {
//获取key的序列化策略
RedisSerializer keySerializer = redisTemplate.getKeySerializer();
//定义的map以redis的节点为key list为value,此处的list存放的该节点下需要存储的key值
HashMap<RedisClusterNode, List<String>> nodeKeyMap = new HashMap<>(8);
List<Object> result = new ArrayList<>(8);
//获取集群的连接对象
RedisClusterConnection redisClusterConnection = redisTemplate.getConnectionFactory().getClusterConnection();
try {
//通过计算每个key的槽点,获取所有的节点
Iterable<RedisClusterNode> redisClusterNodes = redisClusterConnection.clusterGetNodes();
for (RedisClusterNode redisClusterNode : redisClusterNodes) {
//得到节点的槽位的范围
RedisClusterNode.SlotRange slotRange = redisClusterNode.getSlotRange();
for (String key : keys) {
//利用redis的key的hash算法得到该key对应的槽位
int slot = JedisClusterCRC16.getSlot(key);
if (slotRange.contains(slot)) {
List<String> list = nodeKeyMap.get(redisClusterNode);
if (null == list) {
list = new ArrayList<>();
nodeKeyMap.putIfAbsent(redisClusterNode, list);
}
//将对应的key放入进去
list.add(key);
}
}
}
//开始遍历通过管道往redis中放入数据 遍历上边定义的map
for (Map.Entry<RedisClusterNode, List<String>> clusterNodeListEntry : nodeKeyMap.entrySet()) {
//连接节点
RedisClusterNode redisClusterNode = clusterNodeListEntry.getKey();
//获取到每个节点的JedisPool对象 关于jedis和redistemplate的关系下边会进行简单介绍。
JedisPool jedisPool = ((JedisCluster) redisClusterConnection.getNativeConnection()).getClusterNodes()
.get(new HostAndPort(redisClusterNode.getHost(), redisClusterNode.getPort()).toString());
List<String> nodeListEntryValue = clusterNodeListEntry.getValue();
byte[][] arr = new byte[nodeListEntryValue.size()][];
int count = 0;
//获取key数据
for (String nodeKey : nodeListEntryValue) {
//利用之前获取到的键的序列化方式对每个值进行序列化并且放入到byte数组中。
arr[count++] = keySerializer.serialize(nodeKey);
}
//从池子中拿出对应的jedis对象
Jedis jedis = jedisPool.getResource();
List<Response<byte[]>> responses = new ArrayList<>();
try {
//开始使用单节点的pipline对象进行操作。
Pipeline pipeline = jedis.pipelined();
//************************接下来的操作就是对应利用pipline获取值和set值可以根据业务需求选取******************************
//从redis中获取值
for (String nodeKey : nodeListEntryValue) {
responses.add(pipeline.get(keySerializer.serialize(nodeKey)));
}
List<Object> objects = pipeline.syncAndReturnAll();
result = objects;
//往redis中放入值
/* for (String nodeKey : nodeListEntryValue) {
pipeline.set(keySerializer.serialize(nodeKey),valueSerializer.serialize("nnnn"));
}
pipeline.sync();*/
//*************************************************************************************************************
pipeline.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
} finally {
RedisConnectionUtils.releaseConnection(redisClusterConnection, redisTemplate.getConnectionFactory());
}
return result;
}
(2)Scan:
public static List<String> getRedisKeys(String matchKey) {
List<String> list = new ArrayList<>();
RedisClusterConnection redisClusterConnection = redisTemplate.getConnectionFactory().getClusterConnection();
//这里是获取edispool的另外一种方式与上边的pipline可以对比下,两种方式都可以实现
Map<String, JedisPool> clusterNodes = ((JedisCluster) redisClusterConnection.getNativeConnection()).getClusterNodes();
for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
//获取单个的jedis对象
Jedis jedis = entry.getValue().getResource();
// 判断非从节点(因为若主从复制,从节点会跟随主节点的变化而变化),此处要使用主节点从主节点获取数据
if (!jedis.info("replication").contains("role:slave")) {
List<String> keys = getScan(jedis, matchKey);
if (keys.size() > 0) {
Map<Integer, List<String>> map = new HashMap<>(8);
//接下来的循环不是多余的,需要注意
for (String key : keys) {
// cluster模式执行多key操作的时候,这些key必须在同一个slot上,不然会报:JedisDataException:
int slot = JedisClusterCRC16.getSlot(key);
// 按slot将key分组,相同slot的key一起提交
if (map.containsKey(slot)) {
map.get(slot).add(key);
} else {
List<String> list1 = new ArrayList();
list1.add(key);
map.put(slot, list1);
}
}
for (Map.Entry<Integer, List<String>> integerListEntry : map.entrySet()) {
list.addAll(integerListEntry.getValue());
}
}
}
}
return list;
}
public static List<String> getScan(Jedis redisService, String key) {
List<String> list = new ArrayList<>();
//扫描的参数对象创建与封装
ScanParams params = new ScanParams();
params.match(key);
//扫描返回一百行,这里可以根据业务需求进行修改
params.count(100);
String cursor = "0";
ScanResult scanResult = redisService.scan(cursor, params);
//scan.getStringCursor() 存在 且不是 0 的时候,一直移动游标获取
while (null != scanResult.getStringCursor()) {
//封装扫描的结果
list.addAll(scanResult.getResult());
if (! "0".equals( scanResult.getStringCursor())) {
scanResult = redisService.scan(cursor, params);
} else {
break;
}
}
return list;
}
以上便是pipline和scan在集群环境下的实现过程,这只是线下的测试过程具体的redistemplate还需要业务上自行实现,同时一些异常操作安全操作也需要开发者自行实现。