在深入分布式缓存这本书的学习之后,这里给出一些总结性的内容,也会结合自己的工组内容简单分享一下缓存相关的知识。声明一点,我个人对于分布式缓存的理解基本属于小白,并不清楚分布式缓存的各种协议(如果你是技术大牛,那么请跳过这篇文章),只是基于自己接触的业务去理解和解释一下缓存。
缓存的概念在这里就不介绍了,我相信百度比我介绍的还要清楚。
多级缓存概念
缓存这个概念在非常早期的计算机中已经出现过了,现代的计算机也是依然沿用了当时的很多缓存概念,大家比较熟知的就是CPU的三级缓存,内存等等。响应的速度也是由快到慢,大致的响应速度是这样子的:L1 cache 大概消耗3-5个CPU周期(大约1ns),L2 cache大约消耗10个CPU周期(大约3-5ns),L3 cache大约消耗40-60个CPU周期(15ns),然后主存(内存)的话大概消耗120-200个CPU周期(大概100ns)左右,硬盘的话大概需要10-20ms左右的访问时间,SSD的话大约在30-300us之间,从这里就能看出来基本的速度差了,当然我们也希望所有的数据都能想CPU cache一样快速响应,但是事实是响应越快的硬件价格就越贵。
应用缓存
在应用中我们一般使用比较广泛的是分布式缓存和本地缓存,分布式缓存就是独立于应用部署的一个缓存集群,可以是Memcached缓存,也可以Redis缓存,应用读取缓存数据或者写入缓存数据都需要通过网络来进行,也就是说在分布式缓存中网络的开销不容忽视,有时候读一个缓存的绝大多数时间都花在网络开销上。其次就是本地缓存,本地缓存就是服务器本身通过自己的堆内/堆外存储的缓存,因为缓存数据位于服务器本身,所以有一定的大小限制(如果是JVM的堆内缓存的话要考虑Heap的大小等等,如果是堆外缓存的话也要考虑服务器本身缓存的大小)。下面给出一种常见的缓存模型:
在请求(http请求)进入Nginx之后如果请求的数据在Varnish端有缓存数据的话就直接在Varnish端返回,否则的话就回源到tomcat集群处理。这个是比较常见的做法,有些RPC请求并不走Http协议,所以也就是直接将(读)请求打到了服务上,这时候如果能走本地缓存的话就优先通过本地缓存处理,如果本地缓存没有数据的话再通过分布式缓存组建读取,读取数据之后再回写到本地缓存,如果分布式缓存也没有读到数据的话就直接到DB中读取数据,然后依次回写到分布式缓存和本地缓存。
在分布式缓存中有很多需要注意的地方,例如缓存一致性,缓存的可可用性,缓存的持久化等等,这些内容讲真我都只是在网上读取过技术文章,并没有研究过。至于本地缓存是用EhCache还是Guava也没有研究过,所以如果介绍的话估计内容也比较水,这里就不讲了,本文主要侧重点是缓存的一些技巧性。
Tips:
- 缓存穿透 缓存穿透指当查询请求在缓存中找不到指定内容的时候会回源到数据库中查找相应数据,这种行为的弊端在于如果在并发量比较大的时候,某个缓存时效可能会导致大量的请求回源到数据库查询,可能会导致数据库的响应延迟,造成数据风暴。解决这个问题主要有以下几种方式:
- 缓存中查不到的话直接返回null,查不到即认为数据不存在。这种做法比较极端,但是在有的业务中可以适用,我司的项目中就是这么做的,通过定时任务全量刷新缓存,增量更新的话通过队列记录,并且通过ack机制确保缓存刷新成功。但是这种情况只适用于业务中层,底层的业务仍然需要罗落库查询。
- 缓存中查不到的话返回指定字符串,例如“#null”,然后由应用本身决定要不要继续落库查询。这种做法的好处就是在要不要落库的决定权上做出一定的权衡,因为有的场景下查不到数据影响不大,可以考虑忽略;但是有的情况下就一定要查到相关数据,否则影响用户体验(例如订单数据)。
- 使用分布式锁来解决回源问题,在缓存失效的时刻,如果有请求回源到数据库中查询,可以根据查询的key构建一个分布式锁,在查询过程中通过该锁控制查询的并发数量,查询结束后放开锁即可。在等待锁的时候可以设置一个折中的超时时间,例如100ms。
- 缓存失效
在刷新缓存的时候缓存的失效时间是一定要设置的,但是如果我们有定时任务批量更新缓存,一般情况下同一批key会设置同样的缓存失效时间,这么做的弊端就是如果某个时间点上所有的缓存在同一时刻失效,那么这时候会造成大量的回源请求,DB压力忽然增大是有风险的。我们可以通过给每个缓存的失效时间增加一个随机时间来解决这个问题,例如随机增加1-10分钟的失效时间,这样就不容易引起缓存集体失效的情况。 - 淘汰机制
- FIFO 先进先出的淘汰机制是基于一个队列实现的,只会在及其简答的场景下适用,大多数的情况下缓存都有一定的特性,所以这种失效方式用的比较少,在实际应用时也不建议。
- LRU 最近最少使用的淘汰机制实现比较简单,就是通过一个队列记录缓存的访问,如果某个缓存被访问过之后就将其移动到队头,然后队列满的时候直接移除队尾的元素就可以。这种缓存适用于热点缓存的处理,热点缓存在LRU中因为频繁被访问所以不会轻易被移除,具有不错的效果。但是弊端就是如果某些偶发性,周期性的缓存查询则会导致命中率下滑比较严重。
- LRU-K 这种方式主要解决了LRU的缓存污染问题,其核心就是将最近一次使用的标准扩展为最近K次使用。LRU-K的做法就是多维护一个队列,用于记录所有的缓存被访问的次数。只有当数据的访问次数到达K次之后才会将数据放入缓存中。如果需要淘汰数据的话,会淘汰第K次访问时间距离当前时间最长的缓存数据。LRU-K虽然可以较好的解决缓存污染问题,但是也有一定的代价,LRU-K要多维护一个队列,如果访问量比较多的时候,维护这个队列会消耗比较大的内容空间。目前来看使用比较多的LRU-2。
- LFU 根据历史数据的访问频率来淘汰数据,这种方式的弊端就是如果有定时性的访问或者偶发的访问则会导致大量的无用缓存存储在缓存空间中,容易造成数据浪费。
- 热点缓存 热点缓存就是在某个时刻访问量非常大的一批数据,通常在秒杀场景中会出现的比较多,解决热点缓存主要有以下几个方式:
- 本地缓存 可以通过热点推算的机制,定时推算出热点数据,然后推动到所有的服务器端,在服务器上直接通过申请堆内缓存存储起来,这样当流量爆发的时候本地的localcache其实已经帮应用挡住了绝大多数的请求。
- 提前推送 因为热点缓存的访问有时候是可以预测的,例如秒杀活动。这时候可以在秒杀活动开始前将热点数据推送到缓存服务器。但是需要保证热点缓存数据都推送成功,一般情况下不需要预热全量数据,只是预热部分数据即可,具体要根据缓存容量来评估。
文章结束~~~撒花~~