背景描述
在项目中开发一个接口,这个接口是一个大sql查询,数据库使用的是mongodb,一次性查询5千条以上的数据,这就带来了一个问题,就是查询效率的问题,会很慢,要等5秒以上才能等到数据,尽管对关键字段都做了索引,还是很慢。也许你会说不要一次性查这么多数据,搞个分页不就行了吗?外卖这种情况还真不行,得把所有数据拿到才可以进行计算。
为了提高查询效率,就想到要么把数据预先存到redis里面去,到时候直接读取redis不就快了吗?我们真这样做了,结果发现redis读取这种大key的时候发现依然比较慢,那么怎么办?
终极解决办法:
分段数据保存,多线程读取
思想:预先将大sql查询出来的数据拆分成多个小集合,缓存在redis里面,在查询的时候使用多线程异步去读取这几个缓存在redis里面的集合数据,再将读取到的数据组装成一个新的大集合,如此大大提高了数据读取效率,那么问题来了,在使用多线程多去redis里面的数据的时候,如何判断所有线程已经将数据都已经取完了呢?我这边做了一个自旋判断,只有将读取到数据数量等于之前缓存到redis中的数量,那么就认为数据已经取完了,否则就一直等知道所有线程都读取完,当然,在遇到网络不好的情况下,可能遇到redis读取很慢,那我们也不能一直自旋等待下去,就需要设置一个等待超时时间。不多说了,上代码:
1、把查询出来的数据缓存到redis
Criteria criteria = Criteria.where("code").is(code).and("time").lte(endDate);
Query stockQuery = new Query(criteria);
stockQuery.with(Sort.by(Sort.Order.asc("time")));
stockQuery.fields().exclude("id", "turnoverRatio", "amount", "change", "changeRatio");//id
myStockPools = mongoTemplate.find(stockQuery, MyStockPool.class, "myStockPool");
int groupNum = (int) Math.ceil((double) myStockPools.size() / StockConstant.PAGE_SIZE_D_800);
redisTemplate.opsForValue().set(StockConstant.CODE_GROUP_NUM + code, String.valueOf(groupNum));
if (groupNum > StockConstant.ONE) {
//拆成多个组,使用多线程去去读取数据
for (int i = 1; i <= groupNum; i++) {
List<MyStockPool> reslist = pageBySubList(myStockPools, StockConstant.PAGE_SIZE_800, i);
redisTemplate.opsForValue().set(StockConstant.CODE + code + i, JSON.toJSONString(reslist));
log.info("reslist size:{}", reslist.size());
}
log.info("mappedResults end:{}", LocalDateTime.now());
}
2、使用异步多线程读取redis分段数据
public List<MyStockPool> mongoQuotaData(String endDate, String code) {
//保证集合线程的安全性
List<MyStockPool> myStockPools = Collections.synchronizedList(new ArrayList<>());
String codeGroupNum = redisTemplate.opsForValue().get(StockConstant.CODE_GROUP_NUM + code);
if (StringUtils.hasText(codeGroupNum)) {
int goupNumRedis = Integer.parseInt(codeGroupNum);
CompletableFuture<Void>[] jasc = new CompletableFuture[goupNumRedis];
log.info("redisList start:{}", LocalDateTime.now());
CountDownLatch countDownLatch = new CountDownLatch(goupNumRedis);
for (int i = 0; i < goupNumRedis; i++) {
String key = StockConstant.CODE + code + (i + 1);
//异步调用
List<MyStockPool> finalMyStockPools = myStockPools;
jasc[i] = CompletableFuture.runAsync(() -> {
try {
String str = redisTemplate.opsForValue().get(key);
if (StringUtils.hasText(str)) {
List<MyStockPool> redisMyStockPools = JSON.parseArray(str, MyStockPool.class);
log.info("redismyStockPools size:{}", redisMyStockPools.size());
if (CollectionUtil.isNotEmpty(redisMyStockPools)) {
finalMyStockPools.addAll(redisMyStockPools);
}
}
} finally {
//当前线程执行完成之后减一
countDownLatch.countDown();
}
}, executor);
}
log.info("redisList start:{}", LocalDateTime.now());
try {
//阻塞等待所有的线程都执行完成才会进行下一步操作,如果超过5秒还没有完成就不等了
countDownLatch.await(5, TimeUnit.SECONDS);
log.info("countDownLatch完成了:{}", countDownLatch.getCount());
myStockPools = myStockPools.stream().sorted(Comparator.comparing(MyStockPool::getTime)).collect(Collectors.toList());
AtomicInteger index = new AtomicInteger(0);
myStockPools.stream().filter(s -> {
//每比对一个元素,数值加1
index.getAndIncrement();
return s.getTime().equals(endDate);
}).findFirst();
int end = index.get();
//截取该日期之前的数据
myStockPools = myStockPools.subList(0, end);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
return myStockPools;
}