SpringCache整合SpringBoot使用

Spring Boot 整合 Spring Cache 指南

目录

一、整合步骤

1. 添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 缓存实现(可选) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.启用缓存

java 复制代码
@SpringBootApplication
@EnableCaching  // 核心启用注解
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3.配置缓存实现(application.yml)

yml 复制代码
spring:
  cache:
    type: redis  # 可选值:redis, ehcache, caffeine, simple
  redis:         # Redis 特有配置
    host: localhost
    port: 6379
    database: 0 #redis 默认有16个DB,选择存储的DB


自定义的cacheManager会比默认的使配置文件的优先级高

4. 自定义缓存管理器(可选)

java 复制代码
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair
                    //设置json序列化器
                    .fromSerializer(new GenericJackson2JsonRedisSerializer()) 
                )
                //设置缓存存入的过期时间
                .entryTtl(Duration.ofMinutes(30));  // 全局过期时间
        
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

二、核心注解详解

1、@Cacheable - 查询缓存

java 复制代码
@Service
public class ProductService {
    
    @Cacheable(
        value = "products", 
        key = "#id", 
        unless = "#result == null"  // 结果为空时不缓存
    )
    public Product getProductById(Long id) {
        // 数据库查询逻辑
        return productRepository.findById(id).orElse(null);
    }
}

参数说明:

  • value/cacheNames:缓存分区名称(必填

  • key:缓存键(支持SpEL表达式)

  • condition:满足条件才缓存(如 #id > 10

  • unless:结果满足条件时不缓存

2、@CachePut - 更新缓存

java 复制代码
@CachePut(
    value = "products", 
    key = "#product.id"  // 必须与查询key一致
)
public Product updateProduct(Product product) {
    // 更新数据库
    return productRepository.save(product);
}

特点:

  • 始终执行方法体
  • 用返回值更新缓存

3、@CacheEvict - 删除缓存

java 复制代码
// 删除单个缓存
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
    productRepository.deleteById(id);
}

// 清空整个分区缓存
@CacheEvict(value = "products", allEntries = true)
public void reloadProducts() {
    // 重载数据逻辑
}

// 方法执行前删除缓存
@CacheEvict(value = "orders", key = "#orderId", beforeInvocation = true)
public void cancelOrder(String orderId) {
    // 取消订单逻辑
}

4、@Caching - 组合操作

java 复制代码
@Caching(
    put = @CachePut(value = "product", key = "#result.id"),
    evict = @CacheEvict(value = "productList", allEntries = true)
)
public Product updateProduct(Product product) {
    // 更新产品并清除列表缓存
    return productRepository.save(product);
}

5、@CacheConfig - 类级配置

java 复制代码
@Service
@CacheConfig(cacheNames = "products") // 类中所有方法默认使用该缓存
public class ProductService {
    @Cacheable(key = "#id") // 自动应用 products 分区
    public Product getById(Long id) { 
        // ...
    }
    
    @CacheEvict(key = "#id")
    public void delete(Long id) {
        // ...
    }
}

三、关键配置技巧

视频讲解

1、自定义Key生成器

java 复制代码
@Configuration
public class CacheConfig {
    
    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> 
            method.getName() + "_" + Arrays.toString(params);
    }
}

// 使用示例
@Service
public class UserService {
    @Cacheable(value="users", keyGenerator="customKeyGenerator")
    public User getUserById(Long id) {
        // ...
    }
}

2、SpEL表达式高级用法

java 复制代码
// 组合键
@Cacheable(key = "#user.id + '_' + #user.type")

// 条件过滤
@Cacheable(
    value = "products", 
    condition = "#id > 100", 
    unless = "#result.price < 50"
)

// 使用方法名作为键的一部分
@Cacheable(key = "'hot_' + #root.methodName + '_' + #category")

3、TTL随机化防止雪崩

java 复制代码
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
    // 基础配置
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30));  
    
    // 自定义配置:为不同缓存设置不同TTL
    Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
    cacheConfigurations.put("products", 
        config.entryTtl(Duration.ofMinutes(20 + new Random().nextInt(20)))); // 20-40分钟随机
    
    return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigurations)
            .build();
}

四、常见问题解决方案

缓存击穿也可以用 @Cacheable的一个属性sync = true来解决(但是这个只是加的本地锁,但也肯定比不加锁好很多,最多也就放你服务器数量的请求呗)

五、实践

修改商城三级分类代码

因为updateCascade是不知道更改了哪一级分类,所以都要刷新(缓存了一级分类getLevel1Categorys和整个三级分类的getCatalogJson),其实包括deleteCasecade也要用这个注解

复制代码
 @CacheEvict(value = "category",allEntries = true) 
java 复制代码
/**
     * 级联更新所有关联的数据
     *
     * @CacheEvict:失效模式
     * @CachePut:双写模式,需要有返回值
     * 1、同时进行多种缓存操作:@Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
     * 3、存储同一类型的数据,都可以指定为同一分区
     * @param category
     */
    // @Caching(evict = {
    //         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    //         @CacheEvict(value = "category",key = "'getCatalogJson'")
    // })
    @CacheEvict(value = "category",allEntries = true)       //删除某个分区下的所有数据,
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
        //创建写锁
        RLock rLock = readWriteLock.writeLock();

        try {
            rLock.lock();
            this.baseMapper.updateById(category);
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }

        //同时修改缓存中的数据
        //删除缓存,等待下一次主动查询进行更新
    }

查询分类数据,这里写在getLevel1Categorys方法上面,这个方法其实是获取所有分类数据getCatalogJson的调用的一个方法,其实最后获取的数据还是getCatalogJson的数据,所以建议写在对应方法而不是这个方法,这里是为了简洁一点(那个方法体多)

java 复制代码
    /**
     * 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
     * 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
     * 默认行为
     *      如果缓存中有,方法不再调用
     *      key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
     *      缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
     *      默认过期时间是 -1:即不过期
     *
     *   自定义操作:key的生成
     *      指定生成缓存的key:key属性指定,接收一个Spel("'字符串'"/"#Spel表达式")
     *      指定缓存的数据的存活时间:配置文档中修改存活时间,对应spring.cache.redis.time-to-live单位是毫秒
     *      将数据保存为json格式(使用json序列化)
     *
     *
     * 4、Spring-Cache的不足之处:
     *  1)、读模式
     *      缓存穿透:查询一个null数据。解决方案:缓存空数据
     *      缓存击穿:大量并发进来同时查询一个正好过期的数据。
     * 		解决方案:加锁 ? 默认是无加锁的;使用sync = true(加的是本地锁)来解决击穿问题
     *      缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
     *  2)、写模式:(缓存与数据库一致)
     *      1)、读写加锁。
     *      2)、引入Canal,感知到MySQL的更新去更新Redis
     *      3)、读多写多,直接去数据库查询就行
     *
     *  总结:
     *      常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
     *      特殊数据:特殊设计
     *
     *  原理:
     *      CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
     * @return
     */
    @Cacheable(value = "category",key = "#root.method.name")
    //代表当前方法需要缓存,如果有直接查缓存,没有执行原方法返回且放入缓存
    // 第一个参数是缓存名字(缓存分区),第二个参数是key
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("缓存未命中,查询数据库");
        return this.list(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid,0));
    }
    //获取整个三级分类
    @Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)){

        Map<String, List<Catelog2Vo>> catalogJsonDB = getCatalogJsonFromDbWithRedissonLock();
        catalogJson = JSON.toJSONString(catalogJsonDB);
        stringRedisTemplate.opsForValue().set("catalogJson", catalogJson);

        return catalogJsonDB;
        }

        return JSON.parseObject(catalogJson, Map.class);
    }
相关推荐
葫芦和十三2 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp3 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑3 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯4 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan6 小时前
多Agent之间的区别
后端
青石路8 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充8 小时前
1.面向对象设计思想
后端
IT_陈寒9 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro9 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗9 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端