跳转到内容
123xiao | 无名键客

《Spring Boot 中基于 Spring Cache 与 Redis 构建高可用多级缓存的实战指南》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队一开始做缓存,都是“先把 Redis 接上再说”。这当然没错,但业务一旦上量,问题就会慢慢冒出来:

  • 所有请求都直接打 Redis,网络开销开始明显;
  • 热点 Key 被频繁访问,Redis 压力陡增;
  • 单机内存明明很富余,却没利用本地缓存;
  • 缓存一致性 处理不好,更新后读到旧值;
  • Redis 短暂抖动时,整个系统响应时间明显变差。

我自己在项目里踩过一个很典型的坑:接口本身查数据库只要 20ms,接了 Redis 后平均响应反而变成了 30ms。原因不是 Redis 慢,而是**“所有请求都要走一次网络”**,对于高频、读多写少、且允许短时间弱一致的数据,这种模式并不划算。

所以,多级缓存就很自然地出现了:

  • 一级缓存:本地内存缓存(Caffeine)
  • 二级缓存:分布式缓存(Redis)
  • 最终数据源:数据库/MySQL

这样做的目标很明确:

  1. 优先从本地缓存拿数据,降低延迟
  2. 本地未命中再访问 Redis,降低数据库压力
  3. Redis 兜底,保证多实例下共享缓存数据
  4. 更新时同时清理多级缓存,尽量控制一致性问题

这篇文章我会从 Spring Boot + Spring Cache 的角度,带你做一个可运行的多级缓存实战方案。不是只讲概念,而是从代码、流程、坑位到排查,一步步搭起来。


前置知识与环境准备

适合谁看

这篇文章更适合:

  • 已经会写 Spring Boot 项目
  • 用过 @Cacheable@CacheEvict
  • 知道 Redis 基本用法
  • 想把“单层 Redis 缓存”升级成“多级缓存”

技术栈

本文示例使用:

  • JDK 8+
  • Spring Boot 2.6.x
  • Spring Cache
  • Redis
  • Caffeine
  • Maven

多级缓存的目标架构

flowchart LR
    A[客户端请求] --> B[Spring Cache]
    B --> C{本地缓存 Caffeine 命中?}
    C -- 是 --> D[直接返回]
    C -- 否 --> E{Redis 命中?}
    E -- 是 --> F[写回本地缓存]
    F --> D
    E -- 否 --> G[查询数据库]
    G --> H[写入 Redis]
    H --> I[写入本地缓存]
    I --> D

这个流程看起来简单,但真正麻烦的地方在于:

  • Spring Cache 默认并不会直接帮你做“本地 + Redis 联动”
  • 你需要自己定义一个 CompositeCache 或者 自定义 CacheManager
  • 删除缓存时,要保证两级一起清理
  • TTL、序列化、并发加载、空值缓存都需要设计

核心原理

为什么 Spring Cache 能做这件事

Spring Cache 本质上是一个抽象层。你在业务上写:

  • @Cacheable
  • @CachePut
  • @CacheEvict

底层真正读写缓存的是 CacheManagerCache 接口。

也就是说,只要我们自定义一个 Cache 实现,让它同时管理:

  • 本地缓存
  • Redis 缓存

那么业务层仍然可以继续优雅地使用注解,而不需要手工写一堆缓存逻辑。

多级缓存读取策略

常见读取顺序:

  1. 先查本地缓存
  2. 本地没有,再查 Redis
  3. Redis 命中后回填本地缓存
  4. Redis 也没有,再查数据库
  5. 查到后同时写入 Redis 和本地缓存

这是最常用也最稳妥的策略。

多级缓存写入/删除策略

对于更新操作,推荐的策略通常是:

  • 更新数据库
  • 删除本地缓存
  • 删除 Redis 缓存

而不是“更新缓存”。原因很简单:

  • 删除比更新更稳,避免覆盖错误值
  • 让下一次读取自动回源重建缓存
  • Spring Cache 的 @CacheEvict 更容易统一管理

一致性边界

这里要说句实话:多级缓存很难做到强一致

尤其是本地缓存存在于每个应用实例内:

  • A 实例更新了数据并清理了自己的本地缓存
  • B 实例的本地缓存可能还保留旧值

这就是多实例下本地缓存的天然问题。

解决思路一般有三种:

  1. 本地缓存 TTL 设置短一点
  2. 借助 Redis Pub/Sub 或 MQ 做失效通知
  3. 强一致场景不要上本地缓存

这也是多级缓存方案最重要的边界条件:
它适合“高频读、低频写、可接受秒级弱一致”的业务。


方案设计

本文采用的方案如下:

  • Caffeine 做一级缓存
  • Redis 做二级缓存
  • 自定义 MultiLevelCache 实现 Spring Cache
  • 自定义 MultiLevelCacheManager 管理缓存实例
  • 业务侧继续使用 @Cacheable

类关系图

classDiagram
    class Cache {
        <<interface>>
        +get(name)
        +put(key,value)
        +evict(key)
        +clear()
    }

    class CacheManager {
        <<interface>>
        +getCache(name)
    }

    class MultiLevelCache {
        -Cache localCache
        -Cache redisCache
        +get(key)
        +put(key,value)
        +evict(key)
        +clear()
    }

    class MultiLevelCacheManager {
        -CacheManager caffeineCacheManager
        -CacheManager redisCacheManager
        +getCache(name)
    }

    Cache <|.. MultiLevelCache
    CacheManager <|.. MultiLevelCacheManager

实战代码(可运行)

下面给出一套可以直接落地的代码骨架。

1. Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

2. application.yml 配置

server:
  port: 8080

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 3000

  cache:
    type: redis

logging:
  level:
    org.springframework.cache: debug

这里 spring.cache.type 实际上不是关键,因为我们会自己提供 CacheManager
保留它主要是为了兼容默认行为,真正生效的是我们自定义的 Bean。


3. 启动类开启缓存

package com.example.multicache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class MultiCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(MultiCacheApplication.class, args);
    }
}

4. 定义缓存配置

package com.example.multicache.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.example.multicache.cache.MultiLevelCacheManager;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

@Configuration
public class CacheConfig {

    @Bean
    public CaffeineCacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setAllowNullValues(true);
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(1000)
                .expireAfterWrite(Duration.ofMinutes(2)));
        return cacheManager;
    }

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new GenericJackson2JsonRedisSerializer()
                        )
                );

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
    }

    @Bean
    public CacheManager cacheManager(CaffeineCacheManager caffeineCacheManager,
                                     RedisCacheManager redisCacheManager) {
        return new MultiLevelCacheManager(caffeineCacheManager, redisCacheManager);
    }
}

这里的设计要点有两个:

  • 本地缓存 TTL 短
  • Redis TTL 稍长

这样可以兼顾:

  • 热点请求优先走本地
  • 多实例之间通过 Redis 保持相对一致
  • 本地缓存即使脏了,也能较快过期

5. 自定义多级缓存实现

MultiLevelCache.java

package com.example.multicache.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;

import java.util.concurrent.Callable;

public class MultiLevelCache implements Cache {

    private final String name;
    private final Cache localCache;
    private final Cache redisCache;

    public MultiLevelCache(String name, Cache localCache, Cache redisCache) {
        this.name = name;
        this.localCache = localCache;
        this.redisCache = redisCache;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public Object getNativeCache() {
        return this;
    }

    @Override
    public ValueWrapper get(Object key) {
        ValueWrapper localValue = localCache.get(key);
        if (localValue != null) {
            return localValue;
        }

        ValueWrapper redisValue = redisCache.get(key);
        if (redisValue != null) {
            localCache.put(key, redisValue.get());
            return redisValue;
        }

        return null;
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        ValueWrapper valueWrapper = this.get(key);
        if (valueWrapper == null) {
            return null;
        }
        Object value = valueWrapper.get();
        if (type != null && !type.isInstance(value)) {
            throw new IllegalStateException(
                    "Cached value is not of required type [" + type.getName() + "]: " + value
            );
        }
        return (T) value;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        ValueWrapper value = this.get(key);
        if (value != null) {
            return (T) value.get();
        }

        try {
            T result = valueLoader.call();
            this.put(key, result);
            return result;
        } catch (Exception e) {
            throw new ValueRetrievalException(key, valueLoader, e);
        }
    }

    @Override
    public void put(Object key, Object value) {
        localCache.put(key, value);
        redisCache.put(key, value);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        ValueWrapper existing = this.get(key);
        if (existing == null) {
            this.put(key, value);
            return null;
        }
        return existing;
    }

    @Override
    public void evict(Object key) {
        localCache.evict(key);
        redisCache.evict(key);
    }

    @Override
    public boolean evictIfPresent(Object key) {
        localCache.evict(key);
        redisCache.evict(key);
        return true;
    }

    @Override
    public void clear() {
        localCache.clear();
        redisCache.clear();
    }

    @Override
    public boolean invalidate() {
        localCache.clear();
        redisCache.clear();
        return true;
    }
}

MultiLevelCacheManager.java

package com.example.multicache.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MultiLevelCacheManager implements CacheManager {

    private final CacheManager localCacheManager;
    private final CacheManager redisCacheManager;
    private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();

    public MultiLevelCacheManager(CacheManager localCacheManager, CacheManager redisCacheManager) {
        this.localCacheManager = localCacheManager;
        this.redisCacheManager = redisCacheManager;
    }

    @Override
    public Cache getCache(String name) {
        return cacheMap.computeIfAbsent(name, key -> {
            Cache localCache = localCacheManager.getCache(key);
            Cache redisCache = redisCacheManager.getCache(key);

            if (localCache == null || redisCache == null) {
                throw new IllegalStateException("Cannot create cache with name: " + key);
            }

            return new MultiLevelCache(key, localCache, redisCache);
        });
    }

    @Override
    public Collection<String> getCacheNames() {
        return redisCacheManager.getCacheNames();
    }
}

6. 模拟业务实体

package com.example.multicache.model;

import java.io.Serializable;

public class User implements Serializable {

    private Long id;
    private String name;
    private Integer age;

    public User() {
    }

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

7. Service 层使用 Spring Cache 注解

package com.example.multicache.service;

import com.example.multicache.model.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class UserService {

    private final Map<Long, User> database = new ConcurrentHashMap<>();

    public UserService() {
        database.put(1L, new User(1L, "Alice", 18));
        database.put(2L, new User(2L, "Bob", 20));
    }

    @Cacheable(cacheNames = "userCache", key = "#id")
    public User getById(Long id) {
        simulateSlowQuery();
        System.out.println("query db, id = " + id);
        return database.get(id);
    }

    @CacheEvict(cacheNames = "userCache", key = "#user.id")
    public User update(User user) {
        database.put(user.getId(), user);
        System.out.println("update db, id = " + user.getId());
        return user;
    }

    @CacheEvict(cacheNames = "userCache", allEntries = true)
    public void clearAll() {
        System.out.println("clear all cache");
    }

    private void simulateSlowQuery() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {
        }
    }
}

这里的 simulateSlowQuery() 是为了让你在本地更明显地看到缓存效果。


8. Controller 暴露测试接口

package com.example.multicache.controller;

import com.example.multicache.model.User;
import com.example.multicache.service.UserService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        return userService.getById(id);
    }

    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        return userService.update(user);
    }

    @DeleteMapping("/cache")
    public String clearCache() {
        userService.clearAll();
        return "ok";
    }
}

逐步验证清单

实际搭完之后,建议你按下面顺序验证,而不是一上来就压测。

第一步:验证本地缓存是否命中

第一次请求:

curl http://localhost:8080/users/1

你会看到日志里打印:

query db, id = 1

紧接着再次请求:

curl http://localhost:8080/users/1

如果没有再打印 query db,说明缓存生效了。

但这时候你还不能确定命中的是本地还是 Redis。


第二步:验证 Redis 是否有数据

在 Redis 里查看 Key:

redis-cli keys "*userCache*"

如果能看到对应 key,说明二级缓存也已经写入。


第三步:重启应用验证 Redis 回填本地缓存

应用重启后,本地缓存会消失,Redis 数据还在。

重启后再次请求:

curl http://localhost:8080/users/1

如果没有查询数据库,而能直接返回,说明 Redis 命中成功。
接下来再请求一次,大概率会命中本地缓存。


第四步:验证更新时缓存失效

更新用户:

curl -X PUT http://localhost:8080/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice-New","age":25}'

再查一次:

curl http://localhost:8080/users/1

应该重新查数据库并返回新值,随后再次查询则走缓存。


请求时序图

sequenceDiagram
    participant C as Client
    participant S as Spring Cache
    participant L as Local Cache
    participant R as Redis
    participant D as DB

    C->>S: getById(1)
    S->>L: 查询本地缓存
    alt 本地命中
        L-->>S: 返回数据
        S-->>C: 响应
    else 本地未命中
        S->>R: 查询 Redis
        alt Redis 命中
            R-->>S: 返回数据
            S->>L: 回填本地缓存
            S-->>C: 响应
        else Redis 未命中
            S->>D: 查询数据库
            D-->>S: 返回数据
            S->>R: 写入 Redis
            S->>L: 写入本地缓存
            S-->>C: 响应
        end
    end

常见坑与排查

这一部分很关键。多级缓存最烦人的地方不是“写不出来”,而是“看起来能跑,线上却总有诡异问题”。

1. 本地缓存与 Redis 数据不一致

现象

  • A 节点更新后能读到新值
  • B 节点偶尔还能读到旧值

原因

因为每个应用实例都有自己的本地缓存。
你在 A 节点执行了 @CacheEvict,只清掉了 A 自己的本地缓存和 Redis,并不能自动清掉 B 的本地缓存

排查思路

  • 看是否是多实例部署
  • 看旧值持续多久,是否与本地 TTL 相符
  • 看是否存在节点级热点缓存

解决建议

  • 本地缓存 TTL 设短
  • 通过 Redis Pub/Sub 广播失效消息
  • 对强一致业务关闭本地缓存

2. 序列化问题导致读取报错

现象

  • Redis 中有值,但反序列化失败
  • ClassCastException、JSON 结构不匹配

原因

常见原因包括:

  • Redis 使用了 JDK 序列化,而本地是普通对象
  • 对象结构变更后旧缓存未清理
  • 泛型反序列化信息丢失

解决建议

  • 统一使用 GenericJackson2JsonRedisSerializer
  • 实体类结构变更后及时清缓存
  • 对复杂泛型对象单独设计缓存 DTO

3. 空值穿透数据库

现象

一个不存在的 id 被频繁请求,每次都打到数据库。

原因

查库返回 null,但缓存没有记录“这个 key 不存在”。

解决建议

  • 对空值进行短 TTL 缓存
  • 或者引入布隆过滤器
  • 热点不存在数据场景必须单独处理

本文示例里 Redis 配置用了 disableCachingNullValues(),这是保守配置。
如果你的系统存在明显的缓存穿透风险,可以改成允许空值缓存,但 TTL 要更短,比如 30 秒。


4. 缓存雪崩

现象

某个时间点大量缓存同时过期,数据库瞬间被打满。

解决建议

  • TTL 加随机值
  • 热点数据提前预热
  • 使用本地缓存分散 Redis 压力
  • 对回源查询加限流/隔离

例如你可以把 Redis TTL 从固定 10 分钟改成随机区间:

Duration ttl = Duration.ofMinutes(10).plusSeconds((long) (Math.random() * 120));

当然,生产里不要直接在配置类写随机值给所有缓存统一处理,更推荐按业务分类配置。


5. 缓存击穿与并发回源

现象

某个热点 key 失效瞬间,大量请求同时回源数据库。

原因

虽然你用了缓存,但没有做并发加载控制。

解决建议

  • @Cacheable(sync = true) 控制同 JVM 内并发加载
  • Redis 层可配合互斥锁
  • 热点 key 做逻辑过期 + 后台刷新

例如:

@Cacheable(cacheNames = "userCache", key = "#id", sync = true)
public User getById(Long id) {
    simulateSlowQuery();
    return database.get(id);
}

sync = true 很实用,但要注意:
它主要解决单实例内的并发问题,解决不了多实例之间的同时回源。


安全/性能最佳实践

这一部分我建议你在正式上线前过一遍,很多线上事故其实都跟这里有关。

1. 不要缓存敏感数据明文

像下面这些内容,不建议直接进缓存:

  • 身份证号
  • 手机号全量信息
  • access token
  • 密码摘要以外的敏感凭证

如果确实要缓存:

  • 做脱敏
  • 做字段裁剪
  • Redis 开启访问控制
  • 限制缓存对象内容

2. 本地缓存大小必须受控

Caffeine 虽然快,但它吃的是 JVM 堆内存。
如果你不设上限,很容易导致:

  • Full GC 增多
  • 老年代膨胀
  • OOM

建议至少配置:

  • maximumSize
  • expireAfterWriteexpireAfterAccess

比如:

Caffeine.newBuilder()
    .initialCapacity(100)
    .maximumSize(1000)
    .expireAfterWrite(Duration.ofMinutes(2));

不要一上来就把本地缓存开得特别大,先基于热点数据量评估。


3. 分层 TTL 不要一样

如果本地缓存和 Redis 都设置相同 TTL,会出现一个问题:
它们可能在同一时间大面积过期,回源流量抖动更明显。

更合理的方式是:

  • 本地缓存:1~3 分钟
  • Redis:5~30 分钟
  • 热点业务:加随机过期时间

4. 监控比“是否命中”更重要

很多人只关心缓存有没有生效,但线上真正要看的指标是:

  • 本地缓存命中率
  • Redis 命中率
  • 数据库回源次数
  • Key 数量增长趋势
  • Redis 网络延迟
  • JVM 堆使用率与 GC 次数

如果你发现:

  • 本地命中率低
  • Redis 命中率也不高
  • 数据库回源却很高

那说明你的缓存键设计、TTL 设计或数据访问模式可能有问题。


5. Key 设计要稳定、可读、可控

虽然 Spring Cache 自动帮你生成 Key,但复杂参数对象时,默认 Key 不一定适合线上排查。

建议在业务中显式指定 key:

@Cacheable(cacheNames = "userCache", key = "#id")

如果是多维参数:

@Cacheable(cacheNames = "productCache", key = "#shopId + ':' + #productId")

这样排查 Redis 数据时会轻松很多。


6. 多实例一致性建议:失效通知机制

如果你的业务对一致性要求比“短 TTL”更高,推荐增加缓存失效广播

基本思路:

  1. 更新数据库后删除 Redis
  2. 发送失效消息
  3. 各节点收到消息后删除本地缓存

状态变化可以理解为:

stateDiagram-v2
    [*] --> DBUpdated
    DBUpdated --> RedisEvicted: 删除 Redis Key
    RedisEvicted --> PublishEvent: 发布失效事件
    PublishEvent --> LocalEvictedAllNodes: 各节点删除本地缓存
    LocalEvictedAllNodes --> [*]

这一步本文不展开实现,但你在生产里如果是多实例部署,我非常建议认真考虑。


进阶建议:什么时候不该用多级缓存

这个问题很重要。不是所有缓存问题都该靠“本地 + Redis”解决。

以下场景我一般不建议用本地缓存:

1. 强一致业务

例如:

  • 库存扣减
  • 支付状态
  • 账户余额

这种数据哪怕短时间不一致,也可能带来严重后果。
这类场景建议:

  • 直接查 Redis / DB
  • 配合原子操作
  • 明确一致性策略

2. 写多读少业务

如果一个数据刚写完很快又变,缓存命中率会很低。
这时候缓存带来的管理成本,可能比收益更大。

3. 数据体积很大

本地缓存适合热点、小对象、高频访问。
如果每条缓存对象都很大,很容易把 JVM 内存顶爆。


总结

这篇文章的核心思路其实可以归纳成一句话:

用 Spring Cache 保持业务代码简洁,用 Caffeine + Redis 组合出“低延迟 + 跨实例共享”的多级缓存能力。

我们完成了这些事情:

  • 分析了单层 Redis 缓存的局限
  • 解释了 Spring Cache 抽象层的工作方式
  • 实现了一个可运行的 MultiLevelCache
  • 演示了读取、回填、失效的完整链路
  • 梳理了多级缓存里最常见的坑和排查方法
  • 给出了上线前值得执行的安全与性能建议

最后给几个可执行建议,方便你落地:

  1. 先从读多写少的接口开始改造,不要全站一口气上多级缓存。
  2. 本地缓存 TTL 设短,Redis TTL 设长,避免一致性问题扩大。
  3. 更新优先删缓存而不是改缓存,逻辑更稳。
  4. 多实例环境下别忽视本地缓存失效广播,否则很容易读到旧值。
  5. 给缓存加监控,不然你只能凭感觉判断“缓存是不是生效了”。

如果你当前项目已经在用 Spring Cache 和 Redis,那么这套方案其实非常适合作为下一步优化方向。
它不是银弹,但对很多典型的中后台读场景来说,确实是一个成本可控、收益明显的工程实践。


分享到:

上一篇
《Java Web 开发中基于 Spring Boot + JWT 的权限认证实战:从登录鉴权到接口安全落地》
下一篇
《区块链中智能合约安全审计实战:从常见漏洞识别到自动化检测流程搭建-131》