第 5 章 Spring Boot 数据访问
技术栈:Spring Boot 4.1.x + MyBatis-Plus 3.5.x + Spring Data JPA + Spring Data Redis 3.x 本章定位:从内存临时数据落地到持久化存储,全面讲解关系型数据库访问、缓存数据库集成两大核心能力,覆盖三种主流企业级技术方案。 学习目标:重点掌握 MyBatis-Plus 企业级开发流程,理解 JPA 基本用法,掌握 Redis 缓存核心模式与问题处理,能独立完成项目数据层设计与开发。
写给新手的话: 前面我们写的接口、页面,数据都临时存在内存的 List 里,项目重启就全部丢失。实际企业项目中,核心业务数据必须持久化存储在数据库中,通过专门的数据访问层统一操作。 本章系统讲解三种主流数据访问方案:
- Spring Data JPA:全自动 ORM,单表 CRUD 几乎不用写 SQL
- MyBatis-Plus:半自动 ORM,SQL 自主可控,国内企业绝对主流
- Redis:内存缓存数据库,承载热点数据,大幅提升系统并发性能
学习建议:
- 重点攻克 MyBatis-Plus:国内互联网公司标配,面试高频考点
- 掌握 Redis 核心用法与缓存模式:后端工程师必备技能
- 了解 Spring Data JPA:知道基本用法与优缺点,面试能表述即可
- 务必动手实操:数据访问光看没用,必须跑通项目、看 SQL 执行日志、调通异常
5.1 数据访问层基础
5.1.1 为什么需要数据访问层
内存存储的核心问题:
- 重启丢失:项目停止或重启,所有数据清空
- 无法共享:多实例部署时,各节点数据独立不互通
- 容量有限:内存成本高,无法存储海量业务数据
- 查询低效:复杂查询需要遍历集合,性能远低于数据库索引查询
数据访问层(DAO/Mapper/Repository 层)的作用:
- 屏蔽底层数据库差异,业务代码不用关心具体数据库
- 统一数据操作规范,SQL 与业务逻辑分离
- 便于后续优化、监控、扩展,业务与存储解耦
5.1.2 数据访问技术演进史
Java 数据库访问技术经历了四代演进,逐步提升开发效率与灵活性:
| 代际 | 技术方案 | 核心特点 | 优点 | 缺点 |
|---|---|---|---|---|
| 第一代 | 原生 JDBC | 手动写 SQL、手动管理连接、手动封装结果 | 灵活可控、底层透明 | 代码冗余重复、开发效率极低 |
| 第二代 | Spring JDBC / DbUtils | 封装连接与结果映射,减少重复代码 | 仍需手写 SQL、分页等通用逻辑需自己实现 | |
| 第三代 | ORM 框架(Hibernate / JPA) | 对象关系映射,自动生成 SQL、自动映射结果 | 开发效率高、数据库无关 | 复杂 SQL 优化困难、学习曲线陡 |
| 第四代 | 半自动 ORM(MyBatis / MyBatis-Plus) | SQL 自主编写,结果自动映射 | 灵活可控、性能易优化、生态完善 | 单表通用逻辑仍需重复编写 |
国内现状:MyBatis 及其增强版 MyBatis-Plus 占据绝对主流;JPA 在传统企业、国外项目使用较多。
5.1.3 Spring Data 生态
Spring Data 是 Spring 家族的统一数据访问子项目,核心思想是定义统一的 Repository 接口规范,无论底层是关系型数据库、缓存还是 NoSQL,编程风格保持一致,降低不同存储技术的学习成本。
Spring Data 主流成员:
| 项目 | 对应存储类型 |
|---|---|
| Spring Data JPA | MySQL、Oracle 等关系型数据库 |
| Spring Data Redis | Redis 缓存数据库 |
| Spring Data MongoDB | MongoDB 文档数据库 |
| Spring Data Elasticsearch | Elasticsearch 搜索引擎 |
5.2 Spring Data JPA:全自动 ORM
5.2.1 核心概念
- JPA:Java Persistence API,是 Java 持久化的官方规范,只定义接口标准,不提供实现。
- Hibernate:JPA 最主流的实现框架,负责具体的 SQL 生成、对象映射。
- Spring Data JPA:在 JPA 规范基础上再次封装,提供统一的 Repository 接口、方法名查询等增强能力,进一步简化开发。
类比:JPA = 充电接口标准,Hibernate = 充电器厂商,Spring Data JPA = 带快充协议的充电套装。
5.2.2 快速入门
步骤 1:引入依赖
<!-- Spring Data JPA 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>步骤 2:配置数据库与 JPA
spring:
datasource:
url: jdbc:mysql://localhost:3306/chapter05?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 自动更新表结构,开发环境用
show-sql: true # 控制台打印执行的 SQL
properties:
hibernate:
format_sql: true # 格式化 SQL,便于阅读ddl-auto 配置说明(生产环境严禁使用 create):
| 选项 | 作用 | 适用场景 |
|---|---|---|
update | 表不存在则创建,实体变更则更新表结构,保留数据 | 开发环境 |
create | 每次启动都删除重建表,数据全部清空 | 单元测试 |
create-drop | 启动建表,关闭删表 | 单元测试 |
validate | 只校验表结构与实体是否匹配,不修改 | 生产环境 |
none | 不做任何操作 | 生产环境 |
步骤 3:准备数据库
创建数据库,无需手动建表(JPA 自动生成):
CREATE DATABASE chapter05 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;步骤 4:编写实体类
@Entity
@Table(name = "book")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; // 主键,数据库自增
@Column(name = "name", length = 100)
private String name; // 图书名称
private String author; // 作者
private String press; // 出版社
private String status; // 状态:0-上架 1-下架
}核心注解说明:
| 注解 | 作用 |
|---|---|
@Entity | 标记当前类为 JPA 实体类 |
@Table(name = "book") | 指定对应数据库表名,不写默认类名小写 |
@Id | 标记该字段为主键 |
@GeneratedValue | 主键生成策略,IDENTITY 对应 MySQL 自增 |
@Column | 指定列名、长度、约束等,不写默认属性名转下划线 |
@Transient | 标记该字段不映射数据库列 |
步骤 5:编写 Repository 接口
@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
// 无需编写实现类,框架自动生成实现
}继承 JpaRepository<实体类, 主键类型> 后,自动拥有全套 CRUD 方法:
findById(id):根据 ID 查询findAll():查询所有findAll(Pageable):分页查询save(entity):新增/更新(有 ID 则更新,无 ID 则新增)deleteById(id):根据 ID 删除count():统计总数existsById(id):判断是否存在
步骤 6:接口测试
@RestController
@RequestMapping("/jpa/book")
public class JpaBookController {
@Autowired
private BookRepository bookRepository;
@GetMapping("/list")
public List<Book> list() {
return bookRepository.findAll();
}
@GetMapping("/{id}")
public Book getById(@PathVariable Integer id) {
return bookRepository.findById(id).orElse(null);
}
@PostMapping
public Book add(@RequestBody Book book) {
return bookRepository.save(book);
}
}启动项目后,数据库会自动生成 book 表,控制台可看到执行的 SQL 语句。
5.2.3 方法名查询(规则生成 SQL)
JPA 支持按照命名规则编写方法名,框架自动解析生成 SQL,无需手动编写。
@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
// 根据作者精确查询
Book findByAuthor(String author);
// 根据作者 + 状态联合查询
Book findByAuthorAndStatus(String author, String status);
// 书名模糊查询
List<Book> findByNameContaining(String name);
// 根据作者查询并按 ID 倒序
List<Book> findByAuthorOrderByIdDesc(String author);
// 统计指定作者的图书数量
Long countByAuthor(String author);
// 根据作者删除
void deleteByAuthor(String author);
}常用关键字:findBy、And、Or、Between、Like、Containing、OrderBy、countBy、deleteBy、existsBy。
适用场景:简单单表查询;复杂查询不建议使用过长的方法名,可读性差且维护困难。
5.2.4 @Query 自定义查询
复杂查询通过 @Query 注解手动编写,支持 JPQL 和原生 SQL 两种方式。
JPQL 方式(推荐,面向对象)
操作的是类和属性,不是表和列:
@Query("SELECT b FROM Book b WHERE b.author = :author AND b.status = :status")
Book findByAuthorAndStatus(@Param("author") String author,
@Param("status") String status);
// 增删改操作必须加 @Modifying 和 @Transactional
@Modifying
@Transactional
@Query("DELETE FROM Book b WHERE b.id = :id")
Integer deleteBookById(@Param("id") Integer id);原生 SQL 方式
@Query(value = "SELECT * FROM book WHERE author = ?1", nativeQuery = true)
List<Book> findByAuthorNative(String author);5.2.5 分页与排序
// 分页+排序:第0页,每页10条,按ID倒序
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "id"));
Page<Book> page = bookRepository.findAll(pageable);
// 分页结果
long total = page.getTotalElements(); // 总条数
int totalPages = page.getTotalPages(); // 总页数
List<Book> list = page.getContent(); // 当前页数据注意:JPA 页码从 0 开始,与 MyBatis-Plus 不同。
5.2.6 JPA 避坑指南
- N+1 查询问题:关联查询时容易产生大量子查询,性能差,复杂查询建议用原生 SQL。
- 表结构风险:
ddl-auto: update会自动修改表结构,生产环境必须关闭。 - 优化困难:自动生成的 SQL 难以深度优化,复杂业务场景可控性差。
- 学习曲线:关联映射、级联操作等概念多,上手难度高于 MyBatis。
5.3 MyBatis-Plus:国内主流方案(重点⭐)
5.3.1 核心概念
- MyBatis:半自动 ORM 框架,SQL 自主编写,结果自动映射,灵活可控。
- MyBatis-Plus(MP):MyBatis 的增强工具,只做增强不做改变,内置单表 CRUD、条件构造器、分页、代码生成等通用能力,保留原生 MyBatis 全部特性。
简单说:原生 MyBatis 所有 SQL 都要自己写;MyBatis-Plus 单表 CRUD 自动实现,复杂 SQL 自己写,兼顾灵活与效率。
5.3.2 快速入门
步骤 1:引入依赖
<!-- MyBatis-Plus Spring Boot 起步依赖(第三方出品,需指定版本) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>步骤 2:配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/chapter05?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 下划线自动转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL
global-config:
db-config:
id-type: auto # 主键自增策略
table-underline: true # 表名下划线转驼峰步骤 3:手动建表
MyBatis-Plus 不会自动建表,需提前执行建表 SQL:
CREATE TABLE book (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
author VARCHAR(50),
press VARCHAR(100),
status VARCHAR(10)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;步骤 4:编写实体类
@TableName("book")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private String author;
private String press;
private String status;
}核心注解:
| 注解 | 作用 |
|---|---|
@TableName("book") | 指定对应数据库表名 |
@TableId(type = IdType.AUTO) | 标记主键,指定生成策略,AUTO 为数据库自增 |
@TableField("name") | 指定对应列名,不写默认属性名转下划线 |
@TableField(exist = false) | 标记非数据库字段 |
@TableLogic | 标记逻辑删除字段 |
步骤 5:编写 Mapper 接口
public interface BookMapper extends BaseMapper<Book> {
// 继承 BaseMapper,自动拥有单表全套 CRUD 方法
}BaseMapper 内置核心方法:
insert(entity):新增deleteById(id)/deleteBatchIds(ids):根据 ID 删除/批量删除updateById(entity):根据 ID 更新selectById(id)/selectBatchIds(ids):根据 ID 查询selectList(wrapper):条件查询列表selectPage(page, wrapper):条件分页查询selectCount(wrapper):条件统计
步骤 6:添加 Mapper 扫描
启动类添加 @MapperScan 注解,指定 Mapper 包路径:
@SpringBootApplication
@MapperScan("com.itheima.chapter05.dao")
public class Chapter05Application {
public static void main(String[] args) {
SpringApplication.run(Chapter05Application.class, args);
}
}两种扫描方式对比:
@MapperScan:全局扫描一次,企业项目推荐- 每个 Mapper 加
@Mapper:单个标注,适合少量 Mapper 场景
步骤 7:基础 CRUD 测试
@RestController
@RequestMapping("/mp/book")
public class MpBookController {
@Autowired
private BookMapper bookMapper;
@GetMapping("/list")
public List<Book> list() {
return bookMapper.selectList(null); // null 表示无条件
}
@GetMapping("/{id}")
public Book getById(@PathVariable Integer id) {
return bookMapper.selectById(id);
}
@PostMapping
public int add(@RequestBody Book book) {
return bookMapper.insert(book);
}
}5.3.3 条件构造器 QueryWrapper(核心能力⭐)
用 Java 代码动态拼接查询条件,无需编写 XML,满足绝大多数单表查询场景。
基础用法
QueryWrapper<Book> wrapper = new QueryWrapper<>();
wrapper.eq("author", "屈原"); // 等于
wrapper.like("name", "辞"); // 模糊匹配
wrapper.orderByDesc("id"); // 倒序
List<Book> books = bookMapper.selectList(wrapper);常用条件方法
| 方法 | 作用 |
|---|---|
eq/ne | 等于 / 不等于 |
gt/ge/lt/le | 大于 / 大于等于 / 小于 / 小于等于 |
between/notBetween | 在区间内 / 不在区间内 |
like/notLike | 模糊匹配 / 不匹配 |
likeLeft/likeRight | 左模糊 / 右模糊 |
isNull/isNotNull | 为空 / 不为空 |
in/notIn | IN 查询 / NOT IN 查询 |
orderByAsc/orderByDesc | 升序 / 倒序 |
groupBy | 分组 |
last | 拼接到 SQL 末尾(如 last("limit 10")) |
select | 指定查询的列 |
链式调用与条件判断
// 链式调用
List<Book> books = bookMapper.selectList(
new QueryWrapper<Book>()
.like("name", "辞")
.eq("status", "0")
.orderByDesc("id")
.last("limit 10")
);
// 可选条件:参数非空才拼接条件
QueryWrapper<Book> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), "name", name);
wrapper.eq(StringUtils.isNotBlank(author), "author", author);Lambda 条件构造器(推荐)
避免写字段名字符串,编译期校验,防止字段名写错:
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), Book::getName, name);
wrapper.eq(Book::getStatus, "0");5.3.4 分页查询
步骤 1:配置分页插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器,指定数据库类型
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}步骤 2:分页查询
// 第1页,每页10条(MP页码从1开始)
Page<Book> page = new Page<>(1, 10);
QueryWrapper<Book> wrapper = new QueryWrapper<>();
wrapper.eq("status", "0");
// 执行分页查询
Page<Book> result = bookMapper.selectPage(page, wrapper);
long total = result.getTotal(); // 总条数
long pages = result.getPages(); // 总页数
List<Book> records = result.getRecords(); // 当前页数据⚠️ 新手坑:不配置分页插件会导致查询全部数据,分页不生效。
5.3.5 Service 层封装
MyBatis-Plus 提供了 Service 层通用封装,继承即可获得全套业务方法。
// Service 接口
public interface BookService extends IService<Book> {
}
// Service 实现类
@Service
public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements BookService {
}使用:
@Autowired
private BookService bookService;
// Service 层方法
bookService.getById(id);
bookService.list();
bookService.save(book);
bookService.updateById(book);说明:Service 层方法命名风格与 Mapper 层不同(如
list()/save()),企业项目通常都会封装 Service 层存放业务逻辑。
5.3.6 自定义 SQL
复杂多表查询,条件构造器无法满足时,可使用原生 MyBatis 方式。
方式一:注解方式
public interface BookMapper extends BaseMapper<Book> {
@Select("SELECT * FROM book WHERE author = #{author} AND status = #{status}")
Book findByAuthorAndStatus(@Param("author") String author,
@Param("status") String status);
}方式二:XML 方式
在 resources/mapper/ 下创建 BookMapper.xml:
<?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](http://mybatis.org/dtd/mybatis-3-mapper.dtd)">
<mapper namespace="com.itheima.chapter05.dao.BookMapper">
<select id="findByAuthorAndStatus" resultType="com.itheima.chapter05.entity.Book">
SELECT * FROM book
WHERE author = #{author} AND status = #{status}
</select>
</mapper>配置文件指定 XML 位置:
mybatis-plus:
mapper-locations: classpath:mapper/*.xml✅ 最佳实践:简单单表查询用条件构造器,复杂多表查询用 XML。
5.3.7 企业级增强特性
1. 逻辑删除
物理删除数据不可恢复,企业通常用逻辑删除:增加标记字段,标记删除状态,查询时自动过滤。
配置:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段
logic-delete-value: 1 # 删除值
logic-not-delete-value: 0 # 未删除值实体类添加字段:
@TableLogic
private Integer deleted;效果:调用 deleteById 时,自动执行 UPDATE book SET deleted = 1 WHERE id = ?;查询时自动拼接 AND deleted = 0 条件。
2. 自动填充
创建时间、更新时间等公共字段自动填充,无需手动赋值。
实体类字段标记:
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;配置填充处理器:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}3. 乐观锁
解决并发更新丢失问题,通过版本号校验,更新失败可重试。
配置插件:
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());实体类加版本字段:
@Version
private Integer version;效果:更新时自动拼接 AND version = ?,版本不匹配则更新失败。
4. 代码生成器
根据数据库表一键生成 Entity、Mapper、Service、Controller 全套代码,表多的场景大幅提升效率。
5.3.8 MyBatis-Plus 避坑指南
- 分页不生效:未配置分页拦截器,或拦截器配置顺序错误。
- 字段映射不上:未开启下划线转驼峰,或列名与属性名不匹配。
- 逻辑删除全局生效:所有查询都会自动加过滤条件,多表关联需注意。
- 主键冲突:未指定主键策略,默认雪花算法生成 Long 型 ID,与数据库整型不匹配。
- 条件构造器字段名错误:使用 Lambda 构造器可避免。
5.4 Redis:高速缓存数据库
5.4.1 核心概念
Redis 是一款开源的内存型键值数据库,读写性能极高,主要用于缓存、计数器、分布式锁、排行榜等场景。
核心特点:
- 内存存储,读写速度极快(万级 QPS)
- 支持丰富的数据结构
- 支持持久化,重启数据不丢失
- 支持过期时间、分布式、发布订阅
5.4.2 核心数据结构与场景
| 数据结构 | 特点 | 典型应用场景 |
|---|---|---|
| String 字符串 | 最基础的 key-value | 对象缓存、计数器、分布式锁 |
| Hash 哈希 | 键值对集合 | 对象缓存、用户信息存储 |
| List 列表 | 有序可重复 | 消息队列、最新消息列表 |
| Set 集合 | 无序不重复 | 去重、共同好友计算 |
| ZSet 有序集合 | 有序不重复,带权重 | 排行榜、优先级队列 |
5.4.3 Spring Data Redis 快速入门
步骤 1:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>Spring Boot 4.x 默认使用 Lettuce 异步客户端,性能优于旧版 Jedis。
步骤 2:配置 Redis
spring:
redis:
host: localhost # 服务器地址
port: 6379 # 端口
password: "" # 密码
database: 0 # 数据库编号 0-15
timeout: 3000ms # 超时时间
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接5.4.4 RedisTemplate 核心操作
Spring Data Redis 提供 RedisTemplate 统一操作 Redis。
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 字符串操作
redisTemplate.opsForValue().set("key", "value");
String value = redisTemplate.opsForValue().get("key");
// 设置过期时间
redisTemplate.opsForValue().set("key", "value", 1, TimeUnit.HOURS);
// 删除
redisTemplate.delete("key");
// 判断存在
Boolean hasKey = redisTemplate.hasKey("key");其他数据结构操作:
- 哈希:
opsForHash() - 列表:
opsForList() - 集合:
opsForSet() - 有序集合:
opsForZSet()
5.4.5 JSON 序列化配置(重点⭐)
默认 RedisTemplate 使用 JDK 序列化,存入的数据是二进制乱码,可读性差且跨语言不兼容。企业项目统一配置 JSON 序列化。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化器
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
jsonSerializer.setObjectMapper(om);
// 字符串序列化器
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// key 用字符串序列化,value 用 JSON 序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}配置后可直接存取 Java 对象:
// 存对象
redisTemplate.opsForValue().set("book:1", book, 1, TimeUnit.HOURS);
// 取对象
Book book = (Book) redisTemplate.opsForValue().get("book:1");5.4.6 缓存经典模式:Cache Aside 旁路缓存
企业最常用的缓存策略,保证数据库与缓存的最终一致性。
读流程
- 先查询缓存,命中则直接返回
- 缓存未命中,查询数据库
- 数据库结果写入缓存,设置过期时间
- 返回数据
写流程
- 先更新数据库
- 再删除缓存(不是更新缓存)
完整代码示例
@Service
public class BookServiceImpl {
@Autowired
private BookMapper bookMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Book getById(Integer id) {
String key = "book:" + id;
// 1. 先查缓存
Book book = (Book) redisTemplate.opsForValue().get(key);
if (book != null) {
return book;
}
// 2. 缓存未命中,查数据库
book = bookMapper.selectById(id);
if (book != null) {
// 3. 写入缓存,设置1小时过期
redisTemplate.opsForValue().set(key, book, 1, TimeUnit.HOURS);
}
return book;
}
@Transactional
public void update(Book book) {
// 1. 更新数据库
bookMapper.updateById(book);
// 2. 删除缓存(下次读取自动加载最新值)
redisTemplate.delete("book:" + book.getId());
}
}为什么删除缓存而不是更新缓存? 并发场景下更新缓存容易产生脏数据;删除缓存更简单,一致性更高,是业界标准实践。
5.4.7 缓存三大经典问题(面试高频⭐)
1. 缓存穿透
- 问题:查询一个数据库不存在的数据,缓存永远不命中,请求全部打到数据库。
- 原因:恶意攻击、非法参数、不存在资源频繁查询。
- 解决方案:
- 缓存空值:查询结果为空也缓存空值,设置短过期时间
- 布隆过滤器:提前过滤不存在的 key
2. 缓存击穿
- 问题:某个热点 key 过期瞬间,大量并发请求直接打到数据库。
- 原因:热点数据过期,高并发同时穿透到数据库。
- 解决方案:
- 热点数据永不过期
- 互斥锁:缓存失效时只放一个请求去查库并构建缓存
3. 缓存雪崩
- 问题:大量 key 同一时间集体过期,或者 Redis 宕机,所有请求全部打到数据库,导致数据库压力骤增。
- 原因:过期时间集中、Redis 单点故障。
- 解决方案:
- 过期时间加随机偏移,避免同时过期
- Redis 集群部署,保证高可用
- 服务降级、熔断机制,保护数据库
5.5 三种方案对比与选型
5.5.1 对比表
| 对比维度 | Spring Data JPA | MyBatis-Plus | Redis |
|---|---|---|---|
| 类型 | 关系型 ORM | 关系型 ORM | 内存缓存数据库 |
| SQL 可控性 | 低,自动生成 | 高,自主编写 | - |
| 开发效率 | 高(单表零 SQL) | 中(单表零 SQL,复杂需手写) | 高 |
| 性能 | 中(复杂 SQL 难优化) | 高(SQL 可深度优化) | 极高 |
| 学习成本 | 较高 | 中等 | 低 |
| 国内流行度 | 低 | 极高 | 极高 |
| 核心优势 | 数据库无关、面向对象 | 灵活可控、生态完善 | 性能极高、功能丰富 |
5.5.2 选型建议
- 关系型数据库访问:国内项目优先选择 MyBatis-Plus,生态完善、人才充足、SQL 可控,适合复杂业务场景。
- 简单业务、海外项目:可选择 Spring Data JPA,开发速度快。
- 缓存、高并发场景:统一使用 Redis,是事实上的标准方案。
企业标准组合:MySQL(主存储)+ MyBatis-Plus(数据访问)+ Redis(缓存加速),是国内互联网公司最常见的技术栈。
5.6 企业级最佳实践
5.6.1 分层架构规范
标准三层架构,职责单一,边界清晰:
Controller → Service → Mapper → 数据库- Controller:参数校验、调用 Service、返回统一结果,不写业务逻辑
- Service:业务逻辑、事务控制、缓存逻辑,核心业务层
- Mapper:纯数据库操作,不包含业务逻辑
5.6.2 分页规范
- 入参统一:
pageNum(页码,从 1 开始)、pageSize(每页条数) - 出参统一:
total(总数)、list(数据列表) - 限制每页最大条数,防止一次查询海量数据拖垮数据库
5.6.3 数据安全规范
- 逻辑删除代替物理删除,保留数据可追溯
- 敏感字段加密存储,不存明文密码、手机号
- SQL 注入防护:使用 #{} 传参,禁止字符串拼接 SQL
5.6.4 通用字段规范
所有业务表建议包含:
id:主键create_time:创建时间,自动填充update_time:更新时间,自动填充deleted:逻辑删除标记version:乐观锁版本号
5.7 新手常见问题排查
- 找不到 Mapper 报错
- 原因:启动类未加
@MapperScan,或包路径错误 - 解决:正确配置扫描包路径,或 Mapper 接口加
@Mapper
- 原因:启动类未加
- 字段全部为 null
- 原因:未开启下划线转驼峰,或列名与属性名不匹配
- 解决:配置
map-underscore-to-camel-case: true
- 分页不生效
- 原因:未配置分页拦截器
- 解决:注册
MybatisPlusInterceptor并添加分页插件
- Redis 连接失败
- 原因:Redis 未启动、地址端口错误、防火墙拦截
- 解决:确认服务启动,检查配置参数与网络
- Redis 数据乱码
- 原因:默认 JDK 序列化
- 解决:配置 JSON 序列化器
- JPA 建表失败
- 原因:数据库不存在、实体注解错误、字段类型不兼容
- 解决:检查连接配置与实体类定义
5.8 本章小结
本章系统讲解了 Spring Boot 生态下三种主流数据访问技术:
- Spring Data JPA:全自动 ORM,单表开发效率高,适合简单业务,国内使用较少。
- MyBatis-Plus:国内企业主流方案,单表 CRUD 自动实现,复杂 SQL 自主可控,配套分页、逻辑删除、自动填充等企业级特性,是必须掌握的核心技能。
- Redis:内存缓存数据库,性能极高,核心掌握缓存模式与三大经典问题解决方案,是后端面试必考点。
企业项目通常采用「MySQL + MyBatis-Plus + Redis」的组合架构,分别承担主存储、数据访问、热点缓存的角色,共同支撑业务系统的数据需求。
5.9 动手练习作业
基础必做题(全员必做)
- MyBatis-Plus 基础 CRUD
- 搭建 Spring Boot + MyBatis-Plus 项目,整合 MySQL 8.x
- 设计图书表,编写实体类、Mapper 接口
- 实现增删改查 REST 接口,使用 Postman 全部测试通过
- 条件构造器与分页
- 实现多条件查询:书名模糊匹配、作者精确匹配、状态筛选
- 实现分页 + 排序查询,返回总条数与数据列表
- 实现可选条件查询:参数为空时不拼接对应条件
- Redis 缓存集成
- 整合 Spring Data Redis,配置 JSON 序列化
- 实现图书查询旁路缓存:读操作先查缓存,未命中查数据库并回写缓存
- 实现更新逻辑:更新数据库后删除缓存,验证一致性
- 企业特性实践
- 引入 Service 层封装业务逻辑
- 实现逻辑删除功能,验证删除后查询自动过滤
- 实现创建时间、更新时间自动填充
进阶选做题(学有余力完成)
- 多表关联查询:编写 XML 自定义 SQL,实现图书与分类表的关联查询。
- 乐观锁:配置乐观锁插件,编写并发更新测试用例,验证版本校验机制。
- 缓存问题模拟:模拟缓存穿透场景,实现空值缓存解决方案。
- 代码生成器:使用 MyBatis-Plus 代码生成器,根据数据库表生成 Entity、Mapper、Service、Controller 全套代码。
思考题
- MyBatis 和 JPA 有什么核心区别?各自的适用场景是什么?
- 为什么国内企业更倾向于使用 MyBatis 而不是 JPA?
- 缓存更新为什么用删除而不是直接更新?Cache Aside 模式的优势是什么?
- 分别说明缓存穿透、击穿、雪崩的产生原因与解决方案。
- 逻辑删除和物理删除各有什么优缺点?什么场景适合用逻辑删除?