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

一、本地缓存框架

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

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

1、Ehcache

1.1 简介

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

Ehcache 优点:

  • 易于使用:Ehcache 提供了简单易用的 API,可以轻松地将其集成到项目中,同时也提供了丰富的配置选项,可以根据实际需求进行调整。
  • 高性能:Ehcache 的缓存性能非常出色,可以大大减少应用程序访问数据库的次数,提高系统的性能和响应速度。
  • 可扩展性:Ehcache 支持多级缓存、缓存复制和集群部署等功能,可以轻松地实现分布式缓存,提高系统的可扩展性和容错性。
  • 可靠性:Ehcache 提供了完善的缓存管理和监控功能,可以确保缓存数据的可靠性和一致性。
  • 支持多种缓存策略:Ehcache 支持多种缓存策略,如 LFU、LRU、FIFO 等,可以根据实际需求进行选择和配置。

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 提供了简单易用的 API,可以非常方便地使用本地缓存。
  • 高性能:Guava Cache 是一款高性能的本地缓存实现,使用 Guava Cache 可以大大提升应用程序的访问性能。
  • 灵活性高:Guava Cache 提供了灵活的缓存配置选项,可以根据业务场景进行优化和调整。
  • 易于集成:Guava Cache 可以非常容易地集成到 Java 应用程序中,只需要添加对应的 Maven 依赖即可。

Guava Cache 缺点:

  • 本地缓存:Guava Cache 是一款本地缓存实现,只能用于单个节点的缓存场景,无法满足分布式缓存的需求。
  • 大规模缓存:Guava Cache 适用于中小规模缓存,对于大规模缓存场景需要考虑分布式缓存实现。
  • 没有缓存淘汰策略:Guava Cache 缺乏像 Redis、Ehcache 那样完善的缓存淘汰策略,需要手动设置缓存失效时间来达到类似的效果。

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 优点:

  • 更快的缓存读写性能,具有更高的吞吐量和更低的延迟
  • 支持更多的缓存策略和选项,更灵活和可定制
  • 更少的内存占用和更好的缓存效率
  • 支持异步加载和刷新缓存项
  • 可以与Spring等框架无缝集成

Caffeine 缺点:

  • 不支持分布式缓存
  • 没有Guava Cache的缓存预热功能
  • 不支持缓存项失效的监听器

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 都是基于缓存对象的大小和时间限制的 LRU 算法实现的,可以对缓存的元素进行自动过期、剔除和大小限制等操作。而 Caffeine 则是基于缓存的访问模式和大小进行缓存元素的淘汰,可以使用不同的淘汰算法,如 LRU、LFU 和 W-TinyLFU 等。

  • 并发性能:
    在并发性能方面,Caffeine 是最快的本地缓存框架,其使用了多种并发技术来保证缓存的并发访问性能,如并发哈希表、CAS 和读写锁等。而 Ehcache 和 Guava Cache 在并发性能方面相对较差,不支持并发访问,可能会出现线程安全问题。

  • 内存占用:
    在内存占用方面,Caffeine 的内存占用最小,因为其使用了一些特殊的数据结构和算法来减小内存占用,如堆外内存和布隆过滤器等。而 Ehcache 和 Guava Cache 的内存占用相对较大,可能会导致内存浪费和缓存性能下降。

  • 分布式缓存支持:
    Ehcache 提供了分布式缓存的支持,可以将缓存对象存储在多个节点上,以提高缓存的可用性和可扩展性。而 Guava Cache 和 Caffeine 不支持分布式缓存,只能用作本地缓存。

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

缓存策略比较

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

二、分布式缓存

1、简介

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

2、业务场景

  • 高并发读取:对于访问频率较高的数据,可以将其缓存在分布式缓存中,减少对数据库等存储系统的压力,提高读取性能。
  • 高并发写入:对于需要频繁写入的数据,通过分布式缓存可以减少对数据库等存储系统的写入次数,提高写入性能。
  • 减少资源占用:通过缓存可以减少对存储系统、网络等资源的占用,提高系统的可扩展性和可靠性。
  • 数据共享:通过分布式缓存可以实现多个应用程序之间共享数据,提高系统之间的协同能力。
  • 数据安全:通过缓存可以缓存已处理的数据,减少对用户信息的读取和处理,从而提高数据的安全性。

3、作用/目的

  • 提高应用性能:缓存可以将数据存储在内存中,从而提高读取数据的速度,减轻数据库等后端系统的负载,提高应用的性能。
  • 减少数据库访问:缓存可以将一些常用的数据存储在缓存中,从而减少对数据库的访问次数,降低了对数据库的压力,减少了对数据库的负载。
  • 实现数据共享:分布式缓存可以将数据缓存到多个节点中,实现数据共享和数据同步。
  • 提高应用可扩展性:应用的性能和可扩展性往往是紧密关联的,缓存可以提高应用的性能,从而提高应用的可扩展性。
  • 实现高可用性:缓存可以实现多节点的数据备份和数据同步,从而提高系统的可用性,减少单点故障的风险。

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

4、常见问题

  • 缓存穿透:指查询一个不存在的数据,由于缓存中没有,每次请求都会直接请求数据库,导致数据库压力过大,甚至宕机。
  • 缓存击穿:指某个热点key失效或过期,此时大量请求会同时涌入数据库,导致数据库压力过大,甚至宕机。
  • 缓存雪崩:指缓存中大量的key在同一时间过期,导致请求全部落到了数据库上,造成数据库压力过大,甚至宕机。
  • 缓存数据一致性:在分布式环境下,不同节点的缓存数据不一致,可能导致业务异常。
  • 缓存并发竞争:在高并发场景下,由于多个线程同时请求缓存数据,可能导致缓存穿透和缓存击穿问题。

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

  • 使用布隆过滤器等技术解决缓存穿透问题。
  • 使用分布式锁、热点缓存预热等技术解决缓存击穿问题。
  • 对缓存过期时间进行随机化、增加缓存容错等技术解决缓存雪崩问题。
  • 使用缓存更新策略,如缓存失效时异步更新缓存等技术解决缓存数据一致性问题。
  • 使用本地缓存或缓存预热等技术解决缓存并发竞争问题。

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 分布式缓存一致性问题解决的具体步骤

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

  • 单点更新缓存数据

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

  • 更新数据库时同时更新缓存数据

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

  • 延迟失效

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

  • 主动失效

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

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

三、常用中间件

  • Redis:Redis是目前国内外最流行的分布式缓存中间件,支持多种数据类型和丰富的功能,如发布订阅、Lua脚本、事务、持久化等。
  • Memcached:Memcached是最早的分布式缓存中间件之一,主要支持key-value缓存,具有高性能和可扩展性。
  • Tair:Tair是阿里巴巴自主研发的分布式缓存中间件,具有高性能、高可用、高扩展性等特点,支持多种数据结构和应用场景。
  • Pika:Pika是快手开源的一款分布式缓存中间件,具有高性能、高可用、易扩展等特点,支持Redis协议。
  • Ctrip Tcache:Tcache是携程开源的一款分布式缓存中间件,具有高性能、高可用、易扩展等特点,支持多种数据结构和应用场景。
  • QCache:QCache是去哪儿网开源的一款分布式缓存中间件,基于Redis实现,具有高性能、高可用、易扩展等特点。
  • XMemcached:XMemcached是一款高性能的Java分布式缓存中间件,支持Memcached和Ketama两种哈希算法。

1、Redis

1.1 简介

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

Redis的优点:

  • 高性能:Redis是基于内存的数据存储,读写速度快,单个Redis实例可以处理超过100k个QPS。
  • 数据类型多样:Redis支持多种数据类型,如字符串、哈希、列表、集合和有序集合等。
  • 持久化支持:Redis支持持久化存储数据,可以将数据写入磁盘中,即使服务器崩溃也可以恢复数据。
  • 分布式支持:Redis支持分布式架构,可以将数据分布到多个节点上,提高性能和可扩展性。
  • Lua脚本支持:Redis支持使用Lua脚本执行复杂的操作。
  • 主从复制:Redis支持主从复制,可以将写操作集中在主节点,读操作分散到从节点,提高性能和可用性。
  • 高可用:Redis支持哨兵和集群模式,实现高可用和负载均衡。

Redis的缺点:

  • 单机容量有限:Redis是基于内存的,单机容量有限制,无法存储大量数据。
  • 持久化性能较低:Redis的持久化操作会影响性能。
  • 数据一致性问题:Redis在分布式模式下,需要考虑数据一致性的问题。
  • 不支持ACID事务:Redis不支持ACID事务,虽然可以使用Lua脚本实现原子性操作,但是在多个命令之间的一致性无法保证。
  • 内存管理问题:Redis需要管理内存,如果使用不当会导致内存泄漏等问题。

1.2 官网

https://redis.io

1.3 主要特性

  • 内存存储:Redis将所有数据存储在内存中,读写速度非常快。
  • 持久化:Redis可以将数据持久化到磁盘中,保证数据的可靠性。
  • 数据结构丰富:Redis支持多种数据结构,如字符串、列表、哈希表、集合、有序集合等。
  • 高并发、高可用:Redis支持主从复制和哨兵模式,可以保证高可用和数据一致性。
  • 事务支持:Redis支持事务,可以批量执行多个命令。
  • Lua脚本支持:Redis支持使用Lua脚本来扩展其功能。
  • 发布/订阅机制:Redis支持发布/订阅机制,可以实现消息的订阅和推送。

2、Memcached

2.1 简介

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

Memcached 的优点包括:

  • 高性能:Memcached 将缓存数据存储在内存中,因此读取速度非常快,可以显著降低应用程序访问数据库的压力。
  • 分布式架构:Memcached 可以通过分布式架构实现横向扩展,提高系统的可扩展性和可用性。
  • 简单易用:Memcached 的 API 简单易用,学习成本低,支持多种编程语言,易于集成到应用程序中。

Memcached 的缺点包括:

  • 功能较为简单:Memcached 只支持简单的键值对存储,不支持复杂数据类型和事务操作等高级功能。
  • 缺乏安全机制:Memcached 本身没有安全机制,需要使用第三方插件或自己实现安全控制。
  • 不支持持久化:Memcached 数据存储在内存中,不支持数据持久化,系统重启或崩溃后数据会丢失。

2.2 官网

https://memcached.org

2.3 主要特性

  • 快速的内存缓存:Memcached的设计目标是快速地缓存数据,它通过在内存中缓存数据来提高数据访问的速度。
  • 分布式架构:Memcached支持分布式架构,可以在多台服务器上部署,提供更高的可用性和性能。
  • 简单的数据结构:Memcached的数据结构非常简单,主要是键值对,因此易于使用和管理。
  • 高并发性能:Memcached具有很高的并发性能,可以支持成千上万的并发连接。
  • 适应于多种编程语言:Memcached可以支持多种编程语言,如Java、Python、PHP等,非常适用于分布式应用程序的缓存需求。
  • 自动失效:Memcached会自动失效数据,从而避免了占用过多的内存空间。
  • 可扩展性:Memcached可以很容易地扩展到更多的节点,以满足不断增长的应用程序需求。

最后

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

  • 高性能:Redis 是一种基于内存的缓存技术,具有极快的读写速度和低延迟,适合处理高并发和实时数据请求。

  • 数据结构丰富:Redis 支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,可以满足不同类型的数据处理需求。

  • 高可用性:Redis 支持主从复制和 Sentinel 集群等多种高可用性方案,保证系统的可靠性和可用性。

  • 开源社区支持:Redis 拥有活跃的开源社区和丰富的生态系统

more

  • redis 具体操作相关内容
  • mysql 数据库分库分表技术,MyCat代理及客户端ShardingJDBC,及自研分库分表相关内容
  • RabbitMQ & Kafka 消息中间件相关内容

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容