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

1. 缓存的基本思想

缓存是一种用于提高数据访问速度的技术,其核心思想是将经常访问的数据存储在更快的存储介质中,以减少对原始数据源(如数据库或远程服务)的访问次数。通过这种方式,可以显著降低延迟,提高系统的响应速度和吞吐量。

缓存的作用:

  • 提高系统性能
  • 减少请求响应时间

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

1.1 空间换时间

歪个题,提到空间换时间,除了缓存之后,还有哪些应用到空间换时间思想的例子呢?

  • 索引:
    • 索引是一种将数据库表中的某些列或字段按照一定的排序规则组织成一个单独的数据结构,需要额外占用空间,但可以大大提高检索效率,降低数据排序成本。
  • 数据库表字段冗余:
    • 将经常联合查询的数据冗余存储在同一张表中,以减少对多张表的关联查询,进而提升查询性能,减轻数据库压力。
  • CDN(内容分发网络):
    • 将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。
  • 动态规划:
    • 动态规划算法通常会用一个表格来记录子问题的解,从而避免重复求解相同的子问题。
    • 例如:0-1背包问题、最长公共子序列问题。

1.2 缓存思想的应用场景

缓存的思想广泛应用于多个领域,以提高性能、减少延迟和优化资源利用。

  1. Web开发
  • 浏览器缓存(Browser Cache)
    • 浏览器会缓存网页的静态资源(如图片、CSS、JavaScript文件),以便用户再次访问时无需重新下载。
  • CDN(内容分发网络)
    • CDN通过在全球多个节点缓存网站内容,使得用户可以从最近的服务器获取资源,减少延迟。
  1. 数据库
  • 查询缓存(Query Cache)
    • 数据库系统可以缓存常用的SQL查询结果,避免重复执行相同的查询。
  • 内存表(In-Memory Tables)
    • 将频繁访问的数据存储在内存中,加快读取速度。
  • Redis/Memcached
    • 这些内存数据库常用于缓存热数据,减轻主数据库的压力。
  1. 操作系统
  • 磁盘缓存(Disk Cache)
    • 操作系统会在内存中缓存磁盘读写操作的数据,以加速文件系统的访问。
  • 页面缓存(Page Cache)
    • 操作系统会将虚拟内存页面缓存在物理内存中,减少磁盘I/O次数。
  1. 应用程序
  • 对象缓存(Object Caching)
    • 应用程序可以缓存复杂的对象或计算结果,避免重复创建或计算。
  • 函数返回值缓存(Memoization)
    • 缓存函数的返回值,当相同参数再次调用时直接返回缓存结果,避免重复计算。
  1. 硬件
  • CPU缓存(L1/L2/L3 Cache)
    • CPU内部有多级缓存,用于存储最近使用的指令和数据,减少访问主存的时间。
    • CPU Cache缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题;内存缓存的是硬盘数据,用于解决硬盘访问速度过慢的问题。
  • GPU缓存
    • 类似于CPU缓存,GPU也有自己的缓存层次结构,用于加速图形处理任务。
  1. 网络协议
  • HTTP缓存
    • HTTP协议支持缓存机制,服务器可以通过响应头告知客户端哪些资源可以被缓存以及缓存的有效期。
  • DNS缓存
    • DNS解析结果会被缓存一段时间,以减少重复查询域名的时间。
  1. 游戏开发
  • 纹理缓存(Texture Cache)
    • 游戏引擎会缓存常用的纹理资源,以加快渲染速度。
  • AI行为缓存
    • 缓存NPC的行为模式或路径规划结果,减少实时计算的需求。
  1. 云计算
  • 云存储缓存
    • 云服务提供商通常会在本地数据中心缓存常用的数据,以提高访问速度。
  • 边缘计算(Edge Computing)
    • 在靠近用户的地方部署缓存节点,减少数据传输延迟。

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

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

2. 缓存的分类

在此,只讨论在日常开发过程中用到的几种类型的缓存。

2.1 本地缓存

2.1.1 什么是本地缓存

本地缓存是指直接存储在应用程序运行环境中的缓存,通常位于内存中。它与应用程序进程紧密关联,读取速度极快。

2.1.2 适用场景

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

适用于高频率访问且数据不常变化的场景,例如:

  • 用户会话信息
  • 配置参数
  • 系统字典等相对静态的数据

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

2.1.3 实现方案

2.1.3.1 JDK自带的 HashMap 和 ConcurrentHashMap
  1. 使用场景:
  • 适合简单的缓存需求,尤其是单线程环境下。
  • ConcurrentHashMap 适用于多线程环境,提供更高的并发性能。
  1. 代码示例:
java 复制代码
// 使用 HashMap
Map<String, String> cache = new HashMap<>();
cache.put("key", "value");
String value = cache.get("key");

// 使用 ConcurrentHashMap
ConcurrentHashMap<String, String> concurrentCache = new ConcurrentHashMap<>();
concurrentCache.put("key", "value");
String concurrentValue = concurrentCache.get("key");
  1. 优缺点:
  • 优点:简单易用,无额外依赖。
  • 缺点:缺乏高级缓存特性(如过期策略、容量限制),不适合复杂的缓存需求。没有提供其他诸如过期时间之类的功能。

tips:一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。

2.1.3.2 本地缓存框架Ehcache、Guava Cache、Spring Cache
  1. 使用场景:
  • Ehcache:适合需要持久化支持的场景,广泛应用于Java EE项目。
  • Guava Cache:适合轻量级应用,提供丰富的缓存配置选项。
  • Spring Cache:适合基于Spring框架的应用,简化了缓存集成。
  1. 代码示例:

Ehcache:

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.0</version>
</dependency>
java 复制代码
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;

public class EhcacheExample {
    public static void main(String[] args) {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
        cacheManager.init();

        Cache<String, String> cache = cacheManager.createCache("myCache",
            CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(10))
                .build());

        cache.put("key", "value");
        String value = cache.get("key");
        System.out.println(value);
    }
}

Guava Cache:

java 复制代码
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class GuavaCacheExample {
    private static final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return fetchDataFromSource(key);
            }
        });

    private static String fetchDataFromSource(String key) {
        // 模拟从外部数据源获取数据
        return "value-" + key;
    }

    public static void main(String[] args) throws ExecutionException {
        String value = cache.get("key");
        System.out.println(value);
    }
}

Spring Cache:

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
java 复制代码
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Cacheable("myCache")
    public String getData(String key) {
        // 模拟从外部数据源获取数据
        return "value-" + key;
    }
}
  1. 优缺点:
  • Ehcache:
    • 优点:功能丰富,支持持久化,适合大型企业级应用。
    • 缺点:配置较为复杂,学习曲线较陡。
  • Guava Cache:
    • 优点:轻量级,易于集成,提供了灵活的缓存策略。
    • 缺点:缺少分布式支持。
  • Spring Cache:
    • 优点:与Spring框架无缝集成,简化了缓存管理。
    • 缺点:依赖于Spring生态,灵活性稍差。
  1. 比较:
  • Ehcache的话相比于其他两者更加重量。不过,相比于Guava CacheSpring Cache来说,Ehcache支持可以嵌入到hibernate和mybatis作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中,同时也提供了集群方案(比较鸡肋,可忽略)。
  • Guava CacheSpring Cache两者的话比较像。Guava相比于Spring Cache的话使用的更多一点,它提供了API非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方和ConcurrentHashMap的思想有异曲同工之妙。
  • 使用Spring Cache的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题。比如,缓存击穿、内存溢出等。
2.1.3.3 后起之秀 Caffeine
  1. 使用场景:
  • 适合高性能、低延迟要求的场景,特别适合替代Guava Cache。
  1. 代码示例:
java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class CaffeineCacheExample {
    private static final Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

    public static void main(String[] args) {
        cache.put("key", "value");
        String value = cache.getIfPresent("key");
        System.out.println(value);
    }
}
  1. 优缺点:
  • 优点:性能优异,内存占用少,提供了更高效的缓存淘汰算法。
  • 缺点:相对较新,社区资源较少。

2.1.4 本地缓存的优缺点

  • 优点:
    • 低延迟:数据存于本地内存,近乎即时获取,极大提升应用即时响应。
    • 便捷易用:基于编程语言特性或成熟框架,开发成本低。
    • 不依赖网络(低依赖):网络波动或断网时仍能保障基本功能。
    • 成本低
  • 缺点:
    • **本地缓存应用耦合,对分布式框架支持不友好。**比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
    • **本地缓存容量受服务部署所在的机器限制明显。**如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
    • 数据一致性难维护:数据源更新时,本地缓存难同步,易导致数据不一致,影响业务逻辑准确性。
    • 进程级共享受限:多进程或多线程共享缓存需额外同步机制,否则易现并发问题。

2.2 分布式缓存

2.2.1 什么是分布式缓存

分布式缓存是指跨越多个节点的缓存系统,能够处理大规模数据并支持高并发访问。它允许多个应用程序实例共享同一份缓存数据。

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

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

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

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

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

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

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

2.2.2 适用场景

适用于大型分布式系统,特别是当存在大量重复计算或频繁访问相同数据的情况时,例如:

  • 大规模数据缓存:大型社交网络平台需缓存用户动态、好友关系等海量数据,分布式缓存可承载 TB 至 PB 级缓存量。
  • 高并发访问:互联网应用遭遇电商购物节、在线直播高峰等突发高流量,分布式缓存凭多节点并行处理,确保大量用户请求迅速满足,维持系统稳定。
  • 数据共享需求:分布式系统架构下,多服务实例需共享缓存数据,分布式缓存提供统一层,便利数据交互复用。

如:商品详情页数据、热门新闻列表、社交媒体平台上的用户资料等

2.2.3 实现方案

2.2.3.1 Redis
  1. 使用场景:
  • 适合需要高性能、低延迟的场景,广泛应用于Web应用、实时数据分析等领域。
  1. 代码示例:
xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.0.0</version>
</dependency>
java 复制代码
import redis.clients.jedis.Jedis;

public class RedisExample {
    public static void main(String[] args) {
        try (Jedis jedis = new Jedis("localhost")) {
            jedis.set("key", "value");
            String value = jedis.get("key");
            System.out.println(value);
        }
    }
}
  1. 优缺点:
  • 优点:高性能、丰富的数据结构支持(如字符串、哈希、列表、集合等),支持持久化和集群模式,高可用与可扩展性强。
  • 缺点:内存占用较大,不适合存储海量数据。
2.2.3.2 Memcached
  1. 使用场景:
  • 适合需要简单键值对存储且对性能要求极高的场景,广泛应用于内容分发网络(CDN)、社交网络等。
  1. 代码示例:
xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>net.spy</groupId>
    <artifactId>spymemcached</artifactId>
    <version>2.12.3</version>
</dependency>
java 复制代码
import net.spy.memcached.MemcachedClient;

import java.io.IOException;
import java.net.InetSocketAddress;

public class MemcachedExample {
    public static void main(String[] args) throws IOException {
        try (MemcachedClient memcachedClient = new MemcachedClient(new InetSocketAddress("localhost", 11211))) {
            memcachedClient.set("key", 0, "value").get();
            String value = memcachedClient.get("key");
            System.out.println(value);
        }
    }
}
  1. 优缺点:
  • 优点:极高的性能,简单的API设计。简单快速,内存利用高效,适合纯缓存场景。
  • 缺点:功能相对单一,不支持数据持久化,数据结构单一,集群管理功能较弱。

2.2.4 分布式缓存的优缺点:

  • 优点:
    • 可扩展性强:随数据、并发增长,轻松添节点,线性提升缓存容量与处理力。
    • 高可用性:借助冗余、故障转移,部分节点故障不影响对外服务,保障业务连续。
    • 共享性佳:便利分布式系统组件、服务共享缓存资源,促进数据流通协作。
  • 缺点:
    • 部署复杂:搭建维护集群需专业运维,涉及网络、节点协调、数据同步,成本高昂。
    • 网络开销:数据存取依赖网络,较本地缓存延迟略增,网络不佳时影响性能。
    • 数据一致性挑战:分布式下保证多节点缓存强一致性难,常采最终一致性,或现短期不一致。

2.3 多级缓存

2.3.1 什么是多级缓存

多级缓存结合了本地缓存和分布式缓存的优点,形成一个分层结构。通常先查询本地缓存,若未命中再查询分布式缓存,最后才回源到数据库或其他数据源。

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

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

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

2.3.2 适用场景

  • 复杂大型分布式系统:金融交易、在线旅游预订平台,既有本地快速数据需求,又面临全球海量用户并发,多级缓存全方位护航系统性能。
  • 性能严苛且成本敏感:合理配置各级缓存存储与有效期,既满足超高响应,又优化硬件投入,避免过度依赖高端存储或大规模集群。
  • 混合式数据访问:频繁本地操作与需共享、更新不一的全局数据并存,多级缓存依数据特性分类管理,提升整体缓存命中率。

适用于对性能要求极高且数据一致性有一定容忍度的场景,例如:

  • 电商网站的商品推荐系统
  • 视频流媒体平台的内容分发网络(CDN)

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

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

2.3.3 实现方案--Redis + Caffeine 两级缓存

2.3.3.1 原理

Redis + Caffeine 两级缓存利用了Caffeine的高性能和低延迟特点,同时借助Redis的分布式能力和持久化支持。具体流程如下:

  • 先查询本地Caffeine缓存,若命中则直接返回结果。
  • 若未命中,则查询Redis缓存。
  • 若Redis也未命中,则从数据库或其他数据源获取数据,并将其写入Redis和Caffeine缓存中。
2.3.3.2 代码实现
java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import redis.clients.jedis.Jedis;

import java.util.Optional;

public class TwoLevelCacheExample {
    private static final Cache<String, String> localCache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(10, java.util.concurrent.TimeUnit.MINUTES)
        .build();

    private static final Jedis redisClient = new Jedis("localhost");

    public static Optional<String> get(String key) {
        // 尝试从本地缓存获取
        String localValue = localCache.getIfPresent(key);
        if (localValue != null) {
            return Optional.of(localValue);
        }

        // 尝试从Redis获取
        String redisValue = redisClient.get(key);
        if (redisValue != null) {
            // 更新本地缓存
            localCache.put(key, redisValue);
            return Optional.of(redisValue);
        }

        // 从数据库或其他数据源获取数据
        String dbValue = fetchDataFromDatabase(key);
        if (dbValue != null) {
            // 更新Redis和本地缓存
            redisClient.set(key, dbValue);
            localCache.put(key, dbValue);
        }

        return Optional.ofNullable(dbValue);
    }

    private static String fetchDataFromDatabase(String key) {
        // 模拟从数据库获取数据
        return "value-" + key;
    }

    public static void main(String[] args) {
        Optional<String> value = get("key");
        value.ifPresent(System.out::println);
    }
}
2.3.3.3 最佳实践方案
  • 合理划分数据:本地 Caffeine 缓存存放频繁访问且对一致性要求稍低的热数据,如用户近期浏览的新闻列表;Redis 缓存存储全局共享、更新频率相对低的冷数据,如新闻详情内容。
  • 同步策略:利用 Redis 的发布订阅功能,当数据在 Redis 更新时,通知相关应用及时更新本地 Caffeine 缓存,确保一定程度的数据一致性。
  • 监控与调优:实时监控各级缓存命中率、内存使用、响应时间等指标,依据业务流量变化动态调整 Caffeine 的缓存容量、过期策略以及 Redis 的集群配置,保障系统始终处于高效运行状态。

2.3.4 多级缓存的优缺点

  • 优点:
    • 全方位性能优化:兼取本地低延迟与分布式大规模并发处理优势,最大程度提升系统响应,适配多样业务。
    • 灵活资源配置:依数据特性、访问模式分配缓存任务,优化硬件利用,降本增效。
    • 容错降级:某层级缓存故障,其他层级临时兜底,提供降级能力,保基本可用。
  • 缺点:
    • 架构复杂:多层设计、管理、监控需更多开发运维精力,系统复杂度攀升,对团队技术要求高。
    • 数据一致性维护艰难:较单一缓存,多级缓存数据同步环节多,易因层级更新延迟致数据不一致,排查难度大。
相关推荐
猎人everest1 小时前
SpringBoot应用开发入门
java·spring boot·后端
山猪打不过家猪3 小时前
ASP.NET Core Clean Architecture
java·数据库·asp.net
AllowM3 小时前
【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!
java·算法·leetcode
不会Hello World的小苗3 小时前
Java——列表(List)
java·python·list
艾斯比的日常4 小时前
提升接口性能之缓存
缓存
二十七剑4 小时前
jvm中各个参数的理解
java·jvm
东阳马生架构6 小时前
JUC并发—9.并发安全集合四
java·juc并发·并发安全的集合
计算机小白一个6 小时前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
菠菠萝宝6 小时前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗6 小时前
Java——链表(LinkedList)
java·开发语言·链表