欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于Redis热点key的一些思考

程序员文章站 2022-06-24 11:33:01
昨天在和一个已经跳槽的同事聊天时,询问他这段时间面试时碰到的一些问题。自己也想积累一下这方面的知识。其中他说了在面试某赞公司时面试官问他关于热点Key的的解决方案。于是针对这次谈话以及上网查的一些资料后的思考进行一下总结。方便后续自己查阅。 什么是热点Key 其实对于热点Key,网上一查一大堆,这里 ......

昨天在和一个已经跳槽的同事聊天时,询问他这段时间面试时碰到的一些问题。自己也想积累一下这方面的知识。其中他说了在面试某赞公司时面试官问他关于热点key的的解决方案。于是针对这次谈话以及上网查的一些资料后的思考进行一下总结。方便后续自己查阅。

什么是热点key

其实对于热点key,网上一查一大堆,这里我就引用网上的一段话。

从基于用户消费的数据远远大于生产的数据的角度来讲,我们平常使用的知乎等软件时,大多数人平常仅仅只是浏览,并不会去提问问题、发表的文章,偶尔会发表自己的文章或者看法,这就是一个典型的读多写少的情景,当然此类情景不太容易导致热点的产生。

在日常工作生活中一些突发的的事件,诸如:“双11”期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击、购买时,会形成一个较大的需求量,这种情况下就会产生一个单一的key,这样就会引起一个热点;同理,当被大量刊发、浏览的热点新闻,热点评论等也会产生热点;另外,在服务端读数据进行访问时,往往会对数据进行分片切分,此类过程中会在某一主机server上对相应的key进行访问,当访问超过主机server极限时,就会导致热点key问题的产生。

如何解决?

针对于热点key的解决方案网上的查找出来无非就是两种

  • 服务端缓存:即将热点数据缓存至服务端的内存中
  • 备份热点key:即将热点key+随机数,随机分配至redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。

其实这两个解决方案前提都是知道了热点key是什么的情况,那么如何找到热点key呢?

热点检测

  1. 凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此key作为热点key
  2. 客户端收集:在操作redis之前对数据进行统计
  3. 抓包进行评估:redis使用tcp协议与客户端进行通信,通信协议采用的是resp,所以能进行拦截包进行解析
  4. 在proxy层,对每一个 redis 请求进行收集上报
  5. 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))
}

 

写在最后

关于Redis热点key的一些思考