开发者

Redis内存管理之BigKey问题及解决过程

目录
  • Java中的RedisKCrRMtdQ BigKey问题解析
    • 一、BigKey 定义与危害分析
      • 1.1 核心定义
      • 1.2 危害全景图
      • 1.3 典型业务场景
    • 二、BigKey 检测方法论
      • 2.1 内置工具检测
      • 2.2 自定义扫描方案
      • 2.3 监控预警体系
    • 三、BigKey 处理全流程
      • 3.1 分治法处理
      • 3.2 渐进式删除
      • 3.3 数据迁移方案
    • 四、编程Java 开发规范与最佳实践
      • 4.android1 数据建模规范
      • 4.2 客户端配置优化
      • 4.3 监控与熔断
    • 五、生产环境案例
      • 5.1 社交平台用户关系案例
      • 5.2 电商商品属性案例
    • 六、开发方向
    • 总结

      Java中的Redis BigKey问题解析

      一、BigKey 定义与危害分析

      1.1 核心定义

      BigKey 是指 Redis 中 Value 体积异常大的 Key,通常表现为:

      • 字符串类型:Value 超过 10KB
      • 集合类型:元素数量超过 1 万(List/Set)或 5 千(Hash/ZSeKCrRMtdQt)
      • 流类型:Stream 包含数万条消息

      1.2 危害全景图

      Redis内存管理之BigKey问题及解决过程

      1.3 典型业务场景

      场景错误用法推荐方案
      社交用户画像存储单个Hash存储用户所有标签分片存储 + 二级索引
      电商购物车设计单个List存储百万级商品分页存储 + 冷热分离
      实时消息队列单个Stream累积数月数据按时间分片 + 定期归档

      二、BigKey 检测方法论

      2.1 内置工具检测

      2.1.1 redis-cli --bigkeys

      # 扫描耗时型操作,建议在从节点执行
      redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1
      
      # 输出示例
      [00.00%] Biggest string found 'user:1024:info' has 12 bytes
      [12.34%] Biggest hash   found 'product:8888:spec' has 10086 fields
      

      2.1.2 MEMORY USAGE

      // 计算Key内存占用
      Long memUsage = redisTemplate.execute(
          (RedisCallback<Long>) connection -> 
              connection.serverCommands().memoryUsage("user:1024:info".getBytes())
      );
      

      2.2 自定义扫描方案

      2.2.1 SCAN + TYPE 组合扫描

      public List<Map.Entry<String, Long>> findBigKeys(int threshold) {
          List<Map.Entry<String, Long>> bigKeys = new ArrayList<>();
          Cursor<byte[]> cursor = redisTemplate.execute(
              (RedisCallback<Cursphpor<byte[]>>) connection -> 
                  connection.scan(ScanOptions.scanOptions().count(100).build())
          );
      
          while (cursor.hasNext()) {
              byte[] keyBytes = cursor.next();
              String key = new String(keyBytes);
              DataType type = redisTemplate.type(key);
              
              long size = 0;
              switch (type) {
                  case STRING:
                      size = redisTemplate.opsForValue().size(key);
                      break;
                  case HASH:
                      size = redisTemplate.opsForHash().size(key);
                      break;
                  // 其他类型处理...
              }
              
              if (size > threshold) {
                  bigKeys.add(new AbstractMap.SimpleEntry<>(key, size));
              }
          }
          return bigKeys;
      }
      

      2.2.2 RDB 文件分析

      # 使用rdb-tools分析
      rdb -c memory dump.rdb --bytes 10240 > bigkeys.csv
      
      # 输出示例
      database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
      0,hash,user:1024:tags,1048576,hashtable,50000,128
      

      2.3 监控预警体系

      2.3.1 Prometheus 配置

      # redis_exporter配置
      - name: redis_key_size
        rules:
        - record: redis:key_size:bytes
          expr: redis_key_size{job="redis"}
          labels:
            severity: warning
      

      2.3.2 Grafana 看板指标

      监控项查询表达式报警阈值
      大Key数量count(redis_key_size > 10240)>10
      最大Key内存占比max(redis_key_size) / avg(…)>5倍

      三、BigKey 处理全流程

      3.1 分治法处理

      3.1.1 Hash 拆分

      public void splitBigHash(String originalKey, int BATchSize) {
          Map<Object, Object> entries = redisTemplate.opsForHash().entries(originalKey);
          List<List<Map.Entry<Object, Object>>> batches = Lists.partition(
              new ArrayList<>(entries.entrySet()), 
              batchSize
          );
          
          for (int i = 0; i < batches.size(); i++) {
              String shardKey = originalKey + ":shard_" + i;
              redisTemplate.opsForHash().putAll(shardKey, 
                  batches.get(i).stream()
                      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
              );
          }
          redisTemplate.delete(originalKey);
      }
      

      3.1.2 List 分页

      public List<Object> getPaginatedList(String listKey, int page, int size) {
          long start = (page - 1) * size;
          long end = page * size - 1;
          return redisTemplate.opsForList().range(listKey, start, end);
      }
      

      3.2 渐进式删除

      3.2.1 非阻塞删除方案

      public void safeDeleteBigKey(String key) {
          DataType type = redisTemplate.type(key);
          
          switch (type) {
              case HASH:
                  redisTemplate.execute(
                      "HSCAN", key, "0", "COUNT", "100", 
                      (result) -> {
                          // 分批删除字段
                          return null;
                      });
                  break;
              case LIST:
                  while (redisTemplate.opsForList().size(key) > 0) {
                      redisTemplate.opsForList().trim(key, 0, -101);
                  }
                  break;
              // 其他类型处理...
          }
          redisTemplate.unlink(key);
      }
      

      3.2.2 Lua 脚本控制

      -- 分批次删除Hash字段
      local cursor = 0
      repeat
          local result = redis.call('HSCAN', KEYS[1], cursor, 'COUNT', 100)
          cursor = tonumber(result[1])
          for _, field in ipairs(result[2]) do
              redis.call('HDEL', KEYS[1], field)
          end
      until cursor == 0
      

      3.3 数据迁移方案

      3.3.1 集群环境下处理

      public void migrateBigKey(String sourceKey, String targetKey) {
          RedisClusterConnection clusterConn = redisTemplate.getConnectionFactory()
              .getClusterConnection();
          
          int slot = ClusterSlotHashUtil.calculateSlot(sourceKey);
          RedisNode node = clusterConn.clusterGetNodeForSlot(slot);
          
          try (Jedis jedis = new Jedis(node.getHost(), node.getPort())) {
              // 分批迁移数据
              ScanParams params = new ScanParams().count(100);
              String cursor = "0";
              do {
                  ScanResult<Map.Entry<String, String>> scanResult = 
                      jedis.hscan(sourceKey, cursor, params);
                  List<Map.Entry<String, String>> entries = scanResult.getResult();
                  
                  // 分批写入新Key
                  Map<String, String> batch = entries.stream()
                      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
                  jedis.hmset(targetKey, batch);
                  
                  cursor = scanResult.getCursor();
              } while (!"0".equals(cursor));
          }
      }
      

      四、Java 开发规范与最佳实践

      4.1 数据建模规范

      数据类型反例正例
      String存储10MB的jsON字符串拆分成多个Hash + Gzip压缩
      Hash存储用户所有订单信息按订单日期分片存储
      List存储10万条聊天记录按时间分片+消息ID索引

      4.2 客户端配置优化

      4.2.1 JedisPool 配置

      JedisPoolConfig config = new JedisPoolConfig();
      config.setMaxTotal(200);         // 最大连接数
      config.setMaxWaitMillis(1000);   // 最大等待时间
      config.setTestOnBorrow(true);    // 获取连接时验证
      

      4.2.2 Lettuce 调优

      ClientOptions options = ClientOptions.builder()
          .autoReconnect(true)
          .publishOnScheduler(true)
          .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
          .build();
      

      4.3 监控与熔断

      @CircuitBreaker(name = "redisService", fallbackMethod = "fallback")
      public Object getData(String key) {
          return redisTemplate.opsForValue().get(key);
      }
      
      private Object fallback(String key, Throwable t) {
          return loadFromBackup(key);
      }
      

      五、生产环境案例

      5.1 社交平台用户关系案例

      问题:单个Set存储50万粉丝导致节点内存溢出

      解决方案

      1. 按粉丝ID范围拆分成100个Set
      2. 使用SINTERSTORE合并多个Set查询
      3. 新增反向索引(粉丝 -> 关注列表)

      5.2 电商商品属性案例

      问题:Hash存储10万条商品规格导致HGETALL阻塞

      改造方案

      1. 按属性类别拆分Hash
      2. 使用HMGET获取指定字段
      3. 增加缓存版本号控制

      六、开发方向

      1. AI 智能分片:基于机器学习预测数据增长趋势
      2. Serverless 存储:自动弹性伸缩的Key分片服务
      3. 新型数据结构:使用RedisJSON模块处理大文档
      4. 内存压缩算法:ZSTD 压缩算法集成优化

      通过全流程的预防、检测、处理体系建设,结合智能化的监控预警,可有效应对 BigKey 挑战,保障 Redis 高性能服务能力。

      总结

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新数据库

      数据库排行榜