MyBatis知识梳理
2026/7/24 23:14:19 网站建设 项目流程

MyBatis知识梳理

作者:没有四次元口袋的蓝胖
日期:2026-07-23
标签:Java, MyBatis, 缓存, 分页, 面试题


一、MyBatis 缓存

1.1 一级缓存(本地缓存)

作用域:同一个SqlSession内有效。

SqlSessionsession=sqlSessionFactory.openSession();UserMappermapper=session.getMapper(UserMapper.class);// 第一次查询 → 走数据库Useru1=mapper.selectById(1);// 第二次查询同一个 SQL → 走一级缓存,不再查库Useru2=mapper.selectById(1);// u1 == u2(同一个对象引用)

一级缓存失效的四种情况

情况原因
SqlSession 关闭缓存随 session 销毁
SqlSession 执行了增删改操作并 commit缓存被清空,防止脏读
手动清除缓存session.clearCache()主动清空
使用不同的 SqlSession各自独立的缓存

MyBatis默认开启一级缓存,无法关闭。

1.2 二级缓存(全局缓存)

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

SqlSession A(查询)→ 数据写入二级缓存 SqlSession B(查询)→ 从二级缓存读取

开启步骤

<!-- 1. 全局配置开启二级缓存 --><settings><settingname="cacheEnabled"value="true"/></settings><!-- 2. Mapper XML 中声明使用缓存 --><mappernamespace="com.example.UserMapper"><cache/><!-- 加上这行 --></mapper>
// 3. 实体类必须实现 SerializablepublicclassUserimplementsSerializable{...}

注意事项

  • 二级缓存是跨 SqlSession的,多个 session 共享
  • 同样在增删改并 commit 后会清空缓存
  • 实际开发中很少使用,因为多表操作时容易出现脏数据
  • 分布式环境下,多个实例各自的二级缓存不同步

1.3 一级 vs 二级缓存对比

维度一级缓存二级缓存
作用域单个 SqlSession同一个 namespace
默认状态默认开启,不可关闭默认关闭,需手动开启
缓存类型本地缓存跨 session 共享
实际使用自动生效,无感知很少用(分布式+多表场景有坑)

面试追问:为什么实际开发很少用二级缓存?
答:多表查询时缓存清理范围是 namespace 级别,容易脏读;分布式多实例各自缓存不同步。一般用 Redis 做统一缓存更可靠。


二、#{} 与 ${} 的区别(⭐ 高频)

2.1 核心区别

维度#{}${}
本质预编译参数(占位符?字符串拼接(直接拼到 SQL 中)
SQL 示例WHERE name = ?WHERE name = '张三'
是否防 SQL 注入✅ 是❌ 否
使用场景绝大多数情况动态表名、列名、排序等

2.2 #{} 工作原理(预编译)

<selectid="selectByName">SELECT * FROM user WHERE name = #{name}</select>

MyBatis 生成的 SQL:

SELECT*FROMuserWHEREname=?-- 参数:'张三'
  • 使用PreparedStatement,参数用?占位
  • 参数值由 JDBC 驱动处理,不会被当作 SQL 语法
  • 防止 SQL 注入

2.3 ${} 工作原理(字符串替换)

<selectid="selectByColumn">SELECT * FROM user ORDER BY ${columnName}</select>

MyBatis 生成的 SQL:

SELECT*FROMuserORDERBYname-- columnName 的值 "name" 直接拼进去了
  • 使用Statement,参数值直接拼接到 SQL 字符串中
  • 有 SQL 注入风险

2.4 什么时候必须用 ${}?

<!-- 动态表名 -->SELECT * FROM ${tableName} WHERE id = #{id}<!-- 动态列名 -->SELECT ${columnName} FROM user<!-- 动态排序 -->SELECT * FROM user ORDER BY ${orderColumn} ${orderDir}

表名、列名、排序方向这些不能用 ? 占位,只能用${}

安全原则:能用#{}就用#{},只有表名、列名等不能用占位符的场景才用${},且必须在代码层做参数校验,防止注入。


三、MyBatis 分页

3.1 为什么需要分页?

数据量大时不能一次全部查出:

  • 内存可能撑不住(OOM)
  • 数据库压力大
  • 用户体验差

3.2 分页方式对比

方式原理优缺点
SQL 手写 LIMITLIMIT offset, size简单但每页都要写
RowBoundsMyBatis 内置,内存分页❌ 先查全部再截取,数据量大时 OOM
PageHelper 插件拦截 SQL,自动拼接 LIMIT✅ 最常用,对业务代码无侵入

3.3 PageHelper 使用(最推荐)

引入依赖

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.7</version></dependency>

使用

@GetMapping("/users")publicPageInfo<User>listUsers(@RequestParam(defaultValue="1")intpageNum,@RequestParam(defaultValue="10")intpageSize){// ① 设置分页参数(紧跟着的查询会生效)PageHelper.startPage(pageNum,pageSize);// ② 正常写查询,不用改 SQLList<User>users=userMapper.selectAll();// ③ 用 PageInfo 包装结果returnnewPageInfo<>(users);}

PageInfo 返回的关键信息

{"pageNum":1,"pageSize":10,"total":100,"pages":10,"list":[...],"isFirstPage":true,"isLastPage":false}

3.4 PageHelper 原理

// PageHelper 内部做了这些事:// 1. startPage() 把分页参数存入 ThreadLocalpublicstaticvoidstartPage(intpageNum,intpageSize){ThreadLocal<Page<?>>localPage=...;localPage.set(newPage<>(pageNum,pageSize));}// 2. 拦截器拦截 SQL 执行// 3. 从 ThreadLocal 取出分页参数// 4. 自动改写 SQL:SELECT * FROM user → SELECT * FROM user LIMIT ?, ?// 5. 同时自动执行 SELECT COUNT(*) 获取总数// 6. 用 PageInfo 封装结果

3.5 LIMIT 分页的性能问题

-- 第 1 页(快)SELECT*FROMuserLIMIT0,10-- 第 10000 页(慢!)SELECT*FROMuserLIMIT99990,10-- MySQL 会扫描 100000 行,丢弃前 99990 行,只返回 10 行

深度分页优化

-- 方案一:延迟关联(先查 ID,再回表)SELECTu.*FROMuseruINNERJOIN(SELECTidFROMuserORDERBYidLIMIT99990,10)tONu.id=t.id-- 方案二:游标分页(记住上次最后一条的 ID)SELECT*FROMuserWHEREid>#{lastId} ORDER BY id LIMIT 10-- 适合"加载更多"场景,不适合"跳页"

四、思维导图速览

MyBatis 核心知识 ├── 缓存 │ ├── 一级缓存 │ │ ├── SqlSession 级别(本地缓存) │ │ ├── 默认开启,不可关闭 │ │ └── 增删改 / 换 session / 手动清除 → 失效 │ │ │ └── 二级缓存 │ ├── namespace 级别(跨 session 共享) │ ├── 需手动开启(cacheEnabled + <cache/>) │ ├── 实体类要实现 Serializable │ └── 实际很少用(分布式用 Redis) │ ├── #{} vs ${} │ ├── #{} → PreparedStatement 占位符 ? → 防注入 → 默认用这个 │ ├── ${} → Statement 字符串拼接 → 有注入风险 │ └── ${} 使用场景:动态表名、列名、排序 │ └── 分页 ├── RowBounds → 内存分页(不推荐) ├── PageHelper → 拦截器自动改 SQL(最常用) │ ├── startPage() 存入 ThreadLocal │ ├── 拦截器改写 SQL 拼 LIMIT │ └── PageInfo 封装结果 └── 深度分页优化 ├── 延迟关联(先查 ID 再回表) └── 游标分页(WHERE id > lastId)

五、写在最后

面试高频回答

MyBatis 缓存

“MyBatis 有一级缓存和二级缓存。一级缓存是 SqlSession 级别的本地缓存,默认开启,执行增删改或切换 session 会失效。二级缓存是 namespace 级别的全局缓存,需要手动开启,但实际开发很少用,因为分布式多实例缓存不同步,多表查询容易脏读,一般用 Redis 替代。”

#{} 与 ${}

“#{} 是预编译占位符,用 PreparedStatement,参数不会被当作 SQL 语法,能防止 SQL 注入。${} 是字符串拼接,参数直接拼进 SQL,有注入风险。只有动态表名、列名等不能用占位符的场景才用 ${},且必须在代码层做参数校验。”

分页

“实际开发用 PageHelper 插件,它通过拦截器自动改写 SQL 拼接 LIMIT,对业务代码无侵入。深度分页时 LIMIT offset 性能差,可以用延迟关联(先查 ID 再回表)或游标分页(WHERE id > lastId)优化。”

学习建议

  1. 缓存:重点理解一级缓存的失效场景,二级缓存知道为什么不用就行
  2. #{} vs ${}:必须能说出原理和安全区别
  3. 分页:PageHelper 会用就行,深度分页优化是加分点

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询