作者:一个Java菜鸟
1、背景介绍
1.1、现象
QPS突然增长2倍以上(45w~60w每分钟) 将产生下面一些问题:
1)响应接口响应时长增加了5倍(qps增加了2倍);
2)机房局域网交换机带宽报警(1kM带宽使用了900多M);
3)从redis获取数据接口响应时长增加等。
1.2、原因
1)某业务线对有限的产品进行推广;
2)在短时间内有大量重复数据查询请求;
3)短时间从redis获取大量数据。
1.3、解决方案
大量请求获取同一份数据,在本地存储这些数据。
其优点如下:
1)直接从内存取数据,降低响应时间;
2)不走redis,减少服务与redis之间的交互流量;
3)最终实现流量削峰
2、LRU-K模型设计
2.1、LRU算法介绍
Least recently used(LRU,最近最少使用):根据数据的历史访问记录淘汰数据。
核心思想
如果数据最近被访问过,那么将来被访问的几率更高。
命中率
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
LRU算法模型如下图:
1)新数据插入到链表头部;
2)每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3)当链表满的时候,将链表尾部的数据丢弃。
2.2、LRU-K算法设计
LRU-K中的K代表最近使用的次数。
主要目的
解决LRU算法“缓存污染”的问题。
核心思想
“最近使用过1次”的判断标准扩展为“最近使用过K次”。
命中率
LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。
LRU-K模型如下图:
1)数据第一次被访问,加入到访问历史记录表(简称记录表);在记录表中对应的K单元中设置最后访问时间=new(),且设置访问次数为1;
2)如果数据访问次数没有达到K次,则访问次数+1。最后访问时间与当前时间间隔超过预设的值(如30秒),访问次数清0并加1;
3)当数据访问计数超过(>=)K次后,则访问次数+1。将数据保存到LRU缓存队列中,缓存队列重新按照时间排序;
4)LRU缓存队列中数据被再次访问后,重新排序;
5)LRU缓存队列需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
子模块LRU存储模型:
类似ConcurrentHashMap,大致由二维数组+链表+访问队列三部分组成
Segment数组每个节点包含访问队列,访问队列模型如下图:
Segment数组每个节点都包含一个访问队列,通过这个队列来实现lru算法;
访问队列是一个环状双向链表,LRU算法由访问队列实现
3、缓存框架
3.1、系统数据存储组成
数据存储使用DB+本地缓存(LocalCache)+Redis三层结构,如下图:
3.2、数据查询流程
先从本地缓存取,本地缓存没有从redis取(同时更新本地缓存),redis没有从DB取(同时更新Redis)。具体步骤如下图:
1)先计算该数据获取总次数
2)未达到K访问记录时直接从redis取数据
3)达到K次访问记录时,从本地缓存取,本地缓存不存在时从redis获取数据(同时放入本地缓存中)
3.3、数据更新流程
删除缓存数据后,会再次从redis获取并更新缓存
4、调优过程
4.1、参数动态配置
配置参数如果放在Java类或配置文件中,每次调整都需要重启服务,执行不方便。
配置参数包括 K次访问统计数据清0时长(5分钟->30秒)、K次访问阀值参数、调优日志开关(调优时打开,平时关闭)、本地缓存最大数量等等。
动态调整+实时生效:配置参数放在可实时更新的组件中(如apollo),每次修改后会立即生效。
4.2、性能调优
4.2.1、多机器本地缓存同步增加原来的业务响应时长
优化方案:同步缓存操作改成异步
4.2.2、服务发布时接口抖动
优化方案:
1)服务启动时执行比较耗时初始化操作:如jdbc初始化,K次统计结构初始化。
2)模拟核心dubbo接口,提前生成本地机器码。
5、实际效果
5.1、效果(1)
1)优化参数前QPS增长时,响应时间未见明显变短。如左右第1列
2)参数调优上线后。QPS明显增加后,redis请求响应时间增加的情况下,整体响应时间未见变化。见左右最后1列
5.2、效果(2)
QPS增加4倍,响应时间未见变化,跟平时一样
demo源代码见:
参考方法:
com.example.liuyaohua.cache.lruk.LrukCacheTest