# 一、大Key的大小

【1】一般情况下,我们认为字符串类型的keyvalue值超过10kb,就算大key
【2】具体需要根据业务场景,如果一个key的大小为1MBQPS1000,那么每秒会产生1000MB的流量,这会对系统产生影响。

# 二、大Key的危害

【1】占用内存大,空间不均匀
【2】操作耗时,容易阻塞
【3】每次存取网络流量大,容易网络阻塞

# 三、业务场景中常见的大Key

【1】单个简单的key存储的value很大
【2】hashsetzsetlist中存储过多的元素(以万为单位)
【3】一个集群存储了上亿的keyKey本身过多也带来了更多的空间占用

# 四、处理方法

【1】分拆
  ● 单个key存储的value很大:
    ◆ 需要整体存取:分拆成多个key mget
    ◆ 部分存取:使用hash值分拆,或者存入redis hash中的field
  ● value存储过多的元素:
    ◆ 分桶:将大hashsetlist等按fieldhash值模除进行分桶,分拆成多个集合
    ◆ 分区:对于时间有效性的可以加上时间后缀拆分
  ● Key过多:
    ◆ 转为hash结构存储
【2】删除:大key线上删除要使用unlinkdel会阻塞(自然过期也会出现阻塞)
【3】其他
  ● redis hash不能expire fieldredis只能过期顶级key
  ● mget:需要同时获取多个key的值时请使用mget而不是循环get多次
  ● 减少redis操作,每次请求都要消耗时间,比如del操作不需要先判断existsget的值存一个local变量,不要对一个key重复get
  ● 根据业务场景,使用redis的不同的数据结构:list, hash, set, sorted set, bitmap
  ● 热点key问题,可以将key加上后缀拆分到不同机器上
阿里 redis 键值设计规范

# 五、大Key改造案例

# 大 json

【1】业务需求需要保存查询sql对应的结果集k: sql hashcodev:查询结果result存入redis,以及k: sql hashcodev:(时间、sql语句、查询引擎),在本地存进Map序列化成Json,由一个固定key(SQL_CACHE)放入redis,过期时间7天。
【2】缓存设置:一个新查询进来,保存sqlkey以及result,取出SQL_CACHE对应的json,反序列化成mapput新的kv,再序列化重写回 redis(此处会有事务问题,同时请求set会导致一个值丢失)。
【4】相应的缓存删除更新逻辑,需要在监听QMQ表重刷或者QConfig切换表引擎的情况下,取出SQL_CACHE对应的json,遍历此map,对应的每个sql 解析表名,匹配则做相应的删除更新操作。
【5】没有多久SQL_CACHE这个key就变成了大key报警了。

# redis hash

第一次改造就针对这个大key把原本的map直接存成redis hash,避免反复序列化以及大key网络传输成本。

但是此处有个错误使用:为了兼容原来的流程,改造的时候只是简单的替换,导致了故障:遍历hashkeyset然后依次 hget

// 改造前,为了兼容原始流程
public List<CacheVo> getCacheList() {
    Map<String, CacheVo> map = getCacheMap();
    List<CacheVo> list = new ArrayList<>();
    Set<String> keys = map.keySet();
    for (String key : keys) {
      list.add(map.get(key));
    }
    return list;
  }

  private Map<String, CacheVo> getCacheMap() {
    Map<String, CacheVo> map = new HashMap<>();
    if (!provider.exists(CACHE_LIST_HASH)) {
      LogUtils.warn("Cache", "cache list is not exists");
      return map;
    }
    Set<String> fields = provider.hkeys(CACHE_LIST_HASH);
    for (String field : fields) {
            // 此处重新遍历了整个redis hash,但是上层最后只返回了value
      String value = provider.hget(CACHE_LIST_HASH, field);
      map.put(field, JackJsonUtils.parse(value, CacheVo.class));
    }
    return map;
  }

// 解决bug后,由于我们只需要valueList,直接使用hvals
public List<CacheVo> getCacheList() {
    if (!provider.exists(getCacheListHash())) {
      LogUtils.warn("Cache", "cache list is not exists");
      return Collections.emptyList();
    }
    return provider.hvals(getCacheListHash()).stream().map(v -> JackJsonUtils.parse(v, CacheVo.class)).collect(Collectors.toList());
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

某一次请求调用了7000redis操作导致了timeout发现了该异常。

此处还有个编码有问题:是否走缓存的布尔变量应该优先判断,利用java判断短路机制跳出不去请求redis exists

// 此处是先判断在不在缓存里,再判断要不要读缓存
if (cacheManageService.isExists(sql) && readCache) {

//应该改成先判断要不要读缓存,利用布尔短路直接不用判断在不在缓存里,避免每次都要调用redis
if (readCache && cacheManageService.isExists(sql)) {
1
2
3
4
5

# 细分前缀 k/v,set 管理 keys

【1】由于我们的sql自带时间属性,我们将sql和引擎一起做了md5 or SHA1java hashcode碰撞概率大所以不采用,减少key的长度这里截取了前8 位,TTL一天,sql→ result的缓存。这样每天或者切换引擎将自动生成新的key,避免需要去删除或者更新缓存。
【2】根据我们的按表删除逻辑,使用redisset,保存同一个表下面的所有的sqlkey,以tableName+ 时间作为setkeyTTL一天,注意此处的时间要注意时区的问题。同时利用set的幂等操作,只需要调用sset即可,set的大多数操作也是O1的复杂度。
【3】最后用了一个固定前缀+时间作为keyset来管理当天的所有tableKey,这样子的三层结构,把所有的key全部打散,避免了出现大key问题,同时调用链也很清晰,避免了不必要的遍历操作。注意是此处我们调研后只保留了删除逻辑。

# 六、生产案例

国内Redis集群与国外Redis集群数据同步时,Redis超大Key双向同步导致客户端链接超时解决方案?

场景信息: 上云时Redis数据需做双向数据同步,开启后出现Redis连接超时异常,Redis版本为4.0.8
分析问题: 发现其中有超大key,最大的key7.2MB,超大key双向同步导致的资源占用。建议避免使用超大key。根据DBTrace中的Redis慢日志来进行分析。一个实际运行的参考数据是,当key大小为1.6MB时,Redis每日会有多次300-400ms的慢日志。
解决方案:RedisString类型的数据转换为Hash存储,并对Hash中的Filed按照范围划分为多个Hash集合。改造后进行数据同步,没有再出现超时异常。

(adsbygoogle = window.adsbygoogle || []).push({});