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

394

积分

0

好友

48

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

在当今高并发的互联网应用中,缓存已成为提升系统性能不可或缺的关键技术。本文将从零开始,带你系统学习Caffeine这款高性能的Java缓存框架,并通过详尽的代码示例和实战案例,掌握其核心原理与高级应用,助你构建高效、稳定的缓存层。

1. Caffeine简介

Caffeine 是一个由 Google 开发并开源的高性能 Java 缓存库,在众多性能基准测试中表现卓越,常被认为是 Java 缓存领域的标杆。

为什么选择 Caffeine 呢?

  • 高性能:它采用了如 Window TinyLFU 等先进算法,实现了卓越的读写性能。
  • 功能丰富:支持多种过期策略、异步加载、统计信息、事件监听等高级特性。
  • 线程安全:内置优秀的并发控制机制,保证了高并发场景下的数据安全与性能。
  • 易于使用:API 设计简洁直观,学习和集成成本低。
  • 社区活跃:项目持续维护更新,生态完善。

架构设计

Caffeine 核心架构图

Caffeine 的核心架构主要包含以下几个层次:

  1. 缓存容器:底层使用改进的 ConcurrentHashMap 作为数据存储结构。
  2. 访问策略:采用创新的 Window TinyLFU 算法进行访问频率统计,相比传统 LRU/LFU 能更好地适应访问模式变化。
  3. 淘汰策略:结合时间窗口和访问频率的混合淘汰策略,智能管理缓存容量。
  4. 事件机制:提供完整的监听器接口,支持缓存项的创建、更新、移除等事件。

2. 快速入门

Maven 依赖

首先,在你的项目中引入 Caffeine 的 Maven 依赖。

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

基础使用示例

让我们从一个最简单的例子开始,了解如何创建和使用一个基础的缓存。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class BasicCacheExample {
    public static void main(String[] args) {
        // 创建缓存
        Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(1000) // 最大缓存数量
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
            .build();

        // 存储数据
        cache.put("key1", "value1");

        // 获取数据(如果不存在则返回null)
        String value = cache.getIfPresent("key1");
        System.out.println("缓存值: " + value);

        // 获取或计算(如果不存在则通过函数加载)
        String value2 = cache.get("key2", k -> "defaultValue");
        System.out.println("缓存值: " + value2);

        // 移除数据
        cache.invalidate("key1");
    }
}

3. 核心概念

3.1 Cache vs LoadingCache vs AsyncLoadingCache

Caffeine 提供了三种主要的缓存接口以满足不同场景:

  1. Cache:最基础的缓存接口,需要手动调用 put 方法存储数据。
  2. LoadingCache:继承自 Cache,内置了同步加载器 (CacheLoader),当缓存未命中时自动同步加载数据。
  3. AsyncLoadingCache:提供异步加载能力,当缓存未命中时,返回一个 CompletableFuture,数据加载在后台完成,不阻塞调用线程。

3.2 过期策略

Caffeine 提供了灵活的数据过期机制,你可以根据写入时间、访问时间或自定义逻辑来设置。

Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后过期
    .expireAfterAccess(5, TimeUnit.MINUTES)  // 最后访问后过期
    .expireAfter(30, TimeUnit.MINUTES);       // 自定义过期逻辑(需实现Expiry接口)

3.3 淘汰策略

当缓存达到容量上限时,需要通过淘汰策略来移除部分数据。Caffeine 支持基于大小、权重和引用类型的淘汰。

Caffeine.newBuilder()
    .maximumSize(1000)         // 基于条目数量限制
    .maximumWeight(10000)      // 基于权重限制(需提供Weigher)
    .weakKeys()               // 键使用弱引用(允许被GC)
    .weakValues()             // 值使用弱引用
    .softValues();            // 值使用软引用(内存不足时被GC)

4. 高级特性

Caffeine 缓存操作流程图

4.1 异步加载

对于加载成本较高的数据,使用 AsyncLoadingCache 可以避免线程阻塞,极大提升系统的响应能力和吞吐量。在构建现代高并发应用时,这通常是提升性能的有效手段。

AsyncLoadingCache<String, User> asyncCache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .buildAsync(key -> loadUserFromDatabase(key));

// 异步获取,立即返回Future,不阻塞
CompletableFuture<User> userFuture = asyncCache.get("user1");
userFuture.thenAccept(user -> {
    System.out.println("获取用户: " + user.getName());
});

4.2 事件监听

通过 RemovalListener,你可以监听缓存项的移除事件,并执行相应的清理或日志记录操作。

Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .removalListener((key, value, cause) -> {
        System.out.println("缓存移除: " + key + " -> " + value + ", 原因: " + cause);
    })
    .build();

4.3 统计信息

启用统计功能可以帮助你监控缓存的使用效率,为调优提供数据支持。

Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .recordStats() // 启用统计
    .build();

// 使用缓存...
cache.put("key1", "value1");
cache.getIfPresent("key1");

// 获取统计信息
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("平均加载时间: " + stats.averageLoadPenalty());
System.out.println("总请求数: " + stats.requestCount());

5. Spring Boot 集成

将 Caffeine 与 Spring Boot 的缓存抽象层集成,可以让我们通过简单的注解来管理缓存,极大地提升开发效率。

Caffeine 缓存交互时序图

5.1 添加依赖

除了 Caffeine 本身,还需要 Spring Boot 的缓存启动器。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

5.2 配置类

创建一个配置类,定义 Caffeine 作为 Spring 的缓存管理器。

import com.github.ben-manes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    private Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .recordStats();
    }
}

5.3 使用注解

在 Service 层的方法上使用 @Cacheable, @CachePut, @CacheEvict 注解,即可轻松实现缓存逻辑。

package com.example.caffeine.demo.service;

import com.example.caffeine.demo.model.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
public class UserService {

    // 模拟数据库存储
    private final Map<Long, User> userMap = new ConcurrentHashMap<>();

    public UserService() {
        // 初始化一些测试数据
        userMap.put(1L, new User(1L, "张三", "zhangsan@example.com", "13800138000"));
        userMap.put(2L, new User(2L, "李四", "lisi@example.com", "13900139000"));
        userMap.put(3L, new User(3L, "王五", "wangwu@example.com", "13700137000"));
    }

    /**
     * 获取用户信息(带缓存)
     * @param userId 用户ID
     * @return 用户信息
     */
    @Cacheable(value = "users", key = "#userId")
    public User getUserById(Long userId) {
        // 模拟数据库查询耗时
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        User user = userMap.get(userId);
        if (user == null) {
            throw new RuntimeException("用户不存在: " + userId);
        }
        return user;
    }

    /**
     * 获取所有用户信息
     */
    @Cacheable(value = "users", key = "'all'")
    public List<User> getAllUsers() {
        // 模拟数据库查询耗时
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return new ArrayList<>(userMap.values());
    }

    /**
     * 创建用户
     */
    @CachePut(value = "users", key = "#user.id")
    public User createUser(User user) {
        userMap.put(user.getId(), user);
        return user;
    }

    /**
     * 删除用户
     */
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(Long userId) {
        User removed = userMap.remove(userId);
        if (removed == null) {
            throw new RuntimeException("用户不存在: " + userId);
        }
    }

    /**
     * 批量获取用户信息
     */
    @Cacheable(value = "users", key = "'batch:' + #userIds.hashCode()")
    public Map<Long, User> batchGetUsers(Set<Long> userIds) {
        // 模拟批量查询耗时
        try {
            TimeUnit.MILLISECONDS.sleep(150);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        Map<Long, User> result = new HashMap<>();
        for (Long userId : userIds) {
            User user = userMap.get(userId);
            if (user != null) {
                result.put(userId, user);
            }
        }
        return result;
    }

    /**
     * 清空所有缓存
     */
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllCache() {
        System.out.println("清空所有用户缓存");
    }
}

6. 生产环境最佳实践

6.1 缓存雪崩与击穿

缓存雪崩与击穿解决方案流程图

缓存雪崩 指大量缓存数据在同一时间过期,导致所有请求直接打到数据库。
缓存击穿 指某个热点 Key 过期瞬间,大量并发请求同时无法命中缓存,直接查询数据库。

解决方案:

  1. 设置随机过期时间:避免大量 Key 同时失效。

    Caffeine.newBuilder()
        .expireAfterWrite(10 + new Random().nextInt(5), TimeUnit.MINUTES);
  2. 使用互斥锁:防止缓存击穿,同一 Key 只允许一个线程查询数据库。

    private final Lock lock = new ReentrantLock();
    
    public User getUserWithLock(Long id) {
        User user = cache.getIfPresent(id);
        if (user != null) {
            return user;
        }
    
        lock.lock();
        try {
            // 双重检查
            user = cache.getIfPresent(id);
            if (user != null) {
                return user;
            }
    
            user = userRepository.findById(id);
            cache.put(id, user);
            return user;
        } finally {
            lock.unlock();
        }
    }
  3. 利用 Caffeine 的异步加载:其内置机制能天然地防止击穿。

    AsyncLoadingCache<Long, User> asyncCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .buildAsync(key -> loadUserFromDatabase(key));
    
    public User getUser(Long id) {
        return asyncCache.get(id).join();
    }

6.2 缓存预热

在应用启动时,主动将热点数据加载到缓存中,可以避免上线初期大量缓存未命中导致的性能问题。

package com.example.caffeine.demo.cache;

import com.example.caffeine.demo.model.User;
import com.example.caffeine.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Component
public class CacheWarmupRunner implements CommandLineRunner {

    @Autowired
    private UserService userService;

    // 预热配置(可外置到配置文件中)
    private static class WarmupConfig {
        private static final List<Long> HOT_USER_IDS = Arrays.asList(1L, 2L, 3L, 4L, 5L);
        private static final int BATCH_SIZE = 3;
        private static final int THREAD_POOL_SIZE = 2;
        private static final boolean ASYNC_WARMUP = true;
    }

    @Override
    public void run(String... args) throws Exception {
        log.info("开始执行缓存预热...");
        long startTime = System.currentTimeMillis();

        if (WarmupConfig.ASYNC_WARMUP) {
            // 异步预热
            asyncWarmup();
        } else {
            // 同步预热
            syncWarmup();
        }

        long endTime = System.currentTimeMillis();
        log.info("缓存预热完成!耗时: {} ms", endTime - startTime);
    }

    // 同步预热逻辑
    private void syncWarmup() {
        log.info("使用同步方式预热缓存...");
        for (Long userId : WarmupConfig.HOT_USER_IDS) {
            warmupSingleUser(userId);
        }
        // 也可预热所有用户:userService.getAllUsers();
    }

    // 异步预热逻辑
    private void asyncWarmup() {
        log.info("使用异步方式预热缓存...");
        ExecutorService executor = Executors.newFixedThreadPool(WarmupConfig.THREAD_POOL_SIZE);

        List<CompletableFuture<Void>> futures = WarmupConfig.HOT_USER_IDS.stream()
            .map(userId -> CompletableFuture.runAsync(() -> warmupSingleUser(userId), executor))
            .collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("部分预热任务失败", ex);
                } else {
                    log.info("所有预热任务完成");
                }
                executor.shutdown();
            })
            .join();
    }

    private void warmupSingleUser(Long userId) {
        log.info("预热用户: {}", userId);
        try {
            User user = userService.getUserById(userId);
            log.debug("用户 {} 基本信息预热完成", userId);
        } catch (Exception e) {
            log.error("预热用户 {} 失败: {}", userId, e.getMessage());
        }
    }
}

6.3 缓存监控

在生产环境中,对缓存的命中率、加载时间、大小等指标进行监控至关重要。Caffeine 的 recordStats() 功能为此提供了基础。

Caffeine 缓存监控面板

你可以通过暴露 HTTP 端点或集成到现有监控系统(如 Prometheus)来展示这些指标。

// 示例:通过Spring Boot Actuator或自定义端点暴露统计信息
@RestController
@RequestMapping("/cache")
public class CacheMonitorController {

    @Autowired
    private CacheManager cacheManager;

    @GetMapping("/stats/{cacheName}")
    public ResponseEntity<Map<String, Object>> getCacheStat(@PathVariable String cacheName) {
        org.springframework.cache.Cache springCache = cacheManager.getCache(cacheName);
        if (springCache == null) {
            return ResponseEntity.notFound().build();
        }

        Object nativeCache = springCache.getNativeCache();
        if (nativeCache instanceof Cache) {
            Cache<?, ?> cache = (Cache<?, ?>) nativeCache;
            CacheStats stats = cache.stats();

            Map<String, Object> result = new HashMap<>();
            result.put("cacheName", cacheName);
            result.put("estimatedSize", cache.estimatedSize());
            result.put("hitRate", stats.hitRate());
            result.put("requestCount", stats.requestCount());
            result.put("averageLoadPenalty", stats.averageLoadPenalty());
            result.put("evictionCount", stats.evictionCount());
            // ... 添加更多指标

            return ResponseEntity.ok(result);
        }
        return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Unsupported cache type"));
    }
}

7. 性能优化技巧

缓存框架性能对比分析

7.1 批量加载优化

当需要查询多个 Key 时,使用 getAll 方法配合批量加载函数,比循环调用 get 更高效,能减少网络或数据库的往返次数。

public class BatchUserService {

    private final AsyncLoadingCache<Long, User> userCache;

    public BatchUserService() {
        this.userCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .buildAsync(this::batchLoadUsers); // 传入批量加载函数
    }

    private Map<Long, User> batchLoadUsers(Set<Long> userIds) {
        if (userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        // 这里执行一次批量数据库查询
        List<User> users = userRepository.findAllById(userIds);
        return users.stream()
            .collect(Collectors.toMap(User::getId, user -> user));
    }

    public Map<Long, User> getUsers(Set<Long> userIds) {
        // 批量获取,内部会优化调用 batchLoadUsers
        return userCache.getAll(userIds).join();
    }
}

8. 实战案例:用户信息缓存服务

下面是一个更贴近实际业务的、功能完善的缓存服务类示例。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class UserInfoCacheService {

    private final Cache<Long, UserInfo> userInfoCache;

    public UserInfoCacheService() {
        this.userInfoCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .recordStats()
            .build();
    }

    /**
     * 获取用户信息
     */
    public UserInfo getUserInfo(Long userId) {
        return userInfoCache.get(userId, this::loadUserInfoFromDB);
    }

    /**
     * 批量获取用户信息
     */
    public Map<Long, UserInfo> batchGetUserInfo(Set<Long> userIds) {
        return userInfoCache.getAll(userIds, this::batchLoadUserInfoFromDB);
    }

    /**
     * 更新用户信息
     */
    public void updateUserInfo(Long userId, UserInfo userInfo) {
        userInfoCache.put(userId, userInfo);
    }

    /**
     * 删除用户信息
     */
    public void removeUserInfo(Long userId) {
        userInfoCache.invalidate(userId);
    }

    /**
     * 获取缓存统计
     */
    public CacheStats getCacheStats() {
        return userInfoCache.stats();
    }

    // ... loadUserInfoFromDB 和 batchLoadUserInfoFromDB 实现 ...
}

9. 总结

Caffeine 凭借其卓越的性能、丰富的功能特性和简洁的 API 设计,无疑是 Java 生态中本地缓存的最佳选择之一。无论是作为简单的内存缓存,还是与 Spring Boot 深度集成,它都能显著提升应用性能。

掌握其核心概念、高级特性以及生产环境的最佳实践(如防雪崩击穿、预热、监控),能够帮助我们在实际项目中构建出高效、稳定、可观测的缓存层,从容应对高并发挑战。如果你想深入探讨更多Java性能优化或System Design相关话题,欢迎在云栈社区与其他开发者交流分享。同时,研究像 Caffeine 这样的热门开源项目的源码与设计思想,也是提升自身技术深度的绝佳途径。




上一篇:Redis之父Antirez:AI已永久改变编程,手敲代码不再必须
下一篇:Python Flask项目实现统一请求追踪ID:中间件、日志与响应封装详解
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-1-18 18:11 , Processed in 0.219088 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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