MyBatis知识梳理作者没有四次元口袋的蓝胖日期2026-07-23标签Java, MyBatis, 缓存, 分页, 面试题一、MyBatis 缓存1.1 一级缓存本地缓存作用域同一个SqlSession内有效。SqlSessionsessionsqlSessionFactory.openSession();UserMappermappersession.getMapper(UserMapper.class);// 第一次查询 → 走数据库Useru1mapper.selectById(1);// 第二次查询同一个 SQL → 走一级缓存不再查库Useru2mapper.selectById(1);// u1 u2同一个对象引用一级缓存失效的四种情况情况原因SqlSession 关闭缓存随 session 销毁SqlSession 执行了增删改操作并 commit缓存被清空防止脏读手动清除缓存session.clearCache()主动清空使用不同的 SqlSession各自独立的缓存MyBatis默认开启一级缓存无法关闭。1.2 二级缓存全局缓存作用域同一个namespace同一个 Mapper的所有 SqlSession 共享。SqlSession A查询→ 数据写入二级缓存 SqlSession B查询→ 从二级缓存读取开启步骤!-- 1. 全局配置开启二级缓存 --settingssettingnamecacheEnabledvaluetrue//settings!-- 2. Mapper XML 中声明使用缓存 --mappernamespacecom.example.UserMappercache/!-- 加上这行 --/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 #{} 工作原理预编译selectidselectByNameSELECT * FROM user WHERE name #{name}/selectMyBatis 生成的 SQLSELECT*FROMuserWHEREname?-- 参数张三使用PreparedStatement参数用?占位参数值由 JDBC 驱动处理不会被当作 SQL 语法防止 SQL 注入2.3 ${} 工作原理字符串替换selectidselectByColumnSELECT * FROM user ORDER BY ${columnName}/selectMyBatis 生成的 SQLSELECT*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 内置内存分页❌ 先查全部再截取数据量大时 OOMPageHelper 插件拦截 SQL自动拼接 LIMIT✅ 最常用对业务代码无侵入3.3 PageHelper 使用最推荐引入依赖dependencygroupIdcom.github.pagehelper/groupIdartifactIdpagehelper-spring-boot-starter/artifactIdversion1.4.7/version/dependency使用GetMapping(/users)publicPageInfoUserlistUsers(RequestParam(defaultValue1)intpageNum,RequestParam(defaultValue10)intpageSize){// ① 设置分页参数紧跟着的查询会生效PageHelper.startPage(pageNum,pageSize);// ② 正常写查询不用改 SQLListUserusersuserMapper.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){ThreadLocalPage?localPage...;localPage.set(newPage(pageNum,pageSize));}// 2. 拦截器拦截 SQL 执行// 3. 从 ThreadLocal 取出分页参数// 4. 自动改写 SQLSELECT * 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.idt.id-- 方案二游标分页记住上次最后一条的 IDSELECT*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优化。”学习建议缓存重点理解一级缓存的失效场景二级缓存知道为什么不用就行#{} vs ${}必须能说出原理和安全区别分页PageHelper 会用就行深度分页优化是加分点