没看这篇干货,别说你会使用“缓存”( 三 )

进程内缓存
通过了客户端,CDN,负载均衡器,我们终于来到了应用服务器 。应用服务器上部署着一个个应用,这些应用以进程的方式运行着,那么在进程中的缓存是怎样的呢?
进程内缓存又叫托管堆缓存,以 JAVA 为例,这部分缓存放在 JVM 的托管堆上面,同时会受到托管堆回收算法的影响 。
由于其运行在内存中,对数据的响应速度很快,通常我们会把热点数据放在这里 。
在进程内缓存没有命中的时候,我们会去搜索进程外的缓存或者分布式缓存 。这种缓存的好处是没有序列化和反序列化,是最快的缓存 。缺点是缓存的空间不能太大,对垃圾回收器的性能有影响 。
目前比较流行的实现有 Ehcache、GuavaCache、Caffeine 。这些架构可以很方便的把一些热点数据放到进程内的缓存中 。
这里我们需要关注几个缓存的回收策略,具体的实现架构的回收策略会有所不同,但大致的思路都是一致的:

  • FIFO(First In First Out):先进先出算法,最先放入缓存的数据最先被移除 。
  • LRU(Least Recently Used):最近最少使用算法,把最久没有使用过的数据移除缓存 。
  • LFU(Least Frequently Used):最不常用算法,在一段时间内使用频率最小的数据被移除缓存 。
在分布式架构的今天,多应用中如果采用进程内缓存会存在数据一致性的问题 。
这里推荐两个方案:
  • 消息队列修改方案
  • Timer 修改方案
消息队列修改方案
应用在修改完自身缓存数据和数据库数据之后,给消息队列发送数据变化通知,其他应用订阅了消息通知,在收到通知的时候修改缓存数据 。
没看这篇干货,别说你会使用“缓存”

文章插图
 
消息队列修改方案简图
Timer 修改方案
为了避免耦合,降低复杂性,对“实时一致性”不敏感的情况下 。每个应用都会启动一个 Timer,定时从数据库拉取最新的数据,更新缓存 。
不过在有的应用更新数据库后,其他节点通过 Timer 获取数据之间,会读到脏数据 。这里需要控制好 Timer 的频率,以及应用与对实时性要求不高的场景 。
没看这篇干货,别说你会使用“缓存”

文章插图
 
Timer 修改方案简图
进程内缓存有哪些使用场景呢?
  • 场景一:只读数据,可以考虑在进程启动时加载到内存 。当然,把数据加载到类似 redis 这样的进程外缓存服务也能解决这类问题 。
  • 场景二:高并发,可以考虑使用进程内缓存,例如:秒杀 。
分布式缓存
说完进程内缓存,自然就过度到进程外缓存了 。与进程内缓存不同,进程外缓存在应用运行的进程之外,它拥有更大的缓存容量,并且可以部署到不同的物理节点,通常会用分布式缓存的方式实现 。
分布式缓存是与应用分离的缓存服务,最大的特点是,自身是一个独立的应用/服务,与本地应用隔离,多个应用可直接共享一个或者多个缓存应用/服务 。
没看这篇干货,别说你会使用“缓存”

文章插图
 
分布式缓存简图
既然是分布式缓存,缓存的数据会分布到不同的缓存节点上,每个缓存节点缓存的数据大小通常也是有限制的 。
数据被缓存到不同的节点,为了能方便的访问这些节点,需要引入缓存代理,类似 Twemproxy 。他会帮助请求找到对应的缓存节点 。
同时如果缓存节点增加了,这个代理也会只能识别并且把新的缓存数据分片到新的节点,做横向的扩展 。
为了提高缓存的可用性,会在原有的缓存节点上加入 Master/Slave 的设计 。当缓存数据写入 Master 节点的时候,会同时同步一份到 Slave 节点 。
一旦 Master 节点失效,可以通过代理直接切换到 Slave 节点,这时 Slave 节点就变成了 Master 节点,保证缓存的正常工作 。
每个缓存节点还会提供缓存过期的机制,并且会把缓存内容定期以快照的方式保存到文件上,方便缓存崩溃之后启动预热加载 。
高性能
当缓存做成分布式的时候,数据会根据一定的规律分配到每个缓存应用/服务上 。
如果我们把这些缓存应用/服务叫做缓存节点,每个节点一般都可以缓存一定容量的数据,例如:Redis 一个节点可以缓存 2G 的数据 。
如果需要缓存的数据量比较大就需要扩展多个缓存节点来实现,这么多的缓存节点,客户端的请求不知道访问哪个节点怎么办?缓存的数据又如何放到这些节点上?


推荐阅读