深入解析,MyBatis的一二级缓存

深入解析,MyBatis的一二级缓存

引言

在数据密集型应用中,缓存是提升性能的关键技术之一。MyBatis作为Java生态中流行的ORM框架,提供了一套完善的缓存机制,包括一级缓存和二级缓存。本文将深入解析MyBatis的二级缓存,通过实际代码示例和场景分析,帮助你理解这两种缓存的区别,以及如何在项目中合理使用它们。

MyBatis缓存基础

MyBatis的缓存机制旨在减少数据库访问次数,提高应用性能。它通过将查询结果存储在内存中,在后续相同查询时直接返回缓存数据,避免重复访问数据库。MyBatis提供了两级缓存:

  1. 一级缓存(Local Cache):SqlSession级别的缓存,默认开启
  2. 二级缓存(Second Level Cache):Mapper级别的缓存,需要手动配置

这两种缓存在作用范围、生命周期和使用场景上有显著差异,接下来我们将详细探讨。

一级缓存详解

一级缓存是MyBatis的默认缓存,它的作用范围仅限于单个SqlSession内部。每个SqlSession都有自己独立的缓存空间,不同SqlSession之间的缓存数据互不可见。

一级缓存的工作原理

  1. 当执行查询时,MyBatis首先检查一级缓存中是否存在对应的数据
  2. 如果缓存命中,直接返回缓存数据,不访问数据库
  3. 如果缓存未命中,则访问数据库并将查询结果存入一级缓存
  4. 当执行更新、删除、插入操作,或调用clearCache()方法时,一级缓存会被清空

一级缓存的生命周期

一级缓存随SqlSession的创建而创建,随SqlSession的关闭或提交而销毁。它是一种短暂的、会话级的缓存,主要用于优化单次会话内的重复查询。

二级缓存详解

二级缓存是MyBatis提供的跨SqlSession的缓存机制,它的作用范围是namespace(即Mapper接口)级别。同一namespace下的所有SqlSession共享二级缓存数据。

二级缓存的启用方式

要启用二级缓存,需要满足以下条件:

  1. 在MyBatis配置文件中设置cacheEnabled为true(默认值)
  2. 在Mapper XML文件中添加<cache/>标签
  3. 查询语句设置useCache="true"(默认值)
  4. 会话需要提交或关闭后,查询结果才会被二级缓存存储

二级缓存的工作原理

  1. 查询时,MyBatis先检查二级缓存,再检查一级缓存
  2. 如果二级缓存命中,直接返回缓存数据
  3. 如果二级缓存未命中,则继续检查一级缓存
  4. 如果一级缓存也未命中,则访问数据库
  5. 查询结果会先存入一级缓存,当会话提交或关闭时,数据才会被写入二级缓存
  6. 当执行更新操作并提交事务时,会清空相关namespace的二级缓存

代码示例分析

让我们通过一个具体的UserMapper.xml示例来理解MyBatis缓存机制:

<?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">
    <!-- 手动开启二级缓存 -->
    <cache/>

    <select id="selectUserById" resultType="User" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <update id="updateUser" parameterType="User">
        UPDATE user SET name = #{name} WHERE id = #{id}
    </update>
</mapper>

在这个配置中:

  • <cache/>标签开启了UserMapper命名空间的二级缓存
  • useCache="true"表示selectUserById查询结果会被缓存
  • 没有特别配置的情况下,updateUser操作会导致该命名空间的二级缓存失效

一级缓存行为分析

让我们看看一级缓存在实际场景中的行为:

// 场景1:同一个 SqlSession 内重复查询同一数据
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    
    
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    
    // 第一次查询:访问数据库,结果存入一级缓存
    User user1 = mapper.selectUserById(1);
    
    // 第二次查询:直接从一级缓存获取,不访问数据库
    User user2 = mapper.selectUserById(1);
    
    // 执行更新操作:清空一级缓存
    mapper.updateUser(new User(1, "NewName"));
    
    // 第三次查询:更新后一级缓存失效,再次访问数据库
    User user3 = mapper.selectUserById(1);
}

结果分析

扫描二维码关注公众号,回复: 17579539 查看本文章
  • 第一次查询会访问数据库,并将结果存入一级缓存
  • 第二次查询直接从一级缓存获取数据,不会产生数据库查询
  • 执行更新操作后,一级缓存会被自动清空
  • 第三次查询因缓存已失效,会再次访问数据库

这种机制确保了在单个会话内的数据一致性,避免脏读问题。

二级缓存行为分析

接下来,让我们分析二级缓存在跨会话场景中的行为:

// 场景2:跨 SqlSession 查询同一数据(需开启二级缓存)
// 第一个 SqlSession
try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
    
    
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    // 第一次查询:访问数据库,结果存入一级缓存
    User user1 = mapper1.selectUserById(1);
    sqlSession1.commit(); // 提交后数据才会进入二级缓存
}

// 第二个 SqlSession
try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
    
    
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    // 第二次查询:直接命中二级缓存(不访问数据库)
    User user2 = mapper2.selectUserById(1);
}

// 第三个 SqlSession 执行更新
try (SqlSession sqlSession3 = sqlSessionFactory.openSession()) {
    
    
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    mapper3.updateUser(new User(1, "AnotherName"));
    sqlSession3.commit(); // 提交后二级缓存失效
}

// 第四个 SqlSession 再次查询
try (SqlSession sqlSession4 = sqlSessionFactory.openSession()) {
    
    
    UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class);
    // 第三次查询:更新导致二级缓存失效,重新访问数据库
    User user3 = mapper4.selectUserById(1);
}

结果分析

  • 第一个SqlSession执行查询并提交事务,数据被存入二级缓存
  • 第二个SqlSession直接从二级缓存获取数据,无需访问数据库
  • 第三个SqlSession执行更新操作并提交,导致该命名空间的二级缓存失效
  • 第四个SqlSession再次查询时,因二级缓存已失效,需要重新访问数据库

这种机制使得跨会话访问相同数据时,能有效减少数据库交互,同时保证数据的一致性。

一级缓存与二级缓存的比较

特性 一级缓存 二级缓存
作用域 单个 SqlSession 内部 SqlSession(同一 namespace 下共享)
生命周期 SqlSession 关闭或提交而销毁 持久化,直到显式清除或配置的过期策略触发
数据共享 仅当前会话可见 所有会话共享
失效条件 执行更新操作或调用 clearCache() namespace 的更新操作(需提交事务)
配置方式 默认开启,无需配置 需手动添加 <cache/> 标签
适用场景 单次会话内高频重复查询 跨会话的全局静态数据(如配置表、低频更新数据)
实现复杂度 简单,自动管理 需要考虑序列化、缓存策略等因素
数据一致性 会话内一致性高 需要谨慎处理更新操作的影响

使用二级缓存的注意事项

1. 实体类序列化要求

二级缓存中的数据需要在不同的JVM进程间传输,因此缓存的对象必须实现Serializable接口:

public class User implements Serializable {
    
    
    private static final long serialVersionUID = 1L;
    private Integer id;
    private String name;
    // getters and setters
}

如果实体类未实现此接口,使用二级缓存时MyBatis会抛出异常。

2. 事务提交与二级缓存

二级缓存的数据只有在SqlSession提交事务后才会被真正存入缓存。这是因为MyBatis需要确保只有成功执行的查询结果才被缓存,避免存储不完整或错误的数据。

3. 适用场景选择

二级缓存适合用于:

  • 读多写少的数据
  • 变更频率低的参考数据
  • 全局配置信息

不适合用于:

  • 高频更新的数据
  • 对实时性要求高的核心业务数据
  • 涉及多表关联的复杂查询

4. 分布式环境问题

在分布式系统中,MyBatis的二级缓存可能导致数据不一致问题,因为默认的二级缓存是进程级的,不同服务器节点之间的缓存数据不会自动同步。解决方案包括:

  • 使用Redis等分布式缓存替代MyBatis二级缓存
  • 配置MyBatis集成EhCache、Memcached等支持分布式的缓存实现
  • 在缓存配置中设置较短的过期时间,减少不一致窗口期

5. 高级缓存配置

MyBatis提供了丰富的缓存配置选项:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="false"/>

参数说明:

  • eviction:缓存清除策略(LRU、FIFO、SOFT、WEAK)
  • flushInterval:自动刷新时间(毫秒)
  • size:缓存引用数量
  • readOnly:是否只读(true可提高性能但无法修改缓存对象)

总结

MyBatis的二级缓存机制为我们提供了强大的性能优化工具,正确使用可以显著减少数据库访问,提升应用响应速度。

  • 一级缓存适用于单会话内的查询优化,无需配置,自动管理
  • 二级缓存适用于跨会话的数据共享,需要手动配置,并注意数据一致性问题

在实际应用中,应根据业务特性、数据变更频率和一致性要求来选择合适的缓存策略。对于分布式环境,可能需要考虑使用专业的分布式缓存解决方案替代MyBatis原生的二级缓存。

正确理解和使用MyBatis的缓存机制,将帮助你在保证数据一致性的同时,最大限度地提升应用性能。

觉得有用的话可以点点赞 (/ω\),支持一下。

如果愿意的话关注一下。会对你有更多的帮助。

每周都会不定时更新哦 >人< 。