Loading

SpringBoot和缓存

1 JSR107

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

JSR107简介

2 Spring缓存抽象

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

Spring的缓存抽象

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

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

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

  • 使用Spring缓存抽象的时候我们需要关注:

  • 1️⃣确定方法需要被缓存以及它们的缓存策略。

  • 2️⃣从缓存中读取数据之前需要缓存存储的数据。

3 几个重要概念&缓存注解

3.1 接口和缓存注解

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

3.2 Cache中可以书写的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"})),则有两个cache #root.caches[0].name
argument name evaluation context 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; #iban 、 #a0 、 #p0
result evaluation context 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) #result

4 缓存使用

4.1 基本环境搭建

  • 导入相关jar包的Maven坐标:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
  • sql脚本:
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `gender` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
  • 实体类Employee.java
package com.sunxiaping.springboot.domain;

import java.io.Serializable;

/**
 * @author 许威威
 * @version 1.0
 */
public class Employee implements Serializable {

    private Integer id;

    private String name;

    private String gender;

    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 String getGender() {
        return gender;
    }

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

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                '}';
    }
}
  • application.yml
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    # 数据源的其他配置
    druid:
      # 初始化
      initial-size: 5
      # 最小
      min-idle: 5
      # 最大
      max-active: 20
      # 连接等待超时时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-on-borrow: false
      test-while-idle: true
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall
      #合并多个DruidDataSource的监控数据
      use-global-data-source-stat: true
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 配置DruidStatFilter
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      # 配置DruidStatViewServlet
      stat-view-servlet:
        enabled: true
        url-pattern: "/druid/*"
        # IP白名单(没有配置或者为空,则允许所有访问)
        #        allow:
        # IP黑名单 (存在共同时,deny优先于allow)
        #        deny:
        #  禁用HTML页面上的“Reset All”功能
        reset-enable: false
        # 登录名
        login-username: admin
        # 登录密码
        login-password: 123456

# 开启mybatis的日志
logging:
  level:
    com.sunxiaping.springboot.mapper: debug #打印sql
  • EmployeeMapper.java
package com.sunxiaping.springboot.mapper;

import com.sunxiaping.springboot.domain.Employee;
import org.apache.ibatis.annotations.*;

/**
 * @author 许威威
 * @version 1.0
 */
@Mapper
public interface EmployeeMapper {

    @Results(id = "employeeMap", value = {
            @Result(id = true, column = "id", property = "id"),
            @Result(property = "name", column = "name"),
            @Result(property = "gender", column = "gender")
    })
    @Select("SELECT id,`name`,gender FROM employee WHERE id = #{id}")
    Employee findEmpById(Integer id);


    @Insert(value = "INSERT INTO employee (`name`,gender) VALUES (#{name},#{gender})")
    void saveEmp(Employee employee);


    @Update(value = "UPDATE employee SET `name` = #{name},gender =#{gender} WHERE id = #{id}")
    void updateEmp(Employee employee);


    @Delete(value = "DELETE FROM employee WHERE id = #{id}")
    void deleteEmpById(Integer id);

}
  • EmployeeService.java
package com.sunxiaping.springboot.service;

import com.sunxiaping.springboot.domain.Employee;

/**
 * @author 许威威
 * @version 1.0
 */
public interface EmployeeService {

    Employee findEmpById(Integer id);

}
  • EmployeeServiceImpl.java
package com.sunxiaping.springboot.service.impl;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
import com.sunxiaping.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Optional;

/**
 * @author 许威威
 * @version 1.0
 */
@Service
@Transactional
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public Employee findEmpById(Integer id) {
        return Optional.ofNullable(employeeMapper.findEmpById(id)).orElse(new Employee());
    }
}
  • EmployeeController.java
package com.sunxiaping.springboot.web;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 许威威
 * @version 1.0
 */
@RestController
@RequestMapping(value = "emp")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping(value = "/view/{id}")
    public Employee view(@PathVariable(value = "id") Integer id) {
        return employeeService.findEmpById(id);
    }

}
  • 启动类:
package com.sunxiaping.springboot;

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

@SpringBootApplication
@MapperScan(basePackages = "com.sunxiaping.springboot.mapper")
public class SpringbootApplication {

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

}
  • 测试:
package com.sunxiaping.springboot;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
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.test.context.junit4.SpringRunner;

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

    @Autowired
    private EmployeeMapper employeeMapper;

    @Test
    public void test() {
        Employee employee = employeeMapper.findEmpById(1);
        System.out.println("employee = " + employee);
    }
}

4.2 体验缓存

4.2.1 步骤

  • 1️⃣开启注解的缓存(@EnableCaching)。
  • 2️⃣标注缓存注解即可(@Cacheable、@CachePut、@CacheEvict)。

4.2.2 @Cacheable注解

  • @Cacheable注解:

    • 将方法运行的结果进行缓存,以后再要相同的数据,直接从缓存中获取,不需要再调用方法。
    • CacheManager管理多个Cache组件,对缓存真正的CRUD操作在Cache组件中,每一个缓存组件都有自己唯一的名称。
    • 属性:
      • value/cacheNames:指定缓存组件的名称
      • key:缓存数据使用的key,默认是方法参数的值(如果传入的参数是1,那么值就是1对应的返回的返回值。),支持SPEL表达式。
      • keyGenerator:key的生成器,可以自己指定key的生成器的组件id。key和keyGenerator只能二选一。
      • cacheManager:指定缓存管理器。
      • cacheResolver:指定缓存解析器。
      • condition:指定符合条件的情况下才缓存。支持SPEL表达式。
      • unless:否定缓存。当unless指定的条件为true,方法的返回值就不会返回;可以获取到结果进行判断。
      • sync:是否同步,默认值是false,即异步,在多线程环境中需要设置为true,避免缓存击穿的问题。
  • 示例:

  • 在启动类上使用@EnableCaching注解开启缓存:

package com.sunxiaping.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
@MapperScan(basePackages = "com.sunxiaping.springboot.mapper")
@EnableCaching
public class SpringbootApplication {

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

}
  • 使用@Cacheable注解方法返回的结果缓存:
package com.sunxiaping.springboot.service.impl;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
import com.sunxiaping.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * @author 许威威
 * @version 1.0
 */
@Service
@Transactional
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;


    /**
     * @Cacheable注解: 将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不需要再调用方法
     * CacheManager管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件都有自己唯一的名称
     * @Cacheable注解的属性:
     *      value/cacheNames:指定缓存组件的名称
     *      key:缓存数据使用的key,默认是方法参数的值(如果传入的参数是1,那么值就是1对应的返回的返回值。),支持SPEL表达式。
     *      keyGenerator:key的生成器,可以自己指定key的生成器的组件id。key和keyGenerator二选一。
     *      cacheManager:指定缓存管理器。
     *      cacheResolver:指定缓存解析器。
     *      condition:指定符合条件的情况下才缓存。支持SPEL表达式。
     *      unless:否定缓存。当unless指定的条件为true,方法的返回值就不会返回。
     *      sync:是否同步,默认值是false,即异步,在多线程环境中需要设置为true,避免缓存击穿的问题。
     */
    @Cacheable(cacheNames = "emp", key = "#id")
    @Override
    public Employee findEmpById(Integer id) {
        return Optional.ofNullable(employeeMapper.findEmpById(id)).orElse(new Employee());
    }
}

4.3 缓存的工作原理

  • 1️⃣自动配置类:CacheAutoConfiguration,其源码片段如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class,
		HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {
    
    //其他略
}    
  • 2️⃣通过CacheAutoConfiguration上面的@Import注解,可以知道其向Spring容器中导入了一些组件,dubug后的缓存配置类如下所示:

导入到Spring容器中的缓存配置类

  • 3️⃣默认生效的缓存配置类。可以通过在application.yml中增加dubug=true,来查看那个缓存配置类生成。可以得知SimpleCacheConfiguration的缓存配置类生效:
  • 4️⃣SimpleCacheConfiguration给容器中添加了ConcurrentMapCacheManager组件。
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
	
    //给容器中添加了ConcurrentMapCacheManager组件
	@Bean
	ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
			CacheManagerCustomizers cacheManagerCustomizers) {
		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		List<String> cacheNames = cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			cacheManager.setCacheNames(cacheNames);
		}
		return cacheManagerCustomizers.customize(cacheManager);
	}

}
  • 5️⃣ConcurrentMapCacheManager的部分源码如下:可以获取和创建ConcurrentMapCache类型的缓存组件,并把数据保存在ConcurrentMap中。
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

    //重写CacheManager的getCache方法
    @Override
	@Nullable
	public Cache getCache(String name) {
         //根据传入的name从CacheManager中获取Cache对象
		Cache cache = this.cacheMap.get(name);
        //如果cache不存在
		if (cache == null && this.dynamic) {
            //加锁
			synchronized (this.cacheMap) {
                //再获取一次
				cache = this.cacheMap.get(name);
                //如果依然不遵从
				if (cache == null) {
                     //创建一个新的ConcurrentMapCache并返回
					cache = createConcurrentMapCache(name);
                     //将上面创建的ConcurrentMapCache作为value放入到CacheManager中,key就是传入的name
					this.cacheMap.put(name, cache);
				}
			}
		}
		return cache;
	}
    
}    

4.3 @Cacheable注解的工作流程

  • 1️⃣方法运行之前,先去查询Cache(缓存组件),按照cacheNames(指定的缓存名称)从CacheManager中获取相应的缓存组件。

  • 2️⃣第一次获取缓存,如果没有对应的Cache组件,会自动创建Cache组件(默认为ConcurrentMapCache),然后将其保存到ConcurrentMapCacheManager中的cacheMap中,key是cacheNames,value是ConcurrentMapCache。

  • 3️⃣根据指定的key,即@Cacheable注解中属性key对应的值(默认是方法传入的实际参数值),去Cache中查找缓存的内容。

    • key是按照某种策略自自动生成的,默认是使用keyGenerator(SimpleKeyGenerator)生成的。
    • 如果没有参数,key=new SimpleKey()。
    • 如果有一个参数,key=参数值。
    • 如果有多个参数,key就是new SimipleKey(params)。
  • 4️⃣没有查到缓存就调用目标方法。

  • 5️⃣将目标方法返回的结果,放入到缓存。

4.4 @CachePut注解的使用

  • EmployeeService.java
package com.sunxiaping.springboot.service;

import com.sunxiaping.springboot.domain.Employee;

/**
 * @author 许威威
 * @version 1.0
 */
public interface EmployeeService {

    /**
     * 根据id查询员工信息
     *
     * @param id
     * @return
     */
    Employee findEmpById(Integer id);
    
    /**
     * 更新员工信息
     *
     * @param employee
     * @return
     */
    Employee updateEmp(Employee employee);

}
  • EmployeeServiceImpl.java
package com.sunxiaping.springboot.service.impl;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
import com.sunxiaping.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * @author 许威威
 * @version 1.0
 */
@Service
@Transactional
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;


    /**
     * @Cacheable注解: 将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不需要再调用方法
     * CacheManager管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件都有自己唯一的名称
     * @Cacheable注解的属性: value/cacheNames:指定缓存组件的名称
     * key:缓存数据使用的key,默认是方法参数的值(如果传入的参数是1,那么值就是1对应的返回的返回值。),支持SPEL表达式。
     * keyGenerator:key的生成器,可以自己指定key的生成器的组件id。key和keyGenerator二选一。
     * cacheManager:指定缓存管理器。
     * cacheResolver:指定缓存解析器。
     * condition:指定符合条件的情况下才缓存。支持SPEL表达式。
     * unless:否定缓存。当unless指定的条件为true,方法的返回值就不会返回。
     * sync:是否同步,默认值是false,即异步,在多线程环境中需要设置为true,避免缓存击穿的问题。
     */
    @Cacheable(cacheNames = "emp", key = "#id")
    @Override
    public Employee findEmpById(Integer id) {
        return Optional.ofNullable(employeeMapper.findEmpById(id)).orElse(new Employee());
    }

    /**
     * @CachePut employee 既调用方法,又更新缓存数据
     * 修改了数据库的某个数据,同时更新缓存
     * 运行时机:①先调用目标方法②将目标方法的结果放入到缓存中
     */
    @CachePut(cacheNames = "emp",key = "#employee.id")
    @Override
    public Employee updateEmp(Employee employee) {
        employeeMapper.updateEmp(employee);
        return employee;
    }
}

4.5 @CacheEvict注解的使用

  • EmployeeService.java
package com.sunxiaping.springboot.service;

import com.sunxiaping.springboot.domain.Employee;

/**
 * @author 许威威
 * @version 1.0
 */
public interface EmployeeService {

    /**
     * 根据id查询员工信息
     *
     * @param id
     * @return
     */
    Employee findEmpById(Integer id);

    /**
     * 更新员工信息
     *
     * @param employee
     * @return
     */
    Employee updateEmp(Employee employee);


    /**
     * 根据主键删除员工信息
     *
     * @param id
     */
    void deleteEmp(Integer id);
}
  • EmployeeServiceImpl.java
package com.sunxiaping.springboot.service.impl;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
import com.sunxiaping.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * @author 许威威
 * @version 1.0
 */
@Service
@Transactional
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;


    /**
     * @Cacheable注解: 将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不需要再调用方法
     * CacheManager管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件都有自己唯一的名称
     * @Cacheable注解的属性: value/cacheNames:指定缓存组件的名称
     * key:缓存数据使用的key,默认是方法参数的值(如果传入的参数是1,那么值就是1对应的返回的返回值。),支持SPEL表达式。
     * keyGenerator:key的生成器,可以自己指定key的生成器的组件id。key和keyGenerator二选一。
     * cacheManager:指定缓存管理器。
     * cacheResolver:指定缓存解析器。
     * condition:指定符合条件的情况下才缓存。支持SPEL表达式。
     * unless:否定缓存。当unless指定的条件为true,方法的返回值就不会返回。
     * sync:是否同步,默认值是false,即异步,在多线程环境中需要设置为true,避免缓存击穿的问题。
     */
    @Cacheable(cacheNames = "emp", key = "#id")
    @Override
    public Employee findEmpById(Integer id) {
        return Optional.ofNullable(employeeMapper.findEmpById(id)).orElse(new Employee());
    }

    /**
     * @CachePut employee 既调用方法,又更新缓存数据
     * 修改了数据库的某个数据,同时更新缓存
     * 运行时机:①先调用目标方法②将目标方法的结果放入到缓存中
     */
    @CachePut(cacheNames = "emp", key = "#employee.id")
    @Override
    public Employee updateEmp(Employee employee) {
        employeeMapper.updateEmp(employee);
        return employee;
    }

    /**
     * @CacheEvict 清除缓存
     * key:缓存的key。
     * allEntries:清空所有的缓存,默认是false。
     * beforeInvocation:是否在方法之前执行,默认值是false,即表示方法执行之后执行。
     */
    @CacheEvict(cacheNames = "emp",key = "#id")
    @Override
    public void deleteEmp(Integer id) {
        employeeMapper.deleteEmpById(id);
    }
}

5 整合Redis实现缓存

5.1 导入SpringBoot和Redis的整合包的Maven坐标

  • spring-boot-starter-data-redis的坐标:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 完整的pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

5.2 修改application.yml增加redis的相关配置

  • application.yml:
spring:
  #redis的配置
  redis:
    # 主机地址
    host: 192.168.1.57
    # 端口
    port: 6379
    # 密码
    password:
  • 完整的application.yml:
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    # 数据源的其他配置
    druid:
      # 初始化
      initial-size: 5
      # 最小
      min-idle: 5
      # 最大
      max-active: 20
      # 连接等待超时时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-on-borrow: false
      test-while-idle: true
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall
      #合并多个DruidDataSource的监控数据
      use-global-data-source-stat: true
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 配置DruidStatFilter
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      # 配置DruidStatViewServlet
      stat-view-servlet:
        enabled: true
        url-pattern: "/druid/*"
        # IP白名单(没有配置或者为空,则允许所有访问)
        #        allow:
        # IP黑名单 (存在共同时,deny优先于allow)
        #        deny:
        #  禁用HTML页面上的“Reset All”功能
        reset-enable: false
        # 登录名
        login-username: admin
        # 登录密码
        login-password: 123456
  # redis的配置
  redis:
    # 主机地址
    host: 192.168.1.57
    # 端口
    port: 6379
    # 密码
    password:


# 开启mybatis的日志
logging:
  level:
    com.sunxiaping.springboot.mapper: debug #打印sql

5.3 修改默认的序列化机制

  • spring-data-redis的默认序列化机制是JDK序列化器,实际使用的时候,可以改为JSON形式的序列化器。
  • 更改默认的序列化器:
package com.sunxiaping.springboot.config;

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;

/**
 * @author 许威威
 * @version 1.0
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        //开启事务
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

5.4 测试SpringBoot整合Redis

  • 测试:
package com.sunxiaping.springboot;

import com.sunxiaping.springboot.domain.Employee;
import com.sunxiaping.springboot.mapper.EmployeeMapper;
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.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Optional;

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

    /**
     * RedisTemplate k-v都是Object
     */
    @Autowired
    private RedisTemplate redisTemplate;


    @Autowired
    private EmployeeMapper employeeMapper;

    /**
     * StringRedisTemplate k-v都是字符串
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * Redis常见的五大数据类型:
     * String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
     * <p>
     * //操作String字符串
     * ValueOperations valueOperations = stringRedisTemplate.opsForValue();
     * //操作List列表
     * ListOperations listOperations = stringRedisTemplate.opsForList();
     * //操作Set集合
     * SetOperations setOperations = stringRedisTemplate.opsForSet();
     * //操作Hash散列
     * HashOperations hashOperations = stringRedisTemplate.opsForHash();
     * //操作Zset有序集合
     * ZSetOperations zSetOperations = stringRedisTemplate.opsForZSet();
     */
    @Test
    public void test() {
        ValueOperations valueOperations = stringRedisTemplate.opsForValue();
        valueOperations.set("k1", "v1");
        Object value = valueOperations.get("k1");
        System.out.println("value = " + value);
    }

    /**
     * RedisTemplate默认用的是JDK的序列化
     */
    @Test
    public void test2() {
        Employee employee = employeeMapper.findEmpById(1);
        Optional.ofNullable(employee).ifPresent((emp) -> {
            redisTemplate.opsForValue().set("emp01", emp);
        });
    }
}

5.5 自定义CacheManager,使其缓存变为Redis

SpringBoot的版本是2.x。

5.5.1 原理

  • 1️⃣引入spring-boot-starter-data-redis.jar包以后,容器中保存的是RedisCacheManager。

  • 2️⃣RedisCacheManager帮我们创建RedisCache作为缓存组件,RedisCache通过操作Redis缓存数据。

  • 3️⃣默认保存数据k-v都是Object类型,利用的是JDK的序列化机制进行序列化的。

    • 引入了Redis的starter,cacheManager变为RedisCacheManager。
    • 默认创建的RedisCacheManager操作Redis的时候使用的是RedisTemplate<Object,Object>。
    • RedisTemplate<Object,Object>默认默认使用的是JDK的序列化机制。
  • 4️⃣自定义CacheManager。

5.5.2 SpringBoot使用Redis作为缓存管理器

  • RedisConfig.java
package com.sunxiaping.springboot.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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;

/**
 * @author 许威威
 * @version 1.0
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        //开启事务
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }


    /**
     * 自定义Redis缓存管理器
     *
     * @param factory
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        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);

        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ZERO)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}
posted @ 2020-09-22 14:12  许大仙  阅读(562)  评论(0编辑  收藏  举报