MyBatis 从入门到面试:一个订单系统带你吃透持久层框架

本文以电商订单系统为主线案例,从 JDBC 痛点出发,逐步深入 MyBatis 核心机制,覆盖注解式 CRUD、XML 映射、#{} vs ${}、SQL 注入防御、连接池选型、缓存机制等面试高频考点。全文八章,循序渐进,适合 Java 后端初学者和 1-3 年经验面试备战者。


目录

  1. 从 JDBC 到 MyBatis — 为什么要"轮子"
  2. 第一个 MyBatis 程序 — 查询商品列表
  3. MyBatis 日志 — 调试的"火眼金睛"
  4. MyBatis 注解式 CRUD
  5. XML 配置文件方式 — 复杂 SQL 的主场
  6. #{} vs ${} — 面试必问的"送命题"
  7. 数据库连接池 — 性能的幕后英雄
  8. MyBatis 缓存机制 — 被忽略的面试杀器

一、从 JDBC 到 MyBatis — 为什么要"轮子"

1.1 JDBC 的八步"标准流程"

任何一个 Java 后端开发者都绕不开 JDBC。回忆一下操作数据库的标准八步:

  1. 引入数据库驱动依赖
  2. 创建 DataSource 数据源
  3. 获取 Connection 连接
  4. 构造 SQL 字符串
  5. 创建 PreparedStatement 并替换占位符
  6. 执行 SQL(executeQuery / executeUpdate
  7. 遍历 ResultSet 手动映射到对象
  8. finally 中关闭连接、释放资源

每一步都是必不可少的,但每一步都让代码变得臃肿。假设我们要查询所有上架商品:

// JDBC 原生查询 —— 注意这不是本文案例,只是对比演示
DataSource ds = new MysqlDataSource();
try (Connection conn = ds.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT * FROM product WHERE status = ?");
     ResultSet rs = ps.executeQuery()) {
    ps.setInt(1, 1);
    List<Product> list = new ArrayList<>();
    while (rs.next()) {
        Product p = new Product();
        p.setId(rs.getInt("id"));
        p.setProdName(rs.getString("prod_name"));
        p.setPrice(rs.getBigDecimal("price"));
        // ... 还有 6 个字段
        list.add(p);
    }
}

一个简单查询写了近 20 行,而且每个 DAO 方法都要重复这段"连接 → SQL → 执行 → 映射 → 释放"的模板代码。

1.2 JDBC 的三大痛点

痛点 具体表现 后果
模板代码重复 每个方法都要写获取连接、try-catch-finally、释放资源 代码膨胀,维护噩梦
连接手动管理 开发者自己控制 Connection 的创建和销毁 忘记关闭 → 连接泄漏 → 数据库挂掉
SQL 硬编码与参数拼接 SQL 写在 Java 字符串里,参数手动 setXxx,结果集手动映射字段 改一个字段名要改 5 处代码,极易出错

1.3 MyBatis 解决了什么

MyBatis 是一个半自动 ORM 持久层框架。理解"半自动"这个词是面试中的加分项:

  • 全自动 ORM(如 Hibernate):SQL 由框架生成,你只管对象操作。优点是开发快,缺点是复杂查询不可控。
  • 半自动 ORM(MyBatis):SQL 仍然由你编写,框架帮你做连接管理、参数映射、结果映射。优点是你对 SQL 有绝对掌控权。

通俗类比:JDBC 像自己买菜、洗菜、切菜、炒菜、洗碗全流程一个人干。MyBatis 像净菜半成品——菜已洗好切好,锅碗瓢盆(连接管理、资源释放)框架帮你管,你只需要决定怎么炒(写 SQL)。

JDBC 原生 MyBatis
连接管理 手动创建/销毁 Connection 连接池自动管理
SQL 编写 Java 字符串拼接,改一行动全身 注解/XML 集中管理
参数映射 逐个 setXxx() 设参 #{} 占位一行搞定
结果映射 手工 rs.getString() 逐字段 自动映射到实体类
资源释放 try-catch-finally 必写 框架自动处理

📌 面试点:MyBatis 和 Hibernate 的本质区别?—— 半自动 vs 全自动 ORM。MyBatis 由开发者掌控 SQL,适合复杂查询和性能调优;Hibernate 自动生成 SQL,适合标准 CRUD 场景。


二、第一个 MyBatis 程序 — 查询商品列表

本章使用电商订单系统的 product 表,建表语句如下(后文所有代码均基于此表):

CREATE DATABASE ecommerce_db;

USE ecommerce_db;

CREATE TABLE product (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    prod_name   VARCHAR(100)  NOT NULL COMMENT '商品名称',
    price       DECIMAL(10,2) NOT NULL COMMENT '单价',
    stock       INT           NOT NULL COMMENT '库存',
    category    VARCHAR(30)   COMMENT '分类',
    status      TINYINT       DEFAULT 1 COMMENT '状态(0下架/1上架)',
    create_time DATETIME      DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO product (prod_name, price, stock, category) VALUES
('机械键盘 K8', 499.00, 120, '数码'),
('蓝牙耳机 Pro', 299.00, 80, '数码'),
('Java 核心技术 卷I', 149.00, 200, '图书'),
('MySQL 必知必会', 69.00, 150, '图书');

2.1 环境搭建

Spring Boot 项目中引入 MyBatis 起步依赖和 MySQL 驱动:

<!-- pom.xml -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

application.yml 配置数据库连接(四项必填):

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/ecommerce_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

2.2 实体类与 Mapper 接口

实体类遵循驼峰命名,与数据库的下划线字段对应:

@Data
public class Product {
    private Integer id;
    private String prodName;    // 对应 prod_name
    private BigDecimal price;
    private Integer stock;
    private String category;
    private Integer status;
    private Date createTime;    // 对应 create_time
    private Date updateTime;    // 对应 update_time
}

Mapper 接口加上 @Mapper 注解,方法上写 @Select

@Mapper
public interface ProductMapper {

    @Select("SELECT * FROM product WHERE status = 1")
    List<Product> listOnSale();
}

单元测试验证:

@SpringBootTest
class ProductMapperTest {

    @Autowired
    private ProductMapper productMapper;

    @Test
    void testListOnSale() {
        List<Product> products = productMapper.listOnSale();
        products.forEach(p -> System.out.println(p.getProdName()));
    }
}

2.3 @Mapper 注解背后发生了什么

这是面试中容易被追问的点。@Mapper 并非简单的"标记注解",它触发了 MyBatis 的核心机制——动态代理

你的代码调用 productMapper.listOnSale()
        │
        ▼
动态代理对象拦截(MapperProxy)
        │
        ▼
SqlSession 获取 MappedStatement(从 @Select 注解解析出的 SQL + 配置)
        │
        ▼
Executor 执行器处理(含缓存、事务等拦截链)
        │
        ▼
StatementHandler 创建 PreparedStatement、设参
        │
        ▼
ResultSetHandler 将 JDBC 结果集映射为 List<Product>
        │
        ▼
返回结果

四个核心对象的职责:

对象 职责
SqlSession 门面,对外提供 API(selectOne/selectList/insert/update/delete)
Executor 执行器,负责 SQL 执行、缓存维护、事务管理
StatementHandler 封装 PreparedStatement 操作(创建、参数化、执行)
ResultSetHandler 将 JDBC ResultSet 映射为 Java 对象集合

在这里插入图片描述

理解这条链路,你就能回答"为什么 Mapper 接口不需要实现类也能工作"——因为 MyBatis 在运行时为每个 @Mapper 接口生成了 JDK 动态代理对象,代理对象拦截方法调用并委托给 SqlSession 执行。

📌 面试点

  • @Mapper@Repository 的区别?—— @Mapper 是 MyBatis 的注解,告诉框架为此接口生成代理对象并交 IOC 管理;@Repository 是 Spring 的 stereotype 注解,仅标记为持久层组件。两者可共存,但 @Mapper 是 MyBatis 必需的。
  • @MapperScan("com.example.mapper") 的作用?—— 扫描指定包下所有接口并自动注册为 Mapper,省去每个接口单独加 @Mapper

三、MyBatis 日志 — 调试的"火眼金睛"

开发阶段打开 MyBatis SQL 日志是排查问题的标配操作。在 application.yml 中配置:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

执行 listOnSale() 后控制台输出示例:

==>  Preparing: SELECT * FROM product WHERE status = 1
==> Parameters:
<==    Columns: id, prod_name, price, stock, category, status, create_time, update_time
<==        Row: 1, 机械键盘 K8, 499.00, 120, 数码, 1, 2026-01-15 10:00:00, 2026-01-15 10:00:00
<==        Row: 2, 蓝牙耳机 Pro, 299.00, 80, 数码, 1, 2026-01-15 10:00:00, 2026-01-15 10:00:00
<==      Total: 2

日志三要素:

含义 价值
Preparing 执行的 SQL 语句(占位符形式) 检查 SQL 是否正确
Parameters 传入的参数类型和值 检查参数是否匹配
Rows / Total 每行返回值和总行数 检查结果是否符合预期

生产环境注意StdOutImpl 将日志直接输出到标准输出流,性能较差且不便于集中管理。线上建议关闭或切换为 SLF4J 集成,通过日志框架统一管理输出级别。


四、MyBatis 注解式 CRUD

本章覆盖 CRUD 四操作 + 参数传递 + 自增主键返回 + 字段映射三大方案,是全文篇幅最大的模块。建议按子章节顺序阅读,也可直接跳转到你关心的部分。

4.1 参数传递

MyBatis 通过 #{} 占位符获取方法参数,本质是 JDBC 的 ? 预编译占位符。

单参数传递——名称任意:

@Select("SELECT * FROM product WHERE id = #{xxx}")
Product getById(Integer id);

#{xxx} 中写什么名字都可以,因为只有一个参数,MyBatis 直接用值填充唯一的 ?

多参数传递——必须用 @Param 绑定:

@Select("SELECT * FROM product WHERE category = #{cate} AND status = #{st}")
List<Product> getByCategoryAndStatus(@Param("cate") String category,
                                      @Param("st") Integer status);

不加 @Param 会报错,因为 MyBatis 对多参数默认使用 arg0arg1param1param2 命名,你的 SQL 中写 #{cate} 找不到对应参数名。

对象传参——直接用属性名:

@Insert("INSERT INTO product(prod_name, price, stock, category) " +
        "VALUES(#{prodName}, #{price}, #{stock}, #{category})")
int add(Product product);

当参数是一个对象时,#{} 中直接写对象的属性名即可。

4.2 新增 + 返回自增主键

@Insert 的返回值是影响行数,而非主键。要获取自增 ID,需配置 @Options

@Insert("INSERT INTO customer(name, phone, level) VALUES(#{name}, #{phone}, #{level})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int addCustomer(Customer customer);

调用后:

Customer c = new Customer();
c.setName("张三");
c.setPhone("13800138000");
c.setLevel(1);

customerMapper.addCustomer(c);
System.out.println(c.getId()); // 输出自增 ID,如 1

主键被回填到参数对象的 id 属性中,返回值 int 仍是影响行数。

4.3 删除

@Delete("DELETE FROM product WHERE id = #{id}")
int deleteById(Integer id);

这只是物理删除。生产环境更推荐逻辑删除——通过 status 字段标记,配合 @Update 实现:

@Update("UPDATE product SET status = 0 WHERE id = #{id}")
int offShelf(Integer id);

逻辑删除的好处:数据可恢复、保留审计轨迹、避免外键级联问题。

4.4 修改

@Update("UPDATE product SET stock = #{stock}, update_time = NOW() WHERE id = #{id}")
int updateStock(Integer id, Integer stock);

思考:如果只想更新库存,而不更新其他字段(如商品名),传一个完整的 Product 对象就不合适了。这时要么用 @Param 传多参数,要么用后续的 XML 动态 SQL(<if> 标签)实现按需更新。

4.5 查询 — 字段映射三大方案(面试重点)

来看一个经典问题:查询商品列表,prod_name 字段值为 null

根因:数据库字段 prod_name(下划线)与 Java 属性 prodName(驼峰)不匹配。MyBatis 默认按名称精确匹配映射列到属性,找不到就赋 null

方案①:SQL 起别名

@Select("SELECT id, prod_name AS prodName, price, stock, category, status, " +
        "create_time AS createTime, update_time AS updateTime " +
        "FROM product WHERE status = 1")
List<Product> listOnSale();

优点:直接有效。缺点:每个字段都要写 AS,10 个字段就写 10 个,繁琐易漏。

方案②:@Results + @Result

@Results(id = "productMap", value = {
    @Result(property = "id", column = "id"),
    @Result(property = "prodName", column = "prod_name"),
    @Result(property = "createTime", column = "create_time"),
    @Result(property = "updateTime", column = "update_time")
})
@Select("SELECT * FROM product WHERE status = 1")
List<Product> listOnSale();

// 其他方法复用
@ResultMap("productMap")
@Select("SELECT * FROM product WHERE id = #{id}")
Product getById(Integer id);

优点:声明式映射,可复用(@ResultMap)。缺点:每个需要此映射的查询方法都要声明或引用 @ResultMap,接口方法一多仍然冗余。这就是社区最终推荐方案③的原因。

方案③(推荐):驼峰命名自动转换

mybatis:
  configuration:
    map-underscore-to-camel-case: true

一行配置,全局生效。MyBatis 在映射时会自动将 prod_name 匹配到 prodNamecreate_time 匹配到 createTime开发中首选此方案。

三种方案对比:

方案 复杂度 维护性 推荐度
SQL 别名 低但繁琐 差,每个 SQL 都要写 仅临时用
@Results 中,可复用但每个方法都要引用 需要多表不同映射时
驼峰配置 极低 优,一行配置全局生效 ★★★★★

📌 面试点:数据库字段和实体属性名不一致如何解决?—— 三种方案递进说明,最终推荐驼峰配置。加分项:补充说明"如果确实有特殊字段无法用驼峰规则自动匹配(如 deleteFlagis_deleted),才用 @Results 单独处理"。


五、XML 配置文件方式 — 复杂 SQL 的主场

注解适合简单 CRUD,但当 SQL 复杂到包含多表联查、动态条件、<foreach> 批量操作时,XML 是更好的选择。

5.1 配置 XML 路径

mybatis:
  mapper-locations: classpath:mapper/*.xml

5.2 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">
<mapper namespace="com.example.mapper.OrderMapper">

    <select id="getById" resultType="com.example.entity.OrderInfo">
        SELECT * FROM order_info WHERE id = #{id}
    </select>

</mapper>

XML 三要素:namespace(指向 Mapper 接口全限定名)、id(对应接口方法名)、resultType(返回类型)。

5.3 resultType vs resultMap

  • resultType:直接指定实体类全限定名,MyBatis 自动按字段名映射。前提是字段名匹配(或已开启驼峰命名自动转换)。
  • resultMap:手动定义列到属性的映射关系。当字段名差异大、多表查询结果需要自定义映射时使用。
<resultMap id="orderDetailMap" type="com.example.entity.OrderInfo">
    <id property="id" column="id"/>
    <result property="orderNo" column="order_no"/>
    <result property="custName" column="cust_name"/>   <!-- 来自 customer 表 -->
    <result property="prodName" column="prod_name"/>    <!-- 来自 product 表 -->
    <result property="totalPrice" column="total_price"/>
</resultMap>

<select id="getOrderDetail" resultMap="orderDetailMap">
    SELECT o.*, c.name AS cust_name, p.prod_name
    FROM order_info o
    LEFT JOIN customer c ON o.cust_id = c.id
    LEFT JOIN product p ON o.prod_id = p.id
    WHERE o.id = #{id}
</select>

MyBatis 不区分单表和多表查询——它只看三要素:SQL + 映射关系 + 实体类。只要你能写出正确的 SQL 并配置好映射,单表和多表是一样的操作。

5.4 注解 vs XML 选型原则

场景 推荐方式 理由
简单 CRUD(单表、无动态条件) 注解 代码紧凑,SQL 和接口在一起,方便阅读
多表联查、复杂 SQL XML SQL 和 Java 代码分离,可读性强,避免注解中拼接长字符串
动态 SQL(<if><foreach><trim> XML 注解不支持动态 SQL 标签
存储过程调用 XML 语义更清晰

📌 面试点:你们项目用注解还是 XML?为什么?—— 没有标准答案,关键是说出理由:简单查询用注解省事;复杂 SQL 用 XML 便于维护和 DBA 审核。可以补充"我们的项目是混合使用,简单 CRUD 用注解,报表和复杂查询用 XML"。


六、#{}${} — 面试必问的"送命题"

这是全文最重要的章节。如果你只有时间精读一章,请读这章。

6.1 核心区别

#{}${} 底层走的完全是两条路:

在这里插入图片描述

维度 #{} ${}
SQL 生成方式 预编译(PreparedStatement),用 ? 占位 即时拼接(Statement),字符串直接替换
SQL 注入 安全 危险
字符串自动加引号 是,#{name}'张三' 否,需手动加 ${'name'}'张三'
编译缓存 有(语法树只解析一次) 无(每次重新解析整条 SQL)
性能 高(重复执行时复用编译结果)
适用场景 值参数(推荐默认使用) 表名、字段名、排序关键字

6.2 SQL 注入攻击演示

以电商客户登录场景为例,Mapper 中存在这样的查询:

// ❌ 危险写法
@Select("SELECT * FROM customer WHERE name = '${name}'")
Customer loginDangerous(String name);

正常输入:张三 → SQL:SELECT * FROM customer WHERE name = '张三'

恶意输入:' OR 1=1 -- → SQL:

SELECT * FROM customer WHERE name = '' OR 1=1 --'

-- 是 MySQL 注释符,后面的内容被忽略。1=1 恒为真,OR 逻辑导致查询返回所有用户。如果程序取第一条作为登录成功,攻击者直接绕过了身份验证。

// ✅ 安全写法
@Select("SELECT * FROM customer WHERE name = #{name}")
Customer loginSafe(String name);

#{name} 会将参数作为传递,输入 ' OR 1=1 -- 会被当作普通字符串 "' OR 1=1 --'" 来匹配 name 字段,自然查不到任何结果。

在这里插入图片描述

6.3 PreparedStatement 底层防御原理

很多人只知道"#{} 防注入",但不知道为什么。面试追问时,能说出原理是明显加分项:

  1. 预编译阶段:MySQL 服务端收到 SELECT * FROM customer WHERE name = ?,解析并缓存语法树(AST)。此时 ? 被标记为参数占位符,不是 SQL 关键字。
  2. 执行阶段:参数 ' OR 1=1 -- 通过独立的协议通道发送,被当作纯字符串值,直接填充到已解析的语法树中。由于语法树已经固定,参数不可能改变 SQL 结构。

用通俗类比理解:预编译 SQL 像一份填空题试卷(空格已固定),参数像你填的答案。无论你在空格里写什么,都不可能改变试卷上其他题目的内容。而即时 SQL 像让你在试卷上随便写——你可以划掉原来的题目,自己出题。

6.4 ${} 的必要使用场景

既然 ${} 有注入风险,为什么 MyBatis 还要保留它?因为有些场景 #{} 无法工作——它会对参数自动加引号。

场景①:动态排序

// ❌ 错误——执行后 SQL: ORDER BY price 'asc'(语法错误)
@Select("SELECT * FROM product WHERE status = 1 ORDER BY price #{sort}")
List<Product> listSorted(String sort);

// ✅ 正确
@Select("SELECT * FROM product WHERE status = 1 ORDER BY price ${sort}")
List<Product> listSorted(String sort);

#{} 会将 "asc" 变成 'asc'ORDER BY price 'asc' 是非法 SQL。排序关键字、表名、字段名不需要引号,必须用 ${}

⚠️ 安全措施:前端传入的排序参数必须在后端做白名单校验,只允许 "ASC""DESC",否则 ${} 仍然是注入入口。

场景②:动态表名

@Select("SELECT * FROM ${tableName} WHERE status = 1")
List<Product> listByTable(String tableName);

表名同样不能加引号。处理方式同样是白名单校验。

6.5 LIKE 查询的安全写法

模糊搜索商品名称是一个常见需求。三种写法对比:

// ❌ 方案A:报错——#{} 被当作字符串的一部分不是占位符
@Select("SELECT * FROM product WHERE prod_name LIKE '%#{keyword}%'")

// ⚠️ 方案B:功能正常但危险——${} 存在注入风险
@Select("SELECT * FROM product WHERE prod_name LIKE '%${keyword}%'")

// ✅ 方案C:推荐——用 MySQL 的 CONCAT 函数安全拼接
@Select("SELECT * FROM product WHERE prod_name LIKE CONCAT('%', #{keyword}, '%')")
List<Product> searchByName(String keyword);

CONCAT 函数在 MySQL 服务端执行字符串拼接,#{keyword} 仍然走预编译占位符,既实现了模糊查询,又不会引入 SQL 注入风险。

📌 面试点总结

  • 必问#{}${} 有什么区别?—— 预编译 vs 即时拼接,安全性、性能、引号处理。
  • 追问:既然 ${} 不安全,为什么还保留?—— 排序/表名/字段名必须用 ${},但需要白名单校验。
  • 实战:LIKE 查询怎么写?—— CONCAT('%', #{keyword}, '%')

七、数据库连接池 — 性能的幕后英雄

7.1 为什么需要连接池

数据库连接(Connection)的创建和销毁代价高昂:

  • TCP 三次握手建立网络连接
  • 数据库服务端分配线程和内存
  • 认证握手(用户名密码验证)

如果每次请求都新建一个 Connection,高并发下数据库很快会因为连接耗尽而拒绝服务。更严重的是,频繁的创建/销毁会增加大量不必要的网络开销和 CPU 消耗。

连接池的核心思想:启动时预创建一批 Connection 放入池中,请求来了直接取用,用完后归还而非销毁
在这里插入图片描述

7.2 HikariCP — Spring Boot 默认之选

HikariCP 是 Spring Boot 2.x 起的默认连接池,设计哲学是"极致性能"。性能优势来自几个关键优化:

  • 字节码级精简:相比其他连接池,HikariCP 的代码量极少,大量使用 JIT 友好的编码模式,减少方法调用层级。
  • 无锁设计:使用 ConcurrentBag 替代传统 BlockingQueue,减少线程间的锁竞争。
  • 连接代理轻量化:生成的 ProxyConnection 对象极轻,GC 压力小。

不需要额外依赖,Spring Boot 自动配置即用。

7.3 Druid — 阿里的"瑞士军刀"

Druid 不只是连接池,更像一个数据库中间件全家桶:

功能 说明
SQL 监控 统计每条 SQL 的执行次数、耗时、并发、返回行数
WallFilter 防火墙 基于白名单/黑名单拦截 SQL 注入,比 #{} 多了一层应用级防护
可视化面板 内置 Web 监控页面,实时查看 SQL 执行情况和连接池状态
日志 支持慢 SQL 记录、执行异常日志
密码加密 数据库密码在配置文件中可加密存储

引入 Druid(Spring Boot 3.x):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.21</version>
</dependency>
spring:
  datasource:
    druid:
      url: jdbc:mysql://127.0.0.1:3306/ecommerce_db
      username: root
      password: your_password
      stat-view-servlet:
        enabled: true           # 开启监控页面
        url-pattern: /druid/*   # 访问路径

访问 http://localhost:8080/druid/ 即可看到监控面板。

7.4 对比与选型

维度 HikariCP Druid
性能 ★★★★★ 极致 ★★★★ 优秀
监控能力 无内置 ★★★★★ SQL 统计 + 可视化面板
SQL 防火墙 ★★★★ WallFilter
配置复杂度 几乎零配置 中等,功能多配置项也多
社区生态 全球社区 国内生态,中文文档丰富
适用场景 追求极致性能的中小型项目 需要 SQL 监控、运维可视化的企业级项目

📌 面试点

  • Spring Boot 默认连接池是什么?—— HikariCP(2.x 起取代 Tomcat JDBC Pool)。
  • 为什么从 Tomcat 换成 Hikari?—— Hikari 性能更优,代码更精简,Spring 官方认定为最佳选择。
  • Druid 的 WallFilter 原理?—— 拦截 SQL 执行前,基于规则引擎(白名单/黑名单)判断 SQL 是否安全,相当于应用的"WAF"。

八、MyBatis 缓存机制 — 被忽略的面试杀器

缓存是 MyBatis 面试中被询问比例仅次于 #{} ${} 的考点,但很多候选人答不好。这一章让你建立完整认知。

在这里插入图片描述

8.1 一级缓存(SqlSession 级别)

默认开启,无需配置。

作用域:同一个 SqlSession 内。当你在同一个 SqlSession 中执行两次相同的查询,第二次不会发送 SQL 到数据库,而是直接从缓存返回。

// 同一个 SqlSession
Product p1 = productMapper.getById(1); // 发送 SQL
Product p2 = productMapper.getById(1); // 不发送 SQL,从缓存取
System.out.println(p1 == p2);          // true,同一对象引用

一级缓存失效的四种场景(面试常问):

序号 场景 原因
1 不同的 SqlSession 一级缓存是 SqlSession 级别的,换个 SqlSession 就没缓存了
2 同一个 SqlSession 但查询条件不同 缓存的 key 是 statementId + SQL + 参数,条件变了 key 就变了
3 两次查询之间执行了增删改操作 增删改会清空一级缓存(因为数据可能已被修改,缓存不保证一致性)
4 手动清空缓存 sqlSession.clearCache() 显式清除

8.2 二级缓存(Mapper 级别 / namespace 级别)

默认关闭,需要手动配置。

作用域:同一个 namespace(即同一个 Mapper)内的所有 SqlSession 共享。

开启步骤:

Step 1application.yml 全局开关

mybatis:
  configuration:
    cache-enabled: true

Step 2:在 XML Mapper 文件中加 <cache/> 标签

<mapper namespace="com.example.mapper.ProductMapper">
    <cache/>
    <!-- ... -->
</mapper>

Step 3:实体类实现 Serializable 接口

@Data
public class Product implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

二级缓存的工作流程:

SqlSession1 查询 → 查 DB → 结果放入二级缓存
    ↓
SqlSession1 关闭(此时一级缓存数据刷入二级缓存)
    ↓
SqlSession2 查询相同 SQL → 命中二级缓存 → 直接返回

8.3 一、二级缓存对比

维度 一级缓存 二级缓存
作用域 SqlSession Mapper(namespace)
默认状态 开启 关闭
生命周期 随 SqlSession 创建/销毁 整个应用生命周期(或过期策略控制)
是否跨 SqlSession
清空时机 增删改 / close / clearCache 增删改 / 过期策略
序列化要求 实体类需实现 Serializable

8.4 使用建议与注意事项

  • 一级缓存:简单可靠,无需额外配置,放心用。
  • 二级缓存:谨慎使用。以下场景不适合开启:
    • 多表关联查询(A 表数据变了,B 表相关的缓存不会自动失效)
    • 数据频繁修改的表(缓存命中率低,反而增加序列化/反序列化开销)
    • 分布式部署(二级缓存是 JVM 本地缓存,多实例数据不一致)

📌 面试点

  • MyBatis 有一级缓存和二级缓存,分别是什么?—— 一级:SqlSession 级别,默认开启;二级:Mapper 级别,需手动配置。
  • 一级缓存在什么情况下会失效?—— 四种场景(不同 SqlSession、不同查询条件、中间有增删改、手动 clearCache)。
  • 二级缓存的使用风险?—— 多表关联导致脏数据、分布式环境不一致。

总结

本文以电商订单系统的 productcustomerorder_info 三张表为案例,完整覆盖了 MyBatis 入门到面试的全部核心知识点。回顾一下八大模块的主线:

模块 核心收获 面试权重
JDBC → MyBatis 理解框架存在的意义:消除模板代码 ★★
快速入门 能独立搭建 Spring Boot + MyBatis 项目 ★★
日志配置 掌握调试手段
注解式 CRUD 会用注解完成增删改查 + 字段映射三方案 ★★★
XML 方式 知道什么场景用 XML,能写 resultMap ★★★
#{} vs ${} 必须精通:预编译原理 + 注入防御 + ${} 场景 ★★★★★
连接池 理解 Hikari vs Druid 的选型差异 ★★★
缓存机制 一级/二级缓存的区别和失效场景 ★★★★

一句话总结:MyBatis 的本质是"把 SQL 还给你"——它不帮你生成 SQL,但帮你打理好连接、参数、映射、缓存、事务等所有脏活累活。掌握 MyBatis,就是掌握 Java 后端持久层的基本功。


本文技术栈:Spring Boot 3.1.x + MyBatis 3.0.x + MySQL 8.0 | 案例数据库:ecommerce_db


本文为《MyBatis 从入门到面试》系列的第一篇。进阶篇请访问:【MyBatis】进阶篇:动态 SQL + 关联映射 + 插件原理全通关。系列文章持续更新中,感谢关注。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐