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

《Spring Boot 中基于 Spring Cache 与 Redis 的多级缓存实战:提升接口性能与一致性治理》

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

Spring Boot 中基于 Spring Cache 与 Redis 的多级缓存实战:提升接口性能与一致性治理

做接口性能优化时,很多人第一反应就是“上 Redis”。这当然没错,但真正到了线上,往往会发现:只有 Redis 还不够

原因很直接:

  • 单机内热点数据反复查 Redis,网络开销依然存在
  • 高并发下缓存击穿、雪崩、穿透会一起冒出来
  • 数据更新后,本地缓存和 Redis 的一致性治理比“加个 @Cacheable”复杂得多

这篇文章我不打算只停留在“能跑”的层面,而是带你做一个Spring Boot + Spring Cache + Redis + 本地缓存(Caffeine) 的多级缓存方案。目标很明确:

  1. 接口更快:优先命中 JVM 本地缓存
  2. 后端更稳:Redis 扛共享缓存,减数据库压力
  3. 可治理一致性:更新时尽量减少脏数据窗口
  4. 可运行、可验证、可排查

如果你已经会用 Spring Cache,那这篇会帮你把它从“注解层面”推进到“线上可用层面”。


一、背景与问题

先看一个很典型的查询接口:

  • 根据商品 ID 查询商品详情
  • 商品详情读多写少
  • 峰值流量高
  • 商品更新后,要求缓存不能长期脏读

如果只有数据库,问题显而易见:

  • 每次都查 DB,响应时间高
  • 热点商品会把数据库打穿
  • 一旦数据库抖动,接口直接雪崩

如果只加 Redis:

  • 跨机器共享没问题
  • 但单实例高频热点仍然要走网络 I/O
  • Redis 成了所有应用实例的共同依赖点

于是,多级缓存的思路就自然出现了:

  • 一级缓存(L1):本地缓存,例如 Caffeine
  • 二级缓存(L2):分布式缓存,例如 Redis
  • 最终数据源:MySQL 等数据库

整体目标是:先查本地,再查 Redis,最后查 DB;更新时同步清理多级缓存。


二、前置知识与环境准备

1. 技术栈

本文示例使用:

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

2. 依赖

<!-- pom.xml -->
<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-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

三、核心原理

1. 多级缓存访问路径

最常见的查询路径如下:

flowchart TD
    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

这个流程带来的收益很直接:

  • 本地缓存命中:最快
  • Redis 命中:共享缓存,避免打 DB
  • DB 回源:最后兜底

2. Spring Cache 在这里扮演什么角色

Spring Cache 的优势不是“缓存能力最强”,而是统一抽象

你可以通过:

  • @Cacheable:查缓存,没有则执行方法并回填
  • @CachePut:执行方法后更新缓存
  • @CacheEvict:删除缓存
  • @Caching:组合多个缓存操作

把业务代码和缓存逻辑先解耦出来。

3. 为什么默认的 Spring Cache 不够做多级缓存

Spring 默认一般只绑定一个 CacheManager
但多级缓存的关键是:

  • 一个缓存名下,内部需要有两层存储
  • 查询时要按 L1 → L2 → DB 的顺序
  • 删除时要同时清理 L1 和 L2
  • 写入时要同时回填 L1 和 L2

这就意味着,我们通常要自定义一个 Cache 实现,而不是只靠默认配置。

4. 一致性为什么难

缓存和数据库不是一个事务资源,所以天然存在窗口期。

比如更新商品时:

  1. 先更新 DB
  2. 再删 Redis
  3. 再删本地缓存

如果某个读请求刚好夹在中间,就可能读到旧值。
这也是我实际项目里最常见的争议点:缓存不是数据库,不要追求绝对强一致,要做“可接受的一致性治理”。


四、方案设计:一个能落地的多级缓存结构

我们先给出一个比较务实的方案:

  • L1:Caffeine
    • 容量小
    • 过期时间短
    • 面向单机热点加速
  • L2:Redis
    • 容量相对大
    • 多实例共享
    • 设置统一 TTL
  • 更新策略
    • 先更新数据库
    • 再清理 Redis 和本地缓存
  • 防脏读增强
    • TTL 不同层级错开
    • 重要业务可加消息通知清理本地缓存
    • 热点 key 可加互斥回源

五、实战代码(可运行)

下面我们做一个完整示例:商品详情查询


1. 配置文件

# application.yml
server:
  port: 8080

spring:
  cache:
    type: none
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
  redis:
    host: localhost
    port: 6379

logging:
  level:
    org.springframework.cache: debug

这里把 spring.cache.type 设成 none,是因为我们要自己接管缓存实现。


2. 启动类

// DemoApplication.java
package com.example.multicache;

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

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

    @Bean
    CommandLineRunner init(ProductRepository productRepository) {
        return args -> {
            productRepository.save(new Product(1L, "机械键盘", 399.00));
            productRepository.save(new Product(2L, "人体工学鼠标", 259.00));
        };
    }
}

3. 实体与仓库

// Product.java
package com.example.multicache;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;

@Entity
public class Product implements Serializable {

    @Id
    private Long id;
    private String name;
    private Double price;

    public Product() {
    }

    public Product(Long id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Double getPrice() {
        return price;
    }

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

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

    public void setPrice(Double price) {
        this.price = price;
    }
}
// ProductRepository.java
package com.example.multicache;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

4. 自定义多级缓存实现

这部分是核心。

4.1 MultiLevelCache

// MultiLevelCache.java
package com.example.multicache.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;
import java.util.concurrent.Callable;

public class MultiLevelCache implements Cache {

    private final String name;
    private final com.github.benmanes.caffeine.cache.Cache<Object, Object> localCache;
    private final RedisTemplate<Object, Object> redisTemplate;
    private final Duration redisTtl;

    public MultiLevelCache(String name,
                           com.github.benmanes.caffeine.cache.Cache<Object, Object> localCache,
                           RedisTemplate<Object, Object> redisTemplate,
                           Duration redisTtl) {
        this.name = name;
        this.localCache = localCache;
        this.redisTemplate = redisTemplate;
        this.redisTtl = redisTtl;
    }

    private String buildRedisKey(Object key) {
        return this.name + "::" + key;
    }

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

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

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

        Object redisValue = redisTemplate.opsForValue().get(buildRedisKey(key));
        if (redisValue != null) {
            localCache.put(key, redisValue);
            return new SimpleValueWrapper(redisValue);
        }

        return null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(Object key, Class<T> type) {
        ValueWrapper wrapper = get(key);
        if (wrapper == null) {
            return null;
        }
        Object value = wrapper.get();
        if (type != null && !type.isInstance(value)) {
            throw new IllegalStateException("缓存值类型不匹配,期望: " + type + ", 实际: " + value.getClass());
        }
        return (T) value;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        ValueWrapper wrapper = get(key);
        if (wrapper != null) {
            return (T) wrapper.get();
        }
        try {
            T value = valueLoader.call();
            if (value != null) {
                put(key, value);
            }
            return value;
        } catch (Exception e) {
            throw new RuntimeException("加载缓存值失败", e);
        }
    }

    @Override
    public void put(Object key, Object value) {
        localCache.put(key, value);
        redisTemplate.opsForValue().set(buildRedisKey(key), value, redisTtl);
    }

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

    @Override
    public void evict(Object key) {
        localCache.invalidate(key);
        redisTemplate.delete(buildRedisKey(key));
    }

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

注意:clear() 这里只清了本地缓存,没有扫描 Redis 全删。线上如果要支持整库清理,通常要配合前缀设计、专门管理接口或者版本号机制,别轻易在 Redis 里 keys *


4.2 MultiLevelCacheManager

// MultiLevelCacheManager.java
package com.example.multicache.cache;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MultiLevelCacheManager implements CacheManager {

    private final RedisTemplate<Object, Object> redisTemplate;
    private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();

    public MultiLevelCacheManager(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Cache getCache(String name) {
        return cacheMap.computeIfAbsent(name, cacheName -> new MultiLevelCache(
                cacheName,
                Caffeine.newBuilder()
                        .maximumSize(1000)
                        .expireAfterWrite(Duration.ofSeconds(30))
                        .build(),
                redisTemplate,
                Duration.ofMinutes(5)
        ));
    }

    @Override
    public Collection<String> getCacheNames() {
        return Collections.unmodifiableSet(cacheMap.keySet());
    }
}

5. Redis 配置

// CacheConfig.java
package com.example.multicache.config;

import com.example.multicache.cache.MultiLevelCacheManager;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

@Configuration
public class CacheConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        GenericJackson2JsonRedisSerializer jacksonSerializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        return new MultiLevelCacheManager(redisTemplate);
    }
}

6. Service 层:用 Spring Cache 注解接入

// ProductService.java
package com.example.multicache;

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(cacheNames = "product", key = "#id", unless = "#result == null")
    public Product getById(Long id) {
        simulateSlowQuery();
        return productRepository.findById(id).orElse(null);
    }

    @Transactional
    @CacheEvict(cacheNames = "product", key = "#product.id")
    public Product update(Product product) {
        Product dbProduct = productRepository.findById(product.getId())
                .orElseThrow(() -> new IllegalArgumentException("商品不存在"));

        dbProduct.setName(product.getName());
        dbProduct.setPrice(product.getPrice());
        return productRepository.save(dbProduct);
    }

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

这里故意 sleep 500ms,方便你在本地明显看出第一次查询和后续查询的差异。


7. Controller

// ProductController.java
package com.example.multicache;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public Product get(@PathVariable Long id) {
        return productService.getById(id);
    }

    @PutMapping("/{id}")
    public Product update(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        return productService.update(product);
    }
}

六、逐步验证清单

到这里,项目已经能跑了。接下来别急着收工,我建议按下面步骤验证。

1. 启动 Redis

redis-server

或者使用 Docker:

docker run -p 6379:6379 --name myredis -d redis:6.2

2. 启动应用

mvn spring-boot:run

3. 首次查询,预期慢

curl http://localhost:8080/products/1

第一次会有明显延迟,因为走了 DB。

4. 再查一次,预期快

curl http://localhost:8080/products/1

这次应该明显更快,优先命中本地缓存。

5. 查看 Redis 中是否有值

redis-cli
keys *
get "product::1"

如果值是 JSON 或二进制序列化后的内容,属于正常现象。

6. 更新商品后再次查询

curl -X PUT http://localhost:8080/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"机械键盘Pro","price":499.0}'

然后重新查询:

curl http://localhost:8080/products/1

预期结果:

  • 更新时缓存被清理
  • 下次查询重新回源 DB
  • 再重新写入 Redis 和本地缓存

七、一致性治理:不是删缓存这么简单

如果你的系统只有单实例,上面的 @CacheEvict 已经够用了。
但只要变成多实例部署,就会出现一个经典问题:

  • A 实例更新数据并清掉自己本地缓存和 Redis
  • B 实例的本地缓存还保留旧值
  • 用户请求打到 B,读到旧数据

这就是本地缓存一致性问题

1. 多实例更新传播流程

sequenceDiagram
    participant C as 客户端
    participant A as 应用实例A
    participant B as 应用实例B
    participant R as Redis
    participant DB as 数据库

    C->>A: 更新商品
    A->>DB: update product
    A->>R: delete product::id
    A->>A: clear local cache
    Note over B: B 的本地缓存仍可能是旧值
    C->>B: 查询商品
    B->>B: 命中旧本地缓存
    B-->>C: 返回旧数据

2. 解决思路

常见方法有三种:

方法一:缩短本地缓存 TTL

最简单,成本最低。

  • 本地缓存 10~30 秒
  • Redis 缓存 5~10 分钟

优点:

  • 实现简单
  • 不引入新组件

缺点:

  • 存在短时间脏读窗口

这是很多中型业务实际采用的方式。

方法二:Redis Pub/Sub 广播本地缓存失效

更新数据时:

  1. 删 Redis
  2. 发布失效消息
  3. 所有应用实例监听消息并删除本地缓存

流程如下:

flowchart LR
    A[实例A更新DB] --> B[删除Redis缓存]
    B --> C[发布失效消息]
    C --> D[实例A清理本地缓存]
    C --> E[实例B清理本地缓存]
    C --> F[实例C清理本地缓存]

这种方式在多实例下非常实用,延迟低,改造成本也不算高。

方法三:更新版本号或逻辑时间戳

适合对一致性要求更高的业务。
缓存值中带版本号,请求读取时校验版本,旧版本数据拒绝返回或强制回源。

优点:

  • 一致性更强

缺点:

  • 设计复杂
  • 读写链路都要改

八、常见坑与排查

这部分我尽量说得“接地气”一点,因为这些坑真的太常见了。

1. @Cacheable 不生效

典型原因

  • 没加 @EnableCaching
  • 方法是 private
  • 同类内部自调用
  • 异常导致方法根本没执行完

排查方式

先确认:

@EnableCaching

然后注意这种代码:

public Product test(Long id) {
    return this.getById(id); // 同类内部调用,可能绕过代理
}

Spring Cache 依赖 AOP 代理,同类自调用经常让人误以为“缓存失效了”。

建议:把带缓存的方法拆到独立 Bean 里。


2. Redis 里有值,但每次都走数据库

可能原因

  • key 不一致
  • 序列化反序列化失败
  • unless = "#result == null" 条件导致未缓存
  • 查询返回对象类型对不上

排查方式

打开日志,打印缓存 key:

private String buildRedisKey(Object key) {
    String redisKey = this.name + "::" + key;
    System.out.println("redis key = " + redisKey);
    return redisKey;
}

再去 Redis 里看是否一致。


3. 更新后读到旧值

典型原因

  • 多实例本地缓存未同步失效
  • 先删缓存再更新 DB,导致并发下旧值回写
  • TTL 太长

这里尤其要提醒:
不要在高并发更新场景里轻易使用“先删缓存,再更新数据库”
更稳妥的通常是:

  1. 更新数据库
  2. 删除缓存

这是缓存一致性里最经典的一条实践。


4. 缓存穿透

比如查一个根本不存在的商品 ID,每次都会打 DB。

解决办法

缓存空值,但 TTL 要短一些:

@Cacheable(cacheNames = "product", key = "#id")
public Product getById(Long id) {
    return productRepository.findById(id).orElse(null);
}

但如果要缓存 null,需要你的缓存实现支持 NullValue,本文示例里为了简单,用了 unless = "#result == null",所以默认没有缓存空值

线上如果存在大量无效 ID 探测,建议:

  • 增加布隆过滤器
  • 或者缓存空对象占位符

5. 缓存击穿

热点 key 过期瞬间,大量请求同时回源 DB。

解决办法

  • 热点数据不过期,靠主动失效
  • 使用互斥锁 / single-flight
  • 给过期时间加随机值,避免同一时刻集中失效

我自己更推荐:热点 key 互斥回源 + 普通 key 随机 TTL


6. 序列化问题

最容易出现的报错是:

  • 类型转换异常
  • 类结构变更后旧缓存反序列化失败
  • 泛型对象反序列化成 LinkedHashMap

建议

  • 缓存 DTO,不要直接缓存复杂领域对象
  • 谨慎变更缓存对象结构
  • 大版本升级时清理历史缓存

九、安全/性能最佳实践

这部分是上线前最好过一遍的清单。

1. TTL 分层设计

建议不要让本地缓存和 Redis 用同样的 TTL。

一个常用组合:

  • 本地缓存:30 秒
  • Redis:5 分钟

原因:

  • 本地缓存主要解决热点访问
  • Redis 主要解决共享数据和数据库减压
  • 分层 TTL 能降低同时失效风险

2. TTL 加随机值,防雪崩

例如 Redis TTL 不是固定 300 秒,而是:

300 + Random(0~60)

这样不会在某一秒所有 key 一起过期。


3. 热点 key 做互斥回源

如果某个商品接口是超级热点,缓存失效时最好只允许一个线程回源数据库,其它线程等待或返回旧值。

可以基于 Redis 分布式锁,或者本机 ConcurrentHashMap + synchronized 做轻量控制。


4. 不要滥用本地缓存容量

Caffeine 很快,但 JVM 堆不是无限的。

建议:

  • 只缓存高频、体积适中的对象
  • 设置 maximumSize
  • 监控 Full GC 和老年代占用

5. 缓存 key 规范化

建议统一 key 结构:

业务名:实体名:主键

例如:

mall:product:1

本文为了和 Spring Cache 习惯保持一致,用了:

product::1

线上更推荐显式命名,便于排查。


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

尤其是:

  • 用户令牌
  • 身份信息
  • 权限数据
  • 支付相关信息

如果必须缓存:

  • 控制 TTL
  • 做脱敏
  • Redis 开启认证与网络隔离
  • 限制运维和开发访问权限

7. 监控比“加缓存”更重要

至少监控这些指标:

  • 本地缓存命中率
  • Redis 命中率
  • DB 回源次数
  • key 数量与内存占用
  • 慢查询接口耗时
  • 缓存失效次数

如果没有监控,你很难知道“缓存到底是在帮忙,还是在制造幻觉”。


十、一个更完整的演进思路

如果你的系统继续增长,建议按下面阶段演进,而不是一上来就堆复杂度。

stateDiagram-v2
    [*] --> 单级Redis缓存
    单级Redis缓存 --> 本地+Redis多级缓存
    本地+Redis多级缓存 --> 广播失效治理
    广播失效治理 --> 热点互斥回源
    热点互斥回源 --> 版本号/逻辑时钟一致性

我对这条演进路径的建议是:

  • 小系统:先用 Redis 单级缓存
  • 中等流量:加 Caffeine 做多级缓存
  • 多实例且对一致性敏感:加失效广播
  • 极热点业务:再做互斥回源和热点永不过期

别反过来。
很多团队是业务还没起来,缓存方案已经复杂到没人敢维护了。


十一、边界条件:什么场景不适合多级缓存

多级缓存并不是银弹,以下场景要谨慎:

1. 写多读少

如果数据更新非常频繁,本地缓存可能刚写进去就要失效,收益很低。

2. 强一致要求极高

例如账户余额、库存强扣减这类业务,缓存通常只能做旁路辅助,不能作为核心读来源。

3. 对象非常大

超大对象缓存到 JVM 本地,容易带来 GC 压力,得不偿失。

4. key 数量巨大且长尾明显

如果访问很分散,本地缓存命中率不高,维护成本可能超过收益。


十二、总结

这篇文章我们从一个很实际的问题出发:只用 Redis 不一定够,多级缓存才更适合高频读接口的性能优化。

你可以把核心结论记成这几条:

  1. 多级缓存基本结构:Caffeine + Redis + DB
  2. 查询链路:本地缓存 → Redis → 数据库
  3. 更新策略:先更新 DB,再删除缓存
  4. 一致性治理重点:多实例下要考虑本地缓存失效同步
  5. 性能优化关键:TTL 分层、随机过期、热点互斥回源
  6. 上线前必做:监控命中率、内存占用、回源次数

如果你现在就要落地,我建议按这个顺序来:

  • 第一步:先把本文的多级缓存跑起来
  • 第二步:观察本地命中率和 Redis 命中率
  • 第三步:如果是多实例,再补 Redis Pub/Sub 的本地失效广播
  • 第四步:如果有热点 key,再做互斥回源

这样做,复杂度是可控的,收益也是一步步可验证的。

最后说一句经验之谈:
缓存优化的真正价值,不是把响应时间从 20ms 压到 5ms,而是让系统在流量上来时还能稳住。
这也是多级缓存最值得投入的地方。


分享到:

上一篇
《分布式架构中基于一致性哈希与服务发现的灰度发布实践与故障切换设计》
下一篇
《Web逆向实战:从浏览器抓包到还原加签逻辑的完整分析方法》