文章目录
准备
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
底层配置与实现(暂不分析)
线索:CacheAutoConfiguration
,SimpleCacheConfiguration
使用 @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)