SpringBoot 缓存 之@Cacheable 详细介绍

一 前言

  1. 我知道在接口api项目中,频繁的调用接口获取数据,查询数据库是非常耗费资源的,于是就有了缓存技术,可以把一些不常更新,或者经常使用的数据,缓存起来,然后下次再请求时候,就直接从缓存中获取,不需要再去查询数据,这样可以提供程序性能,增加用户体验,也节省服务资源浪费开销
  2. Spring 从 3.1 开始就引入了对 Cache 的支持。定义了 org.springframework.cache.Cache 和
    org.springframework.cache.CacheManager 接口来统一不同的缓存技术。并支持使用 JCache(JSR-107)注解简化我们的开发。 其使用方法和原理都类似于 Spring 对事务管理的支持。Spring Cache
    是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。

二、 Cache 和 CacheManager 接口说明

Cache 接口包含缓存的各种操作集合,你操作缓存就是通过这个接口来操作的。

Cache 接口下 Spring 提供了各种 xxxCache 的实现,比如:RedisCache、EhCache、ConcurrentMapCache

CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache。这些 Cache 存在于 CacheManager 的上下文中。
小结:
每次调用需要缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

三、@Cacheable 注解使用详细介绍

1.缓存使用步骤

1.1 引入MAVEN 依赖

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

1.2 开启基于注解的缓存

使用 @EnableCaching 标注在 springboot 主启动类上
在这里插入图片描述
或者在配置类上加,推荐在配置类上添加
在这里插入图片描述

1.3 标注缓存注解

在这里插入图片描述
注:这里使用 @Cacheable 注解就可以将运行结果缓存,以后查询相同的数据,直接从缓存中取,不需要调用方法。

四.@Cacheable 这个注解常用的几个属性

  1. cacheNames/value :指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定 多个缓存;
  2. key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写如 #i
    d;参数id的值 #a0 #p0 #root.args[0])
  3. keyGenerator :key的生成器;可以自己指定key的生成器的组件id 然后key 和 keyGenerator 二选一使用
  4. cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。或者cacheResolver指定获取解析器
  5. condition :可以用来指定符合条件的情况下才缓存
condition = "#id>0"
condition = "#a0>1":第一个参数的值》1的时候才进行缓存
  1. unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过
    #result 获取方法结果)
unless = "#result == null"
unless = "#a0==2":如果第一个参数的值是2,结果不缓存;
  1. sync :是否使用异步模式。异步模式的情况下unless不支持 默认是方法执行完,以同步的方式将方法返回的结果存在缓存中

1.cacheNames/value属性

用来指定缓存组件的名字,将方法的返回结果放在哪个缓存中,可以是数组的方式,支持指定多个缓存

 /**
     * 获取用户菜单信息
     *
     * @param username 用户名
     * @return
     */
    @Cacheable(cacheNames = "menuCache") 或者// @Cacheable(cacheNames = {"menuCache","neCacge"})
    List<Menu> getUserMenus(String username);

如果只有一个属性,cacheNames可忽略,直接是value属性默认

2.key

缓存数据时使用的 key。默认使用的是方法参数的值。可以使用 spEL 表达式去编写。

Cache SpEL available metadata

在这里插入图片描述

//key = "#username" 就是参数username
 @Cacheable(key = "#username" ,cacheNames = "menuCache")
List<Menu> getUserMenus(String username);

3.keyGenerator

key 的生成器,可以自己指定 key 的生成器,通过这个生成器来生成 key

定义一个@Bean类,将KeyGenerator添加到Spring容器

// 自定义key生成器
    @Bean("dechnicKeyGenerator")
    public KeyGenerator keyGenerator(){
        return (o, method, params) ->{
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName()); // 类目
            sb.append(method.getName()); // 方法名
            for(Object param: params){
                sb.append(param !=null?param.toString():null); // 参数名
            }
            return sb.toString();
        };
    }

在使用指定自己的@Cacheable(cacheNames = “menuCache”,keyGenerator =“dechnicKeyGenerator” )

注意:这样放入缓存中的 key 的生成规则就按照你自定义的 keyGenerator 来生成。不过需要注意的是:@Cacheable 的属性,key 和 keyGenerator 使用的时候,一般二选一。

4.condition

符合条件的情况下才缓存。方法返回的数据要不要缓存,可以做一个动态判断。

 /**
     * 获取用户菜单信息
     *
     * @param username 用户名
     * @return
     */
    //判断username 用户名是kenx开头才会被缓存
    @Cacheable(key = "#username" ,condition = "#username.startsWith('kenx')")
    List<Menu> getUserMenus(String username);

5.unless

否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。


    //判断username 用户名是kenx开头不会被缓存
    @Cacheable(key = "#username" ,condition = "#username.startsWith('kenx')")
    List<Menu> getUserMenus(String username);

spEL 编写 key
在这里插入图片描述
当然我们可以全局去配置,cacheNames,keyGenerator属性通过@CacheConfig注解可以用于抽取缓存的公共配置,然后在类加上就可以,eg:如

//全局配置,下面用到缓存方法,不配置默认使用全局的
@CacheConfig(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )
public interface IMenuService extends IService<Menu> {
    /**
     * 获取用户菜单信息
     *
     * @param username 用户名
     * @return
     */
    @Cacheabl
    List<Menu> getUserMenus(String username);
}


五、深入使用

1. @CachePut

@CachePut注解也是一个用来缓存的注解,不过缓存和@Cacheable有明显的区别是即调用方法,又更新缓存数据,也就是执行方法操作之后再来同步更新缓存,所以这个主键常用于更新操作,也可以用于查询,主键属性和@Cacheable有很多类似的参看 @CachePut源码

/**
     *  @CachePut:既调用方法,又更新缓存数据;同步更新缓存
     *  修改了数据,同时更新缓存
     */
    @CachePut(value = {"emp"}, key = "#result.id")
    public Employee updateEmp(Employee employee){
        employeeMapper.updateEmp(employee);
        LOG.info("更新{}号员工数据",employee.getId());
        return employee;
    }

2.@CacheEvict

清空缓存
主要属性:

  1. key:指定要清除的数据
  2. allEntries = true:指定清除这个缓存中所有的数据
  3. beforeInvocation = false:默认代表缓存清除操作是在方法执行之后执行
  4. beforeInvocation = true:代表清除缓存操作是在方法运行之前执行
@CacheEvict(value = {"emp"}, beforeInvocation = true,key="#id")
    public void deleteEmp(Integer id){
        employeeMapper.deleteEmpById(id);
        //int i = 10/0;
    }

3.@Caching

@Caching 用于定义复杂的缓存规则,可以集成@Cacheable和 @CachePut

// @Caching 定义复杂的缓存规则
    @Caching(
            cacheable = {
                    @Cacheable(/*value={"emp"},*/key = "#lastName")
            },
            put = {
                    @CachePut(/*value={"emp"},*/key = "#result.id"),
                    @CachePut(/*value={"emp"},*/key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String lastName){
        return employeeMapper.getEmpByLastName(lastName);
    }


4.@CacheConfig

@CacheConfig注解可以用于抽取缓存的公共配置,然后在类加上就可以

//全局配置,下面用到缓存方法,不配置默认使用全局的
@CacheConfig(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )
public interface IMenuService extends IService<Menu> {
    /**
     * 获取用户菜单信息
     *
     * @param username 用户名
     * @return
     */
    @Cacheable(key = "#username" )
    List<Menu> getUserMenus(String username);
}

六、实战案例

1.需求:

采用@CacheConfig + @Cacheable根据当前登陆用户id 分别缓存

2.实现

1 CacheManagerConfig 配置类配置
package com.dechnic.omsdc.server.admin.config;

import com.dechnic.omsdc.server.admin.entity.TSysUser;
import com.dechnic.omsdc.server.common.utils.SpringContextUtil;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.io.ResourceUtils;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

import java.io.IOException;
import java.time.Duration;

/**
 * @description:
 * @author:houqd
 * @time: 2020/2/12 10:19
 */
@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class CacheManagerConfig extends CachingConfigurerSupport {
    private final CacheProperties cacheProperties;
    CacheManagerConfig(CacheProperties cacheProperties) {
        this.cacheProperties = cacheProperties;
    }
    /**h
     * cacheManager名字
     */
    public interface CacheManagerNames {
        /**
         * redis
         */
        String REDIS_CACHE_MANAGER = "redisCacheManager";

        /**
         * ehCache
         */
        String EHCACHE_CACHE_MAANGER = "ehCacheCacheManager";
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();

        RedisSerializer<String> redisSerializer = new StringRedisSerializer();

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(new JdkSerializationRedisSerializer());
        //value hashmap序列化
        template.setHashValueSerializer(new JdkSerializationRedisSerializer());

        return template;
    }


    /**
     * 创建ehCacheCacheManager
     */
    @Bean
    public EhCacheCacheManager ehCacheCacheManager() {
        return new EhCacheCacheManager(sfCachManager());
    }
    @Bean
    public net.sf.ehcache.CacheManager sfCachManager(){
        net.sf.ehcache.CacheManager cacheManager = null;
        try {
            cacheManager = net.sf.ehcache.CacheManager.create(ResourceUtils.getInputStreamForPath("classpath:config/echache.xml"));
        } catch (IOException e) {
            if (SpringContextUtil.isDev()){
                e.printStackTrace();
            }else {
                log.info(e.getMessage());
            }
        }
        return cacheManager;
    }


    /**
     * json序列化
     * @return
     */
    @Bean
    public RedisSerializer<Object> jackson2JsonRedisSerializer() {
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        return serializer;
    }

    /**
     * 配置缓存管理器
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存的默认过期时间,也是使用Duration设置
        config = config.entryTtl(Duration.ofSeconds(500))//Duration.ofMinutes(Config.RedisCacheMinite))
                // 设置 key为string序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value为json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
                // 不缓存空值
                .disableCachingNullValues();
        // 使用自定义的缓存配置初始化一个cacheManager
        return RedisCacheManager
                .builder(redisConnectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .build();

    }
    // 自定义key生成器
    @Bean("dechnicKeyGenerator")
    public KeyGenerator keyGenerator(){
        return (o, method, params) ->{
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName()); // 类目
            sb.append(method.getName()); // 方法名
            for(Object param: params){
                sb.append(param !=null?param.toString():null); // 参数名
            }
            return sb.toString();
        };
    }

}

2. 接口service 上统一配置


@CacheConfig(cacheNames = "omsdc:IEnergyAnalysisService",keyGenerator = "dechnicKeyGenerator")
public interface IEnergyAnalysisService{

  @Cacheable
  public Map<String, Object> getFrontPageAllDesc(Map<String, Object> map);

}

3. Controller 里设置
 @RequestMapping("/getFrontAllDesc")
    public JsonResult getFrontAllDesc(@RequestBody JSONObject jsonObject) {
        HashMap map = DechnictUtils.JSONObjectToHashMap(jsonObject);
        setCacheUserID(map);
        Map retMap = this.energyAnalysisService.getFrontPageAllDesc(map);
        Map<String, Object> paramMap = new HashMap<>();
        int carCount = nyCarMapper.getCardCount(paramMap);
        retMap.put("carCount", carCount);
        return toJson(retMap);
    }

BaseController 里添加

    /**
     * 向 redis @Cacheabel 缓存中加入userID 进行缓存
     * @param map
     */
    public void setCacheUserID(Map<String, Object> map) {
        TSysUser tSysUser = (TSysUser) SecurityUtils.getSubject().getPrincipal();
        map.put("userId",tSysUser.getId());
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
@Cacheable注解是Spring框架提供的缓存注解,用于标记方法的返回结果可被缓存。它可以应用在方法级别或类级别。当方法被调用时,Spring会首先从缓存中查找方法的返回结果,如果缓存中存在,则直接返回缓存值,不再执行方法体内的逻辑。如果缓存中不存在,则执行方法体内的逻辑,并将返回结果存入缓存中。 @Cacheable注解默认是使用方法的参数作为缓存的key,所以相同参数调用的方法返回结果会被缓存起来。但是默认情况下,如果在缓存中找不到对应的结果,Spring会执行方法体内的逻辑,并将返回结果存入缓存中。这样会导致并发调用时出现缓存穿透问题,即多个线程同时请求同一个参数值,导致每个线程都执行了方法体内的逻辑,没有从缓存中获取到结果。 为了解决缓存穿透问题,可以使用热加载机制。热加载是指在缓存失效期间,只有一个线程去执行方法体内的逻辑,其他线程等待该线程执行完毕后直接从缓存中获取结果。 实现热加载可以通过在@Cacheable注解中设置sync属性为true。这样在缓存失效期间,只有一个线程去执行方法体内的逻辑,其他线程等待该线程执行完毕后直接从缓存中获取结果。示例代码如下: ```java @Cacheable(value = "myCache", key = "#param", sync = true) public String getData(String param) { // 执行业务逻辑 } ``` 需要注意的是,设置sync属性为true会导致性能损失,因为其他线程在等待期间无法直接从缓存中获取结果。因此,只有在必要的情况下才应该使用热加载机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值