Spring 全家桶之 Spring Boot 2.6.4( Ⅰ )- Caching(Part A)

2022-09-26 15:59:21 浏览数 (1)


一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情。

一、Spring Cache Abstraction

The Spring Framework provides support for transparently adding caching to an application. At its core, the abstraction applies caching to methods, thus reducing the number of executions based on the information available in the cache.

Spring 缓存抽象是一套缓存规范,其中定义了org.springframework.cache.Cache和CacheManager两个接口来统一不同的缓存技术

  • Cache接口为缓存的组件提供规范定义,包含缓存的各种操作集合
  • Cache接口下的Spring提供了各种xxxCache的实现,RedisCache、EhCacheCache、ConcurrentMapCache等
  • CacheManager是缓存管理器,管理各种缓存组件

CacheManager和Cache的关系就类似于数据库连接池和数据库连接一样。

Spring缓存抽象同时支持JCache的注解来简化开发,JCache也是一套规范。

JCache 定义了5个核心接口,分别是:

  • CachingProvider:定义了创建、配置、获取、管理和控制多特CacheManager,一个应用在运行期间可以访问多个CachingProvider
  • CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这个Cache存在于CacheManager的上下文中,一个CacheManager只能属于一个CachingProvider
  • Cache:一个类似Map的数据结构,临时存储以Key为索引的值,一个Cache只能属于一个CacheManager
  • Entry:存储在Cache中的Key-Value对
  • Expiry:每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个事件,条目为过期状态且无法访问、更新和删除,缓存有效期可以通过ExpiryPolicy设置

为了简化开发,更多是使用Spring的缓存抽象,Spring的缓存抽象的底层概念与JSR107是一致的

Spring 缓存抽象中的重要注解

  • @Cacheable:根据方法的请求参数对结果进行缓存
  • @CacheEvict:清空缓存
  • @CachePut:保证方法被调用,同时缓存结果
  • @EnableCaching:开启基于注解的缓存

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

You can also use the standard JSR-107 (JCache) annotations (such as @CacheResult) transparently. However, we strongly advise you to not mix and match the Spring Cache and JCache annotations.

Spring Boot 官方建议不要混用 Spring Cache 和 JCache 的注解

工程搭建与测试

创建spring boot工程spring-boot-cache

根据SQL文件新建tesla、factory两张表

代码语言:javascript复制
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for factory
-- ----------------------------
DROP TABLE IF EXISTS `factory`;
CREATE TABLE `factory` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `factory_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1166057542 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of factory
-- ----------------------------
BEGIN;
INSERT INTO `factory` VALUES (1, '上海特斯拉超级工厂');
INSERT INTO `factory` VALUES (2, '加州弗拉蒙特特斯拉超级工厂');
INSERT INTO `factory` VALUES (3, '得克萨斯州特斯拉超级工厂');
INSERT INTO `factory` VALUES (4, '柏林特斯拉超级工厂');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
代码语言:javascript复制
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tesla
-- ----------------------------
DROP TABLE IF EXISTS `tesla`;
CREATE TABLE `tesla` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `price` double(10,2) DEFAULT NULL,
  `vehicle_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `factory_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1166057542 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tesla
-- ----------------------------
BEGIN;
INSERT INTO `tesla` VALUES (1166057520, 'Model 3P 2021', 280000.00, '四门轿车', 1);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

entity包下增加Tesla和Factory实体类

代码语言:javascript复制
@Data
public class Tesla {

    private Integer id;
    private String name;
    private Double price;
    private String vehicleType;
    private Integer factoryId;
}
代码语言:javascript复制
@Data
public class Factory {

    private Integer id;
    private String factoryName;
}

application.yml中配置druid

代码语言:javascript复制
spring:
  datasource:
    # driver可以不写,会根据连接自动判断
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/test
    druid:
      # 初始化大小,最小,最大
      initial-size: 5
      max-active: 100
      min-idle: 1
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存时间
      min-evictable-idle-time-millis: 300000
      # 用来检测连接是否有效的sql 必须是一个查询语句 注意没有此语句以下三个属性不会生效
      validation-query: SELECT 1 FROM DUAL
      # 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
      test-on-return: false
      # 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
      test-on-borrow: true
      # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      test-while-idle: true
      # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙
      filters: stat,wall
      # 通过 connection-properties 属性打开 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:
        url-pattern: /druid/*
        # IP 白名单,没有配置或者为空,则允许所有访问
        #allow: 127.0.0.1
        # IP 黑名单,若白名单也存在,则优先使用
        #deny: 192.168.31.253
        # 禁用 HTML 中 Reset All 按钮
        reset-enable: true
        # 登录用户名/密码
        login-username: root
        login-password: 123
        # 注意 此处必须开启,否则无法访问druid监控面板
        enabled: true
      use-global-data-source-stat: true
logging:
  level:
    # 输出mapper接口中方法执行的SQL语句
    com.lilith.mapper: debug

mybatis:
  # 全局配置文件的位置
  config-location: classpath:mybatis-config.xml
  mapper-locations: classpath:mappers/*.xml
    # configuration:
  # map-underscore-to-camel-case: true

debug: true

在mybatis-config.xml中配置MyBatis全局配置

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.lilith.entity"/>
    </typeAliases>
</configuration>

mapper包下增加TeslaMapper和FactoryMapper两个接口,实现增删改查方法

代码语言:javascript复制
public interface TeslaMapper {

    void insert(Tesla tesla);

    Tesla selectOneById(Integer id);

    void update(Tesla tesla);

    void deleteOneById(Integer id);
}
代码语言:javascript复制
public interface FactoryMapper {

    Factory selectOneById(Integer id);

    void deleteOneById(Integer id);

    void insert(Factory factory);

    void update(Factory factory);
}

resources目录下mappers文件夹中的Mapper XML映射文件TeslaMapper.xml和FactoryMapper.xml

代码语言:javascript复制
<?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.lilith.mapper.TeslaMapper">

    <sql id="Base_Columns_List">
        id, name, price, vehicle_type, factory_id
    </sql>

    <select id="selectOneById" resultType="tesla">
        select <include refid="Base_Columns_List" /> from tesla where id = #{id}
    </select>

    <insert id="insert">
        INSERT INTO tesla (name, price, vehicle_type, factory_id)
        VALUES (#{name}, #{price}, #{vehicleType},#{factoryId})
    </insert>

    <update id="update">
        UPDATE tesla SET name = #{name}, price = #{price}, vehicle_type = #{vehicleType}, factory_id = #{factoryId}
        WHERE id = #{id}
    </update>

    <delete id="deleteOneById">
        DELETE FROM tesla WHERE id=#{id}
    </delete>

</mapper>
代码语言:javascript复制
<?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.lilith.mapper.FactoryMapper">

    <sql id="Base_Columns_List">
        id, factory_name
    </sql>

    <select id="selectOneById" resultType="factory">
        SELECT <include refid="Base_Columns_List" /> FROM factory WHERE id = #{id}
    </select>

    <delete id="deleteOneById">
        DELETE FROM factory WHERE id=#{id}
    </delete>

    <insert id="insert" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
        INSERT INTO factory (factory_name)
        VALUES (#{factoryName})
    </insert>

    <update id="update">
        UPDATE factory SET factory_name = #{factoryName} where id = #{id}
    </update>

</mapper>

初始化spring-boot-cache工程的步骤:

  1. 创建tesla和factory两张表
  2. spring-boot-cache中entity包下创建Tesla和Factory实体类
  3. 整合Druid数据源
  4. 配置MyBatis
  5. 新建TeslaMapper、FactoryMapper并增加增删改查方法
  6. 测试TeslaMapper、FactoryMapper的增删改查方法

在配置完成数据访问层并测试成功之后,新建service包并在该目录下新增impl包,在service包中新增TeslaService接口并在impl包中新增TeslaService的实现类TeslaServiceImpl

代码语言:javascript复制
public interface TeslaService {
    
    Tesla getTeslaById(Integer id);
}
代码语言:javascript复制
@Service
@Slf4j
public class TeslaServiceImpl implements TeslaService {

    @Autowired
    private TeslaMapper teslaMapper;

    @Override
    public Tesla getTeslaById(Integer id) {
        log.info("查询"   id   "特斯拉");
        return teslaMapper.selectOneById(id);
    }
}

新增controller包,增加TeslaController类,返回JSON格式数据

代码语言:javascript复制
@RestController
public class TeslaController {

    @Autowired
    private TeslaService teslaService;

    @GetMapping("/tesla/{id}")
    public Tesla find(@PathVariable("id") Integer id){
        Tesla teslaById = teslaService.getTeslaById(id);
        return teslaById;
    }
}

启动应用,测试Controller类,不要忘了在主程序类上添加@MapperScan扫描所有的Mapper接口

Spring Cache 的使用

首先在主程序类上使用@EnableCaching来开启基于注解的缓存

@Cacheable

在在TeslaServiceImpl类上的getTeslaById()方法未添加@Cacheable注解之前,每一次查询都会调用数据库执行SQL语句,@Cacheable注解可以将方法运行的结果缓存,以后查询结果相同的数据直接缓存中获取,不会在调用方法

增加@Cacheable注解后,添加cacheNames属性,重启应用,多次查询只会执行一次SQL语句

@Cacheable的几个属性

  • cacheNames/value:指定缓存组件的名字
  • key:缓存数据用的Key,可以用来来指定具体的缓存内容,默认使用的是方法的参数值,也可以通过SpEL指定,如"#id"既获取方法的参数id,"#result"为方法执行的结果
  • keyGenerator:Key的生成器,可以自己指定Key的生成器组件id,key/keyGenerator二选一使用
  • cacheManager:指定缓存管理器,或者CacheResolver指定获取解析器
  • condition:指定符合条件的情况下才会缓存方法的执行结果,如果condition="#id>0"
  • unless:否定缓存,当unless的条件为true时,方法的返回值不会被缓存,如unless="#result==null"既当结果返回为null时不缓存
  • sync:是否使用异步模式

缓存SpELl表达式

名称

位置

描述

示例

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.name[0]

argument name

evaluation context

方法参数的名字,可以直接 "#参数名", 也可以使用"#p0"或者"#a0", 0代表索引

#p0

result

evaluation context

方法执行后的返回值,仅当该结果会被缓存时才可使用

#result

0 人点赞