JAVA缓存的使用RedisCache、LocalCache、复合缓存

1️⃣ RedisCache(分布式缓存)

概念

RedisCache 是基于 Redis 的缓存,数据存储在内存中,并且可以被多个应用实例共享,属于分布式缓存

优点

  1. 高性能:Redis 在内存中读写,速度非常快(毫秒级甚至微秒级)。

  2. 分布式共享:多个应用实例可以共享同一份缓存,保证数据一致性。

  3. 持久化:可以选择持久化策略(RDB/AOF),重启后缓存不会完全丢失。

  4. 功能丰富:支持数据结构(String、Hash、List、Set、SortedSet)、过期策略、订阅/发布等。

  5. 扩展性强:可以通过 Redis Cluster 支持水平扩展,适合大规模系统。

缺点(顺便提醒)

  • 网络访问会增加延迟,尤其在高并发场景下。

  • 内存有限,数据量太大时成本高。

java 复制代码
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Redis缓存工具类
 */
@Slf4j
@Component
public class RedisCacheTemplate {

    @Resource
    private StringRedisTemplate redisTemplate;

    @Resource
    private ObjectMapper objectMapper;

    /**
     * 指定TypeReference类型获取数据
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull TypeReference<T> clazz, @NonNull Supplier<T> supplier) {
        return getT(key, duration, supplier, strValue -> {
            try {
                return objectMapper.readValue(strValue, clazz);
            } catch (Exception e) {
                log.error("Redis反序列化失败,KEY:{}", key, e);
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * Class类型获取数据
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull Class<T> clazz, @NonNull Supplier<T> supplier) {
        return getT(key, duration, supplier, strValue -> {
            try {
                return objectMapper.readValue(strValue, clazz);
            } catch (Exception e) {
                log.error("Redis反序列化失败,KEY:{}", key, e);
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 写入数据
     */
    public <T> void set(@NonNull String key, @NonNull Duration duration, @NonNull T object) {
        String redisKey = this.buildKey(key);
        try {
            String json = objectMapper.writeValueAsString(object);
            redisTemplate.opsForValue().set(redisKey, json, duration);
        } catch (JsonProcessingException e) {
            log.error("Redis序列化失败,KEY:{}", key, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 移除数据
     */
    public void del(@NonNull String key) {
        String redisKey = this.buildKey(key);
        redisTemplate.delete(redisKey);
    }

    /**
     * 公共业务代码处理
     */
    private <T> @Nullable T getT(@NotNull String key, @NotNull Duration duration, @NotNull Supplier<T> supplier, Function<String, T> deserialize) {
        String redisKey = this.buildKey(key);
        String strValue = redisTemplate.opsForValue().get(redisKey);
        if (Objects.isNull(strValue)) {
            T value = supplier.get();
            if (Objects.nonNull(value)) {
                this.set(key, duration, value);
            }
            return value;
        } else {
            return deserialize.apply(strValue);
        }
    }

    /**
     * Redis的KEY拼接
     */
    private String buildKey(String key) {
        return "test:cache:" + key;
    }
}


// 使用方式
public static void main(String[] args) {
    private static final TypeReference<List<String>> LIST_TEST = new TypeReference<>() {
    };

    @Resource
    private RedisCacheTemplate redisCacheTemplate;

    List<String> strings = redisCacheTemplate.get(RedisKey.TEST_LIST, Duration.ofMinutes(60), LIST_TEST, () -> {
        System.out.println("TEST");
        return List.of("Test");
    });
}

2️⃣ LocalCache(本地缓存)

概念

LocalCache 是应用本地内存缓存,数据只存在于当前应用实例的内存里,常见实现有 Guava Cache、Caffeine

优点

  1. 超低延迟:数据在本地内存,访问速度最快(纳秒到微秒级)。

  2. 简单易用:不依赖外部组件,集成方便。

  3. 降低网络压力:读写缓存不需要通过网络访问 Redis。

  4. 可配置丰富:支持过期策略、容量限制、LRU/LFU 等缓存淘汰策略。

缺点

  • 数据不共享:多实例部署时,每个实例都有一份独立缓存,可能出现数据不一致。

  • 内存受限:缓存太大可能会占用应用内存,影响 JVM 性能。

java 复制代码
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Caffeine本地缓存工具类
 */
@Slf4j
@Component
public class LocalCacheTemplate {

    /**
     * JVM 全局缓存
     * 存储 CacheValue 包装对象,支持每条缓存独立 TTL
     */
    private static final Cache<String, CacheValue<Object>> CACHE = Caffeine.newBuilder()
        // LRU 淘汰:数量满了移除长时间未访问或最早的数据
        .maximumSize(30000)
        // TTL 过期:按时间清理
        .expireAfter(new Expiry<String, CacheValue<Object>>() {
            @Override
            public long expireAfterCreate(@NotNull String key, @NotNull CacheValue<Object> value, long currentTime) {
                return value.ttlNanos();
            }

            @Override
            public long expireAfterUpdate(@NotNull String key, @NotNull CacheValue<Object> value, long currentTime, long currentDuration) {
                return value.ttlNanos();
            }

            @Override
            public long expireAfterRead(@NotNull String key, @NotNull CacheValue<Object> value, long currentTime, long currentDuration) {
                // 读取不改变 TTL
                return currentDuration;
            }
        }).build();

    @Resource
    private ObjectMapper objectMapper;

    /**
     * 判断KEY是否存在(非强一致,仅用于弱判断)
     */
    public boolean exists(@NonNull String key) {
        String redisKey = this.buildKey(key);
        return CACHE.getIfPresent(redisKey) != null;
    }

    /**
     * 获取KEY数据
     */
    public <T> T get(@NonNull String key, @NonNull Class<T> clazz) {
        String redisKey = this.buildKey(key);
        CacheValue<Object> wrapper = CACHE.getIfPresent(redisKey);
        return getT(key, clazz, wrapper);
    }

    /**
     * Class类型获取数据,没有就写入
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull Class<T> clazz, @NonNull Supplier<T> supplier) {
        String redisKey = this.buildKey(key);
        CacheValue<Object> wrapper = CACHE.get(redisKey, k -> new CacheValue<>(supplier.get(), duration.toNanos()));
        return getT(key, clazz, wrapper);
    }

    /**
     * 指定TypeReference类型获取数据,没有就写入
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull TypeReference<T> clazz, @NonNull Supplier<T> supplier) {
        String redisKey = this.buildKey(key);
        CacheValue<Object> wrapper = CACHE.get(redisKey, k -> new CacheValue<>(supplier.get(), duration.toNanos()));
        if (wrapper == null || wrapper.value() == null) {
            return null;
        }
        return getSerialize(key, wrapper.value(), strValue -> objectMapper.convertValue(strValue, clazz));
    }

    /**
     * 写入数据
     */
    public void set(@NonNull String key, @NonNull Duration duration, @NonNull Object value) {
        String redisKey = this.buildKey(key);
        CACHE.put(redisKey, new CacheValue<>(value, duration.toNanos()));
    }

    /**
     * 移除数据
     */
    public void del(@NonNull String key) {
        String redisKey = this.buildKey(key);
        CACHE.invalidate(redisKey);
    }

    /**
     * 公共业务代码处理
     */
    private <T> @Nullable T getT(@NotNull String key, @NotNull Class<T> clazz, CacheValue<Object> wrapper) {
        if (wrapper == null || wrapper.value() == null) {
            return null;
        }
        Object value = wrapper.value();
        // 已经是目标类型
        if (clazz.isInstance(value)) {
            return clazz.cast(value);
        }
        return getSerialize(key, value, strValue -> objectMapper.convertValue(strValue, clazz));
    }

    /**
     * 序列化操作
     */
    private <T> @Nullable T getSerialize(@NotNull String key, @NotNull Object object, Function<Object, T> deserialize) {
        try {
            return deserialize.apply(object);
        } catch (Exception e) {
            log.error("本地缓存序列化失败,KEY:{}", key, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Redis的KEY拼接
     */
    private String buildKey(String key) {
        return "test:cache:" + key;
    }

    /**
     * 存储数据和过期时间
     *
     * @param value    内容
     * @param ttlNanos 过期时间
     */
    public record CacheValue<T>(T value, long ttlNanos) {
    }
}

// 使用方式
public static void main(String[] args) {
    private static final TypeReference<List<String>> LIST_TEST = new TypeReference<>() {
    };
    @Resource
    private LocalCacheTemplate localCacheTemplate;

    List<String> strings = localCacheTemplate.get(RedisKey.TEST_LIST, Duration.ofMinutes(60), LIST_TEST, () -> {
        System.out.println("TEST");
        return List.of("Test");
    });
}

3️⃣ 复合缓存(Composite Cache / Two-level Cache)

概念

复合缓存结合了 LocalCache + RedisCache,常见模式是:

  • 一级缓存(LocalCache):应用本地内存,快速响应。

  • 二级缓存(RedisCache):共享分布式缓存,保证跨实例一致性。

优点

  1. 速度快:大部分热点数据在本地缓存,访问本地即可,减少网络请求。

  2. 一致性保证:Redis 做二级缓存,确保跨实例数据一致。

  3. 降低压力:Redis 热点数据由本地缓存缓冲,降低 Redis 压力。

  4. 灵活性高:可以对不同数据设置不同的缓存策略(本地缓存过期时间短,Redis 过期时间长)。

  5. 可扩展性强:支持高并发,结合分布式和本地缓存的优势。

缺点

  • 实现复杂,需要处理缓存一致性问题(如本地缓存与 Redis 的同步)。

  • 本地缓存失效策略设计不当可能导致缓存雪崩或脏数据。

java 复制代码
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.function.Supplier;

/**
 * 本地缓存和Redis缓存实现复合缓存工具类
 */
@Slf4j
@Component
public class CompoundCacheTemplate {

    // 默认10分钟
    public static Duration duration = Duration.ofMinutes(10);

    @Resource
    private RedisCacheTemplate redisCacheTemplate;

    @Resource
    private LocalCacheTemplate localCacheTemplate;

    /**
     * 获取KEY数据
     */
    public <T> T get(@NonNull String key, @NonNull Class<T> clazz) {
        // 先查本地缓存
        T value = localCacheTemplate.get(key, clazz);
        if (value != null) {
            return value;
        }
        // 再查Redis
        value = redisCacheTemplate.get(key, duration, clazz, () -> null);
        if (value != null) {
            // 回写本地缓存
            localCacheTemplate.set(key, Duration.ofMinutes(10), value);
        }
        return value;
    }

    /**
     * Class类型获取数据,没有就写入
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull Class<T> clazz, @NonNull Supplier<T> supplier) {
        // 先查本地缓存
        T value = localCacheTemplate.get(key, clazz);
        if (value != null) {
            return value;
        }
        // 查Redis,如果没有就调用业务逻辑
        value = redisCacheTemplate.get(key, duration, clazz, supplier);
        if (value != null) {
            // 回写本地缓存
            localCacheTemplate.set(key, duration, value);
        }
        return value;
    }

    /**
     * 指定TypeReference类型获取数据,没有就写入
     */
    public <T> T get(@NonNull String key, @NonNull Duration duration, @NonNull TypeReference<T> clazz, @NonNull Supplier<T> supplier) {
        // 先查本地缓存
        T value = localCacheTemplate.get(key, duration, clazz, () -> null);
        if (value != null) {
            return value;
        }
        // 查Redis,如果没有就调用业务逻辑
        value = redisCacheTemplate.get(key, duration, clazz, supplier);
        if (value != null) {
            // 回写本地缓存
            localCacheTemplate.set(key, duration, value);
        }
        return value;
    }

    /**
     * 写入数据
     */
    public void set(@NonNull String key, @NonNull Duration duration, @NonNull Object value) {
        // 同步写入本地缓存和Redis
        localCacheTemplate.set(key, duration, value);
        redisCacheTemplate.set(key, duration, value);
    }

    /**
     * 移除数据
     */
    public void del(@NonNull String key) {
        // 同步删除本地缓存和Redis
        localCacheTemplate.del(key);
        redisCacheTemplate.del(key);
    }
}

// 使用方式
public static void main(String[] args) {
    private static final TypeReference<List<String>> LIST_TEST = new TypeReference<>() {
    };
    @Resource
    private CompoundCacheTemplate compoundCacheTemplate;

    List<String> strings = compoundCacheTemplate.get(RedisKey.TEST_LIST, Duration.ofMinutes(60), LIST_TEST, () -> {
        System.out.println("TEST");
        return List.of("Test");
    });
}
相关推荐
踏雪羽翼2 小时前
android 解决混淆导致AGPBI: {“kind“:“error“,“text“:“Type a.a is defined multiple times
android·java·开发语言·混淆·混淆打包出现a.a
lang201509282 小时前
Tomcat Maven插件:部署与卸载的架构设计
java·tomcat·maven
一切尽在,你来2 小时前
C++ 零基础教程 - 第 5 讲 变量和数据类型
开发语言·c++
serve the people2 小时前
python环境搭建 (六) Makefile 简单使用方法
java·服务器·python
jiunian_cn2 小时前
【Redis】zset数据类型相关指令
数据库·redis·缓存
重生之后端学习2 小时前
146. LRU 缓存
java·数据结构·算法·leetcode·职场和发展
萧曵 丶2 小时前
懒加载单例模式中DCL方式和原理解析
java·开发语言·单例模式·dcl
℡枫叶℡2 小时前
C# - 指定友元程序集
开发语言·c#·友元程序集
回忆是昨天里的海2 小时前
k8s部署的微服务动态扩容
java·运维·kubernetes