记一次线上事故——Redis keys命令

  上周接到了一个需求,主要就是解析日志,缓存中记录对用户某一特定操作的状态、结果、操作时间等,目的是直观展示,方便查询。

  一个用户每天会产生多条记录,一天大概有几百万条记录,需求方不要求查询全部,只要近期就可以。

  我想得很单纯,这个数据结构不复杂,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. 这次的需求不复杂,但是对我来说却是一个教训,很多技术只停留在会用的层面是完全不够的,若想在这个领域深耕下去,必须不断向底层探索。同时领悟到,一切的语言,框架,技术等等等等,无非是你达到目标的工具,重要的还是 思想。道阻且长呀。

 

posted @ 2019-08-10 15:14  _绵绵  阅读(683)  评论(2编辑  收藏  举报