缓存初始

什么是缓存

临时存放数据(读多写少)的地方,介于外部请求和真实数据之间;

什么情况下要缓存

数据的类型,业务的特点:读多写少,在正常工作的前提下响应时间尽可能短。

缓存的类型

根据缓存位置的不同的不同分为硬件缓存,客户端缓存,服务端缓存

  • 硬件缓存:硬盘缓存和cpu缓存,提高硬盘和cpu之间的暂存器
  • 客户端缓存:会把用户之前浏览的东西存在本地,在下次访问是,如果本地的缓存有请求的内容,就直接取,不再向服务器请求
  • 服务端缓存:如果每次客户端请求都要链接一次数据库,当用户请求多的时候,负载过大,这时把一些经常请求的数据存放在内存中,当有请求时直接返回,不经过数据库,这样就可以减轻数据库负担

进程缓存or分布式缓存or多级缓存

失效策略(缓存算法)

如果缓存满了,而当前请求又没有命中缓存,那么会按照一种策略,把缓存中的某一种旧资源剔除,而把新的资源加入缓存,这些决定应该剔除哪个旧资源的策略就是失效策略

  • 成本:如果缓存对象有不同的存储成本,应该把那些难以获得的对象保存下来
  • 容量:如果缓存对象有不同的大小,应该把那些大的缓存清除,让更多的小缓存对象进来
  • 时间:一些缓存设定有过期时间,应该在到时间之后将他们失效

常见失效策略

simple time-based


通过绝对的时间周期删除缓存对象,对于新增的对象,保存设定的时间后,将其失效,原理简单,效果不好

extended time-based expiration


通过相对时间去失效缓存对象,对于新增的缓存对象,会保存特定的时间,比如每5分钟,每天12点

sliding time-based expiration


计算最后访问这个缓存的时间算起,最后一次访问对象1小时后

FIFO:First in First Out


判断被存储的时间,最早缓存数据优先被剔除

LRU:Least Recently Used


最近最少,使用一个链表来保存。新数据插入链表头部,命中后,将数据移到链表头部,满了后,尾部数据丢弃,有个热点数据有段时间没访问,会导致这个热点数据淘汰

LFU:Least Frequently Used


最不经常使用原则,在一段时间内,数据命中 次数最少,优先被剔除,利用额外的空间记录每个数据的使用频率,选出频率最低的进行淘汰

对比与分析

失效策略出发点有两个,基于时间(FIFO)、基于命中次数(LRU,MRU,2Q),最常使用的时FIFO,LRU,LFU

不同的访问模型导致命中率变化较大,实际应用中需要根据业务的需求和对数据的访问请求进行选择,并不是命中率越高越好,考虑点包括:命中率,复杂度?,代价(时间复杂度、空间复杂度)等,如何衡量。。。

更新缓存的方法

定时失效

mq通知

缓存问题

缓存穿透

查询一个不存在的数据,首先,缓存命不中,数据库查不到,这样的请求每次都会请求数据库,流量大的时候,可能数据库就挂了。

解决办法:

  • 过滤器:布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截
  • 设定指定值:如果一个查询返回的数据为空,依然要把空结果进行缓存,缓存的值设定位一个指定值,同时设置它的过期时间很短,最长不超过5分钟

缓存雪崩

在某一时刻,大量缓存同时失效,请求全部到数据库,导致数据压力过大

解决办法:

  • 加锁或者队列的方式保证缓存的单线程来写,把失效时间分开
  • 将数据失效时间均匀分布在时间轴上,在原有的失效时间基础上增加一个随机值

缓存并发

如果一个缓存如果失效,可能出现多个进程同时查询DB,

解决办法:

对缓存查询加锁,如果key不存在,就加锁,然后查DB写入缓存,然后解锁

大厂经验

爱奇艺

  1. 数据同步加Redis:依赖于Redis,一旦redis挂了,整个缓存系统不可用
  2. javamap加redis:进程内缓存作为一级缓存,redis作为二级缓存,进程内缓存无法做到实时更新,当不需要淘汰机制,可以使用map,问题:
    1. 锁竞争严重
    2. 不支持过期时间
    3. 不支持自动刷新
  3. guava cache加redis:设置写后刷新时间,进行刷新,解决了一直不更新的问题,但是依然没有解决实时刷新
  4. 外部缓存异步刷新:利用redis作为消息队列通知机制,通知其他应用进行刷新

参考文献