今天遇到一个业务问题,有百万条数据需要处理,处理过程中需要对每条数据进行校验,与本地表中数据查看是否匹配(存在),如果每次请求DB的话给数据库带来的压力不言而喻;
也曾考虑过在需要的时候将数据分批查到一个List中,但是数据量太大,加上可能多个业务场景用到,每次取数的耗时包括占用的JVM空间也pass掉了这种方式;
再者就是采用缓存方式,一来也不会对DB造成压力,二来也方便查询。
但是如何将这一千万数据存入缓存?循环百万次插入是不可能的,肯定有更好的方式。
redis-Pipeline(管道)
简单讲 Redis-Pipeline是Redis客户端的一个高级功能,它可以在一次网络往返中发送多个命令。
这个功能可以提高Redis客户端与Redis服务器之间的性能,因为通过一次网络往返发送多个命令可以减少网络延迟时间,并且可以减少Redis服务器的CPU负载。
直接上代码:
/**
* 普通方式进行数据刷入
*/
public void ordinaryRedis() {
//分批取数据
Set<String> dmDevLabel = batchSelectBranch();
long sl = System.currentTimeMillis();
System.out.println("删除之前缓存");
redisUtil.del("iot_Set_BranchDevLabel_ordinary");
//数据同步刷入缓存
dmDevLabel.parallelStream().forEach(devLabel ->{
redisUtil.sSet("iot_Set_BranchDevLabel_ordinary", devLabel);
});
long el = System.currentTimeMillis();
System.out.println("刷入缓存用时:"+ (sl - el)/1000 +" s");
//判断数据在缓存中是否存在 不存在 说明与DM中数据不匹配 删除(不纳入计算)
long redisSize = redisUtil.sGetSetSize("iot_Set_BranchDevLabel_ordinary");
if (redisSize > 0) {
boolean isExist = redisUtil.sIsMember("iot_Set_BranchDevLabel_ordinary", "T23131D012012021010504976c2fcbc9e6e8805b36000036915460");
System.out.println(isExist);
}
}
采用管道刷入数据:
/**
* 管道方式分批进行数据刷入
*/
public void pipelineRedis() {
//分批取数据
Set<String> dmDevLabel = batchSelectBranch();
long sl = System.currentTimeMillis();
System.out.println("删除之前缓存");
redisUtil.del("iot_Set_BranchDevLabel");
System.out.println("开始刷入缓存");
Set<Set<String>> setPipe = splitSet(dmDevLabel, 100);
for (Set<String> set : setPipe) {
Boolean isPipe = redisUtil.executePipelinedBySet(set, "Set_BranchDevLabel", 0);
if (!isPipe) {
redisUtil.executePipelinedBySet(set, "Set_BranchDevLabel", 0);
}
}
long el = System.currentTimeMillis();
System.out.println("刷入缓存用时:"+ (el - sl)/1000 +" s");
//判断数据在缓存中是否存在
long redisSize = redisUtil.sGetSetSize("Set_BranchDevLabel");
if (redisSize > 0) {
boolean b = redisUtil.sHasKey("Set_BranchDevLabel", "66666666666666666666666");
System.out.println(b);
}
}
//这里也可以采用第二种方式,在循环总量数据时候根据fori循环下标进行分批。
//Set<String> dmDevLabelSet = new HashSet<>();
//for (int i = 0; i < courtsBranchDetails.size(); i++) {
// IotCourtsBranchDetails BranchDetail = courtsBranchDetails.get(i);
// String esnLabel = BranchDetail.getEsn() + "_" + BranchDetail.getDevLabel();
// if (i == 0) {
// dmDevLabelSet.add(esnLabel);
// } else if (i % 100000 == 0 || i == courtsBranchDetails.size()) {
// dmDevLabelSet.add(esnLabel);
// //不设置缓存过期时间
// Boolean isPipe = redisUtil.executePipelinedBySet(dmDevLabelSet, "iot_Set_BranchDevLabel", 0);
// if (!isPipe) {
// //如果某批刷入失败重试一次
// redisUtil.executePipelinedBySet(dmDevLabelSet, "iot_Set_BranchDevLabel", 0);
// WriteTxtUtil.writeTxtLog("数据批量刷入缓存", "异常时间:\t" + DateUtil.getTime() + "失败批次:\t" + i);
// }
// dmDevLabelSet = new HashSet<>();
// } else {
// dmDevLabelSet.add(esnLabel);
// }
//}
/**
* 将一个 Set<String> 均分成多个 Set<String>
*
* @param originalSet 原始 Set<String>
* @param subsetSize 每个子集的大小
* @return 均分后的多个 Set<String>
*/
public static Set<Set<String>> splitSet(Set<String> originalSet, int subsetSize) {
Set<Set<String>> subsets = new HashSet<>();
int count = 0;
Set<String> subset = new HashSet<>();
for (String element : originalSet) {
subset.add(element);
count++;
if (count == subsetSize) {
subsets.add(subset);
subset = new HashSet<>();
count = 0;
}
}
if (count > 0) {
subsets.add(subset);
}
return subsets;
}
redis管道封装方法:
/**
* 功能描述: 使用pipelined批量存储
* redis 类型Set
* 如果返回true,则表示刷入成功;如果返回false,则表示刷入失败。
*
* @auther: jiangfy
*/
public Boolean executePipelinedBySet(Set<String> set, String key, long seconds) {
RedisSerializer<String> serializer = redisTemplate.getValueSerializer();
RedisSerializer<String> keySerializer = redisTemplate.getKeySerializer();
List<Object> resultList = redisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
set.forEach((value) -> {
connection.sAdd(keySerializer.serialize(key), serializer.serialize(value));
});
if (seconds > 0) {
connection.expire(keySerializer.serialize(key), seconds);
}
return null;
}
}, serializer);
return resultList != null && resultList.size() > 0;
}
需要注意的是,最开始使用的 redisTemplate.getStringSerializer() 的字符串序列化器 导致数据虽然插入了但是不能正常的获取,排查了好久问题,redis中有就是取不到,后来调整了对应的序列化器后正常;
了解下这几个序列化器吧:
这三个都是 RedisTemplate 中的序列化器,用于将数据序列化为字节数组存储到 Redis 中,或将从 Redis 中获取的字节数组反序列化为对应的数据类型。
- getValueSerializer():用于序列化对象值的序列化器。
- getKeySerializer():用于序列化键的序列化器。
- getStringSerializer():用于序列化字符串的序列化器。
它们的区别在于序列化的对象类型不同。getValueSerializer() 序列化的是 Redis 中存储的对象值,getKeySerializer() 序列化的是 Redis 中存储的键名,而 getStringSerializer() 则序列化的是字符串类型的值。
在实际使用中,需要根据存储的数据类型来选择合适的序列化器,否则可能会出现数据类型转换错误等问题。例如,当存储的值为字符串类型时,应该使用 getStringSerializer() 序列化器。当存储的值为 Java 对象时,可以使用默认的 JdkSerializationRedisSerializer,或根据需要选择其他的序列化器。