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的缓存机制。

Mybatis - 精巧的持久层框架-缓存机制的深刻理解的更多相关文章

  1. MyBatis(四):自定义持久层框架优化

    本文所有代码已上传至码云:https://gitee.com/rangers-sun/mybatis 修改IUserDao.UserMapper.xml package com.rangers; im ...

  2. MyBatis(三):自定义持久层框架实现

    代码已上传至码云:https://gitee.com/rangers-sun/mybatis 新建Maven工程 架构端MyPersistent.使用端MyPersistentTest,使用端引入架构 ...

  3. MyBatis(二):自定义持久层框架思路分析

    使用端 引入架构端Maven依赖 SqlMapConfig.xml-数据库配置信息(数据库连接jar名称.连接URL.用户名.密码),引入Mapper.xml的路径 XxMapper.xml-SQL配 ...

  4. 开源顶级持久层框架——mybatis(ibatis)——day02

    mybatis第二天    高级映射 查询缓存 和spring整合          课程复习:         mybatis是什么?         mybatis是一个持久层框架,mybatis ...

  5. Mybatis详解系列(一)--持久层框架解决了什么及如何使用Mybatis

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  6. Mybatis学习之自定义持久层框架(二) 自定义持久层框架设计思路

    前言 上一篇文章讲到了JDBC的基本用法及其问题所在,并提出了使用Mybatis的好处,那么今天这篇文章就来说一下该如何设计一个类似Mybatis这样的持久层框架(暂时只讲思路,具体的代码编写工作从下 ...

  7. 持久层框架:MyBatis 3.2(1)

    MyBatis 的前身就是 iBatis .是一个数据持久层(ORM)框架. iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架.iBATIS提供的持久 ...

  8. Java 持久层框架之 MyBatis

    MyBatis 简介 MyBatis 是一个基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不用再花费精力去处理诸如注册驱动.创建 Connection.配 ...

  9. Mybatis(一):手写一套持久层框架

    作者 : 潘潘 未来半年,有幸与导师们一起学习交流,趁这个机会,把所学所感记录下来. 「封面图」 自毕业以后,自己先创业后上班,浮沉了近8年,内心着实焦躁,虽一直是走科班路线,但在技术道路上却始终没静 ...

  10. MyBatis持久层框架使用总结

    MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis . 2 ...

随机推荐

  1. 结合钉钉机器人用python写监控打印机碳粉状态程序

    点击查看代码 from pysnmp.hlapi import * import requests import json # 配置信息 PRINTER_IP = '1.1.1.1' # 打印机IP ...

  2. 小结.NET 9性能优化黑科技:从内存管理到Web性能的最全指南

    引言:性能优化的重要性与 .NET 9 的性能提升 ❝ 性能优化不仅关乎代码执行效率,还直接影响用户满意度和系统可扩展性.例如,一个响应缓慢的 Web 应用可能导致用户流失,而一个内存占用过高的服务可 ...

  3. 多模态模型 Grounding DINO 初识

    简介 Grounding DINO 是一种先进的零样本目标检测模型,由 IDEA Research 开发.它通过将基于 Transformer 的检测器 DINO 与Grounded Pre-Trai ...

  4. 在 Go 中,如何实现一个带过期时间的字典映射

    有些时候,应用系统用不上 redis,我们也可以用锁和 goroutine 实现一个带有过期时间的线程安全的字典. 这种字典的应用场景,比较倾向于数据规模较小,没有分布式要求. 下面是实现: 1.定义 ...

  5. access vba实现OLE对象保存到本地

    参考oletodisk的实现方法,更新为在64位office上野可以运行,函数模块代码如下: 1 Option Compare Database 2 Option Explicit 3 4 5 'DE ...

  6. 服务器时间漂移,如何开启Linux NTP自动同步

    前言 在日常服务器运维中,我们往往默认服务器的时间是精准的.但最近一次偶然的 date 查询,让我发现--服务器时间竟然悄悄地漂移了-- 本文记录了整个排查与解决的过程,希望能帮到遇到类似问题的朋友, ...

  7. 35.2K star!双链笔记+知识图谱+本地优先,这款开源知识管理神器绝了!

    一款融合「双链笔记+知识图谱+本地优先」理念的开源知识管理工具,支持Markdown/Org-mode双格式,打造你的第二大脑! 项目介绍 "Logseq 是一个注重隐私.开源的知识管理平台 ...

  8. 【经验】日常|WakeUp、Outlook、Google日历导入飞书日历

    以飞书团队账号登录时,设置的日历就能被团队其他成员看到(可选择私密.仅忙碌[默认].完全公开三种模式),以便相互查看空闲时间. Wakeup日历导出到Outlook日历 Wakeup支持从各大学校便利 ...

  9. Django踩坑之ExtendsNode: extends 'base/base.html'> must be the first tag in the template.

    模板继承报错: extends 'base/base.html'> must be the first tag in the template base.html如下: <!-- {% l ...

  10. Vue-lazyload实现图片懒加载

    前端多半是和页面打交道,我们在进行页面的展示的时候,对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载.即需要使用到 ...