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

《Spring Boot 中基于 Spring Cache 与 Redis 的多级缓存实战:一致性、穿透与热点 Key 处理方案》

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

Spring Boot 中基于 Spring Cache 与 Redis 的多级缓存实战:一致性、穿透与热点 Key 处理方案

在很多 Spring Boot 项目里,缓存一开始都很简单:先上个 Redis,配个 @Cacheable,接口响应时间立刻好看不少。

但业务一复杂,问题就会接踵而来:

  • 本地缓存和 Redis 缓存怎么配合?
  • 数据更新后,怎么保证缓存别“脏太久”?
  • 遇到缓存穿透、缓存击穿、热点 Key,系统怎么扛?
  • Spring Cache 很方便,但默认抽象背后有哪些坑?

这篇文章我会带你从问题出发,一步步做出一个Spring Boot + Spring Cache + Redis + 本地缓存(Caffeine) 的多级缓存方案,并把一致性、穿透和热点 Key 的处理方式一起串起来。文章会尽量贴近真实项目,而不是只停留在“能跑”的 demo。


背景与问题

先看一个典型业务场景:商品详情查询。

调用链通常是:

  1. 先查缓存
  2. 缓存没有,再查数据库
  3. 查到结果后回填缓存

当 QPS 上来以后,仅有 Redis 还不一定够,原因一般有几个:

1. Redis 不是“零成本”

Redis 快,但网络调用、序列化、反序列化都是成本。对于极高频、读多写少的数据,本地缓存依然很有价值。

2. 更新时容易出现数据不一致

比如:

  • 先更新数据库,再删缓存
  • 或先删缓存,再更新数据库

这两种都不是绝对安全。并发情况下,很容易把旧值重新写回缓存。

3. 缓存穿透与击穿

  • 缓存穿透:查询不存在的数据,每次都打到数据库
  • 缓存击穿:某个热点 Key 失效瞬间,大量请求同时回源
  • 缓存雪崩:大量 Key 同时过期,导致数据库被打爆

4. Spring Cache 用起来简单,但默认行为不够“业务化”

@Cacheable@CachePut@CacheEvict 很方便,但多级缓存、一致性控制、空值缓存、热点保护等,通常都需要再补一层能力。


前置知识与环境准备

技术栈

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

示例业务

我们用一个很常见的模型:

  • Product:商品
  • 查询接口:GET /products/{id}
  • 更新接口:PUT /products/{id}

本文实现目标

我们会实现下面这套链路:

  1. 一级缓存:Caffeine 本地缓存
  2. 二级缓存:Redis
  3. 回源:数据库(这里用内存 Map 模拟)
  4. 空值缓存:防止缓存穿透
  5. 热点 Key 互斥加载:防止击穿
  6. 更新时双删策略 + 短 TTL:降低不一致窗口

核心原理

先把整体结构看清楚。

flowchart TD
    A[客户端请求] --> B[Spring Cache]
    B --> C{一级缓存 Caffeine 命中?}
    C -- 是 --> D[返回结果]
    C -- 否 --> E{二级缓存 Redis 命中?}
    E -- 是 --> F[写入一级缓存]
    F --> D
    E -- 否 --> G[加互斥锁/热点保护]
    G --> H[查询数据库]
    H --> I[写入 Redis]
    I --> J[写入 Caffeine]
    J --> D

多级缓存的基本思路

多级缓存不是“堆技术”,而是分层处理不同问题:

  • Caffeine:解决超高频读、降低 Redis 网络开销
  • Redis:解决多实例共享缓存
  • 数据库:最终数据源

一致性怎么理解

缓存和数据库本质上就是弱一致性方案。只要你不是做分布式事务缓存,通常都只能做到:

  • 大多数时候一致
  • 极端并发下有短暂不一致窗口
  • 通过 TTL、删除策略、异步修复把影响收敛

所以这里别追求“绝对一致”,要追求的是:一致性窗口足够小,业务可接受,异常可恢复

穿透、击穿、雪崩分别怎么治

缓存穿透

查一个根本不存在的 id,缓存里没有,数据库也没有,请求次次穿透。

常见手段:

  • 缓存空对象
  • 布隆过滤器
  • 参数合法性校验

本文我们先实现最实用的:空值缓存

缓存击穿

热点 Key 过期瞬间,大量并发同时查库。

常见手段:

  • 互斥锁
  • 热点永不过期 + 异步刷新
  • 请求合并

本文实现:互斥锁 + 短暂等待重试

缓存雪崩

大量 Key 同时过期。

常见手段:

  • TTL 加随机值
  • 多级缓存兜底
  • 限流降级

方案设计

这里我建议把职责拆开,不要把所有逻辑都硬塞进 @Cacheable 注解里。我们用一个缓存门面服务来控制查询流程。

classDiagram
    class ProductController {
        +getById(Long id)
        +update(Long id, Product product)
    }

    class ProductService {
        +getProduct(Long id)
        +updateProduct(Long id, Product product)
    }

    class ProductRepository {
        +findById(Long id)
        +save(Product product)
    }

    class MultiLevelCacheService {
        +get(String cacheName, String key, Callable loader, Class type)
        +evict(String cacheName, String key)
    }

    ProductController --> ProductService
    ProductService --> ProductRepository
    ProductService --> MultiLevelCacheService

核心点有两个:

  1. 查询走统一门面
    • 先查本地缓存
    • 再查 Redis
    • 再查数据库
  2. 更新时主动删除多级缓存
    • 更新数据库
    • 删除 Redis
    • 删除本地缓存
    • 延迟二次删除,降低并发脏读窗口

实战代码(可运行)

下面给出一个可以直接拼起来运行的示例。为了突出缓存逻辑,数据库部分我用内存 Map 模拟。

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: 2000ms

cache:
  ttl:
    product: 300
    null-value: 60

3. 启动类

package com.example.cache;

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

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

4. 实体类

package com.example.cache.model;

import java.io.Serializable;
import java.math.BigDecimal;

public class Product implements Serializable {
    private Long id;
    private String name;
    private BigDecimal price;

    public Product() {
    }

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

    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 BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}

5. 模拟 Repository

package com.example.cache.repository;

import com.example.cache.model.Product;
import org.springframework.stereotype.Repository;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Repository
public class ProductRepository {

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

    @PostConstruct
    public void init() {
        database.put(1L, new Product(1L, "iPhone", new BigDecimal("6999")));
        database.put(2L, new Product(2L, "MacBook", new BigDecimal("12999")));
    }

    public Product findById(Long id) {
        sleep(100);
        return database.get(id);
    }

    public Product save(Product product) {
        sleep(100);
        database.put(product.getId(), product);
        return product;
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ignored) {
        }
    }
}

6. Caffeine 配置

package com.example.cache.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class LocalCacheConfig {

    @Bean
    public com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .recordStats()
                .build();
    }
}

7. Redis 配置

这里用 RedisTemplate<String, Object>,并配置 JSON 序列化,避免默认 JDK 序列化可读性差、兼容性差。

package com.example.cache.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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 RedisConfig {

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

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

        Jackson2JsonRedisSerializer<Object> valueSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        valueSerializer.setObjectMapper(mapper);

        StringRedisSerializer keySerializer = new StringRedisSerializer();

        template.setKeySerializer(keySerializer);
        template.setHashKeySerializer(keySerializer);
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

8. 多级缓存服务

这是本文的核心实现。

package com.example.cache.service;

import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

@Service
public class MultiLevelCacheService {

    private static final String NULL_MARKER = "__NULL__";

    private final Cache<String, Object> caffeineCache;
    private final RedisTemplate<String, Object> redisTemplate;

    @Value("${cache.ttl.product:300}")
    private long productTtlSeconds;

    @Value("${cache.ttl.null-value:60}")
    private long nullValueTtlSeconds;

    public MultiLevelCacheService(Cache<String, Object> caffeineCache,
                                  RedisTemplate<String, Object> redisTemplate) {
        this.caffeineCache = caffeineCache;
        this.redisTemplate = redisTemplate;
    }

    public <T> T get(String cacheName, String key, Callable<T> loader, Class<T> type) {
        String fullKey = buildKey(cacheName, key);

        Object localValue = caffeineCache.getIfPresent(fullKey);
        if (localValue != null) {
            if (NULL_MARKER.equals(localValue)) {
                return null;
            }
            return type.cast(localValue);
        }

        Object redisValue = redisTemplate.opsForValue().get(fullKey);
        if (redisValue != null) {
            caffeineCache.put(fullKey, redisValue);
            if (NULL_MARKER.equals(redisValue)) {
                return null;
            }
            return type.cast(redisValue);
        }

        String lockKey = "lock:" + fullKey;
        String lockValue = UUID.randomUUID().toString();
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(locked)) {
            try {
                T loaded = loader.call();
                if (loaded == null) {
                    redisTemplate.opsForValue().set(fullKey, NULL_MARKER, nullValueTtlSeconds, TimeUnit.SECONDS);
                    caffeineCache.put(fullKey, NULL_MARKER);
                    return null;
                }
                long ttl = productTtlSeconds + (long) (Math.random() * 60);
                redisTemplate.opsForValue().set(fullKey, loaded, ttl, TimeUnit.SECONDS);
                caffeineCache.put(fullKey, loaded);
                return loaded;
            } catch (Exception e) {
                throw new RuntimeException("load data error", e);
            } finally {
                Object currentLockValue = redisTemplate.opsForValue().get(lockKey);
                if (lockValue.equals(currentLockValue)) {
                    redisTemplate.delete(lockKey);
                }
            }
        }

        try {
            Thread.sleep(50);
        } catch (InterruptedException ignored) {
        }

        Object retryValue = redisTemplate.opsForValue().get(fullKey);
        if (retryValue != null) {
            caffeineCache.put(fullKey, retryValue);
            if (NULL_MARKER.equals(retryValue)) {
                return null;
            }
            return type.cast(retryValue);
        }

        try {
            T loaded = loader.call();
            if (loaded == null) {
                return null;
            }
            caffeineCache.put(fullKey, loaded);
            return loaded;
        } catch (Exception e) {
            throw new RuntimeException("retry load data error", e);
        }
    }

    public void evict(String cacheName, String key) {
        String fullKey = buildKey(cacheName, key);
        caffeineCache.invalidate(fullKey);
        redisTemplate.delete(fullKey);
    }

    private String buildKey(String cacheName, String key) {
        return cacheName + "::" + key;
    }
}

这个实现做了什么?

  • 先查本地缓存
  • 本地没命中,再查 Redis
  • Redis 没命中,再尝试拿分布式锁
  • 拿到锁的线程负责查库并回填
  • 没拿到锁的线程短暂等待,再读 Redis
  • 数据不存在时缓存空值
  • Redis TTL 加随机数,防止雪崩

9. 业务服务

package com.example.cache.service;

import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    private static final String CACHE_NAME = "product";

    private final ProductRepository productRepository;
    private final MultiLevelCacheService cacheService;

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

    public Product getProduct(Long id) {
        return cacheService.get(CACHE_NAME, String.valueOf(id),
                () -> productRepository.findById(id), Product.class);
    }

    public Product updateProduct(Long id, Product product) {
        product.setId(id);
        Product saved = productRepository.save(product);

        cacheService.evict(CACHE_NAME, String.valueOf(id));
        delayedDoubleDelete(id);

        return saved;
    }

    @Async
    public void delayedDoubleDelete(Long id) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException ignored) {
        }
        cacheService.evict(CACHE_NAME, String.valueOf(id));
    }
}

这里用到了延迟双删,所以别忘了开启异步。


10. 异步配置

package com.example.cache.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
public class AsyncConfig {
}

11. Controller

package com.example.cache.controller;

import com.example.cache.model.Product;
import com.example.cache.service.ProductService;
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 getById(@PathVariable Long id) {
        return productService.getProduct(id);
    }

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

逐步验证清单

项目启动后,可以按下面顺序验证。

1. 查询已存在商品

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

第一次通常会稍慢,因为会回源数据库;第二次会明显更快。

2. 查询不存在商品,观察空值缓存

curl http://localhost:8080/products/999

连续请求多次,数据库不应该每次都被打到。

3. 更新商品,验证缓存删除

curl -X PUT http://localhost:8080/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"iPhone 15","price":7999}'

然后再次查询:

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

应能看到新值。

4. 并发压测热点 Key

可以用 abwrk 压测:

ab -n 1000 -c 100 http://127.0.0.1:8080/products/1

如果日志里加上数据库查询输出,你会发现数据库访问次数远低于请求数。


一致性时序分析

更新时最容易出问题。下面这个图描述了“更新数据库 + 删除缓存”的典型并发窗口。

sequenceDiagram
    participant C1 as 请求A-更新
    participant C2 as 请求B-查询
    participant DB as 数据库
    participant R as Redis
    participant L as 本地缓存

    C1->>DB: 更新商品数据
    C2->>L: 查询本地缓存
    L-->>C2: 未命中
    C2->>R: 查询 Redis
    R-->>C2: 未命中
    C2->>DB: 查询旧值/临界值
    C1->>R: 删除 Redis 缓存
    C1->>L: 删除本地缓存
    C2->>R: 回填旧值
    C2->>L: 回填旧值

这就是为什么简单“更新数据库后删缓存”仍然可能脏。

为什么要用延迟双删?

第一次删除是为了尽快把旧缓存清掉;
第二次延迟删除是为了清理由并发查询回填进去的旧值。

它不能彻底解决所有问题,但在大部分读多写少场景里很实用。

更严格的一致性方案

如果业务真的很敏感,比如库存、账户余额,建议考虑:

  • 订阅 binlog 异步删缓存
  • 基于 MQ 的变更通知
  • 只缓存只读视图数据
  • 关键链路避免依赖缓存正确性

常见坑与排查

这些坑我基本都踩过,尤其是“看起来缓存加上了,实际上并没有”。

1. Spring Cache 注解不生效

常见原因:

  • 没加 @EnableCaching
  • 同类内部方法调用,AOP 没代理到
  • 方法不是 public

排查建议:

  • 看启动日志里是否创建了 Cache 相关 Bean
  • 打断点确认是否进入代理逻辑
  • 避免同类自调用缓存方法

2. Redis 序列化问题

表现:

  • Redis 中值乱码
  • 反序列化报错
  • 类结构变更后旧缓存读不出来

建议:

  • 优先 JSON 序列化
  • 明确 key/value serializer
  • 生产环境要考虑对象版本兼容

3. 空值缓存时间设置不当

如果空值 TTL 太长,数据刚创建出来时,缓存里还保留着“空结果”,用户会短时间查不到。

建议:

  • 空值缓存 TTL 明显短于正常值,比如 30~60 秒
  • 对新增频繁的数据,空值缓存要更谨慎

4. 本地缓存导致多实例不一致

这是多级缓存里最常见的误区。

Redis 是共享的,但 Caffeine 不是。多实例部署时:

  • A 实例刚更新并删除本地缓存
  • B 实例本地缓存里可能还保留旧值

解决思路:

  • 本地缓存 TTL 设置更短
  • 更新时通过 Redis Pub/Sub 广播失效消息
  • 对强一致要求高的 Key,绕过本地缓存

5. 热点 Key 锁竞争严重

如果锁设计不好,会出现:

  • 大量线程阻塞
  • 锁超时误删
  • 回源仍然打爆数据库

建议:

  • 锁 TTL 不要太短
  • 解锁时校验锁值
  • 极热点数据考虑逻辑过期 + 后台刷新

6. 延迟双删并不是银弹

它只是降低脏数据概率,不是强一致方案。

如果你遇到以下情况,要谨慎:

  • 写非常频繁
  • 同一 Key 高频更新
  • 对一致性要求极高

这时候更适合事件驱动失效、版本号控制,甚至直接不走缓存。


安全/性能最佳实践

这一节我尽量给可执行建议,不讲空话。

1. Key 设计要规范

推荐格式:

业务名:实体名:主键[:版本]

比如:

mall:product:1

好处:

  • 易排查
  • 易批量管理
  • 降低 key 冲突

2. TTL 不要整齐划一

错误做法:

  • 所有商品缓存都 300 秒过期

正确做法:

  • 在基础 TTL 上加随机抖动

示例:

long ttl = 300 + ThreadLocalRandom.current().nextInt(60);

这样能有效缓解雪崩。


3. 对不存在参数先做校验

比如:

  • id <= 0
  • 非法格式
  • 越权访问

这类请求不要放进缓存链路里浪费资源。


4. 热点数据考虑“永不过期 + 异步刷新”

对极热点 Key,过期就容易击穿。一个更稳妥的思路是:

  • Redis 中保存数据 + 逻辑过期时间
  • 请求读到过期数据时先返回旧值
  • 后台线程异步刷新

适合:

  • 商品详情
  • 配置信息
  • 首页聚合数据

不太适合:

  • 强一致库存
  • 账户余额

5. 多级缓存不是越多越好

我通常的建议是:

  • 普通业务:Redis 即可
  • 高频热点读:Caffeine + Redis
  • 高一致性业务:少用本地缓存,缩短 TTL

别因为“架构看起来高级”就把系统搞复杂。


6. 给缓存留监控

至少要监控这些指标:

  • 本地缓存命中率
  • Redis 命中率
  • DB 回源次数
  • 空值缓存数量
  • 热点 Key QPS
  • 锁等待与失败次数

没有监控,缓存问题往往只能靠猜。


7. 注意缓存中的敏感数据

不要把以下数据直接明文放缓存:

  • 身份证号
  • 手机号全量信息
  • token、session 等认证信息
  • 高敏业务字段

建议:

  • 能不缓存就不缓存
  • 必须缓存时脱敏或加密
  • 控制 TTL,限制访问范围

进阶:如果想更贴近 Spring Cache 抽象

上面为了可控性,我们是手写了一个缓存门面。那 Spring Cache 在这套架构里怎么用更合理?

我的建议是:

适合直接用 @Cacheable 的场景

  • 读多写少
  • 单层缓存
  • 一致性要求不高
  • Key 规则简单

例如:

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

不适合只靠注解的场景

  • 多级缓存
  • 互斥锁防击穿
  • 逻辑过期
  • 空值缓存
  • 自定义删除与延迟双删

这时更适合像本文一样,用 Spring Cache 提供抽象能力,但核心流程自己掌控


常见优化取舍

这里给一个简短决策表,方便你在项目里落地时判断。

场景推荐方案备注
普通列表、详情页Redis + @Cacheable成本低,足够用
高频商品详情Caffeine + Redis降低 Redis 压力
不存在数据被频繁查询空值缓存 / 布隆过滤器防穿透
单个热点 Key 超高并发互斥锁 / 逻辑过期防击穿
多实例缓存不一致敏感缩短本地 TTL / 广播失效降低本地缓存影响
强一致业务尽量少缓存或只缓存只读视图不要强行上多级缓存

总结

这篇文章我们做了一套比较实用的缓存方案:

  • Caffeine 做一级缓存,提升极高频访问性能
  • Redis 做二级缓存,实现多实例共享
  • 空值缓存 处理缓存穿透
  • 互斥锁 处理热点 Key 击穿
  • TTL 随机化 降低雪崩风险
  • 更新后删除 + 延迟双删 缩小一致性窗口

最后给几个落地建议,都是比较“接地气”的:

  1. 别一上来就做复杂多级缓存
    先确认瓶颈到底是数据库、Redis,还是接口逻辑本身。

  2. 缓存方案要看业务一致性要求
    商品详情和库存扣减不是一回事,别用同一套标准。

  3. 强依赖缓存的地方一定要可观测
    没有命中率、回源次数、热点 Key 监控,线上出问题会很被动。

  4. 本地缓存要克制使用
    它很好用,但天然带来多实例不一致,适合热点读,不适合强一致数据。

  5. Spring Cache 很适合做起点,不一定适合做终点
    注解能快速起步,但真到复杂场景,还是要回到明确的缓存流程控制。

如果你现在正好在做 Spring Boot 项目的性能优化,这套方案基本可以作为一个中型项目的缓存骨架。先把流程搭起来,再根据业务热点和一致性要求逐步演进,通常比一开始追求“完美架构”更靠谱。


分享到:

上一篇
《区块链智能合约安全审计实战:从常见漏洞识别到自动化检测流程搭建-56》
下一篇
《前端开发中的微前端落地实践:基于 Module Federation 的应用拆分、共享依赖与部署优化》