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

  • 相关阅读:
    php实现频率限制
    手机号打码
    qxx项目大文件上传
    502错误
    mac 安装phpunit
    文件权限问题
    无题
    php安装redis扩展全
    linux中whereis、which、find、location的区别和用法
    php安装redis扩展
  • 原文地址:https://www.cnblogs.com/youtang/p/11331698.html
Copyright © 2020-2023  润新知