关于热点key读的一点思考
在业务上,对于热点key的定义一般有如下特征:
- 访问频次较大
- 访问在时间维度有稠密稀疏等明显区别
- key的数量规模可大可小
- 数据可能有一定的刷新需求
那什么场景下会存在这种热点key呢?
典型场景如:
缓存读:
- 首页
- 商品详情
一般打开应用或者小程序之类的,默认会跳转到首页,故而这个页面的数据日常流量就比较大。目前成规模的业务都会尝试千人千面的营销能力,但是局部内容还是会有一定相同性。故而在这个页面仍然会有大量的请求命中同一个key的缓存。
商品详情类似,比如秒杀商品场景,秒杀品往往比正常商品优惠,数量也要少得多。故而容易引来大量的流量集中访问某些商品的缓存数据。
针对缓存读的热点key问题,一般可以通过升级redis规格,对缓存的key做一致性hash分散到多个redis实例,或者做对等redis缓存实例等方式解决访问热点key的问题。
这种方式,在大规模缓存key的情况下,是一种非常好的解决方案。但是中小规模缓存key的情况下,这种方式效益不佳。主要是这几个问题:
- 缓存的访问主要集中于几个场景,主要用于抗读请求波峰,缓存使用率偏低;
- 另外一方面缓存的数据规模较少,redis升级规格或者一致性hash分散访问热点等方式会提整体处理能力,但是针对热点key的处理能力提升有限;
- 对等redis实例下,存在较严重的资源浪费,除非是业务的关键环节,否则这种方式收益太差;
RPC读,SQL读:
- 查询商品详情
- 查询店铺详情
- 查询个人详情
这些查询往往由其它微服务系统提供支持,当前业务系统并不会对其做缓存处理。但是当访问量急速上升的时候呢,对方服务往往会通过限流或者拒绝访问等方式进行自我保护,这种时候在访问端进行热点处理就有意义了。
针对RPC是这样,针对DB其实同样存在这种场景。比如查询商品详情如果部分商品缓存被穿透的情况下,如何防止请求压死DB。
针对RPC读,如果访问过多,一般的做法也是水平扩容服务提供方,增强处理能力。这种方式也存在几个问题:
- 访问流量有明显的波峰波谷,如果要支持弹性伸缩,还需要依赖一套弹性资源调度平台来帮助控制成本
针对SQL读,就只能要求命中主键索引,最次也要求命中一个二级索引了。如果访问量过大,还可以要求必须批量查询
基于上述认知,整理出如下几个特点:
- 正常情况下,ToC业务中,线程级别访问缓存,RPC,SQL等,针对单个数据项的比较多
- 需要扩容的情况,往往是突发流量超出当前服务集群能提供的能力上限了,但是正常情况下,往往资源严重过剩
- 获取数据基本都需要网络IO,能本地完成的场景较少
只要能保证在不影响读结果的情况下,缩小读数量,降低资源占用就可以有效解决上述问题。
针对这些特点,设计了一个并发转串行的热点key锁结构。
- 支持针对相同key的读请求线程,仅一个执行者线程发起远程读取,其它参与者线程本地读执行者结果
- 支持聚合多个单个key读的请求线程,执行者线程按组批量读,其它参与者线程本地读执行者结果
代码后续整理再上传。
那是否可以引入本地缓存来解决呢?
本地缓存一般占用堆内存,缓存数据量较小的情况下,本地缓存能起到一个较好的效果。但是,如果缓存数据存在刷新需求,会导致数据晋升老年代较多,增加fullGC的概率。缓存数据量较大的情况下,受限于内存容量,命中率会降的比较厉害。故而,本地缓存不能有效解决问题。
基于锁来做,是出于以下几点的考虑:
- 缓存,RPC,SQL这类都可以归属于数据读资源,对于这类读访问,单个读转换为批量读在时间上往往相近,但是资源占用上相去甚远
- 线程级别的锁换网络IO&链接资源,是一个合算的交易
- 缓存,RPC,SQL,都有或者可以提供批量操作的API,充分利用这个能力能有效降低资源负载。