Spring Cache技术使用方案详解

本文深入探讨Spring Cache技术,从缓存基础概念出发,逐步深入到核心原理、最佳实践及常见问题解决方案。


一、缓存概述

1.1 为什么需要缓存

数据库往往成为系统性能瓶颈,主要原因:

问题 表现 影响
磁盘I/O瓶颈 数据库查询涉及磁盘读取 响应时间长
连接资源有限 数据库连接数有限 高并发时请求排队
CPU资源消耗 复杂SQL计算消耗CPU 数据库负载高

缓存的本质:用空间换时间,将频繁访问的数据存储在更快的存储介质中。

1.2 缓存应用场景

适合缓存: 读多写少、一致性要求不严格、计算成本高、热点数据集中

典型场景: 商品信息、用户信息、字典数据、热点内容、会话信息

不适合缓存: 写多读少、强一致性要求、数据量大且冷门、敏感数据

1.3 缓存优缺点

优点 缺点
性能提升、减轻数据库压力 数据一致性挑战
提升吞吐量、降低网络开销 额外维护成本、内存成本

二、缓存实现方案

2.1 传统实现方式

传统Redis API实现示例:

@Service
public class UserServiceImpl {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(Long id) {
        String cacheKey = "user:" + id;
        User user = (User) redisTemplate.opsForValue().get(cacheKey);
        if (user != null) return user;
        
        user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, user, 24, TimeUnit.HOURS);
        }
        return user;
    }
    
    public void updateUser(User user) {
        userMapper.updateById(user);
        redisTemplate.delete("user:" + user.getId());
    }
}

传统方式的痛点:

  • 代码侵入性强(业务与缓存耦合)
  • 代码重复(相似逻辑多处重复)
  • 维护困难(修改策略需改多处)
  • 缺乏统一标准(命名风格不一致)
  • 功能受限(难以实现条件缓存、多级缓存)

2.2 缓存更新策略

模式 说明 适用场景
Cache Aside 读时先查缓存,未命中则查数据库并写入缓存;写时先更新数据库再删除缓存 最常用,适合大多数场景
Read/Write Through 应用只与缓存交互,缓存层负责与数据库同步 对一致性要求较高
Write Behind 写操作只更新缓存,数据库异步批量更新 写入频繁、一致性要求不高

Cache Aside模式流程:

读取:应用 → 缓存(命中返回) → 未命中 → 数据库 → 写入缓存 → 返回
更新:应用 → 数据库 → 删除缓存

为什么删除缓存而非更新?

  • 避免并发写问题(两线程同时更新可能顺序不一致)
  • 资源优化(数据修改但不常读取时,更新缓存是浪费)
  • 实现简单(删除操作更轻量)

2.3 缓存删除策略

策略 原理 适用场景
直接删除 数据变更时立即删除缓存 一般场景,一致性较好
延迟双删 删除缓存 → 更新数据库 → 延迟后再次删除 高并发读取、数据库有主从延迟
异步删除(MQ) 通过消息队列异步处理缓存删除 需要解耦、可靠性要求高

延迟双删实现:

@Transactional
public void updateUser(User user) {
    String cacheKey = "user:" + user.getId();
    redisTemplate.delete(cacheKey);          // 第一次删除
    userMapper.updateById(user);             // 更新数据库
    CompletableFuture.runAsync(() -> {
        Thread.sleep(500);                   // 延迟(大于主从同步时间)
        redisTemplate.delete(cacheKey);      // 第二次删除
    });
}

三、缓存三大经典问题

3.1 缓存穿透

问题: 查询根本不存在的数据,请求每次都穿透到数据库。

解决方案:

方案 优点 缺点 适用场景
缓存空对象 实现简单 占用内存、需清理 查询Key固定
布隆过滤器 内存小、效率高 有误判率、需预热 数据量大

布隆过滤器实现:

@PostConstruct
public void init() {
    RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("user:bloom");
    bloomFilter.tryInit(1000000, 0.0001);  // 100万元素,0.01%误判率
    userMapper.selectAllIds().forEach(bloomFilter::add);
}

public User getUserById(Long id) {
    if (!bloomFilter.contains(id)) return null;  // 确定不存在
    // ... 查询缓存和数据库
}

3.2 缓存击穿

问题: 热点Key过期瞬间,大量并发请求穿透到数据库。

解决方案:

方案 优点 缺点 适用场景
互斥锁 数据强一致 并发等待 一致性要求高
逻辑过期 无等待、高可用 短暂不一致 可用性优先

互斥锁实现(Redisson):

public User getUserWithLock(Long id) {
    String key = "user:" + id;
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) return user;
    
    RLock lock = redissonClient.getLock("lock:user:" + id);
    try {
        if (lock.tryLock(10, TimeUnit.SECONDS)) {
            user = (User) redisTemplate.opsForValue().get(key);  // 双重检查
            if (user != null) return user;
            user = userMapper.selectById(id);
            if (user != null) {
                redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
            }
            return user;
        }
        Thread.sleep(50);  // 获取锁失败,短暂等待后重试
        return getUserWithLock(id);
    } finally {
        if (lock.isHeldByCurrentThread()) lock.unlock();
    }
}

逻辑过期实现:

// 缓存数据结构(包含逻辑过期时间)
@Data
public class CacheData<T> {
    private T data;
    private LocalDateTime expireTime;
}

public User getUserWithLogicalExpire(Long id) {
    String key = "user:" + id;
    CacheData<User> cacheData = (CacheData<User>) redisTemplate.opsForValue().get(key);
    
    if (cacheData == null) {
        // 缓存不存在,需要重建(加锁)
        return rebuildCacheWithLock(id);
    }
    
    if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
        return cacheData.getData();  // 未过期,直接返回
    }
    
    // 已过期,异步重建,返回旧数据
    CompletableFuture.runAsync(() -> rebuildCacheWithLock(id));
    return cacheData.getData();
}

3.3 缓存雪崩

问题: 大量Key同时过期或缓存服务宕机,请求全部打到数据库。

解决方案:

方案 说明
过期时间加随机值 避免同时过期
缓存高可用架构 主从+哨兵、多级缓存
限流降级 保护数据库
多级缓存 本地缓存(L1) + 分布式缓存(L2)

多级缓存架构:

请求 → Caffeine(L1) → Redis(L2) → Database
       (毫秒级)      (亚毫秒级)   (瓶颈点)

四、Spring Cache技术介绍

4.1 Spring Cache是什么

Spring Cache是Spring框架提供的缓存抽象层,通过注解将缓存逻辑从业务代码分离。

┌─────────────────────────────────────────────────┐
│              Spring Cache 架构                   │
├─────────────────────────────────────────────────┤
│  应用层:@Cacheable/@CachePut/@CacheEvict        │
│                    ↓                             │
│  抽象层:CacheManager/Cache/CacheResolver        │
│                    ↓                             │
│  实现层:Redis/Caffeine/Ehcache                  │
└─────────────────────────────────────────────────┘

核心特性: 声明式缓存、抽象化、多种缓存支持、SpEL支持、条件缓存

4.2 Spring Cache的优势

维度 传统方式 Spring Cache
代码量 每方法手动处理 一行注解
可读性 代码混杂 业务逻辑清晰
维护性 改多处代码 修改注解即可
灵活性 大量改动 更换配置

4.3 Cache Aside模式的注解实现

重要说明: Spring Cache注解底层采用的是Cache Aside模式,这与第二章介绍的传统手动实现方式逻辑完全一致,但代码更加简洁优雅。

Cache Aside模式对比:

操作 手动实现 Spring Cache注解
读取 先查缓存→未命中查DB→写入缓存 @Cacheable
更新 更新DB→删除缓存 @CacheEvict@CachePut
删除 删除DB→删除缓存 @CacheEvict

@Cacheable的Cache Aside流程:

┌──────────────────────────────────────────────────────────┐
│                    @Cacheable 执行流程                    │
├──────────────────────────────────────────────────────────┤
│  1. 方法调用 → 检查缓存                                   │
│       ↓                                                  │
│  2. 缓存命中? ──是──→ 直接返回缓存数据                    │
│       │                                                  │
│       否                                                 │
│       ↓                                                  │
│  3. 执行方法体(查询数据库)                               │
│       ↓                                                  │
│  4. 将结果写入缓存                                        │
│       ↓                                                  │
│  5. 返回结果                                              │
└──────────────────────────────────────────────────────────┘

代码对比及效果等价示例:

// ========== 手动实现(Cache Aside模式) ==========
public User getUserById(Long id) {
    String key = "users::" + id;
    User user = (User) redisTemplate.opsForValue().get(key);  // 1. 查缓存
    if (user != null) {
        return user;                                          // 2. 命中返回
    }
    user = userMapper.selectById(id);                        // 3. 查数据库
    if (user != null) {
        redisTemplate.opsForValue().set(key, user);          // 4. 写入缓存
    }
    return user;
}

// ========== Spring Cache注解(效果完全相同) ==========
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
    return userMapper.selectById(id);  // 注解自动处理缓存逻辑
}

核心结论: Spring Cache注解本质上是对Cache Aside模式的封装,开发者只需关注业务逻辑,缓存策略由框架自动处理,既保证了缓存模式的正确性,又大大简化了代码。


五、Spring Cache核心注解

5.1 @Cacheable

作用: 查询缓存,有则返回,无则执行方法并缓存结果。

属性 说明 示例
value/cacheNames 缓存区域名称 "users"
key 缓存Key(SpEL) "#id"
condition 条件判断(SpEL) "#id > 0"
unless 排除条件(SpEL) "#result == null"
sync 同步模式(防击穿) true
// 基础用法
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) { return userMapper.selectById(id); }

// 条件缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 0", unless = "#result == null")
public User getUserById(Long id) { return userMapper.selectById(id); }

// 防击穿(sync=true)
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) { return userMapper.selectById(id); }

5.2 @CachePut

作用: 执行方法并将结果放入缓存(不检查缓存是否存在)。

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    userMapper.updateById(user);
    return user;
}

5.3 @CacheEvict

作用: 删除缓存。

属性 说明 默认值
allEntries 删除整个缓存区域 false
beforeInvocation 方法执行前删除 false
// 删除单个缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) { userMapper.deleteById(id); }

// 删除整个缓存区域
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() { }

// 方法执行前删除(防止异常导致缓存未清除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) { userMapper.deleteById(id); }

5.4 @Caching

作用: 组合多个缓存操作。

@Caching(evict = {
    @CacheEvict(value = "users", key = "#user.id"),
    @CacheEvict(value = "userList", allEntries = true)
})
public void updateUser(User user) {
    userMapper.updateById(user);
}

5.5 @CacheConfig

作用: 类级别缓存配置,避免重复配置。

@Service
@CacheConfig(cacheNames = "users")
public class UserServiceImpl {
    
    @Cacheable(key = "#id")  // 继承类级别cacheNames
    public User getUserById(Long id) { return userMapper.selectById(id); }
}

六、缓存管理器与解析器

6.1 CacheManager

常用实现:RedisCacheManagerCaffeineCacheManagerEhcacheCacheManager

Redis缓存管理器配置:

@Configuration
@EnableCaching
public class RedisCacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
            .entryTtl(Duration.ofMinutes(30))
            .computePrefixWith(cacheName -> "app:" + cacheName + ":")
            .disableCachingNullValues();
        
        // 不同缓存区域个性化配置
        Map<String, RedisCacheConfiguration> configMap = Map.of(
            "users", defaultConfig.entryTtl(Duration.ofHours(1)),
            "dictionaries", defaultConfig.entryTtl(Duration.ofHours(24)),
            "captcha", defaultConfig.entryTtl(Duration.ofMinutes(5))
        );
        
        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(configMap)
            .transactionAware()
            .build();
    }
}

6.2 自定义KeyGenerator

@Component
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return target.getClass().getSimpleName() + ":" 
            + method.getName() + ":" 
            + Arrays.toString(params);
    }
}

七、Spring Expression Language (SpEL)

7.1 SpEL简介

SpEL是什么

Spring Expression Language(SpEL)是Spring框架提供的一种强大的表达式语言,用于在运行时查询和操作对象图。它类似于EL表达式(JSP中使用),但功能更强大,支持方法调用、属性访问、运算符、集合操作等。

语法特点

  • 使用 #{...} 作为表达式定界符
  • 支持字符串、数字、布尔值等基本类型
  • 支持对象属性访问、方法调用
  • 支持算术、关系、逻辑运算
  • 支持正则匹配、集合投影、选择等高级操作

应用领域

领域 应用场景 示例
Spring Cache 缓存Key生成、条件判断 @Cacheable(key = "#user.id")
Spring Security 权限表达式 @PreAuthorize("hasRole('ADMIN')")
Spring Config 配置属性引用 @Value("#{systemProperties['app.name']}")
Spring AOP 切点表达式条件 @Pointcut("execution(* com.*.*(..))")
Thymeleaf 模板变量渲染 <p th:text="${user.name}">
Spring Data 查询条件动态绑定 @Query("select u from User u where u.age > :#{#age}")

7.2 SpEL上下文对象详解

root对象说明

在SpEL中,#root是一个特殊的内置对象,代表当前表达式的根上下文对象。Spring Cache在解析SpEL表达式时,会创建一个CacheExpressionRootObject作为root对象,包含方法、参数、目标对象等信息。

┌─────────────────────────────────────────────────────┐
│              #root 对象结构                          │
├─────────────────────────────────────────────────────┤
│  #root.target      → 被调用的目标对象(Service实例)  │
│  #root.targetClass → 目标对象的Class                 │
│  #root.method      → 当前调用的Method对象            │
│  #root.methodName  → 方法名称                        │
│  #root.args        → 方法参数数组                    │
│  #root.caches      → 缓存集合                        │
│  #root.result      → 方法返回值(@CachePut可用)      │
└─────────────────────────────────────────────────────┘

上下文对象一览

对象 说明 示例
#root.method 当前调用的Method对象 #root.method.name → "getUserById"
#root.methodName 方法名称字符串 #root.methodName → "getUserById"
#root.target 目标对象(被调用的Service) #root.target.class → UserServiceImpl
#root.targetClass 目标对象的Class #root.targetClass.simpleName
#root.args 方法参数数组 #root.args[0] → 第一个参数
#root.caches 当前操作涉及的缓存集合 #root.caches[0].name
#root.result 方法返回值(仅@CachePut/@Cacheable的unless可用) #root.result.id
#参数名 直接引用方法参数 #id#user.name
#p0#a0 按位置索引引用参数 #p0 → 第一个参数
#meta 元数据信息 #meta.methodName

使用示例

// 使用方法名作为Key的一部分
@Cacheable(key = "#root.methodName + ':' + #id")
public User getUserById(Long id) { ... }  // Key: "getUserById:123"

// 使用目标类名和方法名组合
@Cacheable(key = "#root.targetClass.simpleName + ':' + #root.methodName + ':' + #id")
public User getUserById(Long id) { ... }  // Key: "UserServiceImpl:getUserById:123"

// 动态获取参数(适用于参数名不确定场景)
@Cacheable(key = "'user:' + #root.args[0]")
public User getUserById(Long id) { ... }

// 条件判断:根据方法名决定是否缓存
@Cacheable(condition = "#root.methodName.startsWith('get')")
public User getUserById(Long id) { ... }

7.3 常用表达式示例

// 基本参数
@Cacheable(key = "#id")

// 对象属性
@Cacheable(key = "#user.id")

// 字符串拼接
@Cacheable(key = "'user:' + #userId + ':order:' + #orderId")

// 条件运算
@Cacheable(key = "#id != null ? #id : 'default'")

// 条件判断
@Cacheable(condition = "#id > 0 && #status == 'ACTIVE'")

// 正则匹配
@Cacheable(condition = "#email matches '^[A-Za-z0-9+_.-]+@(.+)$'")

// 排除null
@Cacheable(unless = "#result == null")

7.4 SpEL运算符速查

类型 运算符
算术 +, -, *, /, %, ^
关系 ==, !=, <, >, <=, >=
逻辑 &&, ||, !
条件 ?: (三元)、?: (Elvis)
正则 matches
类型 T()

八、命名规范与最佳实践

8.1 缓存Key命名规范

格式: [应用名]:[业务模块]:[业务含义]:[唯一标识]

示例:
order:trade:order:123456           订单ID
user:auth:token:abc123             用户token
system:dict:province:all           省份字典

8.2 过期时间策略

数据变化频率 过期时间
极少变化(字典、配置) 24小时或更长
偶尔变化(用户信息) 1-6小时
经常变化(商品信息) 5-30分钟
高频变化(库存、价格) 不缓存或秒级
实时数据(验证码) 分钟级

8.3 最佳实践

实践 说明
防穿透 缓存空对象或布隆过滤器
防击穿 sync=true 或互斥锁
防雪崩 过期时间加随机值、多级缓存
Key规范 使用常量类统一管理
条件缓存 condition/unless 灵活控制

九、项目实战示例

9.1 Maven依赖

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

9.2 字典服务缓存

@Service
@CacheConfig(cacheNames = "dictionaries")
public class DictionaryServiceImpl {
    
    @Autowired
    private DictionaryMapper dictionaryMapper;
    
    // 查询字典树(缓存)
    @Cacheable(key = "'all'", sync = true)
    public List<Dictionary> getDictionaryTree() {
        return buildTree(dictionaryMapper.selectAll());
    }
    
    // 新增字典(清除缓存)
    @CacheEvict(allEntries = true)
    public void addDictionary(Dictionary dict) {
        dictionaryMapper.insert(dict);
    }
    
    // 更新字典(清除缓存)
    @CacheEvict(allEntries = true)
    public void updateDictionary(Dictionary dict) {
        dictionaryMapper.updateById(dict);
    }
}

最佳实践:使用@CacheConfig在类级别统一配置cacheNames,方法上只需配置key等差异化属性,代码更简洁。

9.3 用户服务缓存

@Service
@CacheConfig(cacheNames = "users")
public class UserServiceImpl {
    
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Cacheable(key = "#id", unless = "#result == null")
    public User getById(Long id) { return userMapper.selectById(id); }
    
    @CachePut(key = "#user.id")
    public User update(User user) {
        userMapper.updateById(user);
        return userMapper.selectById(user.getId());
    }
    
    // 删除用户:@CacheEvict清除主缓存(users:id),辅助缓存(users:username:xxx)需手动清除
    @CacheEvict(key = "#id")
    public void delete(Long id) {
        User user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.delete("users:username:" + user.getUsername());  // 手动清除按username索引的辅助缓存
            userMapper.deleteById(id);
        }
    }
}

十、分布式环境高级问题

10.1 缓存一致性保障

问题: 缓存与数据库数据不一致

解决方案:

方案 说明
Cache-Aside + 重试 更新数据库后删除缓存,失败时重试或发MQ
延迟双删 解决并发读写导致脏数据

10.2 序列化问题

问题: 类型信息丢失、日期时间格式、泛型集合反序列化

解决方案: 使用Jackson配置类型信息保留

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
    .allowIfBaseType(Object.class).build(), ObjectMapper.DefaultTyping.NON_FINAL);

10.3 分布式缓存同步

问题: 多实例本地缓存无法及时同步

解决方案: Redis Pub/Sub通知各实例清除本地缓存

实现示例:

// 发布缓存清除消息
@Service
public class CacheSyncPublisher {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String CACHE_INVALIDATE_CHANNEL = "cache:invalidate";
    
    public void publishInvalidation(String cacheName, String key) {
        Map<String, String> message = Map.of("cacheName", cacheName, "key", key);
        redisTemplate.convertAndSend(CACHE_INVALIDATE_CHANNEL, JSON.toJSONString(message));
    }
}

// 监听缓存清除消息
@Component
public class CacheSyncListener {
    @Autowired
    private CacheManager cacheManager;
    
    @RedisListener(channel = "cache:invalidate")
    public void onMessage(String message) {
        Map<String, String> data = JSON.parseObject(message, Map.class);
        Cache cache = cacheManager.getCache(data.get("cacheName"));
        if (cache != null) {
            cache.evict(data.get("key"));  // 清除本地缓存
        }
    }
}

十一、总结

核心要点回顾

类别 内容
注解 @Cacheable(查询)、@CachePut(更新)、@CacheEvict(删除)、@Caching(组合)、@CacheConfig(类配置)
属性 value/key/condition/unless/sync/allEntries/beforeInvocation
防范 穿透(空对象/布隆)、击穿(sync/锁)、雪崩(随机TTL/多级缓存)

适用场景建议

场景 推荐
读多写少 @Cacheable
写后读 @CachePut
写后删 @CacheEvict(推荐)
热点数据 sync=true
高可用 多级缓存
分布式环境 Pub/Sub同步

posted @ 2026-05-11 15:33  flycloudy  阅读(29)  评论(0)    收藏  举报