找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

671

积分

0

好友

91

主题
发表于 18 小时前 | 查看: 0| 回复: 0

在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到 Redis 或 Memcached 这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力。

随着业务发展,这一架构也在不断演进。在一些对响应速度要求极高的场景下,单纯使用 Redis 这类远程缓存可能已无法满足需求,此时需要引入本地缓存(如 Guava Cache 或 Caffeine)进行配合,从而进一步提升程序的响应速度与服务性能。这就形成了以本地缓存为一级缓存,远程缓存为二级缓存的两级缓存架构。

在不考虑并发等复杂问题的情况下,两级缓存的基本访问流程如下图所示:

图片

为什么要使用本地缓存

  • 访问速度极快:本地缓存基于 JVM 堆内存,访问速度远超网络 I/O,非常适合存储变更频率低、实时性要求不高的数据。
  • 减少网络开销:使用本地缓存能够减少与 Redis 等远程缓存的交互频率,从而降低网络通信带来的延迟和开销。

本地缓存的核心功能需求

一个成熟的本地缓存方案通常需要具备以下功能:数据的存储、读取和写入;线程安全的原子操作;可设置缓存容量上限;支持LRU、LFU等淘汰策略;支持基于过期时间的缓存清理(定时、惰性、定期);可选的持久化功能;以及监控统计能力。

本地缓存方案选型

1. 使用ConcurrentHashMap实现本地缓存

缓存的本质是存储在内存中的键值对数据结构,因此可以直接使用 JDK 中线程安全的 ConcurrentHashMap。但若要实现一个完整的缓存,还需要自行开发容量限制、淘汰策略、过期清理等功能。
优点是无需引入第三方依赖,适合极其简单的场景。缺点是功能需要定制开发,成本高,且稳定性和可靠性难以保障。对于复杂场景,建议使用成熟的开源方案。

2. 基于Guava Cache实现本地缓存

Guava 是 Google 开源的一个核心 Java 库,其 Cache 组件提供了稳定可靠的功能支持:

  • 支持最大容量限制
  • 支持基于插入时间和访问时间的过期策略
  • 提供简单的统计功能
  • 基于 LRU 算法实现

使用示例

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
@Slf4j
public class GuavaCacheTest {
    public static void main(String[] args) throws ExecutionException {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .initialCapacity(5)  // 初始容量
                .maximumSize(10)     // 最大缓存数,超出淘汰
                .expireAfterWrite(60, TimeUnit.SECONDS) // 过期时间
                .build();
        String orderId = String.valueOf(123456789);
        // 获取orderInfo,如果key不存在,callable中调用getInfo方法返回数据
        String orderInfo = cache.get(orderId, () -> getInfo(orderId));
        log.info("orderInfo = {}", orderInfo);
    }
    private static String getInfo(String orderId) {
        String info = "";
        // 先查询redis缓存
        log.info("get data from redis");
        // 当redis缓存不存在查db
        log.info("get data from mysql");
        info = String.format("{orderId=%s}", orderId);
        return info;
    }
}
3. Caffeine

Caffeine 是基于 Java 8 开发的新一代缓存库,性能接近理论最优。它可以看作是 Guava Cache 的增强版,功能相似,但其采用了结合 LRU 和 LFU 优点的 W-TinyLFU 淘汰算法,在性能上有显著优势。

使用示例

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>
@Slf4j
public class CaffeineTest {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .initialCapacity(5)
                // 超出时淘汰
                .maximumSize(10)
                //设置写缓存后n秒钟过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                //设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                .build();
        String orderId = String.valueOf(123456789);
        String orderInfo = cache.get(orderId, key -> getInfo(key));
        System.out.println(orderInfo);
    }
    private static String getInfo(String orderId) {
        String info = "";
        // 先查询redis缓存
        log.info("get data from redis");
        // 当redis缓存不存在查db
        log.info("get data from mysql");
        info = String.format("{orderId=%s}", orderId);
        return info;
    }
}
4. Encache

Encache 是一个纯 Java 的进程内缓存框架,功能更为丰富,扩展性更强:

  • 支持多种缓存淘汰算法(LRU, LFU, FIFO)。
  • 缓存数据可存储在堆内、堆外甚至磁盘(支持持久化)。
  • 支持多种集群方案来解决数据共享问题。

使用示例

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.7</version>
</dependency>
@Slf4j
public class EhcacheTest {
    private static final String ORDER_CACHE = "orderCache";
    public static void main(String[] args) {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                // 创建cache实例
                .withCache(ORDER_CACHE, CacheConfigurationBuilder
                        // 声明一个容量为20的堆内缓存
                        .newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20)))
                .build(true);
        // 获取cache实例
        Cache<String, String> cache = cacheManager.getCache(ORDER_CACHE, String.class, String.class);
        String orderId = String.valueOf(123456789);
        String orderInfo = cache.get(orderId);
        if (StrUtil.isBlank(orderInfo)) {
            orderInfo = getInfo(orderId);
            cache.put(orderId, orderInfo);
        }
        log.info("orderInfo = {}", orderInfo);
    }
    private static String getInfo(String orderId) {
        String info = "";
        // 先查询redis缓存
        log.info("get data from redis");
        // 当redis缓存不存在查db
        log.info("get data from mysql");
        info = String.format("{orderId=%s}", orderId);
        return info;
    }
}

本地缓存常见问题及解决方案

1. 缓存一致性

当数据库数据更新时,必须同步更新本地缓存和远程缓存,以保证数据一致性。

解决方案1:消息队列(MQ)
在集群部署环境下,多个节点拥有各自的本地缓存。可以在数据更新时,向MQ发送一条广播消息,各节点消费此消息后删除本地对应缓存,达到最终一致性。
图片

解决方案2:Canal + MQ
如果不想在业务代码中耦合MQ发送逻辑,可以采用订阅数据库变更日志的方式。通过Canal监听MySQL的Binlog,当数据发生变化时,Canal解析Binlog并发送消息到MQ,节点消费消息后清理缓存。这种方式对业务代码无侵入。
图片

2. 如何提高本地缓存命中率

合理的键设计、缓存容量规划、淘汰策略选择以及热点数据预加载等都是提高命中率的关键。

3. 本地缓存技术选型建议
  • 易用性:Guava Cache、Caffeine 和 Encache 都有成熟的接入方案,使用简单。
  • 功能性:Guava Cache 和 Caffeine 主要支持堆内缓存,而 Encache 功能更为丰富,支持多级存储和集群。
  • 性能:从性能基准测试来看,Caffeine 最优,Guava Cache 次之,Encache 相对最慢(性能对比如下图)。
    图片

综上所述,对于大多数 Java 后端场景,推荐使用 Caffeine 作为本地缓存,其性能优势明显。虽然 Encache 功能丰富,但其持久化、集群等特性通常可以通过 Redis 等外部组件更好地实现。

在实际生产系统中,构建 Caffeine (一级本地缓存) + Redis (二级分布式缓存) 的多级缓存架构,是兼顾高性能与高可靠性的常见最佳实践




上一篇:手搓博客 | 10分钟搭建好若伊开源
下一篇:Java final关键字深度解析:变量、方法与类的不可变实战指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-10 19:59 , Processed in 0.078993 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表