当前位置: 首页 > news >正文

深入理解MyBatis缓存机制:一二级缓存全解析

引言

在现代Web应用中,数据库访问往往是性能瓶颈之一。MyBatis作为流行的持久层框架,其缓存机制是提升应用性能的关键特性。理解MyBatis的一二级缓存不仅有助于优化应用性能,还能避免因缓存不当导致的数据一致性问题。本文将从基础概念到高级原理,全方位解析MyBatis缓存机制。

一、缓存的基本概念:为什么需要缓存?

1.1 缓存的价值

想象一下,如果你每次需要知道时间都去天文台查询,效率会很低。相反,看一眼手表(缓存)就能立即获取时间。MyBatis缓存扮演的就是这个“手表”的角色,它避免了频繁访问数据库(天文台),极大提升了查询效率。

1.2 缓存的经济学原理

  • 时间局部性:刚被访问的数据很可能再次被访问
  • 空间局部性:相邻的数据很可能被一起访问
  • 访问成本:内存访问(纳秒级)vs 磁盘/网络访问(毫秒级)

二、一级缓存:SqlSession级别的缓存

2.1 什么是SqlSession?

在深入一级缓存前,需要先理解SqlSession。SqlSession不是数据库连接(Connection),而是一次数据库对话的抽象:

// SqlSession相当于一次完整对话,不是一通电话
SqlSession session = sqlSessionFactory.openSession();
try {// 对话中的多次查询userMapper.getUser(1);  // 第一次查询orderMapper.getOrders(1);  // 第二次查询accountMapper.getBalance(1);  // 第三次查询session.commit();  // 确认对话内容
} finally {session.close();  // 结束对话
}

2.2 一级缓存的核心特性

作用范围:SqlSession内部(一次对话)
默认状态:自动开启,无法关闭
生命周期:随SqlSession创建而创建,随其关闭而销毁

2.3 一级缓存的工作原理

// 示例代码展示一级缓存行为
public void demonstrateLevel1Cache() {SqlSession session = sqlSessionFactory.openSession();UserMapper mapper = session.getMapper(UserMapper.class);System.out.println("第一次查询用户1:");User user1 = mapper.selectById(1);  // 发SQL:SELECT * FROM user WHERE id=1System.out.println("第二次查询用户1:");User user2 = mapper.selectById(1);  // 不发SQL!从一级缓存读取System.out.println("查询用户2:");User user3 = mapper.selectById(2);  // 发SQL:参数不同,缓存未命中System.out.println("修改用户1:");mapper.updateUser(user1);  // 清空一级缓存System.out.println("再次查询用户1:");User user4 = mapper.selectById(1);  // 发SQL:缓存被清空session.close();
}

2.4 一级缓存的数据结构

一级缓存的实现非常简单直接:

// 一级缓存的核心实现类
public class PerpetualCache implements Cache {// 核心:就是一个ConcurrentHashMap!private final Map<Object, Object> cache = new ConcurrentHashMap<>();@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);  // 简单的Map.put()}@Overridepublic Object getObject(Object key) {return cache.get(key);  // 简单的Map.get()}
}

缓存Key的生成规则

// CacheKey包含以下要素,决定两个查询是否"相同"
// 1. Mapper Id(namespace + method)
// 2. 分页参数(offset, limit)
// 3. SQL语句
// 4. 参数值
// 5. 环境Id// 这意味着:即使SQL相同,参数不同,也会生成不同的CacheKey

2.5 一级缓存的失效场景

  1. 执行任何UPDATE/INSERT/DELETE操作
  2. 手动调用clearCache()
  3. 设置flushCache="true"
  4. SqlSession关闭
  5. 查询参数变化(因为CacheKey不同)

三、二级缓存:Mapper级别的全局缓存

3.1 二级缓存的核心特性

作用范围:Mapper级别(跨SqlSession共享)
默认状态:默认关闭,需要手动开启
生命周期:随应用运行而存在

3.2 二级缓存的配置

<!-- 1. 全局配置开启二级缓存 -->
<settings><setting name="cacheEnabled" value="true"/>
</settings><!-- 2. Mapper XML中配置 -->
<mapper namespace="com.example.UserMapper"><!-- 基本配置 --><cache/><!-- 详细配置 --><cacheeviction="LRU"           <!-- 淘汰策略 -->flushInterval="60000"    <!-- 刷新间隔(毫秒) -->size="1024"              <!-- 缓存对象数 -->readOnly="true"          <!-- 是否只读 -->blocking="false"/>       <!-- 是否阻塞 -->
</mapper><!-- 3. 在具体查询上使用缓存 -->
<select id="selectById" resultType="User" useCache="true">SELECT * FROM user WHERE id = #{id}
</select><!-- 4. 增删改操作刷新缓存 -->
<update id="updateUser" flushCache="true">UPDATE user SET name = #{name} WHERE id = #{id}
</update>

3.3 二级缓存的数据结构

二级缓存不像一级缓存那么简单,它采用了装饰器模式

二级缓存装饰器链(层层包装):
┌─────────────────────────┐
│  SerializedCache        │ ← 序列化存储
│  LoggingCache           │ ← 日志统计
│  SynchronizedCache      │ ← 线程安全
│  LruCache               │ ← LRU淘汰
│  PerpetualCache         │ ← 基础HashMap
└─────────────────────────┘

每个装饰器都有特定功能:

  • PerpetualCache:基础存储,使用HashMap
  • LruCache:最近最少使用淘汰
  • SynchronizedCache:保证线程安全
  • LoggingCache:记录命中率
  • SerializedCache:序列化对象,防止修改

3.4 二级缓存的工作流程

public void demonstrateLevel2Cache() {// 用户A查询(第一个访问者)SqlSession sessionA = sqlSessionFactory.openSession();UserMapper mapperA = sessionA.getMapper(UserMapper.class);User user1 = mapperA.selectById(1);  // 查询数据库sessionA.close();  // 关键:关闭时才会写入二级缓存// 用户B查询(不同SqlSession)SqlSession sessionB = sqlSessionFactory.openSession();UserMapper mapperB = sessionB.getMapper(UserMapper.class);User user2 = mapperB.selectById(1);  // 从二级缓存读取,不发SQL// 管理员更新数据SqlSession sessionC = sqlSessionFactory.openSession();UserMapper mapperC = sessionC.getMapper(UserMapper.class);mapperC.updateUser(user1);  // 清空相关二级缓存sessionC.commit();sessionC.close();// 用户D再次查询SqlSession sessionD = sqlSessionFactory.openSession();UserMapper mapperD = sessionD.getMapper(UserMapper.class);User user3 = mapperD.selectById(1);  // 缓存被清,重新查询数据库sessionD.close();
}

3.5 二级缓存的同步机制

二级缓存有一个重要特性:事务提交后才更新。这意味着:

// 场景:事务内查询,事务提交前其他会话看不到更新
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);// 修改数据,但未提交
mapper1.updateUser(user);
// 此时二级缓存还未更新// 另一个会话查询
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1);  // 可能读到旧数据!session1.commit();  // 提交后,二级缓存才会更新
// 之后的新查询才会看到新数据

四、一二级缓存的对比与选择

4.1 核心差异对比

特性 一级缓存 二级缓存
作用范围 SqlSession内部 Mapper级别,跨SqlSession
默认状态 开启 关闭
数据结构 简单HashMap 装饰器链
共享性 私有,不共享 公共,所有会话共享
生命周期 随SqlSession创建销毁 随应用运行持久存在
性能影响 极小(内存访问) 中等(可能有序列化开销)
适用场景 会话内重复查询 跨会话共享查询

4.2 生活化比喻

一级缓存 = 私人对话记忆

  • 你和朋友的聊天内容,只有你们两人知道
  • 聊天结束(SqlSession关闭),记忆逐渐模糊

二级缓存 = 公司公告栏

  • 重要通知写在公告栏,所有员工都能看到
  • 通知更新时,需要擦掉旧的,写上新的
  • 公告栏内容持久存在,直到被更新

4.3 使用场景建议

适合一级缓存的场景:

// 场景1:方法内多次查询相同数据
public void processOrder(Long orderId) {Order order1 = validateOrder(orderId);      // 第一次查数据库Order order2 = calculateDiscount(orderId);  // 走一级缓存Order order3 = generateInvoice(orderId);    // 走一级缓存
}// 场景2:循环内查询
for (int i = 0; i < 100; i++) {Config config = configMapper.getConfig("system_timeout");// 只有第一次查数据库,后续99次走缓存
}

适合二级缓存的场景:

// 场景1:读多写少的配置数据
SystemConfig config = configMapper.getConfig("app_settings");
// 多个用户频繁读取,很少修改// 场景2:热门商品信息
Product product = productMapper.getHotProduct(666);
// 商品详情页,大量用户访问同一商品// 场景3:静态字典数据
List<City> cities = addressMapper.getAllCities();
// 城市列表,很少变化

不适合缓存的场景:

// 场景1:实时性要求高的数据
Stock stock = stockMapper.getRealTimeStock(productId);
// 库存信息,需要实时准确// 场景2:频繁更新的数据
UserBalance balance = accountMapper.getBalance(userId);
// 用户余额,每次交易都变化// 场景3:大数据量查询
List<Log> logs = logMapper.getTodayLogs();
// 数据量大,缓存占用内存过多

五、缓存的高级特性与原理

5.1 缓存淘汰策略

MyBatis提供了多种淘汰策略:

<cache eviction="策略类型" size="缓存大小">

可用策略:

  • LRU(Least Recently Used):最近最少使用(默认)
  • FIFO(First In First Out):先进先出
  • SOFT:软引用,内存不足时被GC回收
  • WEAK:弱引用,GC时立即回收

5.2 LRU缓存的实现原理

public class LruCache implements Cache {private final Cache delegate;// 使用LinkedHashMap实现LRUprivate Map<Object, Object> keyMap;private Object eldestKey;public void setSize(final int size) {keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {eldestKey = eldest.getKey();}return tooBig;}};}@Overridepublic Object getObject(Object key) {// 访问时更新顺序keyMap.get(key);return delegate.getObject(key);}
}

5.3 缓存查询的完整流程

查询执行流程:
1. 请求到达CachingExecutor(二级缓存入口)
2. 生成CacheKey(包含SQL、参数等信息)
3. 查询二级缓存└─ 命中 → 返回结果└─ 未命中 → 继续
4. 查询一级缓存└─ 命中 → 返回结果,并放入二级缓存(事务提交时)└─ 未命中 → 继续
5. 查询数据库
6. 结果存入一级缓存
7. 事务提交时,一级缓存刷入二级缓存
8. 返回结果

六、缓存的最佳实践与避坑指南

6.1 最佳实践

1. 合理配置缓存大小

<!-- 根据数据特点设置合适的大小 -->
<cache size="1024"/>  <!-- 缓存1024个对象 -->

2. 设置合理的刷新间隔

<!-- 对于变化不频繁但需要定期更新的数据 -->
<cache flushInterval="1800000"/>  <!-- 30分钟自动刷新 -->

3. 选择性使用缓存

<!-- 某些查询跳过缓存 -->
<select id="getRealTimeData" useCache="false">SELECT * FROM realtime_table
</select><!-- 某些查询强制刷新缓存 -->
<select id="getImportantData" flushCache="true">SELECT * FROM important_table
</select>

4. 关联查询的缓存策略

<!-- 关联查询时,使用cache-ref同步缓存 -->
<mapper namespace="com.example.UserMapper"><cache/><!-- 其他配置 -->
</mapper><mapper namespace="com.example.OrderMapper"><!-- 引用UserMapper的缓存 --><cache-ref namespace="com.example.UserMapper"/>
</mapper>

6.2 常见问题与解决方案

问题1:脏读问题

场景:一个会话修改数据但未提交,另一个会话从二级缓存读取到旧数据。

解决方案

// 设置事务隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateUser(User user) {userMapper.updateUser(user);
}// 或者在Mapper中设置flushCache
@Update("UPDATE user SET name=#{name} WHERE id=#{id}")
@Options(flushCache = Options.FlushCachePolicy.TRUE)
int updateUser(User user);

问题2:内存溢出

场景:缓存大量数据导致JVM内存不足。

解决方案

  1. 设置合理的缓存大小和淘汰策略
  2. 使用软引用/弱引用缓存
  3. 定期清理不活跃的缓存

问题3:分布式环境缓存不一致

场景:多台服务器,每台有自己的缓存,数据不一致。

解决方案

  1. 使用集中式缓存(Redis、Memcached)替代默认二级缓存
  2. 实现自定义Cache接口:
public class RedisCache implements Cache {private JedisPool jedisPool;@Overridepublic void putObject(Object key, Object value) {try (Jedis jedis = jedisPool.getResource()) {jedis.set(serialize(key), serialize(value));}}@Overridepublic Object getObject(Object key) {try (Jedis jedis = jedisPool.getResource()) {byte[] value = jedis.get(serialize(key));return deserialize(value);}}
}

问题4:缓存穿透

场景:查询不存在的数据,每次都查数据库。

解决方案

// 缓存空对象
public User getUser(Long id) {User user = userMapper.selectById(id);if (user == null) {// 缓存空值,设置短过期时间cacheNullValue(id);return null;}return user;
}

6.3 监控与调试

开启缓存日志

# 查看缓存命中情况
logging.level.org.mybatis=DEBUG
logging.level.com.example.mapper=TRACE

监控缓存命中率

// 获取缓存统计信息
Cache cache = sqlSession.getConfiguration().getCache("com.example.UserMapper");
if (cache instanceof LoggingCache) {LoggingCache loggingCache = (LoggingCache) cache;System.out.println("命中次数: " + loggingCache.getHitCount());System.out.println("未命中次数: " + loggingCache.getMissCount());System.out.println("命中率: " + (loggingCache.getHitCount() * 100.0 / (loggingCache.getHitCount() + loggingCache.getMissCount())) + "%");
}

七、总结与思考

7.1 核心要点回顾

  1. 一级缓存:SqlSession级别,自动开启,基于HashMap,简单高效
  2. 二级缓存:Mapper级别,需手动开启,基于装饰器模式,功能丰富
  3. 缓存Key:由SQL、参数等要素生成,决定查询是否"相同"
  4. 事务同步:二级缓存在事务提交后才更新,避免脏读
  5. 适用场景:根据数据特点选择合适的缓存策略

7.2 设计思想启示

MyBatis缓存设计体现了几个重要软件设计原则:

  1. 单一职责原则:每个缓存装饰器只负责一个功能
  2. 开闭原则:通过装饰器模式,无需修改原有代码即可扩展功能
  3. 接口隔离:Cache接口定义清晰,便于自定义实现

7.3 实际应用建议

在实际项目中:

  1. 从小开始:先使用一级缓存,确有需要再开启二级缓存
  2. 测试验证:上线前充分测试缓存效果和内存占用
  3. 监控调整:生产环境监控缓存命中率,根据实际情况调整配置
  4. 文档记录:记录缓存配置和策略,便于团队协作和维护

7.4 未来展望

随着微服务和云原生架构的普及,MyBatis缓存也在演进:

  1. 分布式缓存集成:更好支持Redis等分布式缓存
  2. 多级缓存策略:本地缓存+分布式缓存的组合使用
  3. 智能缓存管理:基于访问模式的自动缓存优化

结语

MyBatis缓存机制是一个看似简单实则精妙的设计。理解它不仅能帮助我们优化应用性能,还能加深对缓存设计模式的理解。记住,缓存是提升性能的利器,但也可能成为数据一致的陷阱。合理使用、谨慎配置、持续监控,才能让缓存真正为应用赋能。

缓存不是银弹,而是需要精心调校的利器。 在实际开发中,应根据业务特点、数据特性和访问模式,选择最合适的缓存策略,在性能与一致性之间找到最佳平衡点。

http://www.jsqmd.com/news/111247/

相关文章:

  • 破局者胜:2025年中国法律科技市场案件管理系统深度测评——以“案件云”为例
  • [机器学习] 类别变量编码库category_encoders使用指南
  • 2025.12.18
  • 上海打印机租赁|复印机租赁推荐榜——上海博莱办公-深耕20年,覆盖上海16个区 - 老百姓的口碑
  • 群晖docker镜像拉取-新手教程mdash;包教包会-针对小白
  • 这段代码,为什么不能加if(mOnKeyListener == null)
  • MinIO再见!RustFS性能飙升5倍,我们团队全面迁移的实战全记录
  • Ubuntu SSH密钥登录:告别密码
  • Springboot+Easyexcel将数据写入模板文件并导出Excel
  • JetBrains Fleet倒了,Cursor还能撑多久?
  • 运维系列数据库系列【仅供参考】:达梦:DM8归档日志挖掘
  • VMware ESXI 8.0安装vCenter 8.0
  • 郑州新广发30年专注河南抗风卷帘门!源头厂家8条生产线,月产8000扇接单无忧 - 朴素的承诺
  • 文生中英双语的AI视频工具怎么选?一个英语老师的实测结论
  • zz测试18种RAG技术找到最优方案
  • CANN视频增强实战:基于Ascend实用的平台的历史影像修复
  • 高精度时钟测试仪覆盖多行业的时间同步测试利器 gps时钟测试仪
  • Java经典设计模式可以解决 99% 的 业务场景
  • Xiaomi mimo大模型API接入Claude code
  • Python构建AI Agent自主智能体系统
  • 2025年最新测评:为了保住头发,我把市面上这6款工具测了个遍,专治知网维普“一片红”
  • Python实现Transformer神经网络时间序列模型可视化分析商超蔬菜销售数据筛选高销量单品预测|附代码数据
  • 0代码实现接口自动化测试 —— RF框架实践
  • 测试Mini小车的情况
  • 河南堆积门首选郑州新广发!30年源头厂家,8条生产线月产8000扇,接单无忧 - 朴素的承诺
  • 再也不用看别人脸色!国产CPU带火国产软件,“卡脖子”将成为过去
  • 2025年口碑好的河南铝合金卷帘门厂家最新权威实力榜 (2) - 朴素的承诺
  • 【RTOS】EasyLog的移植与使用
  • 【系统架构】服务器部件说明
  • 2025年口碑好的河南铝合金卷帘门厂家最新权威实力榜 (1) - 朴素的承诺