解构缓存:基本思想、本地缓存、分布式缓存和多级缓存的探索

  • 为什么要用缓存?
  • 本地缓存应该怎么做?
  • 为什么要有分布式缓存?/为什么不直接用本地缓存?
  • 为什么要用多级缓存?
  • 多级缓存适合哪些业务场景?

缓存的基本思想

缓存可以提高系统性能以及减少请求响应时间。

缓存的基本思想就是 空间换时间。

所谓空间换时间,就是用更多的存储空间来储存一些可能重复使用或计算的数据,从而减少数据的重新获取或计算的时间。

除了缓存之外,用到空间换时间思想的还有这些例子:

  • 索引: 索引是一种将数据库表中的某些列或字段按照一定的排序规则组织成一个单独的数据结构,需要额外占用空间,但可以大大提高检索效率,降低数据排序成本。
  • 数据库表字段冗余: 将经常联合查询的数据冗余存储在同一张表中,以减少对多张表的关联查询,进而提升查询性能,减轻数据库压力。
  • CDN(内容分发网络): 将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。

缓存的思想实际在CPU、操作系统或者其他很多地方都被大量用到。

比如:CPU Cache缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题;内存缓存的是硬盘数据,用于解决硬盘访问速度过慢的问题。

再比如,为了提高虚拟地址到物理地址的转换速度,操作系统在页表方案基础之上引入了转址旁路缓存(Translation Lookasjde Buffer,TLB,也被成为快表)。

再拿我们日常使用的软件来说,一般都会对你访问过的一些图片或者文件进行缓存,这样你下次再访问的时候加载速度就很快了。

我们日常开发过程中用到的缓存,其中的数据通常储存于内存中,因此访问速度非常快。为了避免内存中的数据在重启或者宕机之后丢失,很多缓存中间件会利用磁盘做持久化。也就是说,缓存相比较于我们常用的关系型数据库(比如MySQL)来说访问速度要快非常多。为了避免用户请求数据库中的数据速度过于缓慢,我们可以在数据库之上增加一层缓存。

除了能够提高访问速度之外,缓存支持的并发量也要更大,有了缓存之后,数据库的压力也会随之变小。

缓存的分类

在日常开发中用到的缓存通常分为以下几种:

本地缓存

什么是本地缓存?

本地缓存适合在单体架构中使用。数据量不大,并且没有分布式要求的话,使用本地缓存是可以的。

本地缓存位于应用内部,其最大的优点是应用于同一个进程内部,请求本地缓存的速度非常快,不存在额外的网络开销。

常见的单体架构图如下,我们使用Nginx来做负载均衡,部署两个相同的应用到服务器,两个服务器使用同一个数据库,并且使用的是本地缓存。

本地缓存的方案有哪些?

1、JDK自带的 HashMap ConcurrentHashMap

ConcurrentHashMap 可以看做是线程安全版本的 HashMap,两者都是存放key/value形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供: 过期时间、淘汰机制、命中率统计 这三点。

2、 Ehcache****Guava Cache**Spring Cache 这三者是使用的比较多的本地缓存框架。

  • Ehcache 的话相比于其他两者更加重量。不过,相比于 Guava CacheSpring Cache 来说, Ehcache 支持可以嵌入到hibernate和mybatis作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中,同时也提供了集群方案(比较鸡肋,可忽略)。
  • Guava CacheSpring Cache 两者的话比较像。 Guava 相比于 Spring Cache 的话使用的更多一点,它提供了API非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方和 ConcurrentHashMap 的思想有异曲同工之妙。
  • 使用 Spring Cache 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题。比如,缓存击穿、内存溢出等。

3、后起之秀Caffeine

相比于 Guava 来说, Caffeine 在各个方面比如性能都要更加优秀,一般建议使用其来代替 Guava。并且, GuavaCaffeine 的使用方式很像!

本地缓存有什么痛点?

本地缓存的优势非常明显: 低依赖、轻量、简单、成本低。

但是,本地缓存存在下面这些缺陷:

  • 本地缓存应用耦合,对分布式框架支持不友好。 比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
  • 本地缓存容量受服务部署所在的机器限制明显。 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。

分布式缓存

什么是分布式缓存?

我们可以把分布式缓存(Distributed Cache)看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。

分布式缓存脱离于应用独立存在,多个应用可直接的共同使用同一个分布式缓存服务。

如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用Nginx来做负载均衡,部署两个相同的应用到服务器,两个服务器使用同一个数据库和缓存。

使用分布式缓存之后,缓存服务可以部署在一台单独的服务器上,即使同一个相同的服务部署在多台机器上,也是使用的同一份缓存。并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。

软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。 你使用的方式得当,就能为系统带来很大的收益。否则,只是费了精力不讨好。

简单来说,为系统引入分布式缓存之后往往会带来下面这些问题:

  • 系统复杂性增加:引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存、保证缓存服务的高可用等等。
  • 系统开发成本往往会增加:引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。

分布式缓存的方案有哪些?

比较老的项目有用Memcached的,先在基本上用Redis比较多。

多级缓存

我们这里只来简单聊聊 本地缓存 + 分布式缓存 的多级缓存方案,这也是最常用的多级缓 存实现方式。

既然用了分布式缓存,为什么还要用本地缓存呢?

本地缓存和分布式缓存虽然都属于缓存,但本地缓存的访问速度要远大于分布式缓存, 这是因为访问本地缓存不存在额外的网络开销,我们在上面也提到了。

不过,一般情况下,我们也是不建议使用多级缓存的,这会增加维护负担(比如你需要 保证一级缓存和二级缓存的数据一致性)。而且,其实际带来的提升效果对于绝大部分业务场景来说其实并不是很大。

这里简单总结一下适合多级缓存的两种业务场景:

  • 缓存的数据不会频繁修改,比较稳定;
  • 数据访问量特别大比如秒杀场景。

多级缓存方案中,第一级缓存(L1)使用本地内存(比如 Caffeine)),第二级缓存 (L2)使用分布式缓存(比如 Redis)。

读取缓存数据的时候,我们先从L1中读取,读取不到的时候再去L2读取。这样可以降低L2的压力,减少L2的读次数。如果L2也没有此数据的话,再去数据库查询,数据查询成功后再将数据写入到L1和L2中。

J2Cache就是一个基于本地内存和分布式缓存的两级Java缓存框架。

原文阅读