上周接到了一个需求,主要就是解析日志,缓存中记录对用户某一特定操作的状态、结果、操作时间等,目的是直观展示,方便查询。
一个用户每天会产生多条记录,一天大概有几百万条记录,需求方不要求查询全部,只要近期就可以。
我想得很单纯,这个数据结构不复杂,key由前缀+用户ID+操作时间时间戳组成,可以保证唯一性,value使用String类型,存放相关信息的JSON,同时设置过期时间为两个月。
但是呢,由于接口只接收用户ID,而key为用户ID+时间戳,一个用户在缓存中有多条记录,这样以来我就只能根据用户ID进行模糊匹配,所以redis相关知识还停留在面试层面的我,自然想到了keys命令,查询接口大概是这样的。
public Page<RecordDTO> getRecoreList(Long studentId, Integer pageIndex, Integer pageSize) {
if (Objects.isNull(studentId)){
return null;
}
Set<String> recordsSet = stringRedisTemplate.keys(KEY_PR + studentId + "*");
List<RecordDTO> recordDTOS = new ArrayList<>();
recordsSet.forEach(key -> {
String record = stringRedisTemplate.opsForValue().get(key);
long startTimestamp = Long.valueOf(key.substring(key.length() - 13));
LocalDateTime startTime = LocalDateTimeUtil.getDateTimeOfTimestamp(StartTimestamp);
RecordDTO recordDTO = JSON.parseObject(record, RecordDTO.class);
RecordDTO.setStudentId(studentId);
RecordDTO.setStartTime(startTime);
recordDTOS.add(recordDTO);
});
List<RecordDTO> dtos = recordDTOS.stream()
.sorted(Comparator.comparing(RecordDTO::getStartTime).reversed())
.collect(Collectors.toList());
return PageUtil.convertToPage(dtos, pageIndex, pageSize);
}
自测时没有问题,同时因为测试环境查询请求并发量小,提测后也没有发现问题,就在周五晚上心安理得的上线了,导致周六在业务高峰期,进程阻塞,多个请求遭到拒绝,线上报警keys命令。发现问题出在我这里,leader决定回滚前端代码,使接口不再对外开放。
周一重新讨论存储策略,最后方案是这样的:使用zSet结构存储,key为前缀+用户Id,操作时间时间戳为分数,不再设置过期时间,而是设置阈值,记录数超过阈值移除旧的数据,确保每个用户维持最近的、固定数量的记录。为什么不用Hash结构,设置过期时间呢,因为Hash结构的过期时间设置是Key层面的,也就是说,一旦到达过期时间,用户的所有记录都将会失效,是不符合业务场景的。
新的存储策略数据入库主要代码如下。
String key = KEY_PR + studentId; String value = JSONObject.toJSONString(RecordDTO); try { redisTemplate.opsForZSet().add(key, value, Double.valueOf(startTime)); Long size = redisTemplate.opsForZSet().size(key); // redis中维持固定数量记录,超过阈值删除旧的记录 if (THRESHOLD < size) { redisTemplate.opsForZSet().removeRange(key, 0, size - THRESHOLD - 1); } } catch (Exception e) { LOGGER.error("操作redis失败 message:{}", e.getMessage()); return; }
查询接口如下。
public Page<RecordDTO> getRecoreList(Long studentId, Integer pageIndex, Integer pageSize) { if (Objects.isNull(studentId)) { return null; } String key = REMINDING_KEY_PR + studentId; // 对现有的redis set中元素倒叙排列 Set<String> records = stringRedisTemplate.opsForZSet().reverseRange(key, 0, -1);if (CollectionUtils.isEmpty(records)){ return null; } // 返回集合 List<RecordDTO> recordDTOS = new ArrayList<>(); Objects.requireNonNull(records).forEach(record -> { RecordDTO recordDTO = JSON.parseObject(record, RecordDTO.class); recordDTO.setStudentId(studentId); recordDTOS.add(recordDTO); }); return PageUtil.convertToPage(recordDTO, pageIndex, pageSize);
}
最后放上leader对我说的一句话:千万不要把Redis仅仅当作一个检索工具。
PS. 这次的需求不复杂,但是对我来说却是一个教训,很多技术只停留在会用的层面是完全不够的,若想在这个领域深耕下去,必须不断向底层探索。同时领悟到,一切的语言,框架,技术等等等等,无非是你达到目标的工具,重要的还是 思想。道阻且长呀。