优质博文 IT-BLOG-CN
一、大Key的大小
【1】一般情况下,我们认为字符串类型的key
的value
值超过10kb
,就算大key
。
【2】具体需要根据业务场景,如果一个key
的大小为1MB
,QPS
为1000
,那么每秒会产生1000MB
的流量,这会对系统产生影响。
二、大Key的危害
【1】占用内存大,空间不均匀
【2】操作耗时,容易阻塞
【3】每次存取网络流量大,容易网络阻塞
三、业务场景中常见的大Key
【1】单个简单的key
存储的value
很大
【2】hash
、set
、zset
、list
中存储过多的元素(以万为单位)
【3】一个集群存储了上亿的key
,Key
本身过多也带来了更多的空间占用
四、处理方法
【1】分拆
● 单个key
存储的value
很大:
◆ 需要整体存取:分拆成多个key mget
。
◆ 部分存取:使用hash
值分拆,或者存入redis hash
中的field
● value
存储过多的元素:
◆ 分桶:将大hash
、set
、list
等按field
的hash
值模除进行分桶,分拆成多个集合
◆ 分区:对于时间有效性的可以加上时间后缀拆分
● Key
过多:
◆ 转为hash
结构存储
【2】删除:大key
线上删除要使用unlink
,del
会阻塞(自然过期也会出现阻塞)
【3】其他
● redis hash
不能expire field
,redis
只能过期顶级key
● mget
:需要同时获取多个key
的值时请使用mget
而不是循环get
多次
● 减少redis
操作,每次请求都要消耗时间,比如del
操作不需要先判断exists
,get
的值存一个local
变量,不要对一个key
重复get
● 根据业务场景,使用redis
的不同的数据结构:list
, hash
, set
, sorted set
, bitmap
● 热点key
问题,可以将key
加上后缀拆分到不同机器上
阿里 redis 键值设计规范
五、大Key改造案例
大 json
【1】业务需求需要保存查询sql
对应的结果集k: sql hashcode
→v:
查询结果result
存入redis
,以及k: sql hashcode
→v:
(时间、sql
语句、查询引擎),在本地存进Map
序列化成Json
,由一个固定key(SQL_CACHE)
放入redis
,过期时间7
天。
【2】缓存设置:一个新查询进来,保存sqlkey
以及result
,取出SQL_CACHE
对应的json
,反序列化成map
,put
新的kv
,再序列化重写回 redis
(此处会有事务问题,同时请求set
会导致一个值丢失)。
【4】相应的缓存删除更新逻辑,需要在监听QMQ
表重刷或者QConfig
切换表引擎的情况下,取出SQL_CACHE
对应的json
,遍历此map
,对应的每个sql
解析表名,匹配则做相应的删除更新操作。
【5】没有多久SQL_CACHE
这个key
就变成了大key
报警了。
redis hash
第一次改造就针对这个大key
把原本的map
直接存成redis hash
,避免反复序列化以及大key
网络传输成本。
但是此处有个错误使用:为了兼容原来的流程,改造的时候只是简单的替换,导致了故障:遍历hash
的keyset
然后依次 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());
}
某一次请求调用了7000
次redis
操作导致了timeout
发现了该异常。
此处还有个编码有问题:是否走缓存的布尔变量应该优先判断,利用java
判断短路机制跳出不去请求redis exists
。
// 此处是先判断在不在缓存里,再判断要不要读缓存
if (cacheManageService.isExists(sql) && readCache) {
//应该改成先判断要不要读缓存,利用布尔短路直接不用判断在不在缓存里,避免每次都要调用redis
if (readCache && cacheManageService.isExists(sql)) {
细分前缀 k/v,set 管理 keys
【1】由于我们的sql
自带时间属性,我们将sql
和引擎一起做了md5 or SHA1
,java hashcode
碰撞概率大所以不采用,减少key
的长度这里截取了前8
位,TTL
一天,sql→ result
的缓存。这样每天或者切换引擎将自动生成新的key
,避免需要去删除或者更新缓存。
【2】根据我们的按表删除逻辑,使用redis
的set
,保存同一个表下面的所有的sqlkey
,以tableName
+ 时间作为set
的key
,TTL
一天,注意此处的时间要注意时区的问题。同时利用set
的幂等操作,只需要调用sset
即可,set
的大多数操作也是O1
的复杂度。
【3】最后用了一个固定前缀+时间作为key
的 set
来管理当天的所有tableKey
,这样子的三层结构,把所有的key
全部打散,避免了出现大key
问题,同时调用链也很清晰,避免了不必要的遍历操作。注意是此处我们调研后只保留了删除逻辑。
六、生产案例
国内Redis
集群与国外Redis
集群数据同步时,Redis
超大Key
双向同步导致客户端链接超时解决方案?
场景信息: 上云时Redis
数据需做双向数据同步,开启后出现Redis
连接超时异常,Redis
版本为4.0.8
。
分析问题: 发现其中有超大key,最大的key
为7.2MB
,超大key
双向同步导致的资源占用。建议避免使用超大key
。根据DBTrace
中的Redis
慢日志来进行分析。一个实际运行的参考数据是,当key
大小为1.6MB
时,Redis
每日会有多次300-400ms
的慢日志。
解决方案: 将Redis
的String
类型的数据转换为Hash
存储,并对Hash
中的Filed
按照范围划分为多个Hash
集合。改造后进行数据同步,没有再出现超时异常。