Skip to content

第 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 为什么需要数据访问层 ​

内存存储的核心问题:

  1. 重启丢失:项目停止或重启,所有数据清空
  2. 无法共享:多实例部署时,各节点数据独立不互通
  3. 容量有限:内存成本高,无法存储海量业务数据
  4. 查询低效:复杂查询需要遍历集合,性能远低于数据库索引查询

数据访问层(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 JPAMySQL、Oracle 等关系型数据库
Spring Data RedisRedis 缓存数据库
Spring Data MongoDBMongoDB 文档数据库
Spring Data ElasticsearchElasticsearch 搜索引擎

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:引入依赖 ​

xml
<!-- 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 ​

yaml
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:编写实体类 ​

java
@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 接口 ​

java
@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:接口测试 ​

java
@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,无需手动编写。

java
@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 方式(推荐,面向对象) ​

操作的是类和属性,不是表和列:

java
@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 分页与排序 ​

java
// 分页+排序:第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 避坑指南 ​

  1. N+1 查询问题:关联查询时容易产生大量子查询,性能差,复杂查询建议用原生 SQL。
  2. 表结构风险:ddl-auto: update 会自动修改表结构,生产环境必须关闭。
  3. 优化困难:自动生成的 SQL 难以深度优化,复杂业务场景可控性差。
  4. 学习曲线:关联映射、级联操作等概念多,上手难度高于 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:引入依赖 ​

xml
<!-- 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:配置文件 ​

yaml
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:

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:编写实体类 ​

java
@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 接口 ​

java
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/notInIN 查询 / 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 避坑指南 ​

  1. 分页不生效:未配置分页拦截器,或拦截器配置顺序错误。
  2. 字段映射不上:未开启下划线转驼峰,或列名与属性名不匹配。
  3. 逻辑删除全局生效:所有查询都会自动加过滤条件,多表关联需注意。
  4. 主键冲突:未指定主键策略,默认雪花算法生成 Long 型 ID,与数据库整型不匹配。
  5. 条件构造器字段名错误:使用 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 旁路缓存 ​

企业最常用的缓存策略,保证数据库与缓存的最终一致性。

读流程 ​

  1. 先查询缓存,命中则直接返回
  2. 缓存未命中,查询数据库
  3. 数据库结果写入缓存,设置过期时间
  4. 返回数据

写流程 ​

  1. 先更新数据库
  2. 再删除缓存(不是更新缓存)

完整代码示例 ​

@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 JPAMyBatis-PlusRedis
类型关系型 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 新手常见问题排查 ​

  1. 找不到 Mapper 报错
    • 原因:启动类未加 @MapperScan,或包路径错误
    • 解决:正确配置扫描包路径,或 Mapper 接口加 @Mapper
  2. 字段全部为 null
    • 原因:未开启下划线转驼峰,或列名与属性名不匹配
    • 解决:配置 map-underscore-to-camel-case: true
  3. 分页不生效
    • 原因:未配置分页拦截器
    • 解决:注册 MybatisPlusInterceptor 并添加分页插件
  4. Redis 连接失败
    • 原因:Redis 未启动、地址端口错误、防火墙拦截
    • 解决:确认服务启动,检查配置参数与网络
  5. Redis 数据乱码
    • 原因:默认 JDK 序列化
    • 解决:配置 JSON 序列化器
  6. JPA 建表失败
    • 原因:数据库不存在、实体注解错误、字段类型不兼容
    • 解决:检查连接配置与实体类定义

5.8 本章小结 ​

本章系统讲解了 Spring Boot 生态下三种主流数据访问技术:

  1. Spring Data JPA:全自动 ORM,单表开发效率高,适合简单业务,国内使用较少。
  2. MyBatis-Plus:国内企业主流方案,单表 CRUD 自动实现,复杂 SQL 自主可控,配套分页、逻辑删除、自动填充等企业级特性,是必须掌握的核心技能。
  3. Redis:内存缓存数据库,性能极高,核心掌握缓存模式与三大经典问题解决方案,是后端面试必考点。

企业项目通常采用「MySQL + MyBatis-Plus + Redis」的组合架构,分别承担主存储、数据访问、热点缓存的角色,共同支撑业务系统的数据需求。


5.9 动手练习作业 ​

基础必做题(全员必做) ​

  1. MyBatis-Plus 基础 CRUD
    • 搭建 Spring Boot + MyBatis-Plus 项目,整合 MySQL 8.x
    • 设计图书表,编写实体类、Mapper 接口
    • 实现增删改查 REST 接口,使用 Postman 全部测试通过
  2. 条件构造器与分页
    • 实现多条件查询:书名模糊匹配、作者精确匹配、状态筛选
    • 实现分页 + 排序查询,返回总条数与数据列表
    • 实现可选条件查询:参数为空时不拼接对应条件
  3. Redis 缓存集成
    • 整合 Spring Data Redis,配置 JSON 序列化
    • 实现图书查询旁路缓存:读操作先查缓存,未命中查数据库并回写缓存
    • 实现更新逻辑:更新数据库后删除缓存,验证一致性
  4. 企业特性实践
    • 引入 Service 层封装业务逻辑
    • 实现逻辑删除功能,验证删除后查询自动过滤
    • 实现创建时间、更新时间自动填充

进阶选做题(学有余力完成) ​

  1. 多表关联查询:编写 XML 自定义 SQL,实现图书与分类表的关联查询。
  2. 乐观锁:配置乐观锁插件,编写并发更新测试用例,验证版本校验机制。
  3. 缓存问题模拟:模拟缓存穿透场景,实现空值缓存解决方案。
  4. 代码生成器:使用 MyBatis-Plus 代码生成器,根据数据库表生成 Entity、Mapper、Service、Controller 全套代码。

思考题 ​

  1. MyBatis 和 JPA 有什么核心区别?各自的适用场景是什么?
  2. 为什么国内企业更倾向于使用 MyBatis 而不是 JPA?
  3. 缓存更新为什么用删除而不是直接更新?Cache Aside 模式的优势是什么?
  4. 分别说明缓存穿透、击穿、雪崩的产生原因与解决方案。
  5. 逻辑删除和物理删除各有什么优缺点?什么场景适合用逻辑删除?

基于 Vite 强力驱动 | 纯静态轻量托管