凌晨三点半了,太困了,还差一些,明天补上…
因为自己最近做的项目涉及到了缓存,所以水一篇缓存相关的文章,供大家作为参考,若发现文章有纰漏,希望大家多指正。
缓存涉及到的范围颇广,从CPU缓存,到进程内缓存,到进程外缓存。再加上已经凌晨一点了,我得保住我的几丝残发,本文不会将每一处的细枝末节都写到,见谅。
这里提一句CPU缓存,因为缓存的核心思想都是那点事,命中、淘汰、一致性等。
以前着重写过CPU的一些东西,这里只附一张图。
ps:听说最近有哪个厂商的CPU把三级缓存架构和总线锁改了,有相关资源的小伙伴快发给我,我观摩一下,hhh~
本文重点不在多级缓存,因为以前我也专门写过一篇关于多级缓存的详细设计。
简要步骤:
二级缓存最佳实践:Caffeine + Redis
性能优化:
市面上也有二级缓存框架,比如J2Cache,该框架本身并没有做额外工作,主要是集成了常见的进程内缓存和进程外缓存。
如果基于Spring开发,基于AOP设计的Spring Cache框架适配常用的缓存,自身的注解和策略天然和业务解耦,很不错,但是,如何集成Redis,这里需要特别注意!!!
因为集成Redis时,Spring Cache的清除策略,在从Redis中删除缓存时使用的是 keys指令,keys指令时间复杂度是O(N),如果缓存数量较大会产生明显的阻,因此在生产环境中Redis会禁用这个指令,导致报错。
//keys 指令
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
.toArray(new byte[0][]);
if (keys.length > 0) {
statistics.incDeletesBy(name, keys.length);
connection.del(keys);
}
所以,我们可以重写DefaultRedisCacheWriter(spring cache提供的默认的Redis缓存写出器,其内部封装了缓存增删改查等逻辑)
使用scan命令代替keys命令
//使用scan命令代替keys命令
Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)).count(1000).build());
Set<byte[]> byteSet = new HashSet<>();
while (cursor.hasNext()) {
byteSet.add(cursor.next());
}
byte[][] keys = byteSet.toArray(new byte[0][]);
讲真的,多级缓存和二级缓存这东西,不要为了炫技乱用,可能会增加没必要的开发成本和未知问题,而且还要做好数据量的评估,别搞了缓存,造成雪崩,那就真的血本无归了。
至理名言:不结合业务的技术都是耍流氓。
与没有缓存相比,进程内缓存的好处是,数据读取不再需要访问后端,例如数据库。
与进程外缓存相比(例如redis/memcache),进程内缓存省去了网络开销,所以一来节省了内网带宽,二来响应时延会更低。
如果数据缓存在站点和服务的多个节点内,数据存了多份,一致性比较难保障。
站点与服务的进程内缓存,实际上违背了分层架构设计的无状态准则
如果缓存挂掉,所有的请求会压到数据库,如果未提前做容量预估,可能会把数据库压垮(在缓存恢复之前,数据库可能一直都起不来),导致系统整体不可服务。
应提前做容量预估,如果缓存挂掉,数据库仍能扛住,才能执行上述方案。
否则,就要进一步设计:
使用高可用缓存集群(例如主备),一个缓存实例挂掉后,能够自动做故障转移。
使用缓存水平切分,一个缓存实例挂掉后,不至于所有的流量都压到数据库上。
例如,我做过的一个单体架构项目,缓存用Caffeine,每个业务都会有一个Caffeine实例。
对于读请求:
(1)先读cache,再读db;
(2)如果,cache hit,则直接返回数据;
(3)如果,cache miss,则访问db,并将数据set回缓存;
对于写请求:
(1)淘汰缓存,而不是更新缓存;
(2)先操作数据库,再淘汰缓存;
修改成本太大了,无脑选淘汰,问题不大
FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此之外,还有一些简单策略比如:
根据过期时间判断,清理过期时间最长的元素;
根据过期时间判断,清理最近要过期的元素;
随机清理;
根据关键字(或元素内容)长短清理等。
底层数据结构,W-TinyLFU算法,当然还有权威给出个各个组件性能对比图,谁不愿意用好的呢,对吧。(关于Caffeine源码,改天单写一篇)
没有为什么,无脑选就完了,下周我写一篇Redis7的源码文章,你就懂了。