Mybatis07-缓存

发布于:2025-07-17 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、缓存机制的原理

计算机每次从mysql中执行sql语句,都是内存与硬盘的通信,对计算机来说,影响效率。

因此使用缓存机制。

 

1-1、MyBatis 的缓存机制:

执行 DQL(select 语句)的时候,将查询结果放到缓存当中(内存当中),如果下一次还是执行完全相同的 DQL(select 语句)语句,直接从缓存中拿数据,不再查数据库了。不再去硬盘上找数据了。

示例:

第一次执行这个 SQL:

select * from t_car where id=1;

第二次还是执行这个 SQL,完全一样:

select * from t_car where id=1;

此时,从缓存中获取,不再查数据库了。

当两条sql语句之间,对数据库做了任何修改操作,缓存将从内存中清除。

目的:提高执行效率。

缓存机制:使用减少 IO 的方式来提高效率。

IO:读文件和写文件。

缓存通常是我们程序开发中优化程序的重要手段:

  • 字符串常量池
  • 整数型常量池
  • 线程池
  • 连接池
  • ……

【小结】:

        缓存(cache)就是内存,提前把数据放到内存中,下一次用的时候,直接从缓存中拿,效率高!

二、几种常见的缓存/池化技术

这些“池”技术,其实都是 Java 中的 缓存/复用机制,目的是:提升性能、减少资源消耗、避免频繁创建和销毁对象。下面来系统讲解几种常见的缓存/池化技术:


2-1、字符串常量池(String Constant Pool)

1、原理:

Java 中字符串是不可变的(final,所以 JVM 会把相同的字符串常量只保留一份副本,存放在一个称为 字符串常量池 的内存区域。

String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true
  • "hello" 是一个字符串字面量,保存在常量池中

  • s1 和 s2 都指向同一个地址

2、注意:

String s3 = new String("hello");
System.out.println(s1 == s3); // false(堆 vs 常量池)

如果你想把 new 出来的字符串放入常量池:

String interned = s3.intern();
System.out.println(s1 == interned); // true

 

2-2、整数型常量池(Integer Cache)

1、原理:

Java 对于包装类型 Integer,有一个缓存区[-128, 127]),当你使用 valueOf() 方法创建时,会从缓存中取对象而不是创建新对象。

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存)

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(未缓存)

2、范围:

JVM 默认缓存范围为 [-128, 127],可以通过启动参数修改

-XX:AutoBoxCacheMax=300

2-3、线程池(Thread Pool)

1、原理:

线程的创建与销毁成本高(涉及操作系统资源),频繁创建新线程会拖慢系统。

所以,线程池把线程复用起来,让多个任务共享固定线程,提高并发效率。

2、常用方式:

        // 创建固定大小为 3 的线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        // 模拟提交 5 个任务
        for (int i = 1; i <= 15; i++) {
            int taskId = i;
            pool.submit(new Runnable() {
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println("任务 " + taskId + " 开始,线程:" + threadName);
                    try {
                        Thread.sleep(20000); // 模拟任务耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("任务 " + taskId + " 结束,线程:" + threadName);
                }
            });
        }

        // 关闭线程池(注意不是立刻关闭)
        pool.shutdown();

打印结果: 

 

可以看到线程是复用的。

Executors 提供常见线程池工厂方法:

  • newFixedThreadPool(n)

  • newCachedThreadPool()

  • newSingleThreadExecutor()

  • newScheduledThreadPool(n)


2-4、连接池(Connection Pool)

1、 原理:

数据库连接创建代价高(要连接服务器、授权、建会话),所以使用连接池 复用已建立的连接

2、常见实现:

  • HikariCP(Spring Boot 默认)

  • DBCP

  • C3P0

  • Druid

3、示例(Spring Boot):

spring.datasource.hikari.maximum-pool-size: 10

应用启动后,会提前建立 10 个连接,放入连接池,供业务查询复用。


2-5、对象池(Object Pool)

对于那些频繁使用又比较重量级的对象(如:ByteBuffer, Socket, 数据库连接),也可以池化处理。

Java 标准库没有通用的 ObjectPool,但 Apache Commons Pool 提供支持。

GenericObjectPool<MyReusableObject> pool = new GenericObjectPool<>(new MyObjectFactory());

MyReusableObject obj = pool.borrowObject(); // 借
obj.doSomething();                          // 用
pool.returnObject(obj);                     // 还

你需要在项目中引入 Apache Commons Pool 的依赖(如果用 Maven):

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version> <!-- 可根据需要换版本 -->
</dependency>

 


2-6、内存缓存(如 LRU 缓存)

Java 中可以自己实现缓存算法(如 LRU),也可以使用:

  • Guava Cache

  • Caffeine(高性能)

  • Ehcache

  • Redis(分布式)

示例(Caffeine):

Cache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

2-7、类加载缓存(ClassLoader)

JVM 对每个类只加载一次,会将 .class 文件缓存到内存中,后续实例化只需创建对象而不重复加载。


2-8、反射缓存(Method/Field 缓存)

使用反射获取字段/方法(如 Class.getDeclaredMethods())是慢操作。JVM 会自动缓存这些反射结构,Spring、MyBatis 等框架也会自己做缓存。


2-9、JVM 运行时常见缓存(底层)

缓存类型 说明
字符串常量池 复用 String 常量
Integer 缓存 避免频繁装箱创建 Integer
Class 常量池 常量值如 final 字段、枚举等
方法句柄缓存 JVM 优化调用性能
Lambda 表达式缓存 编译后只创建一次匿名类对象

2-10、总结对比表

技术名称 类型 缓存对象 控制方式 是否可配置
字符串常量池 编译期/运行期 String 自动/intern()
Integer 缓存 运行期 Integer 自动/valueOf()
线程池 并发 Thread Executors
连接池 IO资源 DB连接 DataSource
ObjectPool 自定义 业务对象 Apache Commons
Guava/Caffeine 本地缓存 任意对象 API构建
Redis 分布式缓存 任意对象 客户端控制

三、Mybatis的缓存

mybatis 缓存包括:

  • 一级缓存:将查询到的数据存储到 SqlSession 中。
  • 二级缓存:将查询到的数据存储到 SqlSessionFactory 中。
  • 集成其它第三方的缓存:比如 EhCache【Java 语言开发的】、Memcache【C 语言开发的】等。

SqlSessionFactory是一个数据库一个,SqlSession作用域是当前的sql会话。

缓存只针对DQL语句,也就是说:缓存只针对select语句!

 

3-1、MyBatis 一级缓存

3-1-1、什么是一级缓存?

一级缓存是 MyBatis 的默认缓存机制,作用范围是 一次 SqlSession 内部。简单说:

同一个 SqlSession 中,相同的查询语句和参数,MyBatis 会从缓存中取数据,不会再次访问数据库

一级缓存mybatis默认开启,不需要任何配置!


3-1-2、一级缓存工作流程图

SqlSession
  ├── 查询语句 1(未命中缓存) → 查数据库,缓存结果
  ├── 查询语句 1(再次执行) → 命中缓存,直接返回
  └── SqlSession.close() → 缓存销毁

3-1-3、一级缓存使用示例

@Test
public void testFirstLevelCache() {
    SqlSession session = sqlSessionFactory.openSession();

    UserMapper mapper = session.getMapper(UserMapper.class);

    // 第一次查询,去数据库
    User u1 = mapper.selectById(1L);
    System.out.println("第一次查询:" + u1);

    // 第二次查询相同 ID,命中缓存
    User u2 = mapper.selectById(1L);
    System.out.println("第二次查询:" + u2);

    System.out.println(u1 == u2); // true(同一个对象)

    session.close();
}

【注意】:

此时,控制台只执行一条sql select语句! 


 

3-1-4、哪些情况会导致缓存失效?

  1. SqlSession 不是同一个

    每次 openSession() 创建新的 Session,缓存就不同。

    示例:

        @Test
        public void testFirstCache2() throws IOException {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
            CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
            Car car1 = mapper1.selectOneById(1L);
            System.out.println(car1);
    
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
            Car car2 = mapper2.selectOneById(1L);
            System.out.println(car2);
    
            sqlSession1.close();
            sqlSession2.close();
        }

    此时,控制台会打印两条sql select语句。

  2. 执行了 update / insert / delete 操作

    写操作会清空缓存(保证数据一致性),修改任意一张表,都会清空缓存!

  3. 手动清空缓存

    session.clearCache();
    
  4. 查询的 SQL 或参数不同


 

3-1-5、一级缓存原理简述

  • MyBatis 内部维护了一个 PerpetualCacheHashMap 实现)

  • 每次查询前会根据 SQL+参数,生成 key,先查缓存

  • 如果命中,直接返回

  • 如果没命中,查数据库并存入缓存

3-2、MyBatis 二级缓存

MyBatis 的二级缓存,这对优化多次查询、减少数据库压力非常重要,尤其是跨 SqlSession 的查询场景


 

3-2-1、什么是二级缓存?

二级缓存是 MyBatis 提供的跨 SqlSession 的缓存机制。

它的作用范围是:Mapper 映射级别(namespace)不同 SqlSession 之间共享缓存数据


3-2-2、一级 vs 二级缓存对比

对比项 一级缓存(默认) 二级缓存(需开启)
缓存范围 单个 SqlSession 内 多个 SqlSession 共享
默认状态 开启 默认关闭
生命周期 SqlSession 生命周期 应用级、映射器级别
存储结构 基于 HashMap(PerpetualCache) 可自定义实现
典型用途 同一次操作内避免重复查询 缓解高频读操作数据库压力

3-2-3、使用二级缓存的三步配置


Step 1:开启全局二级缓存
<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>

【注意】:

是的,<setting name="cacheEnabled" value="true"/> 在 MyBatis 中默认就是开启。 


Step 2:在 Mapper 映射文件中开启 <cache/>

例如:UserMapper.xml

<mapper namespace="com.example.mapper.UserMapper">
    <cache />
    
    <select id="selectById" resultType="User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>
  • 你也可以配置更多参数(见下方扩展)


Step 3:实体类实现 Serializable 接口(因为缓存需要序列化)
public class User implements Serializable {
    private Long id;
    private String name;
    // ...其他字段
}

3-2-4、二级缓存使用示例

示例1:没有使用二级缓存

示例2:数据从二级缓存中获取

sqlSession1关闭,数据保存到二级缓存中,再执行sqlSession2中的select语句,会从二级缓存中获取。

【注意】:

一级缓存的优先级高,先从一级缓存中取数据,若是一级缓存关闭,则从二级中取数据!

 

示例3:跨namespace测试二级缓存

【注意】:

两个mapper不一样,但是执行的select语句和参数都是一样的,但是控制台依旧会执行两条select查询语句,说明二级缓存不能跨namespace!


3-2-5、哪些操作会清空二级缓存?

  • 对该 namespace 进行了 update/insert/delete(增、删、改)

  • 显式调用了 clearCache()

  • 配置 <cache flushInterval="..."/> 自动过期

  • 跨 namespace 无法共享(除非手动自定义)


3-2-6、常见 <cache> 配置项

<cache 
    eviction="LRU"               <!-- 缓存淘汰策略:LRU, FIFO, SOFT, WEAK -->
    flushInterval="60000"        <!-- 自动刷新间隔:毫秒;刷新后二级缓存失效 -->
    size="512"                   <!-- 最大缓存对象数量 -->
    readOnly="false"             <!-- 是否只读(只读更快但不可修改), car1 == car2 -->
    blocking="true"              <!-- 防止缓存击穿 -->
/>

 

3-2-7、一级缓存 vs 二级缓存(图解理解)

+------------------------+
| SqlSession A           |
|  └── 一级缓存(仅自己用)     |
|                        |
| SqlSession B           |
|  └── 一级缓存(仅自己用)     |
+------------------------+
       ↓(关闭 SqlSession 后)
+------------------------+
|     二级缓存(共享)          |
|     key: SQL + 参数          |
|     value: 查询结果对象      |
+------------------------+

3-3、自定义缓存实现(可选)

MyBatis 允许你自定义二级缓存逻辑(如整合 Redis),也就是集成第三方的缓存组件。

【注意】:

MyBatis的一级缓存是不可替代的!集成第三方的缓存组件,替代的是二级缓存!

MyBatis 如何集成第三方缓存组件,比如 Redis、EhCache、Caffeine 等。这种方式可以将 MyBatis 的二级缓存升级为分布式或高性能缓存,实现更强的可扩展性与性能提升。 

1、示例:集成EhCache 

step1:pom.xml中添加依赖
<!-- MyBatis 对 EhCache 的支持 -->
<dependency>
  <groupId>org.mybatis.caches</groupId>
  <artifactId>mybatis-ehcache</artifactId>
  <version>1.2.1</version>
</dependency>

step2: 添加 ehcache.xml 配置文件
<ehcache>
    <cache name="com.example.mapper.UserMapper"
           maxEntriesLocalHeap="1000"
           timeToLiveSeconds="600"/>
</ehcache>

step3: 在对应的xxxMapper.xml 中配置:

step4: 编写测试类

测试类和测试二级缓存一样,没有变动!


网站公告

今日签到

点亮在社区的每一天
去签到