Mybatis – 精巧的持久层框架-缓存机制的深刻理解

Mybatis缓存机制

Mybatis的缓存机制是其性能优化的核心,也是面试中的高频考点。理解它不仅能写出更高性能的代码,还能明白框架设计中对性能与数据一致性权衡的智慧。

此教程从概念到实战,从基础到企业应用,确保不仅能看懂,更能跟着动手实践,彻底掌握它。


Mybatis缓存机制深度解析与实战

引子:为什么需要缓存?

想象一下,你每次去图书馆借同一本《Java编程思想》,都得重新在前台办理一遍完整的借书手续。这显然效率低下。如果前台有个小架子,放着最近常被借阅的书,你来了直接拿走,效率是不是就高多了?

在数据库交互中,缓存(Cache) 就是这个“小架子”。它是一块内存区域,用于存储那些已经被查询过的数据。当下次再需要同样的数据时,程序可以直接从缓存中获取,而不必再次访问慢速的数据库,从而大幅提升应用性能。

Mybatis内置了两种缓存:一级缓存二级缓存


第一部分:一级缓存 (SqlSession级别)

1. 概念解析

  • 别名:本地缓存 (Local Cache)。

  • 作用域 (Scope):它的生命周期与 SqlSession 完全绑定。也就是说,每个SqlSession对象都有自己独立的一级缓存。当SqlSession被创建时,它的一级缓存就诞生了;当SqlSession被关闭时,它的一级缓存也随之销毁。

  • 工作状态默认开启,无法关闭。这是Mybatis的内置特性。

  • 工作原理(核心)

    1. 在一个SqlSession中,当你第一次执行某个查询时,Mybatis会从数据库获取数据,并将这份数据存入当前SqlSession的一级缓存中。
    2. 在该SqlSession未关闭未执行任何增删改操作的情况下,你再次执行完全相同的查询(SQL语句、参数都一样),Mybatis会直接从一级缓存中返回数据,而不会再次访问数据库。
  • 缓存失效的场景

    1. SqlSession被关闭 (session.close())。
    2. 在当前SqlSession中执行了任何增、删、改(DML)操作 (insert, update, delete)。因为这可能导致缓存中的数据与数据库不一致(“脏数据”),所以Mybatis会清空缓存以保证数据准确性。
    3. 手动调用session.clearCache()方法。

2. 动手实践:验证一级缓存

项目结构准备:我们将使用一个标准的Maven项目结构。

mybatis-cache-demo/ ├── pom.xml └── src/     ├── main/     │   ├── java/     │   │   └── com/     │   │       └── example/     │   │           ├── entity/     │   │           │   └── User.java      // 用户实体类     │   │           ├── mapper/     │   │           │   └── UserMapper.java  // Mapper接口     │   │           └── test/     │   │               └── L1CacheTest.java // 我们的一级缓存测试类     │   └── resources/     │       ├── mappers/     │       │   └── UserMapper.xml     // SQL映射文件     │       └── mybatis-config.xml         // Mybatis全局配置     └── test/         └── ... (我们这里为了方便,测试类也放在main下) 

准备代码

  1. pom.xml (依赖)

    <dependencies>     <dependency>         <groupId>org.mybatis</groupId>         <artifactId>mybatis</artifactId>         <version>3.5.9</version>     </dependency>     <dependency>         <groupId>mysql</groupId>         <artifactId>mysql-connector-java</artifactId>         <version>8.0.28</version>     </dependency>     <dependency>         <groupId>org.projectlombok</groupId>         <artifactId>lombok</artifactId>         <version>1.18.24</version>         <scope>provided</scope>     </dependency> </dependencies> 
  2. mybatis-config.xml (全局配置)

    <!-- src/main/resources/mybatis-config.xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration>     <environments default="development">         <environment id="development">             <transactionManager type="JDBC"/>             <dataSource type="POOLED">                 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>                 <property name="url" value="jdbc:mysql://localhost:3306/your_db?useSSL=false&amp;serverTimezone=UTC"/>                 <property name="username" value="root"/>                 <property name="password" value="your_password"/>             </dataSource>         </environment>     </environments>     <mappers>         <mapper resource="mappers/UserMapper.xml"/>     </mappers> </configuration> 
  3. User.java (实体类)

    // src/main/java/com/example/entity/User.java package com.example.entity;  import lombok.Data; import lombok.ToString;  @Data // 使用Lombok简化代码 public class User {     private Integer id;     private String username;     private String password;      // 我们特意添加一个构造函数,方便观察对象是否被重新创建     public User() {         System.out.println("User对象被创建了!(A new User object was created!)");     } } 
  4. UserMapper.javaUserMapper.xml

    // src/main/java/com/example/mapper/UserMapper.java package com.example.mapper; import com.example.entity.User; public interface UserMapper {     User findById(Integer id);     int updateUsername(User user); } 
    <!-- src/main/resources/mappers/UserMapper.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.UserMapper">     <select id="findById" resultType="com.example.entity.User">         SELECT * FROM user WHERE id = #{id}     </select>     <update id="updateUsername">         UPDATE user SET username = #{username} WHERE id = #{id}     </update> </mapper> 
  5. L1CacheTest.java (核心测试代码)

    // src/main/java/com/example/test/L1CacheTest.java package com.example.test;  import com.example.entity.User; import com.example.mapper.UserMapper; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream;  public class L1CacheTest {     public static void main(String[] args) throws IOException {         String resource = "mybatis-config.xml";         InputStream inputStream = Resources.getResourceAsStream(resource);         SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);          // 使用同一个SqlSession         try (SqlSession session = sqlSessionFactory.openSession(true)) {             UserMapper mapper = session.getMapper(UserMapper.class);              System.out.println("--- 场景1:验证一级缓存的存在 ---");             System.out.println("第一次查询ID为1的用户...");             User user1 = mapper.findById(1);             System.out.println(user1);              System.out.println("n第二次查询ID为1的用户 (在同一个session中)...");             User user2 = mapper.findById(1);             System.out.println(user2);             System.out.println("user1 == user2 ? " + (user1 == user2)); // 验证是否是同一个对象              System.out.println("n--- 场景2:验证DML操作会清空一级缓存 ---");             System.out.println("执行更新操作...");             user1.setUsername("admin_updated");             mapper.updateUsername(user1);              System.out.println("n更新后,再次查询ID为1的用户...");             User user3 = mapper.findById(1);             System.out.println(user3);             System.out.println("user1 == user3 ? " + (user1 == user3));         }     } } 

预期输出与分析:

--- 场景1:验证一级缓存的存在 --- 第一次查询ID为1的用户... User对象被创建了!(A new User object was created!)  <-- 第一次查询,创建了对象 User(id=1, username=admin, password=...)  第二次查询ID为1的用户 (在同一个session中)... User(id=1, username=admin, password=...)  <-- 第二次查询,没有打印“User对象被创建了” user1 == user2 ? true  <-- 证明了第二次是从缓存中拿的同一个对象!  --- 场景2:验证DML操作会清空一级缓存 --- 执行更新操作...  更新后,再次查询ID为1的用户... User对象被创建了!(A new User object was created!) <-- DML后,缓存失效,重新查询数据库,创建了新对象 User(id=1, username=admin_updated, password=...) user1 == user3 ? false <-- 证明了缓存被清空,拿到了新的对象 

3. 企业级思考

一级缓存非常有用,它能有效减少单个业务逻辑单元(例如一个Service方法内部)的数据库查询次数。但在典型的Web应用中,每个用户请求通常会创建一个新的SqlSession,执行完后就关闭。这意味着一级缓存无法跨请求共享数据。为了解决这个问题,二级缓存应运而生。


第二部分:二级缓存 (SqlSessionFactory级别)

1. 概念解析

  • 别名:全局缓存 (Global Cache)。
  • 作用域 (Scope):它的生命周期与 SqlSessionFactory 绑定,或者说它是在Mapper的命名空间Namespace级别共享的。这意味着,所有SqlSession都可以共享同一个Mapper的二级缓存
  • 工作状态默认关闭,需要手动开启
  • 工作原理(核心)
    1. 当一个SqlSession执行完查询并提交/关闭 (commit/close)后,它的一级缓存中的数据会被转移到对应Mapper的二级缓存中。
    2. 另一个新的SqlSession来执行相同的查询时,它会先去二级缓存中查找数据。
    3. 如果找到了,就直接返回数据;如果没找到,再走“查询数据库 -> 放入自己的一级缓存”的老路。
  • 开启二级缓存的三个步骤(缺一不可)
    1. mybatis-config.xml中开启全局缓存开关。
    2. 在需要缓存的Mapper.xml文件中添加<cache/>标签。
    3. 需要被缓存的实体类(POJO)必须实现 java.io.Serializable 接口。因为二级缓存可能将对象存储在硬盘或通过网络传输,这需要序列化。

2. 动手实践:开启并验证二级缓存

修改代码 (在之前的基础上)

  1. 修改 mybatis-config.xml

    <configuration>     <!-- 开启全局缓存开关 -->     <settings>         <setting name="cacheEnabled" value="true"/>     </settings>     <!-- 其他配置... --> </configuration> 
  2. 修改 UserMapper.xml

    <mapper namespace="com.example.mapper.UserMapper">     <!-- 开启当前Mapper的二级缓存 -->     <cache></cache>     <!-- 其他SQL... --> </mapper> 
  3. 修改 User.java

    // src/main/java/com/example/entity/User.java import java.io.Serializable; // 引入接口  @Data public class User implements Serializable { // 实现Serializable接口     // ... 内容不变 } 
  4. 创建 L2CacheTest.java (新的测试类)

    // src/main/java/com/example/test/L2CacheTest.java package com.example.test;  import com.example.entity.User; import com.example.mapper.UserMapper; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream;  public class L2CacheTest {     public static void main(String[] args) throws IOException {         String resource = "mybatis-config.xml";         InputStream inputStream = Resources.getResourceAsStream(resource);         SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);          System.out.println("--- 验证二级缓存 ---");          User user1 = null;         // 第一个 session         try (SqlSession session1 = sqlSessionFactory.openSession(true)) {             UserMapper mapper1 = session1.getMapper(UserMapper.class);             System.out.println("Session 1: 第一次查询...");             user1 = mapper1.findById(1);             System.out.println(user1);         } // session1关闭时,数据会从它的一级缓存刷新到二级缓存          System.out.println("nSession 1 已关闭。n");          User user2 = null;         // 第二个 session         try (SqlSession session2 = sqlSessionFactory.openSession(true)) {             UserMapper mapper2 = session2.getMapper(UserMapper.class);             System.out.println("Session 2: 再次查询相同数据...");             user2 = mapper2.findById(1);             System.out.println(user2);         }          System.out.println("nuser1.equals(user2) ? " + user1.equals(user2));         System.out.println("user1 == user2 ? " + (user1 == user2));     } } 

预期输出与分析:

--- 验证二级缓存 --- Session 1: 第一次查询... User对象被创建了!(A new User object was created!) <-- 第一个session查询,创建对象 User(id=1, username=admin_updated, password=...)  Session 1 已关闭。  Session 2: 再次查询相同数据... User(id=1, username=admin_updated, password=...) <-- 第二个session查询,没有打印“User对象被创建了”                                                 <-- 这证明了数据来自缓存,而不是数据库!  user1.equals(user2) ? true   <-- 内容相同 user1 == user2 ? false  <-- 但对象不同!因为二级缓存返回的是序列化后再反序列化的副本,不是原对象。 

这个false的结果是理解二级缓存的关键,它与一级缓存的true形成鲜明对比。

3. 企业级应用与思考

  • 适用场景:二级缓存非常适合读多写少数据不常变化的场景。

    • 绝佳例子:系统配置表、国家/地区/省份代码表、商品分类信息、用户角色权限。这些数据被频繁读取,但很少修改。为它们开启二级缓存能极大地提升性能。
    • 不适用例子:商品库存、用户余额、订单状态。这些数据变化频繁,如果使用缓存,很容易出现数据不一致的问题。
  • 缓存击穿与第三方缓存:Mybatis自带的二级缓存功能相对基础。在大型分布式系统中,为了解决缓存击穿、雪崩等问题,以及实现更精细的缓存控制(如设置过期时间),企业通常会整合专业的第三方缓存框架,如 RedisEhcache

    • 企业实践:在Mapper.xml<cache>标签中,可以通过type属性指定使用Redis作为二级缓存的实现。这样做的好处是,缓存由独立的Redis服务管理,可以被多个应用实例共享,并且应用重启后缓存依然存在。

总结与对比

特性 一级缓存 (L1) 二级缓存 (L2)
作用域 SqlSession SqlSessionFactory (或Mapper Namespace)
生命周期 SqlSession共存亡 与应用共存亡
默认状态 默认开启,无法关闭 默认关闭,需手动开启
共享性 不共享,SqlSession之间隔离 所有SqlSession共享
数据一致性 强,DML操作自动清空 弱,依赖于配置和DML刷新
对象引用 返回同一个对象 (==true) 返回对象的副本 (反序列化,==false)
核心用途 优化单个业务流程内的重复查询 优化跨业务、跨请求的全局热点数据查询

核心记忆点缓存是性能和数据一致性之间的一种权衡。 一级缓存牺牲了小部分内存,换取了单个会话内的性能提升,且能保证强一致性。二级缓存牺牲了更强的实时一致性,换取了全局范围的巨大性能提升。理解这个核心思想,你就真正掌握了Mybatis的缓存机制。

发表评论

评论已关闭。

相关文章