关于 Redis 热点 key 的一些思考
昨天在和一个已经跳槽的同事聊天时,询问他这段时间面试时碰到的一些问题。自己也想积累一下这方面的知识。其中他说了在面试某赞公司时面试官问他关于热点 Key 的的解决方案。于是针对这次谈话以及上网查的一些资料后的思考进行一下总结。方便后续自己查阅。
什么是热点 Key
其实对于热点 Key,网上一查一大堆,这里我就引用网上的一段话。
从基于用户消费的数据远远大于生产的数据的角度来讲,我们平常使用的知乎等软件时,大多数人平常仅仅只是浏览,并不会去提问问题、发表的文章,偶尔会发表自己的文章或者看法,这就是一个典型的读多写少的情景,当然此类情景不太容易导致热点的产生。
在日常工作生活中一些突发的的事件,诸如:“双 11”期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击、购买时,会形成一个较大的需求量,这种情况下就会产生一个单一的 Key,这样就会引起一个热点;同理,当被大量刊发、浏览的热点新闻,热点评论等也会产生热点;另外,在服务端读数据进行访问时,往往会对数据进行分片切分,此类过程中会在某一主机 Server 上对相应的 Key 进行访问,当访问超过主机 Server 极限时,就会导致热点 Key 问题的产生。
如何解决?
针对于热点 Key 的解决方案网上的查找出来无非就是两种
- 服务端缓存:即将热点数据缓存至服务端的内存中
- 备份热点 Key:即将热点 Key+ 随机数,随机分配至 Redis 其他节点中。这样访问热点 key 的时候就不会全部命中到一台机器上了。
其实这两个解决方案前提都是知道了热点 Key 是什么的情况,那么如何找到热点 key 呢?
热点检测
- 凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此 Key 作为热点 Key
- 客户端收集:在操作 Redis 之前对数据进行统计
- 抓包进行评估:Redis 使用 TCP 协议与客户端进行通信,通信协议采用的是 RESP,所以能进行拦截包进行解析
- 在 proxy 层,对每一个 redis 请求进行收集上报
- Redis 自带命令查询:Redis4.0.4 版本提供了
redis-cli –hotkeys
就能找出热点 Key
如果要用 Redis 自带命令查询时,要注意需要先把内存逐出策略设置为 allkeys-lfu 或者 volatile-lfu,否则会返回错误。进入 Redis 中使用
config set maxmemory-policy allkeys-lfu
即可。
服务端缓存
假设我们已经统计出了一些热点 Key,将这些数据缓存到了服务端,那么还有一个问题。就是如何 保证 Redis 和服务端热点 Key 的数据一致性。我这里想到的解决方案是利用 Redis 自带的消息通知机制,对于热点 Key 客户端建立一个监听,当热点 Key 有更新操作的时候,客户端也随之更新。
主要代码如下,监听类负责接收到 Redis 的事件,然后筛选出热点 Key 进行相应的动作
public class KeyExpiredEventMessageListener implements MessageListener {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {String key = new String(message.getChannel());
key = key.substring(key.indexOf(":")+1);
String action = new String(message.getBody());
if (HotKey.containKey(key)){String value = redisTemplate.opsForValue().get(key)+"";
switch (action){
case "set":
log.info("热点 Key:{} 修改",key);
HotKeyAction.UPDATE.action(key,value);
break;
case "expired":
log.info("热点 Key:{} 到期删除",key);
HotKeyAction.REMOVE.action(key,null);
break;
case "del":
log.info("热点 Key:{} 删除",key);
HotKeyAction.REMOVE.action(key,null);
break;
}
}
}
}
建立一个存储热点 Key 的数据结构ConcurrentHashMap
,并设置相应的操作方法,这里设置了假数据,在 static 代码块中直接设置了两个热点 Key
public class HotKey {private static Map<String,String> hotKeyMap = new ConcurrentHashMap<>();
private static List<String> hotKeyList = new CopyOnWriteArrayList<>();
static {setHotKey("hu1","1");
setHotKey("hu2","2");
}
public static void setHotKey(String key,String value){hotKeyMap.put(key,value);
hotKeyList.add(key);
}
public static void updateHotKey(String key,String value){hotKeyMap.put(key,value);
}
public static String getHotValue(String key){return hotKeyMap.get(key);
}
public static void removeHotKey(String key){hotKeyMap.remove(key);
}
public static boolean containKey(String key){return hotKeyList.contains(key);
}
}
其实用 Redis 的事件通知机制挺不好的,因为只要开启了事件通知,那么每个 Key 的变化都会发消息,这样也会平白无故的加重 Redis 服务器的负担。当然我只是简单的演示一下,除了这种通知方案以外还有很多种方法。
备份热点 Key
这个方案说起来其实也很简单,就是不要让 key 走到一台机器上就行,但是我们知道在 Redis 集群中包含了 16384
个哈希槽 (Hash slot),集群使用公式CRC16(key) % 16384
来计算 Key 属于哪个槽。那么同一个 Key 计算出来的值应该都是一样的,如何将 Key 分到其他机器上呢?只要再后面加上随机数就行了,这样就能保证同一个 Key 分布在不同机器上,在访问的时候通过 Key+ 随机数的方式进行访问。
伪代码如下
const M = N * 2
// 生成随机数
random = GenRandom(0, M)
// 构造备份新 key
bakHotKey = hotKey +“_”+ random
data = redis.GET(bakHotKey)
if data == NULL {
// 从数据库中取数据
data = GetFromDB()
// 存放在 Redis 中,以便下次能取到
redis.SET(bakHotKey, expireTime + GenRandom(0,5))
}
代码地址 Github
参考文章
- http://mysql.taobao.org/monthly/2018/09/08/
- https://www.cnblogs.com/rjzheng/p/10874537.html