缓存技术:本地缓存与分布式缓存

2023-04-02  本文已影响0人  BruceOuyang

一、本地缓存框架

本地缓存框架适用于单机应用场景,可以通过缓存来提高数据访问的速度和效率。Ehcache、Guava Cache 和 Caffeine 都是常见的本地缓存框架,它们在实现本地缓存的功能方面有所不同。

适用场景:适用于单机应用场景,可以通过缓存来提高数据访问的速度和效率,减少数据库的压力和响应时间。

1、Ehcache

1.1 简介

Ehcache是一个开源的Java分布式缓存框架,由Terracotta公司开发,它是一个广泛使用的缓存框架,为Java应用程序提供了性能优化和可扩展性。Ehcache可以在Java应用程序中存储缓存数据,提供快速响应和优化性能。Ehcache还支持多个缓存策略,包括FIFO、LRU和LFU等,可以根据具体场景选择最适合的缓存策略。除此之外,Ehcache还提供了分布式缓存功能,支持多个缓存节点之间的数据同步和共享。

Ehcache 优点:

Ehcache 缺点:

1.2 官方文档

https://www.ehcache.org/documentation/

1.3 Java配置示例

// 创建缓存管理器
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
cacheManager.init();

// 创建缓存实例
Cache<Integer, String> cache = cacheManager.createCache("myCache",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class,
                ResourcePoolsBuilder.heap(100)));

// 将元素放入缓存
cache.put(1, "Hello, Ehcache!");

// 从缓存中获取元素
String result = cache.get(1);

// 关闭缓存管理器
cacheManager.close();

1.4 缓存策略

最近最少使用策略(Least Recently Used, LRU)

将最近最少使用的对象从缓存中删除,保留最常用的对象。

先进先出策略(First In First Out, FIFO)

按照对象进入缓存的顺序,先进先出地淘汰对象。

最近最少使用时间策略(Least Recently Used Time, LRUT)

类似于LRU策略,但考虑了对象最后一次访问时间,最近使用时间较早的对象被淘汰。

最不经常使用策略(Least Frequently Used, LFU)

淘汰一段时间内使用频率最少的对象。

基于权重的策略(Weighted Random Early Detection, WRED)

将权重较低的对象从缓存中删除,权重越高的对象保留的几率越大。

同步策略(Synchronous)

所有的写操作将被同步到缓存和持久化存储中,适合要求高可靠性的场景。

异步策略(Asynchronous)

写操作首先写入缓存,然后异步地写入持久化存储,适合对可靠性要求不高的场景。

永久缓存策略(Eternal)

对象永远不会过期,适合静态数据。

时间缓存策略(Time To Live, TTL)

对象存活一定的时间后将过期。

访问缓存策略(Time To Idle, TTI)

对象在一定时间内未被访问后将过期。

2、Guava Cache

2.1 简介

Guava Cache是Google Guava库中提供的一种本地缓存框架,它为应用程序提供了一种轻量级的缓存解决方案。Guava Cache提供了一些简单易用的API,可以用来构建基于内存的本地缓存,并支持多种缓存策略,包括基于时间和大小的缓存清除、手动清除缓存、缓存项回收等。

Guava Cache 优点:

Guava Cache 缺点:

2.2 官方文档

https://github.com/google/guava/wiki/CachesExplained

2.3 Java配置示例

// 创建缓存实例
Cache<Integer, String> cache = CacheBuilder.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(1000)
        .build();

// 将元素放入缓存
cache.put(1, "Hello, Guava Cache!");

// 从缓存中获取元素
String result = cache.getIfPresent(1);

// 关闭缓存
cache.invalidateAll();

2.4 缓存策略

基于容量的回收策略

该策略基于缓存中元素的数量,设置缓存最大容量,一旦超过该容量,将使用最近最少使用算法(LRU)清除最近最少使用的缓存项。

基于大小的回收策略

该策略基于缓存项占用的空间大小来限制缓存项的数量。一旦超过了缓存的最大容量,就会使用LRU算法来清除最近最少使用的缓存项,以便为新缓存项腾出空间。

基于时间的回收策略

该策略基于缓存项的过期时间来清除缓存项。当缓存项超过指定的时间范围时,它将被清除。缓存项可以在创建时指定过期时间,也可以通过调用CacheBuilder.expireAfterWrite(long, TimeUnit)方法来设置默认的过期时间。

基于引用的回收策略

该策略将缓存项与GC的回收进行结合。这意味着缓存项将在虚拟机需要回收内存时被清除。Guava Cache支持两种类型的引用:弱引用和软引用。当缓存项的键或值被回收时,缓存项将被自动清除。

手动回收策略

该策略允许使用者手动清除缓存项,通过调用Cache.invalidate(Object)或Cache.invalidateAll()方法实现。这对于在缓存中放置需要即时失效的数据非常有用。

组合回收策略

Guava Cache还支持将多个回收策略组合使用。例如,可以将基于时间的回收策略与基于容量的回收策略相结合,以便在缓存过期之前限制缓存的最大容量。这个可以通过调用CacheBuilder.maximumSize(long).expireAfterWrite(long, TimeUnit)方法实现。

总的来说,Guava Cache提供了丰富的缓存策略,可以根据应用程序的不同需求进行自由组合,参考官方文档。

3、Caffeine

3.1 简介

Caffeine是一个基于Java 8的高性能缓存库,提供了一些常用缓存功能,如缓存过期、自动加载、统计等,并且具有内存友好、高性能、线程安全等特点。Caffeine支持多种缓存策略,如基于容量、基于时间、基于权重、手动移除、定时刷新等,并提供了丰富的配置选项,能够适应不同的应用场景和需求。Caffeine也是Spring框架中缓存抽象接口的默认实现。

Caffeine 优点:

Caffeine 缺点:

3.2 官方文档

https://github.com/ben-manes/caffeine/wiki

3.3 Java配置示例

// 创建缓存实例
Cache<Integer, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(1000)
        .build();

// 将元素放入缓存
cache.put(1, "Hello, Caffeine!");

// 从缓存中获取元素
String result = cache.getIfPresent(1);

// 关闭缓存
cache.invalidateAll();

3.4 缓存策略

最近最少使用策略(LRU)

缓存的数据按照访问时间排序,每次淘汰最久未被访问的数据。优点是容易实现,缺点是无法适应一些特殊场景,例如缓存数据的访问频率差异很大的场景。

最少使用次数策略(LFU)

缓存的数据按照被访问次数排序,每次淘汰访问次数最少的数据。该策略适合访问频率稳定的数据。

固定容量策略(Fixed Size)

缓存数据容量固定,当容量满时,采用LRU或其他策略进行淘汰。

固定重量策略(Weigher)

缓存数据容量固定,但不是按照数据条目数量来计算的,而是按照缓存数据的重量来计算,当重量达到上限时,采用LRU或其他策略进行淘汰。

时间过期策略(Time-based expiration)

缓存数据存储的时间固定,一旦存储时间超过预设时间,缓存数据将被淘汰。

写入过期策略(Write expiration)

缓存数据在写入时,可以设置其存储时间,一旦存储时间超过预设时间,缓存数据将被淘汰。

访问过期策略(Access expiration)

缓存数据在最近一次访问之后,经过预设时间后将被淘汰。

异步刷新策略(Asynchronous Refreshing)

缓存数据在被淘汰前,会异步地进行刷新。该策略适用于缓存数据很难预测何时失效,但刷新缓存数据的代价比重新加载要小的情况。

记录统计策略(Record Statistics)

对缓存的访问次数、命中率等进行统计,便于后续的优化和管理。

手动加载策略(Manual Loading)

手动将缓存数据加载到缓存中,适用于加载缓存数据的代价较大的情况。

SoftValues

使用软引用作为value的引用类型,当JVM内存不足时,可能会回收一些value,以尽量保留剩余的内存。

WeakKeys

使用弱引用作为key的引用类型,当key没有被其他对象引用时,可能会被垃圾回收器回收。

WeakValues

使用弱引用作为value的引用类型,当value没有被其他对象引用时,可能会被垃圾回收器回收。

WeakKeysWeakValues

同时使用弱引用作为key和value的引用类型,当key和value都没有被其他对象引用时,可能会被垃圾回收器回收。

这些缓存策略的具体实现方式和使用场景可以参考Caffeine的官方文档。

比较

特性比较

特性/框架 Ehcache Guava Cache Caffeine
缓存类型 磁盘、内存 内存 内存
并发处理 一般 一般
自动回收
容量限制
内存占用 中等
分布式支持 支持 不支持 不支持
支持API版本 2.x、3.x 10.x、11.x、21.x 2.x、3.x
开源许可证 Apache 2.0 Apache 2.0 Apache 2.0

Ehcache 3.x 支持分布式,但与 2.x 版本有较大差异。Guava Cache 主要作为本地缓存框架使用。Caffeine 支持 Java 8 及以上版本,比 Guava Cache 性能更好,拥有更多高级特性,但同时也更复杂。

综上所述,Ehcache 在分布式缓存和并发性能方面较为优秀,但内存占用较大;Guava Cache 是一个轻量级的本地缓存框架,具有简单易用的特点,但并发性能较弱;Caffeine 则是一个快速、高效、内存占用小的本地缓存框架,适合需要高性能和低内存占用的场景。

缓存策略比较

缓存策略 Ehcache Guava Cache Caffeine
最近最少使用 (LRU) 支持 支持 支持
最近最少使用 (LIRS) 不支持 不支持 支持
最近最少使用 (LFU) 支持 不支持 支持
时间过期 (TTI) 支持 支持 支持
基于容量的大小 (基于条目数量) 支持 支持 支持
基于容量的大小 (基于缓存值的大小) 支持 支持 支持
固定大小 支持 不支持 不支持
内存敏感 支持 支持 支持
弱引用 支持 支持 不支持
软引用 支持 支持 不支持

二、分布式缓存

1、简介

分布式缓存指的是将缓存数据存储在多台服务器上,通过协调和通信来实现数据的一致性和高可用性的一种缓存技术。与传统的本地缓存相比,分布式缓存可以提高系统的性能和可扩展性,适用于高并发和大规模数据的场景。

2、业务场景

3、作用/目的

综上所述,分布式缓存可以提高应用的性能和可扩展性,降低后端系统的负载,实现数据共享和高可用性等。

4、常见问题

针对上述问题,常见的解决方案包括:

4.1 布隆过滤器解决缓存穿透

布隆过滤器(Bloom Filter)是一种空间效率很高的概率型数据结构,用于判断某个元素是否属于一个集合,其最大的优势在于可以判断出某个元素肯定不存在于集合中,因此可以用于缓存穿透的解决。

具体来说,我们可以在缓存中存储一个布隆过滤器,用于判断请求的 key 是否存在于缓存中。如果布隆过滤器判断 key 不存在于缓存中,我们就可以直接返回空结果,而不需要查询数据库,从而避免了缓存穿透的问题。

下面是一个 Java 代码示例,演示如何使用 Google Guava 提供的布隆过滤器来实现缓存穿透的处理:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;

public class BloomFilterCache {

    // 预期插入元素数量
    private static final int EXPECTED_INSERTIONS = 1000000; 
    // 期望的误判率 
    private static final double FPP = 0.001;  

    private static final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.        stringFunnel(Charset.defaultCharset()), EXPECTED_INSERTIONS, FPP);

    public static void put(String key) {
        bloomFilter.put(key);
    }

    public static boolean mightContain(String key) {
        return bloomFilter.mightContain(key);
    }
}

在这个示例中,我们使用 Google Guava 提供的 BloomFilter 类来创建一个布隆过滤器,用于存储字符串类型的数据。在实际应用中,我们可以将这个布隆过滤器存储在缓存中,用于判断请求的 key 是否存在于缓存中。

当我们需要往缓存中存储数据时,我们可以调用 put 方法将数据的 key 添加到布隆过滤器中。当有请求需要查询缓存中的数据时,我们可以调用 mightContain 方法来判断请求的 key 是否存在于布隆过滤器中。如果布隆过滤器判断 key 不存在,我们就可以直接返回空结果,避免了缓存穿透的问题。

4.2 使用分布式锁解决缓存击穿问题

使用分布式锁解决缓存击穿问题的原理是在缓存失效的同时,使用分布式锁来保证只有一个线程去访问数据库,其他线程等待该线程的结果,并从缓存中获取数据。

在实现过程中,可以使用Redis的setnx(SET if Not eXists)命令来实现分布式锁,其原理是利用Redis的单线程特性,在设置键时,只有一个客户端能够成功地获取到锁。

以下是Java示例代码

public Object getData(String key) {
    Object value = redisTemplate.opsForValue().get(key);
    if (value == null) {
        // 使用分布式锁,保证只有一个线程能够访问数据库
        String lockKey = "lock:" + key;
        String requestId = UUID.randomUUID().toString();
        boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 5, TimeUnit.SECONDS);
        if (lock) {
            // 获取到锁,从数据库中获取数据,并将数据保存到缓存中
            value = getDataFromDatabase(key);
            redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
            // 释放锁
            String lockedRequestId = redisTemplate.opsForValue().get(lockKey);
            if (requestId.equals(lockedRequestId)) {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 没有获取到锁,等待其他线程获取数据并返回结果
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            getData(key);
        }
    }
    return value;
}

private Object getDataFromDatabase(String key) {
    // 从数据库中获取数据
    return null;
}

在上述代码中,首先从缓存中获取数据,如果缓存中没有数据,则使用分布式锁来保证只有一个线程能够访问数据库。如果获取到锁,则从数据库中获取数据,并将数据保存到缓存中;如果没有获取到锁,则等待其他线程获取数据并返回结果。最后,释放锁。

4.3 缓存雪崩的具体解决方案

方案一:缓存过期时间随机化

通常情况下,缓存过期时间都是固定的,比如10分钟或30分钟,这样容易导致缓存同时失效,引发缓存雪崩。因此,我们可以考虑将缓存过期时间进行随机化,使得不同的缓存具有不同的过期时间,这样可以分散缓存失效的时间点,减少缓存雪崩的概率。

示例代码:

// 生成随机过期时间,范围为10分钟到30分钟之间
int expireTime = 10 * 60 + new Random().nextInt(20 * 60);
// 设置缓存,并指定过期时间
cache.put(key, value, expireTime, TimeUnit.SECONDS);

方案二:多级缓存

将缓存分为多个层级,缓存层级越高的缓存数据过期时间越长,容错能力越强。比如可以将缓存分为2个层级:本地缓存、分布式缓存,其中本地缓存使用Guava Cache等本地缓存框架,过期时间比较短,分布式缓存使用Redis等分布式缓存框架,过期时间比本地缓存长。

代码示例:

public class MultiLevelCache<K, V> {

    private final Cache<K, V> localCache;
    private final Cache<K, V> redisCache;
    private final RedisTemplate<K, V> redisTemplate;
    private final String redisKeyPrefix;

    public MultiLevelCache(Cache<K, V> localCache, Cache<K, V> redisCache, RedisTemplate<K, V> redisTemplate, String redisKeyPrefix) {
        this.localCache = localCache;
        this.redisCache = redisCache;
        this.redisTemplate = redisTemplate;
        this.redisKeyPrefix = redisKeyPrefix;
    }

    public V get(K key) {
        V value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        value = redisCache.getIfPresent(key);
        if (value != null) {
            localCache.put(key, value);
            return value;
        }
        value = redisTemplate.opsForValue().get(getRedisKey(key));
        if (value != null) {
            localCache.put(key, value);
            redisCache.put(key, value);
        }
        return value;
    }

    public void put(K key, V value) {
        localCache.put(key, value);
        redisTemplate.opsForValue().set(getRedisKey(key), value);
    }

    public void invalidate(K key) {
        localCache.invalidate(key);
        redisTemplate.delete(getRedisKey(key));
    }

    private K getRedisKey(K key) {
        return (K) (redisKeyPrefix + ":" + key.toString());
    }
}

方案三:使用限流器

使用限流器(Rate Limiter)来控制并发量,避免瞬间大量请求同时访问缓存造成雪崩效应。限流器可以控制单位时间内的请求次数或并发量,确保系统在高并发情况下能够保持稳定,不会因为瞬时高并发而导致系统崩溃。

限流器的实现方式可以使用令牌桶算法或漏桶算法等。在Java中,Guava库和Spring Cloud框架都提供了限流器的实现。

以下是Guava库中使用令牌桶算法实现限流器的示例代码:

RateLimiter rateLimiter = RateLimiter.create(10); // 每秒最多处理10个请求
if (rateLimiter.tryAcquire()) {
    // 处理缓存请求
} else {
    // 返回限流提示
}

在以上代码中,RateLimiter.create(10)表示每秒最多处理10个请求,rateLimiter.tryAcquire()尝试获取令牌,如果获取成功,则处理缓存请求;如果获取失败,则返回限流提示。通过控制每秒处理的请求数,可以有效地避免系统因瞬时高并发而导致的缓存雪崩问题。

除了上述方法外,还可以使用多级缓存、缓存预热、动态扩容缓存等技术来解决缓存雪崩问题。在设计分布式缓存系统时,应根据实际情况选择合适的技术和方案,以确保系统的稳定性和可靠性。

4.4 分布式缓存一致性问题解决的具体步骤

缓存数据一致性问题通常有两个方面:缓存数据的更新和缓存数据的失效。以下是解决缓存数据一致性问题的一些具体步骤:

单点更新缓存数据指的是在数据更新时,通过某个单独的节点来更新缓存数据。这种方式虽然简单,但是存在单点故障和性能瓶颈的问题。

更新数据库时同时更新缓存数据是常见的解决方案。这种方式能够保证数据的一致性,但是也会带来一定的性能开销。

延迟失效指的是在缓存数据过期之前,更新缓存数据,保证缓存中的数据一直有效。这种方式能够提高性能,但是可能会出现数据不一致的问题。

主动失效指的是在数据更新时,主动将缓存中的数据失效,使得下次请求时,可以从数据库中获取最新的数据。这种方式可以保证数据的一致性,但是会带来性能开销。

综合来说,解决缓存数据一致性问题的具体步骤要根据具体的业务场景来确定,需要在保证数据一致性的前提下,尽可能地提高系统的性能和可用性。

三、常用中间件

1、Redis

1.1 简介

Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis具有内存数据结构存储、数据持久化、分布式高可用、发布订阅、事务、Lua脚本等功能。它支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合等,并且支持多种持久化方式,包括RDB快照和AOF日志。Redis的特点是速度快,操作简单易用,支持复杂数据结构和高并发,是一个非常受欢迎的开源缓存和存储解决方案。

Redis的优点:

Redis的缺点:

1.2 官网

https://redis.io

1.3 主要特性

2、Memcached

2.1 简介

Memcached是一个免费的开源高性能分布式内存对象缓存系统,它可以用于减轻动态数据库负载,提高Web应用程序的速度、可扩展性和可靠性。它最初是由LiveJournal的Brad Fitzpatrick在2003年创建的。Memcached通过缓存最常用的数据和对象,减少数据库访问,提高应用程序的性能和响应速度。它使用了分布式内存缓存的方式来存储键/值对,可以轻松地扩展到多台服务器上,因此成为一个非常流行的分布式缓存系统。

Memcached 的优点包括:

Memcached 的缺点包括:

2.2 官网

https://memcached.org

2.3 主要特性

最后

推荐使用 Redis 作为 NFT 产品开发中的缓存技术,原因如下:

more

文章首发在老欧的个人站点:老欧的issueList

上一篇 下一篇

猜你喜欢

热点阅读