springboot中Cache缓存的使用 | 8月更文挑战

1,213 阅读9分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

一、Cache缓存的作用

随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一,Spring3开始提供了强大的基于注解的缓存支持,可以通过注解方式低侵入的给原有的Sping应用增加缓存功能,提高数据访问性能,Springboot为缓存提供了一系列的自动化的配置,使我们可以非常方便的使用缓存。

1,主要的核心类

Java Caching定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry.

CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager.一个应用可以在运行期访问多个CachingProvider.

CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CacheProvider所拥有。

Cache是一个类似Map的数据结构并临时存储以Key为索引的值,一个Cache仅被一个CacheManager所拥有。

Entry是一个存储在Cache中的key-value对。

Expiry每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个时间,条目为过期的状态,一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

image.png

2,Spring缓存对象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解来简化我们的开发。

Cache接口为缓存的组件规范定义,包含缓存的各种的各种操作集合。

Cache接口下Spring提供了各种xxxCache的实现,如RedisCache,EhCacheCache,ConcurentMapCache.

每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:

1,确定方法需要被缓存以及他们的缓存策略

2,从缓存中读取之前缓存存储的数据

二、几个重要概念&缓存注解

概念&注解作用
Cache缓存接口,定义缓存操作,实现有RedisCache,EhCacheCache,ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(Cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略

Cache SpEL available metadata

名字位置描述实例
methodNameroot object当前被调用的方法名#root,methodName
methodroot object当前被调用的方法root method name
targetroot object当前被调用的目标对象root target
targetClassroot object当前被调用的目标对象类root targetClass
argsroot object当前被调用的方法的参数列表root args[0]
cachesroot object当前方法调用使用的缓存列表(如@Cacheable(value={"cache1","cache2"})),则有两个cachoroot caches[0]name
argument nameevaluation context方法参数的名字,可以直接#参数名,也可以使用#p0或#a0的形式,0代表参数的索引#a0,#P0
resultevaluation context方法执行后的返回值(仅当方法执行之后的判断有效,如'unless','cache put'的表达式'cache evict'的表达式beforeInvocation=false)result

三、Springboot中Cache缓存的使用,将缓存存到Redis中

首先项目目录:

image.png

首先加入依赖

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

再主启动类上加入配置开启缓存功能

package com.sense;
​
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
​
@SpringBootApplication
@MapperScan(basePackages = "com.sense.dao")
@EnableCaching
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

创建实体类

package com.sense.entity;
​
import lombok.Data;
​
/**
 @desc ...
 @date 2021-07-20 14:37:35
 @author 
 */
@Data
public class User {
   private int id;
   private String username;
   private int password;
}

创建dao


package com.sense.dao;
​
import com.sense.entity.User;
import org.apache.ibatis.annotations.Mapper;
​
import java.util.List;
​
/**
 @desc ...
 @date 2021-08-02 10:42:33
 @author 
 */
@Mapper
public interface UserDao {
    List<User> getUser();
​
    User getUserById(Integer id);
​
    void update(User user);
​
    void deleteUser(Integer id);
​
    User queryByName(String name);
}

创建service


package com.sense.service;
​
import com.sense.entity.User;
​
import java.util.List;
​
/**
 @desc ...
 @date 2021-08-02 10:37:24
 @author 
 */
public interface UserService {
    List<User> getUser();
​
    User getUserById(Integer id);
​
    void update(User user);
​
    void deleteUser(Integer id);
​
    User queryByName(String name);
}

创建impl


package com.sense.service.impl;
​
import com.sense.dao.UserDao;
import com.sense.entity.User;
import com.sense.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
​
import java.util.List;
​
/**
 @desc ...
 @date 2021-08-02 10:40:48
 @author 
 */
@Service
@Slf4j
@CacheConfig(cacheNames = "user") //将cacheNames抽取出来
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
​
    @Override
    public List<User> getUser() {
        return userDao.getUser();
    }
​
    /**
     * @desc @Cacheable的几个属性详解:
     *          cacheNames/value:指定相关组件的名字
     *          key: 缓存数据使用的key,可以用它来指定。默认使用方法参数的值,一般不需要指定
     *          keyGenerator:作用和key一样,二选一
     *          cacheManager和cacheResolver作用相同,指定缓存管理器,二选一
     *          condition: 指定符合条件才缓存,比如: condition="#id>3"
     *                      也就是说传入的参数id>3才缓存数据
     *          unless: 否定缓存,当unless为true时不缓存,可以获取的方法结果进行判断
     *          sync: 是否使用异步模式
     * @param
     * @return java.util.List<com.sense.dingtalkdemo.entity.User>
     *
     * @date 2021/7/30 13:36
     */
    @Override
    @Cacheable(cacheNames = "user",key = "#id")
    public User getUserById(Integer id) {
        log.info("得到"+id+"的信息");
        return userDao.getUserById(id);
    }
    /**
     * @desc @CachePut必须结合@Cacheable一起使用,否则没什么意义
     *        @CachePut: 即调用方法,又更新缓存数据
     *        修改了数据库中的数据,同时又更新了缓存
     *     运行时机:
     *      1,先调用目标方法,
     *      2,将目标方法返回一的结果缓存起来
     *     测试步骤:
     *      1.查询1号的个人信息
     *      2,以后查询还是之前的结果
     *      3,更新1号的个人信息
     *      4,查询1号员工返回的结果是什么
     *          应该是更新后的员工
     *          但只更新了数据库,但没有更新缓存是什么原因?
     *      5,如何解决缓存和数据库同步更新
     *         @CachePut(cacheNames="user",key="#user.id")
     *         @CachePut(cacheNames="user",key="#result.id")
     * @param user 1
     * @return com.sense.dingtalkdemo.entity.User
     *
     * @date 2021/7/30 13:47
     */
    @Override
    @CachePut(cacheNames = "user",key = "#result.id")
    public void update(User user) {
        System.out.println("修改"+user.getId()+"的信息");
        userDao.update(user);
    }
​
    /**
     * @desc @CacheEvict:清除缓存
     *        1.key:指定要清除缓存中的某条数据
     *        2.allEntries=true:删除缓存中的所有数据
     *        beforeInvocation=true;在方法执行之前执行清除缓存
     *        作用是:只清除缓存,不删除数据库数据
     * @param id 1
     * @return void
     *
     * @date 2021/7/30 14:10
     */
    @Override
    // @CacheEvict(cacheNames = "user",key = "#id")
    @CacheEvict(cacheNames = "user",allEntries = true)
    public void deleteUser(Integer id){
        System.out.println("删除"+id+"号信息");
        //删除数据库数据的同时删除缓存信息
       userDao.deleteUser(id);
        /**
         * @desc beforeInvocation=true
         * 使用在方法之前的好处:
         * 如果方法出现异常,缓存依旧会被删除
         */
        //int a=1/0;
    }
​
    /**
     * @desc 注解的含义
     *            1.当使用指定的名字查询数据库后,数据保存到缓存
     *            2.现在使用id,age就会直接查询缓存,而不是查询数据库
     * @param name 1
     * @return com.sense.dingtalkdemo.entity.User
     *
     * @date 2021/7/30 14:24
     */
    @Override
    @Caching(cacheable = {@Cacheable(value = "user",key = "#name")},
            put = { @CachePut(value = "user",key = "#result.id"),
                    @CachePut(value = "user",key = "#result.password")})
    public User queryByName(String name) {
        System.out.println("查询的姓名"+name);
        return userDao.queryByName(name);
    }
}

创建config


package com.sense.config;
​
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.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
​
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
​
/**
 @desc ...
 @date 2021-08-02 09:39:44
 @author 
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    /**
     * @desc 配置自定义的redisTemplate
     * @param connectionFactory 1
     * @return org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object>
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setValueSerializer(jackson2JsonRedisSerializer());
        //使用StringRedisSerializer来序列化和反序列化redis中key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
​
    /**
     * @desc json序列化
     * @param
     * @return org.springframework.data.redis.serializer.RedisSerializer<?>
     *
     * @date 2021/8/2 9:51
     */
    @Bean
    public RedisSerializer<?> jackson2JsonRedisSerializer() {
        //使用jackson2JsonRedisSerializer来序列化和反序列化Redis的value值
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        return serializer;
    }
​
    /**
     * @desc 配置缓存管理器
     * @param redisConnectionFactory 1
     * @return org.springframework.cache.CacheManager
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        //设置缓存的默认过期时间,也是对Duration设置
        config = config.entryTtl(Duration.ofMinutes(1))
                //设置key为string序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                //设置value为json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
                //不缓存空值
                .disableCachingNullValues();
        //设置一个初始化的缓存空间set集合
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("timeGroup");
        cacheNames.add("user");
        //对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("timeGroup", config);
        configMap.put("user", config.entryTtl(Duration.ofSeconds(120)));
        //使用自定义的缓存配置初始化一个cacheManager
        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
                //一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }
​
    /**
     * @desc 缓存的key是包名+方法名+参数列表
     * @return org.springframework.cache.interceptor.KeyGenerator
     * @date 2021/8/2 10:13
     */
    @Override
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append("::" + method.getName() + ":");
            for (Object obj : objects) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }
}

创建xml文件


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sense.dao.UserDao">
    <select id="getUser" resultType="com.sense.entity.User">
        select *
        from user
    </select>
    <select id="getUserById" resultType="com.sense.entity.User">
        select *
        from user
        where id = #{id}
    </select>
    <update id="update" parameterType="com.sense.entity.User">
        update user
        set username=#{username},
            password=#{password}
        where id = #{id}
    </update>
    <delete id="deleteUser" parameterType="java.lang.Integer">
        delete from user  where id=#{id}
    </delete>
    <select id="queryByName" resultType="com.sense.entity.User">
        select *
        from user
        where username = #{username}
    </select>
</mapper>

引入Ehcache

关于Ehcahe的一些说明:

  • name:缓存名称。
  • maxElementsInMemory:缓存最大数目
  • maxElementsOnDisk:硬盘最大缓存个数。
  • eternal:对象是否永久有效,一但设置了,timeout将不起作用。
  • overflowToDisk:是否保存到磁盘。
  • timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大。
  • diskPersistent:是否缓存虚拟机重启期数据,默认值为false。
  • diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
  • memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
  • clearOnFlush:内存数量最大时是否清除。
  • memoryStoreEvictionPolicy:Ehcache的三种清空策略:FIFO,first in first out,这个是大家最熟的,先进先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。

导入依赖


<!-- ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

创建xml文件


<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120" />
​
    <cache
            name="user"
            maxEntriesLocalHeap="2000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            statistics="true"/>
</ehcache>

yml配置


spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  redis:
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
  cache:
    ehcache:
      config: ehcache.xml

对于Ehcache来说,更新方法加不加@CachePut注解,结果都一样。

redis中存的数据格式如下

image.png