SpringBoot缓存

SpringBoot 缓存

1. Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;

并支持使用JCache(JSR-107)注解简化我们开发;

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

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

ConcurrentMapCache等;

• 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否

已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法

并缓存结果后返回给用户。下次调用直接从缓存中获取。

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

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

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

2. 几个重要概念&缓存注解

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

3. @Cacheable/@CachePut/@CacheEvict 主要的参数

value缓存的名称,在 spring 配置文件中定义,必须指定至少一个例如:@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合例如:@Cacheable(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
allEntries(@CacheEvict )是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存例如:@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation**(@CacheEvict)**是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存例如:@CachEvict(value=”testcache”,beforeInvocation=true)

4.搭建环境

pom.xml:

        <!-- cache -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

sql文件(参考):

CREATE DATABASE db_user;

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `true_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `nick_name` varchar(20) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES ('1', '二钊', '二二', '16');
INSERT INTO `tb_user` VALUES ('2', 'admin', '最高权限', '18');
INSERT INTO `tb_user` VALUES ('3', '炮', 'bilibili', '20');

application.properties:

#数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_user?useSSL=false
spring.datasource.username=root
spring.datasource.password=333666999520
#开启mybatis驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
#日志级别设置为debug(方便查看sql语句的调用)
logging.level.com.example.springcache.mapper.UserMapper=debug
server.port=9090

debug=true

User.java

public class User implements Serializable {

    private Integer id;
    private String trueName;
    private String nickName;
    private Integer age;

    public User(){

    }

    public User(Integer id, String trueName, String nickName, Integer age) {
        this.id = id;
        this.trueName = trueName;
        this.nickName = nickName;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTrueName() {
        return trueName;
    }

    public void setTrueName(String trueName) {
        this.trueName = trueName;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", trueName='" + trueName + '\'' +
                ", nickName='" + nickName + '\'' +
                ", age=" + age +
                '}';
    }
}

UserMapper.java

@Mapper
public interface UserMapper {

   @Delete("DELETE FROM tb_user WHERE id=#{id}")
    public int deleteUser(Integer id);

    @Update("UPDATE tb_user SET true_name = #{trueName},nick_name=#{nickName},age=#{age} WHERE id=#{id}")
    public int updateUser(User user);

    @Select("SELECT * FROM tb_user WHERE id=#{id}")
    public User selectUserByID(Integer id);


}

UserService.java

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    @CacheEvict(cacheNames = "user",key="#id")
    public int deleteUser(Integer id){
        return userMapper.deleteUser(id);
    }

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


    @Cacheable(cacheNames = "user",key="#id")
    public User selectUserByID(Integer id){
        return userMapper.selectUserByID(id);
    }
}

UserController.java

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public User user(@PathVariable("id")Integer id){
        return userService.selectUserByID(id);
    }

    @RequestMapping("/user/update")
    public User update(User user){
        return userService.updateUser(user);
    }

    @RequestMapping("/user/delete/{id}")
    public String del(@PathVariable("id")Integer id){
        int rs = userService.deleteUser(id);
        return rs==1?"成功":"失败";
    }
}


最后在启动类上使用@EnableCaching注解,开启缓存

4.1 测试@Cacheable

测试方法:调用user/{id},若注解生效则第一次调用时会调用数据库的数据,控制台上会看见sql的日志,第二次调用则不会:

4.2 测试@CachePut

@CachePut:既调用方法,又更新缓存数据;修改了数据库的某个数据,同时更新缓存;

运行时机:
1、先调用目标方法
2、将目标方法的结果缓存起来

测试步骤:

  1. 查询2号用户;查到的结果会放在缓存中; http://localhost:9090/user/2
    {“id”:2,“trueName”:“admin”,“nickName”:“最高权限”,“age”:18}
  2. 以后查询还是之前的结果
  3. 更新2号用户;http://localhost:9090/user/update?id=2&trueName=admin&nickName=管理员&age=17
    将方法的返回值也放进缓存了;
    key: 传入的employee对象值: 返回的employee对象;
  4. 查询2号用户?http://localhost:9090/user/2
    应该是更新后的员工:{“id”:2,“trueName”:“admin”,“nickName”:“管理员”,“age”:17}

3.测试@CacheEvict

注:三步测试是连在一起的

@CacheEvict:缓存清除
key:指定要清除的数据
allEntries = true:
指定清除这个缓存中所有的数据
beforeInvocation = false: 缓存的清除是否在方法之前执行
默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
beforeInvocation = true:
代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除|

测试步骤:

  1. 经过第二步操作后,缓存内存有2号用户的数据,删除2号用户http://localhost:9090/user/delete/2的同时,缓存会被删除,再次访问http://localhost:9090/user/2时应该是看不到任何数据的

5. @Cacheable的属性

@Cacheable:

将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;

CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字;
几个属性:

  • cacheNames/value:指定缓存组件的名字;,

  • key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 1 -方法的返回值
    编写SpEL; #id;参数id的值 #a0 #pθ #root. args[0]

  • keyGenerator: key的生成器; 可以自己指定key的生成器的组件id

    • key/keyGenerator:二选一使用
  • cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器

  • condition:指定符合条件的情况下才缓存;

  • unless:否定缓存;当unless指定的条件为 true,方法的返回值就不会被缓存;可以获取到结果进行判断
    unless = “#result == null”

6. 缓存的原理探索(@Cacheable)

1.自动配置类 CacheAutoConfiguration 加载的配置(SimpleConfiguration)

搜索CacheAutoConfiguration,进入该类

在这里插入图片描述

在这个类中有如下方法,打上断点,用debug模式启动

在这里插入图片描述

可以看到它加载了10个配置类

在这里插入图片描述

0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
8 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"
9 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"

那么哪个配置类生效了呢,我们在全局配置文件上配上debug=true,重新启动,点击Console一栏,Ctrl+F查找CacheConfiguration

在这里插入图片描述
在这里插入图片描述

2. SimpleConfiguration为我们做了什么(注册CacheManager)

SimpleConfiguration生效,那么这个类为我们做了什么呢?Ctrl+N,进入这个类

在这里插入图片描述
可以看到,它为我们注册了一个ConcurrentMapCacheManager,它实现了CacheManager接口,接口中有一个根据name取Cache的方法

在这里插入图片描述
在这里插入图片描述
在ConcurrentMapCacheManager找到getCahce()方法,该方法尝试根据name获取Cache,若Cache为空则锁一下,再次获取,若仍为空,则根据name属性创建一个Cache,并将Cache保存在本类中的cacheMap

在这里插入图片描述

在这里插入图片描述

点开createConcurrentMapCache,点击ConcurrentMapCache,进入ConcurrentMapCache

在这里插入图片描述

3. Cache

在这里插入图片描述

可以看见ConcurrentMapCache里面是使用一个map保存缓存数据的,里面有以下两个函数,分别执行get,put操作
在这里插入图片描述
在这里插入图片描述

4. KeyGenerator

那么上面的key是从哪里来的呢?在以上两个方法打断点,debug模式运行
在这里插入图片描述

沿着红色箭头方向一个一个找,可以发现它是由一个KeyGenerator生成的

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

以上的步骤最好自己亲手动手试验一下,否则可能会很晕。以上部分打的断点有:

  • CacheConfigurationImportSelector的selectImports方法

  • ConcurrentMapCacheManager的getCache方法

  • ConcurrentMapCache的lookup和put方法

7. 原理总结

测试如下方法:

  @Cacheable(cacheNames = "user",key="#id")
    public User selectUserByID(Integer id){
        return userMapper.selectUserByID(id);
    }

运行流程:

@Cacheable:

1、方法运行之前,先去查询Cache (缓存组件),按照cacheNames指定的名字获取:

​ (CacheManager先获取相应的缓存),第-次获取缓存如果没有Cache组件会自动创建。

在这里插入图片描述

2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数:

在这里插入图片描述
​ key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;
在这里插入图片描述
​ SimpleKeyGenerator生成key的默认策略:

 - 如果没有参数: key=new SimpleKey();
 - 如果有一个参数: key=参数的值
 - 如果有多个参数: key=new SimpleKey(params);

3、没有查到缓存就调用目标方法;

4、将目标方法返回的结果,放进缓存中

在这里插入图片描述

@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为hey去查询缓存,_

如果没有就运行方法并将结果放入缓存:以后再来调用就可以直接使用缓存中的数据:

核心:

1)、使用CacheManager [ConcurrentMapCacheManager]按照名字得到Cache [ConcurrentMapCache]组件:

2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator

8.整合Redis

SpringBoot1.x版本和SpringBoot2.x版本的redis配置不同,注意区分,这里使用的是SpringBoot2.x版本。

(1)Springboot中使用redis操作的两种方式:lettuce和jedis,两者在进行操作时都需要序列化器来实现序列化(推荐使用jackson2JsonRedisSerializer,相比于JDK提供的序列化器和String序列化器长度更短),lettuce和redis都是 redis的客户端。

(2)Springboot 1.x整合Spring-data-redis底层用的是jedis,Springboot 2.x整合spring-data-redis用的是lettuce,jedis在多线程环境下是非线程安全的,使用了jedis pool连接池,为每个Jedis实例增加物理连接。Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问。

pom文件:

        <!-- cache -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--spring2.0集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.4.2</version>
        </dependency>

application.properties:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.cache.redis.time-to-live=-1ms
# 数据库连接超时时间,2.0 中该参数的类型为Duration,这里在配置的时候需要指明单位
spring.redis.timeout=1000s
# 最大活跃连接数,负数为不限制
spring.redis.lettuce.pool.max-active=-1
# 最大空闲连接数
spring.redis.lettuce.pool.max-idle=8
#最小空闲连接数
spring.redis.lettuce.pool.min-idle=8
# 等待可用连接的最大时间,负数为不限制
spring.redis.lettuce.pool.max-wait=-1ms

配置类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisConfig {

    //lettuce客户端连接工厂
    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

    //日志
    private Logger logger=LoggerFactory.getLogger(RedisConfig.class);

    //json序列化器
    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

    //缓存生存时间
    private Duration timeToLive = Duration.ofDays(1);

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
       //redis缓存配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(this.timeToLive)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .disableCachingNullValues();
        //缓存配置map
        Map<String,RedisCacheConfiguration> cacheConfigurationMap=new HashMap<>();

        //自定义缓存名,后面使用的@Cacheable的CacheName
        //cacheConfigurationMap.put("users",config);
        cacheConfigurationMap.put("default",config);

       //根据redis缓存配置和reid连接工厂生成redis缓存管理器
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .withInitialCacheConfigurations(cacheConfigurationMap)
                .build();
        logger.debug("自定义RedisCacheManager加载完成");
        return redisCacheManager;
    }

   //redisTemplate模板提供给其他类对redis数据库进行操作
    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        logger.debug("自定义RedisTemplate加载完成");
        return redisTemplate;
    }
  
          
    //redis键序列化使用StrngRedisSerializer
    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }
  

    //redis值序列化使用json序列化器
    private RedisSerializer<Object> valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
  

    //缓存键自动生成器
    @Bean
    public KeyGenerator myKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }
}

在这里插入图片描述

关于Redis整合SpringBoot的个人不是非常清楚,这里只是为了做个记录,可以参考一下其他人的博客:

https://www.cnblogs.com/coder-lichao/p/10889457.html

https://blog.csdn.net/zhulier1124/article/details/82154937

https://www.jianshu.com/p/0d4aea41a70c

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值