java框架之SpringBoot(11)-缓存抽象及整合Redis

Spring缓存抽象

介绍

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

关于 JSR107:

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

  • CachingProvider :可以用来创建、配置、获取、管理和控制多个 CacheManager。一个应用可以在运行期间访问多个 CachingProvider。
  • CacheManager: 可以用来创建、配置、获取、管理和控制多个唯一命名的 Cache,这些 Cache 存在于 CacheManager 的上下文中。一个 CacheManager 仅被一个 CachingProvider 所拥有。
  • Cache :是一个类似 Map 数据结构并临时存储以 key 为索引的值。一个 Cache 仅被一个 CacheManager 所拥有。
  • Entry :是一个存储在 Cache 中的 key-value 对。
  • Expiry:每一个存储在 Cache 中的条目有一个定义的有效期。一旦超过这个时间,条目就变更为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过 ExpiryPolicy 设置。

由于 JSR107 的使用相对来说比较繁杂,所以这里我们使用 Spring 提供的缓存抽象。

  • Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合。
  • Cache 接口下 Spring 提供了各种 Cache 的实现,如 RedisCache、EhCacheCache、ConcurrentMapCache 等。
关于Spring缓存抽象的使用:

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

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

  1. 确定方法需要被缓存以及它们的缓存策略。
  2. 从缓存中读取之前缓存存储的数据。

概念&注解

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

简单使用

搭建测试环境

1、新建 SpringBoot 项目,引入如下场景启动器:Cache、Web、Mysql、MyBatis。

2、初始化测试数据:

;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` ) NOT NULL AUTO_INCREMENT,
  `name` ) DEFAULT NULL,
  `gender` ) DEFAULT NULL COMMENT '0:女 1:男',
  `birthday` date DEFAULT NULL,
  `address` ) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;

-- ----------------------------
-- Records of user
-- ----------------------------
', '1997-02-23', '北京');
', '1998-02-03', '武汉');
', '1996-06-04', '上海');

user.sql

3、编写与表对应的 JavaBean:

package com.springboot.bean;

import java.util.Date;

public class User {
    private Integer id;
    private String name;
    private Integer gender;
    private Date birthday;
    private String address;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", address='" + address + '\'' +
                '}';
    }
}

com.springboot.bean.User

4、配置数据库连接信息:

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.202.136:3306/springboot_cache
    driver-class-name: com.mysql.jdbc.Driver

application.yml

5、编写测试Mapper:

package com.springboot.mapper;

import com.springboot.bean.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserMappper {
    @Select("select * from user")
    public List<User> getAll();

    @Select("select * from user where id=#{id}")
    public User getById(Integer id);

    @Delete("delete from user where id=#{id}")
    public void deleteById(Integer id);

    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into user(name,gender,birthday,address) values(#{name},#{gender},#{birthday},#{address})")
    public void add(User user);

    @Update("update user set name=#{name},gender=#{gender},birthday=#{birthday},address=#{address} where id=#{id}")
    public void update(User user);

    @Select("select * from user where name=#{name}")
    public User getByName(String name);
}

com.springboot.mapper.UserMappper

6、编写 Service 并使用注解缓存功能:

package com.springboot.service;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMappper userMappper;

    @Cacheable(cacheNames = {"users"})
    public List<User> getAll(){
        System.out.println("UserService.getAll() 执行了");
        return userMappper.getAll();
    }
}

com.springboot.service.UserService

7、配置 mapper 包扫描,开启注解缓存:

package com.springboot;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 开启注解缓存
@MapperScan("com.springboot.mapper") // mapper 包扫描
public class CacheTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(CacheTestApplication.class, args);
    }

}

com.springboot.CacheTestApplication

8、编写 Controller 并测试:

package com.springboot.controller;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import com.springboot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public List<User> getAll(){
        return userService.getAll();
        /*
        首次访问会输出以下内容:
            UserService.getAll() 执行了
        后续访问不会输出,说明实际上并没有执行 userService.getAll() 方法,即未查询数据库从中获取数据,而是从缓存中获取
        缓存成功
         */
    }
}

com.springboot.controller.UserController

CacheManager 是用来管理多个 Cache 组件的,对缓存的 CRUD 操作实际上还是通过 Cache 组件,而每一个 Cache 组件有自己唯一名称。

@Cacheable

主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。

属性

  • cacheNames/value:指定 Cache 组件名称。
  • key:指定缓存数据所使用的 key,默认 key 使用参数值。比如 id 为 1,那么在 Cache 组件中的 key 就为 1。

    key 还可以通过 SpEL 表达式进行取值。规则如下:

    名字 位置 描述 实力
    methodName root object 当前被调用的方法名 #root.methodName
    method root object 当前被调用的方法对象 #root.method.name
    target root object 当前被调用的目标对象 #root.target
    targetClass root object 当前被调用的目标对象类 #root.targetClass
    args root object 当前被调用方法的参数列表 #root.args[0]
    caches root object 当前被调用方法使用的缓存组件列表,如 @Cacheable(value={"cache1","cache2"}) 就有两个缓存组件 #root.caches[0].name
    argument name evaluation context 可以直接使用 #参数名 来获取对应参数值,也可以使用 #p0 或 #a0 的形式,0 代表参数索引 #id、#a0、#p0
    result evaluation context 获取方法执行后的返回值 #result
  • keyGenerator:通过 IoC 容器中的生成器组件 id 来指定 key 的生成器,key 与 keyGenerator 二选一使用。
  • cacheManager:指定缓存管理器 bean。
  • cacheSolver:指定缓存解析器 bean。
  • condition:指定条件,当符合指定条件时才进行缓存,也可使用 SpEL 表达式同 key。
  • unless:指定条件,当符合指定条件时不进行缓存,也可使用 SpEL 表达式同 key。
  • sync:是否使用异步模式,如果启用,那么 unless 属性失效。

示例

@Cacheable(cacheNames = {"users"})
public User getById(Integer id) {
    System.out.println("UserService.getById(" + id + ") 执行了");
    return userMappper.getById(id);
}

com.springboot.service.UserService#getById

@GetMapping("/user/{id}")
public User getById(@PathVariable Integer id) {
    return userService.getById(id);
    /*
    首次访问 http://localhost:8080/user/1 会输出一下内容:
        UserService.getById(1) 执行了
    后续访问不会输出,说明实际上并没有执行 userService.getById(1) 方法,即未查询数据库从中获取数据,而是从缓存中获取
    而如果访问 http://localhost:8080/user/2 会输出一下内容:
        UserService.getById(2) 执行了
    后续访问也不会输出,即会自动根据参数的不同进行缓存
    OK 缓存成功
     */
}

com.springboot.controller.UserController#getById

@CachePut

主要针对方法配置,目标方法正常执行完成后将其结果更新到缓存。

属性

参考 @Cacheable 注解属性

示例

/*
注意:这里要保证 key 与 getById 方法所使用 key 的生成后的值相同,否则会出现更新缓存后通过 getById 获取数据依旧为旧数据的情况
 */
@CachePut(cacheNames = {"users"}, key = "#user.id")
public User update(User user) {
    System.out.println("UserService.update(" + user + ") 执行了");
    userMappper.update(user);
    return user;
}

com.springboot.service.UserService#update

@PutMapping("/user")
public User update(User user){
    return userService.update(user);
    /*
    1、以 get 方式访问 localhost:8080/user/1 会输出一下内容:
            UserService.getById(1) 执行了
        后续访问不会输出
    2、以 put 方式访问 localhost:8080/user,修改数据,输出以下内容:
            UserService.update(User{id=1, name='张三new', gender=0, birthday=Wed Apr 03 00:00:00 CST 1996, address='深圳'}) 执行了
        每次访问都会输出
    3、以 get 方式访问 localhost:8080/user/1 不会输出内容,返回数据是更新后的数据
     */
}

com.springboot.controller.UserController#update

@CacheEvict

主要针对方法配置,默认在目标方法正常执行完成后清除缓存。

属性

  • beforeInvocation:默认为 false,标识是否在目标方法执行之前清除缓存。
  • allEntries:默认为 false,标识是否清除缓存组件中所有内容。

更多可参考 @Cacheable 注解属性

示例

/*
删除缓存,key 可以不指定,因为 key 默认就是以 id 为基础生成的
 */
@CacheEvict(cacheNames = {"users"}, key = "#id")
public void delete(Integer id) {
    System.out.println("UserService.delete(" + id + ") 执行了");
}

com.springboot.service.UserService#delete

@DeleteMapping("/user/{id}")
public void delete(@PathVariable Integer id){
    userService.delete(id);
    /*
    1、以 get 方式访问 localhost:8080/user/1 会输出以下内容
            UserService.getById(1) 执行了
        后续访问不会输出
    2、以 delete 方式访问 localhost:8080/user/1 会输出以下内容
            UserService.delete(1) 执行了
        每次访问都会输出
    3、以 get 方式再次访问 localhost:8080/user/1 会输出以下内容
            UserService.getById(1) 执行了
    即缓存被删除了重新执行方法获取了数据
     */
}

com.springboot.controller.UserController#delete

@Caching

主要针对方法配置,为 @Cacheable、@CachePut、CacheEvict 的组合注解,常用于定制较复杂的缓存策略。

属性

  • cacheable:放置 @Cacheable 注解数组。
  • put:放置 @CachePut 注解数组。
  • evict:放置 @CacheEvict 注解数组。

示例

@Caching(
        cacheable = {
                // 以 name 生成 key 进行缓存
                @Cacheable(value = "users", key = "#name")
        },
        put = {
                // 以 id 生成 key ,且执行结果不为空时更新缓存
                @CachePut(value = "users", key = "#result.id", condition = "#result != null")
        },
        evict = {
                // name 为 deleteAll 清除所有缓存
                @CacheEvict(value = "users", condition = "#name=='deleteAll'", allEntries = true)
        }
)
public User getByName(String name) {
    System.out.println("UserService.getByName(" + name + ") 执行了");
    return userMappper.getByName(name);
}

com.springboot.service.UserService#getByName

@GetMapping("/user/name/{name}")
public User getByName(@PathVariable String name){
    return userService.getByName(name);
    /*
    1、以 get 方式请求 localhost:8080/user/name/李四,输出以下内容:
            UserService.getByName(李四) 执行了
        每次访问都会输出,因为 @CachePut 需要执行结果更新缓存
    2、以 get 方式请求 localhost:8080/user/2,没有输出内容,因为通过 [1] 已经完成了 id 为 2 的缓存,直接从缓存中取出结果返回了
    3、以 get 方式请求 localhost:8080/user/name/deleteAll,输出以下内容:
            UserService.getByName(deleteAll) 执行了
        每次访问都会输出,因为 @CachePut 需要执行结果更新缓存
    4、以 get 方式请求 localhost:8080/user/2,输出以下内容:
            UserService.getById(2) 执行了
        后续访问不会输出,这次输出的原因是通过 [3] 清楚了所有缓存
     */
}

com.springboot.controller.UserController#getByName

@CacheConfig

通常用来标注在类上,用来定义公共缓存配置。

属性

具有如下属性:cachenames、keyGenerator、cacheManager、cacheResolver,可参考 @Cacheable 注解属性

示例

package com.springboot.service;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

/**
 * 因为下面方法都是使用同一个 Cache 组件,所以可以在类上一次性指定所有方法使用的 Cache 组件名称
 */
@CacheConfig(cacheNames = {"users"} )
@Service
public class UserService {
    @Autowired
    private UserMappper userMappper;

    @Cacheable(keyGenerator = "myKeyGenerator")
    public User getById(Integer id) {
        System.out.println("UserService.getById(" + id + ") 执行了");
        return userMappper.getById(id);
    }

    @CachePut(key = "#user.id")
    public User update(User user) {
        System.out.println("UserService.update(" + user + ") 执行了");
        userMappper.update(user);
        return user;
    }

    @CacheEvict(key = "#id")
    public void delete(Integer id) {
        System.out.println("UserService.delete(" + id + ") 执行了");
    }

    @Caching(
            cacheable = {
                    @Cacheable(key = "#name")
            },
            put = {
                    @CachePut(key = "#result.id", condition = "#result != null")
            },
            evict = {
                    @CacheEvict(condition = "#name=='deleteAll'", allEntries = true)
            }
    )
    public User getByName(String name) {
        System.out.println("UserService.getByName(" + name + ") 执行了");
        return userMappper.getByName(name);
    }
}

com.springboot.service.UserService

原理及总结

依旧是从自动配置类入手以 @Cacheable 注解执行流程进行分析:

 package org.springframework.boot.autoconfigure.cache;

 @Configuration
 @ConditionalOnClass(CacheManager.class)
 @ConditionalOnBean(CacheAspectSupport.class)
 @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
 @EnableConfigurationProperties(CacheProperties.class)
 @AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
 @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
         RedisAutoConfiguration.class })
 @Import(CacheConfigurationImportSelector.class)
 public class CacheAutoConfiguration {

     // 注册缓存管理器定制器
     @Bean
     @ConditionalOnMissingBean
     public CacheManagerCustomizers cacheManagerCustomizers(
             ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
         return new CacheManagerCustomizers(customizers.getIfAvailable());
     }

     @Bean
     public CacheManagerValidator cacheAutoConfigurationValidator(
             CacheProperties cacheProperties, ObjectProvider<CacheManager> cacheManager) {
         return new CacheManagerValidator(cacheProperties, cacheManager);
     }

     @Configuration
     @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
     @ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
     protected static class CacheManagerJpaDependencyConfiguration
             extends EntityManagerFactoryDependsOnPostProcessor {

         public CacheManagerJpaDependencyConfiguration() {
             super("cacheManager");
         }

     }

     static class CacheManagerValidator implements InitializingBean {

         private final CacheProperties cacheProperties;

         private final ObjectProvider<CacheManager> cacheManager;

         CacheManagerValidator(CacheProperties cacheProperties,
                 ObjectProvider<CacheManager> cacheManager) {
             this.cacheProperties = cacheProperties;
             this.cacheManager = cacheManager;
         }

         @Override
         public void afterPropertiesSet() {
             Assert.notNull(this.cacheManager.getIfAvailable(),
                     "No cache manager could "
                             + "be auto-configured, check your configuration (caching "
                             + "type is '" + this.cacheProperties.getType() + "')");
         }

     }

     static class CacheConfigurationImportSelector implements ImportSelector {

         @Override
         public String[] selectImports(AnnotationMetadata importingClassMetadata) {
             CacheType[] types = CacheType.values();
             String[] imports = new String[types.length];
             for (int i = 0; i < types.length; i++) {
                 imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
             }
             return imports;
         }
     }
 }

org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration

看到第 11 行,该行是通过 @Import(CacheConfigurationImportSelector.class) 导入缓存配置类, CacheConfigurationImportSelector 为自动配置类中的内部类,对应的导入方法为 65 行 selectImports 方法。通过调试会发现它实际导入了如下配置类:

org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration

那么实际生效的是哪个配置类呢?可以在配置文件中开启 debug 模式,接着在控制台中就可以看到:

SimpleCacheConfiguration matched:
  - Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
  - @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)

即生效的配置类为 org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration 。查看该配置类:

 package org.springframework.boot.autoconfigure.cache;

 @Configuration
 @ConditionalOnMissingBean(CacheManager.class)
 @Conditional(CacheCondition.class)
 class SimpleCacheConfiguration {

     private final CacheProperties cacheProperties;

     private final CacheManagerCustomizers customizerInvoker;

     SimpleCacheConfiguration(CacheProperties cacheProperties,
             CacheManagerCustomizers customizerInvoker) {
         this.cacheProperties = cacheProperties;
         this.customizerInvoker = customizerInvoker;
     }

     @Bean
     public ConcurrentMapCacheManager cacheManager() {
         ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
         List<String> cacheNames = this.cacheProperties.getCacheNames();
         if (!cacheNames.isEmpty()) {
             cacheManager.setCacheNames(cacheNames);
         }
         return this.customizerInvoker.customize(cacheManager);
     }
 }

org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration

在第 19 行可以看到往 IoC 容器中注册了一个 org.springframework.cache.concurrent.ConcurrentMapCacheManager ,该类实现了 org.springframework.cache.CacheManager 接口,可以使用它的 getCache(String) 方法来获取缓存组件:

 public Cache getCache(String name) {
     // this.cacheMap : private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
     Cache cache = (Cache)this.cacheMap.get(name);
     if (cache == null && this.dynamic) {
         synchronized(this.cacheMap) {
             // 从 ConcurrentMap 对象中获取 Cache 组件
             cache = (Cache)this.cacheMap.get(name);
             if (cache == null) { // 如果获取的 Cache 组件为空
                 // 新创建一个 ConcurrentMapCache 组件
                 cache = this.createConcurrentMapCache(name);
                 // 将其放入 ConcurrentMap 对象中
                 this.cacheMap.put(name, cache);
             }
         }
     }
     return cache;
 }

org.springframework.cache.concurrent.ConcurrentMapCacheManager#getCache

可以看到该方法的作用实际上就是用来获取一个 ConcurrentMapCache 类型的 Cache 组件,而我们通过 Cache 组件获取数据时是通过 get 方法,最终是通过该类的 get 方法调用 lookup(key) 方法:

 @Override
 protected Object lookup(Object key) {
     // this.store : private final ConcurrentMap<Object, Object> store;
     return this.store.get(key);
 }

org.springframework.cache.concurrent.ConcurrentMapCache#lookup

而这个 key 默认是使用 org.springframework.cache.interceptor.SimpleKeyGenerator#generateKey 生成的:

 package org.springframework.cache.interceptor;

 import java.lang.reflect.Method;

 public class SimpleKeyGenerator implements KeyGenerator {

     @Override
     public Object generate(Object target, Method method, Object... params) {
         return generateKey(params);
     }

     public static Object generateKey(Object... params) {
         if (params.length == 0) {
             //SimpleKey.EMPTY : public static final SimpleKey EMPTY = new SimpleKey();
             return SimpleKey.EMPTY;
         }
         if (params.length == 1) {
             Object param = params[0];
             if (param != null && !param.getClass().isArray()) {
                 return param;
             }
         }
         return new SimpleKey(params);
     }
 }

org.springframework.cache.interceptor.SimpleKeyGenerator

可以看到它的生成规则如下:

  • 当目标方法的参数个数为空时,使用 new SimpleKey() 实例化一个 SimpleKey 作为 key 值。
  • 当目标方法的参数个数为 1 且该参数不为空也不是数组类型时,直接使用该参数值作为 key 值。
  • 当目标方法的参数不止一个时,使用 new SimpleKey(params) 实例化一个 SimpleKey 作为 key 值。

即 ConcurrentMapCache 组件也是将实际缓存数据存放在 ConcurrentMap 对象中。

ConcurrentMapCache 类实现了 Cache 接口,Cache 接口中定义了 get 方法用来获取数据、put 方法用来存放数据、evict 方法用来删除指定数据、clear 方法用来清空所有数据。

上述运行流程如下:

  1. 目标方法运行之前会使用 CacheManager 先根据 cacheNames 获取 Cache 组件,如果没有,则会创建一个新的 Cache 组件返回。
  2. 通过返回的 Cache 组件使用生成的 key 获取缓存内容。
  3. 如果获取到的缓存内容为空,就直接调用目标方法,然后将目标方法执行后的返回值通过 Cache 组件的 put 方法放入缓存(ConcurrentMap 对象)中;如果获取到的缓存内容不为空,就直接返回获取到的缓存内容,不会执行目标方法。

默认情况下:

  • 生效的自动配置类为 SimpleCacheConfiguration。
  • CacheManager 的实现为 ConcurrentMapCacheManager。
  • key 是使用 KeyGenerator 生成的,实现为 SimpleKeyGenerator。

整合Redis

redis安装

参考【Linux 下源码安装 Redis】或【Docker 安装连接 Redis】。

准备环境

1、在上面简单使用示例项目的基础上再引入 redis 缓存的场景启动器:

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

2、配置 redis:

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.202.136:3306/springboot_cache
    driver-class-name: com.mysql.jdbc.Driver
  redis:
    # 指定 redis 主机地址
    host: 192.168.202.136

application.yml

3、测试:

访问 localhost:8080/user/1,然后查看 Redis:

java框架之SpringBoot(11)-缓存抽象及整合Redis

会发现 id 为 1 的用户已经被缓存到 redis。

test

RedisTemplate使用

package com.springboot;

import com.springboot.bean.User;
import com.springboot.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {

    @Autowired
    private RedisTemplate redisTemplate; // k-v 都为字符串

    @Autowired
    private StringRedisTemplate stringRedisTemplate; // k-v 都为 Object

    @Autowired
    private UserService userService;

    // 操作字符串
    @Test
    public void testString(){
        stringRedisTemplate.opsForValue().set("msg","hello 张三");
        String msg = stringRedisTemplate.opsForValue().get("msg");
        System.out.println(msg);
        /*
        hello 张三
         */
    }

    // 操作对象
    // 注意:序列化类型需要实现 Serializable 接口
    @Test
    public void testObject(){
        User user = userService.getById(2);
        redisTemplate.opsForValue().set("id_2", user);
        Object user2 = redisTemplate.opsForValue().get("id_2");
        System.out.println(user2);
        /*
        User{id=2, name='李四', gender=0, birthday=Tue Feb 03 00:00:00 CST 1998, address='武汉'}
         */
    }

    /*
    还可通过如下几种方式操作列表、集合、有序集合、哈希
        RedisTemplate.opsForList()
        RedisTemplate.opsForSet()
        RedisTemplate.opsForZSet()
        RedisTemplate.opsForHash()
     */
}

test

经过上述测试我们查看 redis 中数据会发现 redis 中保存的是 SpringBoot 以默认方式序列化后的数据,如果我们想要以 json 方式序列化保存数据到 redis 我们该怎么做呢?

我们使用的 RedisTemplate 和 StringRedisTemplate bean 都是在 redis 的自动配置类中注册的,查看:

 @Configuration
 protected static class RedisConfiguration {

     @Bean
     @ConditionalOnMissingBean(name = "redisTemplate")
     public RedisTemplate<Object, Object> redisTemplate(
             RedisConnectionFactory redisConnectionFactory)
             throws UnknownHostException {
         RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
         template.setConnectionFactory(redisConnectionFactory);
         return template;
     }

     @Bean
     @ConditionalOnMissingBean(StringRedisTemplate.class)
     public StringRedisTemplate stringRedisTemplate(
             RedisConnectionFactory redisConnectionFactory)
             throws UnknownHostException {
         StringRedisTemplate template = new StringRedisTemplate();
         template.setConnectionFactory(redisConnectionFactory);
         return template;
     }

 }

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.RedisConfiguration

每个 RedisTemplate 对象都可定制自己的序列化器,查看源码会发现它默认使用的序列化器为 org.springframework.data.redis.serializer.JdkSerializationRedisSerializer 。我们只需要修改它默认的序列化器即可:

package com.springboot.config;

import com.springboot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

@Configuration
public class CacheConfig {

    @Bean
    public RedisTemplate<Object, User> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, User> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>(User.class);
        template.setDefaultSerializer(serializer);
        return template;
    }
}

com.springboot.config.CacheConfig

package com.springboot;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import com.springboot.service.DeptService;
import com.springboot.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate userRedisTemplate;

    @Test
    public void test(){
        User user = userService.getById(2);
        userRedisTemplate.opsForValue().set("id_2", user);
        Object user2 = userRedisTemplate.opsForValue().get("id_2");
        System.out.println(user2);
        /*
        User{id=2, name='李四', gender=0, birthday=Tue Feb 03 00:00:00 CST 1998, address='武汉'}
         */
    }
}

java框架之SpringBoot(11)-缓存抽象及整合Redis

test

自定义CacheManager

上面说的是通过 RedisTemplate 操作保存 json 数据到 redis,如果要使用注解方式该怎么做呢?

一旦我们引入了 redis 缓存的场景启动器,那么默认生效的缓存配置类就变成了 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration :

 package org.springframework.boot.autoconfigure.cache;

 @Configuration
 @AutoConfigureAfter(RedisAutoConfiguration.class)
 @ConditionalOnBean(RedisTemplate.class)
 @ConditionalOnMissingBean(CacheManager.class)
 @Conditional(CacheCondition.class)
 class RedisCacheConfiguration {

     private final CacheProperties cacheProperties;

     private final CacheManagerCustomizers customizerInvoker;

     RedisCacheConfiguration(CacheProperties cacheProperties,
             CacheManagerCustomizers customizerInvoker) {
         this.cacheProperties = cacheProperties;
         this.customizerInvoker = customizerInvoker;
     }

     @Bean
     public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
         RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
         cacheManager.setUsePrefix(true);
         List<String> cacheNames = this.cacheProperties.getCacheNames();
         if (!cacheNames.isEmpty()) {
             cacheManager.setCacheNames(cacheNames);
         }
         return this.customizerInvoker.customize(cacheManager);
     }

 }

org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration

缓存管理器也更换为第 21 行的 RedisCacheManager,RedisCacheManager 帮我们创建 RedisCache 来作为缓存组件,而 RedisCache 组件就是通过操作 Redis 缓存数据的。

在第 22 行也可以看到,创建 RedisCacheManager 实例时通过构造方法传入的 RedisTemplate,所以我们只需要自己定义一个 RedisCacheManager,让其 RedisTemplate 是使用 json 序列化器即可:

package com.springboot.config;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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;

@Configuration
public class CacheConfig {

    @Bean
    public RedisTemplate<Object, User> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, User> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>(User.class);
        template.setDefaultSerializer(serializer);
        return template;
    }

    @Bean
    public RedisTemplate<Object, Dept> deptRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Dept> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Dept> serializer = new Jackson2JsonRedisSerializer<Dept>(Dept.class);
        template.setDefaultSerializer(serializer);
        return template;
    }

    @Primary // 默认
    @Bean
    public RedisCacheManager userCacheManager(RedisTemplate<Object, User> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        // key 使用 cacheName 作为前缀
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }

    @Bean
    public RedisCacheManager deptCacheManager(RedisTemplate<Object, Dept> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        // key 使用 cacheName 作为前缀
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }
}

com.springboot.config.CacheConfig

然后再使用注解时指定要使用的缓存管理器即可。

还可以通过手动编码方式获取到缓存管理器对指定缓存组件进行操作:

package com.springboot.service;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import com.springboot.mapper.DeptMappper;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.*;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheManager = "deptCacheManager")
public class DeptService {
    @Autowired
    private DeptMappper deptMappper;

    @Autowired
    private RedisCacheManager deptCacheManager;

    public Dept getById(Integer id) {
        System.out.println("DeptService.getById(" + id + ") 执行了");
        Dept dept = deptMappper.getById(id);
        // 获取到缓存组件
        Cache cache = deptCacheManager.getCache("dept");
        cache.put(id, dept);
        return dept;
    }
}

com.springboot.service.DeptService

上一篇:html5自带表单验证-美化改造


下一篇:java的 IO流之缓冲流(转载)