缓存 | 从本地缓存到分布式缓存, Guava, Caffeine, Memcached, Redis

2020-10-10 09:52:47 浏览数 (1)

从本地缓存到分布式缓存

本文档中部分代码不保证可以运行

虽然标题为缓存,但在这里不仅仅会涉及缓存,还会涉及一些其他提高应用性能的方案。

在程序设计中,经常能听到的就是以时间换空间以空间换时间缓存作为一种能加快程序性能的银弹,它是典型的后者(以空间换时间).

随着用户数和访问量越来越大,我们的应用需要支撑更多的并发量,同时我们的应用服务器和数据库服务器所做的计算也越来越多。但是往往我们的应用服务器资源是有限的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是引入缓存,每个环节中请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。

缓存并不是包治百病的银弹

第一次接触缓存`MAP`

我第一次接触缓存的时候是在大三开始出去工作的时候。在一个系统中,基本每个接口都有可能要获取一次用户信息和一些用户配置,当时我们的系统查多改少,这也注定缓存可以大大提高我们的性能,当时的做法是维护一个全局的单例的Map作为缓存存储.记得当时的类名叫DBMirror

大致如下:

代码语言:javascript复制
class DBMirror {
    private static Map<String, User> userCache = new HashMap<>();

    public static void putUser(String key, User user) {
        userCache.put(key, user);
    }

    public static User getUser(String key) {
        return userCache.get(key);
    } 

    private DBMirror() {}

}

代码很简单,基本满足了当时系统的要求,减少了很多数据库读写操作,在当时也是第一次开始意识到 数据库 并不是唯一的存储. 原来 Map 还能这样使用

但是上面的代码有个很大的缺点,随着用户的增多,里面并没有合适的剔除算法,会导致 Map 越来越大,极端情况会导致内存溢出

常见淘汰策略

如上所述,如果不使用剔除算法,会导致内存占用越来越大,且无法回收,那下面讲一下常见的淘汰策略

FIFO(first in first out)

先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。

LFU(less frequently used)

最少使用策略,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。

LRU(least recently used)

最近使用策略,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。

其他

  1. 为缓存元素设置过期时间,清理超过过时时间的元素
  2. 随机清理
  3. 优先清理大对象

缓存简单分类

本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。

分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。缺点是:优点也就是缺点,因为自身是一个独立的应用,本地节点都需要与其进行通信,导致依赖网络,同时如果缓存服务崩溃可能会影响所有依赖节点

对于一些单个实例的服务,或者数据基本不会变化的数据都可以使用本地缓存来提高性能,反之可以使用分布式缓存

技术方案本身没有最好的,只有最合适的.

缓存的使用

Java集合类

在上面提供了一个简单的例子,DBMirror使用Map来实现一个简单的内存缓存,同时SetList都可以达到内存缓存的功能,根据并发情况可以选择不同的实现类,例如HashMapLinkedHashMapTreeMapLinkedTreeMapConcurrentHashMap… 总有一个满足你

这样实现很简单,但是也致命缺点:无法回收不常用的缓存

Guava Cache

说起 Guava, 很多人都不会陌生,它是 Google 提供的一个非常好用的 Java 工具包。Guava Cache 是 Guava 中的一个本地缓存实现,基于LRU算法实现,并提供了多种缓存过期策略,过期时间、容量等. 简化了缓存的使用,方便我们更加大胆的使用缓存

Caffeine

Caffeine是一个基于 Java8 开发的提供了近乎最佳命中率的高性能的缓存库。

在本地缓存方面,SpringFramework5.0(SpringBoot2.0)放弃了Google的GuavaCache,选择了「Caffeine」(Drop Guava caching - superseded by Caffeine [SPR-13797] #18370)。足以见证其在性能和可靠性上的优势.

其性能测试可以查看 https://github.com/ben-manes/caffeine/wiki/Benchmarks

Ehcache

Ehcache是纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现,我们常用的Hibernate里面就集成了相关缓存功能。

在早期开发的时候也用过这个,现在不知道是否还在使用

Memcached

一个高性能的、分布式的基于内存的key-value对象存储系统,用来存储小块的任意数据(字符串、对象)

通过访问其来较少数据库的读写压力

Redis

Redis 同样是一个高性能的基于内存中数据结构存储,用作数据库,缓存和消息代理。

它支持更多的数据结构,例如 strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes。

Redis具有内置的复制,Lua脚本,LRU逐出,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供高可用性

Spring Cache

Spring Cache 并不是缓存的实现,而是一个缓存管理的抽象解决方案,这种方案消除了样板方法的使用,屏蔽了缓存的使用细节,而这是 Spring 最擅长干的.

Spring 的缓存技术还具备相当的灵活性,可以使用 SpEL 来定义缓存的 key 和各种 condition,提供了灵活的开箱即用的解决方案.

注意事项

在使用缓存的过程中,我们还要注意缓存不一致、缓存穿透、缓存击穿与缓存雪崩等问题,每种问题都是不小的问题

这篇写的并不长,每种都是简单介绍了一下,马上分几篇分别介绍一下各自的具体使用方法,敬请期待

参考

  1. spring cache
  2. https://github.com/google/guava/wiki/CachesExplained
  3. https://github.com/ben-manes/caffeine
  4. https://www.memcached.org/
  5. https://redis.io/

0 人点赞