Spring Boot - 缓存

准备

IDE

Spring Tool Suite 4
Version: 4.4.0.RELEASE

JDK

$ java -version
java version "1.8.0_241"
Java(TM) SE Runtime Environment (build 1.8.0_241-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode)

MySQL 数据库

建表语句以及预设数据:

CREATE TABLE `commodity` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品标识符',
  `name` varchar(16) NOT NULL COMMENT '商品名称',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

INSERT INTO `commodity` (id, name) VALUES (DEFAULT, '李子');
INSERT INTO `commodity` (id, name) VALUES (DEFAULT, '栗子');
INSERT INTO `commodity` (id, name) VALUES (DEFAULT, '梨子');

项目搭建

新建 Spring Starter Project:

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

修改 pom.xml 文件,引入 Druid、MyBatis Plus 等依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
    
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
    
	<groupId>com.mk</groupId>
	<artifactId>spring-boot-cache</artifactId>
	<version>1.0.0</version>
    
	<name>spring-boot-cache</name>
	<description>缓存</description>

	<properties>
		<java.version>1.8</java.version>
        <druid-spring-boot-starter.version>1.1.21</druid-spring-boot-starter.version>
        <mybatis-plus-boot-starter.version>3.3.1.tmp</mybatis-plus-boot-starter.version>
	</properties>

	<dependencies>
		<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>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

        <!-- Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

新建 application.yml 文件,配置 Druid、MyBatis-Plus 等参数:

server:
  port: 8080
  servlet:
    session:
      timeout: 12h

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.88.158:3306/spring-boot?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=true
      username: root
      password: 123456
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 30000 # 获取连接等待超时的时间
      time-between-eviction-runs-millis: 2000 # 间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      min-evictable-idle-time-millis: 600000 # 一个连接在池中最小生存的时间,单位是毫秒
      max-evictable-idle-time-millis: 900000
      validation-query: select '1'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat,wall,slf4j
      web-stat-filter: # WebStatFilter 配置
        enabled: true
        url-pattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
      stat-view-servlet: # StatViewServlet 配置
        enabled: true
        url-pattern: /druid/*
        reset-enable: false
        login-username: admin
        login-password: 123456
#        allow: 127.0.0.1
#        deny: 192.168.1.6
#      aop-patterns: # Spring 监控配置
#      - com.mk.service.*
      filter:
        stat: # 慢 SQL 记录
          slow-sql-millis: 3000
          log-slow-sql: true

logging:
  level:
    com.mk: DEBUG

mybatis-plus:
#  mapper-locations:
#  - classpath:mapper/*Mapper.xml
#  global-config:
#    db-config:
#      id-type: AUTO # 数据库的表的主键生成策略
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 日志
  type-aliases-package: com.mk.pojo

新建 MyBatis-Plus 配置类:

package com.mk.configuration;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;

@Configuration
@MapperScan(basePackages = {
    
     "com.mk.mapper" }) // 扫描 Mapper 接口
@EnableTransactionManagement // 开启事务管理
public class MyBatisPlusConfiguration {
    
    

    /**
     * <p> 分页拦截器
     * @return
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
    
    
        return new PaginationInterceptor();
    }
}

启动类:

package com.mk;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootCacheApplication {
    
    

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

商品表实体类:

package com.mk.pojo;

import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Commodity extends Model<Commodity> {
    
    

    private static final long serialVersionUID=1L;

    /**
     * 商品标识符
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 商品名称
     */
    private String name;

    /**
     * 创建时间
     */
    private String createTime;

    /**
     * 更新时间
     */
    private String updateTime;


    @Override
    protected Serializable pkVal() {
    
    
        return this.id;
    }
}

商品表 Mapper 接口及其映射文件:

package com.mk.mapper;

import com.mk.pojo.Commodity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface CommodityMapper extends BaseMapper<Commodity> {
    
    

}
<?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.mk.mapper.CommodityMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.mk.pojo.Commodity">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, create_time, update_time
    </sql>

</mapper>

商品表服务接口及其实现类:

package com.mk.service;

import com.mk.pojo.Commodity;
import com.baomidou.mybatisplus.extension.service.IService;

public interface CommodityService extends IService<Commodity> {
    
    

}
package com.mk.service.impl;

import com.mk.pojo.Commodity;
import com.mk.mapper.CommodityMapper;
import com.mk.service.CommodityService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class CommodityServiceImpl extends ServiceImpl<CommodityMapper, Commodity> implements CommodityService {
    
    

}

商品表前端控制器:

package com.mk.controller;


import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

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;

import com.mk.service.CommodityService;

@RestController
@RequestMapping("/commodity")
public class CommodityController {
    
    

    @Autowired
    private CommodityService commodityService;
    
    /**
     * <p> 根据标识符查询
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Map<String, Object> getById(@PathVariable("id") Integer id) {
    
    
        Map<String, Object> result = new HashMap<>();
        result.put("datetime", LocalDateTime.now());
        result.put("data", this.commodityService.getById(id));
        return result;
    }
}

运行启动类,访问 http://localhost:8080/commodity/1,若能返回预期的数据即可:

在这里插入图片描述

使用 @Cacheable 缓存数据

修改商品服务实现类,重写 getById 方法,添加 @Cacheable 注解,实现查询缓存或缓存数据:

// ... 其他,略

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CommodityServiceImpl extends ServiceImpl<CommodityMapper, Commodity> implements CommodityService {
    
    

    @Override
    @Cacheable(cacheNames = "commodity", key = "#id") // cacheNames 指定缓存容器的名称,一个缓存容器中可以有多条缓存数据。key 指定缓存数据的键值。
    public Commodity getById(Serializable id) {
    
    
        return super.getById(id);
    }
}

重启应用,清空控制台输出,多次访问 http://localhost:8080/commodity/1,观察控制台输出,可以看到只有第一次访问是查询了数据库,随后几次都没有,说明随后的查询是直接从缓存中获取数据,而非数据库:

2020-09-20 16:10:04.404 DEBUG 10644 --- [nio-8080-exec-1] c.mk.mapper.CommodityMapper.selectById   : ==>  Preparing: SELECT id,name,create_time,update_time FROM commodity WHERE id=? 
2020-09-20 16:10:04.405 DEBUG 10644 --- [nio-8080-exec-1] c.mk.mapper.CommodityMapper.selectById   : ==> Parameters: 1(Integer)
2020-09-20 16:10:04.407 DEBUG 10644 --- [nio-8080-exec-1] c.mk.mapper.CommodityMapper.selectById   : <==      Total: 1

底层配置与实现(暂不分析)

线索:CacheAutoConfigurationSimpleCacheConfiguration

使用 @CachePut 更新缓存数据

在没有使用 @CachePut 的情况下更新数据库中的数据,是不会同步更新到缓存中的。如果要实现同步更新到缓存,就需要使用 @CachePut

修改商品服务接口,添加 updateById 方法:

public interface CommodityService extends IService<Commodity> {
    
    

    /**
     * <p> 根据标识符更新
     * @param id
     * @param entity
     * @return
     * @throws Exception
     */
    Commodity updateById(Serializable id, Commodity entity) throws Exception;
}

修改商品服务实现类,实现 updateById 方法:

// ... 其他,略

import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CommodityServiceImpl extends ServiceImpl<CommodityMapper, Commodity> implements CommodityService {
    
    

    // ... 其他,略

    @Override
    @CachePut(cacheNames = "commodity", key = "#id") // @CachePut 在方法执行完成之后起作用,其使用方法的返回值做为缓存数据,key 指定更新缓存容器中的那一条数据。
    public Commodity updateById(Serializable id, Commodity entity) throws Exception {
    
    
        entity.setId((Integer) id);
        boolean result = super.updateById(entity);
        
        Commodity record = null;
        if (result) {
    
    
            record = super.getById(id);
        } else {
    
    
            throw new Exception("更新失败");
        }
        return record;
    }
}

修改商品前端控制器类,添加 updateById 方法:

// ... 其他,略

@RestController
@RequestMapping("/commodity")
public class CommodityController {
    
    

    // ... 其他,略
    
    /**
     * <p> 根据标识符更新数据
     * @param entity
     * @return
     */
    @PostMapping("")
    public Map<String, Object> updateById(Commodity entity) {
    
    
        Map<String, Object> result = new HashMap<>();
        
        result.put("datetime", LocalDateTime.now());
        try {
    
    
            result.put("data", this.commodityService.updateById(entity.getId(), entity));
        } catch (Exception e) {
    
    
            result.put("message", e.getMessage());
        }
        
        return result;
    }
}

重启应用,使用 Postman 向 http://127.0.0.1:8080/commodity 发起一个 POST 请求,更新 id = 1 的数据:

在这里插入图片描述

更新成功之后,数据库表中的数据:

mysql> select * from commodity where id = 1;
+----+--------+---------------------+---------------------+
| id | name   | create_time         | update_time         |
+----+--------+---------------------+---------------------+
|  1 | 苹果   | 2020-09-20 02:37:59 | 2020-09-20 09:50:30 |
+----+--------+---------------------+---------------------+
1 row in set (0.00 sec)

控制台返回的数据:

2020-09-20 17:50:46.070 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.updateById   : ==>  Preparing: UPDATE commodity SET name=? WHERE id=? 
2020-09-20 17:50:46.071 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.updateById   : ==> Parameters: 苹果(String), 1(Integer)
2020-09-20 17:50:46.074 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.updateById   : <==    Updates: 1
2020-09-20 17:50:46.075 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : ==>  Preparing: SELECT id,name,create_time,update_time FROM commodity WHERE id=? 
2020-09-20 17:50:46.076 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : ==> Parameters: 1(Integer)
2020-09-20 17:50:46.079 DEBUG 11452 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : <==      Total: 1

完成以上操作之后,清空控制台信息,访问 http://localhost:8080/commodity/1,查询 id = 1 的数据:

在这里插入图片描述

观察控制台的输出,可以发现,控制台并没有任何输出,说明数据来自缓存,并且该数据与更新结果一致,说明更新的数据同步到缓存中。

使用 @CacheEvict 删除缓存数据

在没有使用 @CacheEvict 的情况下删除数据库中的数据,是不会同步删除缓存数据。如果要实现同步删除缓存数据,就需要使用 @CacheEvict

修改商品服务实现类,重写 removeById 方法,添加 @CacheEvict 注解,指定缓存容器和将要删除的缓存数据的键值:

// ... 其他,略

import org.springframework.cache.annotation.CacheEvict;

@Service
public class CommodityServiceImpl extends ServiceImpl<CommodityMapper, Commodity> implements CommodityService {
    
    

    // ... 其他,略
    
    @Override
    @CacheEvict(cacheNames = "commodity", key = "#id")
    public boolean removeById(Serializable id) {
    
    
        return super.removeById(id);
    }
}

修改商品前端控制器,添加 deleteById 方法:

// ... 其他,略

@RestController
@RequestMapping("/commodity")
public class CommodityController {
    
    

    @Autowired
    private CommodityService commodityService;
    
    // ... 其他,略
    
    /**
     * <p> 根据标识符删除
     * @return
     */
    @DeleteMapping("/{id}")
    public Map<String, Object> deleteById(@PathVariable("id") Integer id) {
    
    
        Map<String, Object> result = new HashMap<>();
        
        result.put("datetime", LocalDateTime.now());
        result.put("data", this.commodityService.removeById(id));
        
        return result;
    }
}

重启应用,使用浏览器访问 http://localhost:8080/commodity/1,使 id = 1 的数据被缓存。

使用 Postman 向 http://127.0.0.1:8080/commodity/1 发起一个 DELETE 请求,删除 id = 1 的数据:

在这里插入图片描述

再次使用浏览器访问 http://localhost:8080/commodity/1,可以看到返回的数据为空:

在这里插入图片描述

观察控制台输出的信息,可以看到 Spring Boot 在数据库查询数据,说明缓存中的数据已经被删除了。

2020-09-20 21:09:34.752 DEBUG 4824 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : ==>  Preparing: SELECT id,name,create_time,update_time FROM commodity WHERE id=? 
2020-09-20 21:09:34.752 DEBUG 4824 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : ==> Parameters: 1(Integer)
2020-09-20 21:09:34.754 DEBUG 4824 --- [nio-8080-exec-8] c.mk.mapper.CommodityMapper.selectById   : <==      Total: 0

整合 Redis 缓存

整合 Redis 之前,需要准备 Redis 服务器,参考:Docker - 部署 Redis 6.0.8

接着,在 pom.xml 文件引入以下依赖:

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

修改 application.yml 配置文件,添加 Redis 配置:

spring:
  redis:
    database: 0 # Database index used by the connection factory.
    host: 192.168.88.158 # Redis server host.
    port: 6379 # Redis server port.
    password: 123456 # Login password of the redis server.
    timeout: 3000 # Connection timeout.
    lettuce:
      pool:
        max-active: 8
        max-wait: 1
        min-idle: 0
        max-idle: 8

debug: true

Spring Boot 默认使用 SimpleCacheConfiguration 配置,引入 Redis 依赖之后,Spring Boot 将使用 Redis 做为缓存,控制台将输出如下信息:

   RedisCacheConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.data.redis.connection.RedisConnectionFactory' (OnClassCondition)
      - Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)
      - @ConditionalOnBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) found bean 'redisConnectionFactory'; @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)

猜你喜欢

转载自blog.csdn.net/qq_29761395/article/details/108719439