当前位置: 首页 > 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=1 System.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<>(); @Override public void putObject(Object key, Object value) { cache.put(key, value); // 简单的Map.put() } @Override public 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/> <!-- 详细配置 --> <cache eviction="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实现LRU private Map<Object, Object> keyMap; private Object eldestKey; public void setSize(final int size) { keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); } return tooBig; } }; } @Override public 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; @Override public void putObject(Object key, Object value) { try (Jedis jedis = jedisPool.getResource()) { jedis.set(serialize(key), serialize(value)); } } @Override public 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.cnnetsun.cn/news/152883.html

相关文章:

  • SOES开源EtherCAT从站开发终极指南:从理论到工业实战
  • Spring Boot全局日期格式配置方法
  • REAL-Video-Enhancer终极教程:5分钟掌握免费视频增强神器
  • Win11 VMware蓝屏修复终极方案:告别虚拟机崩溃困扰
  • Langchain-Chatchat社区生态现状与发展前景展望
  • LlamaIndex架构解密:7步构建高性能LLM数据管理系统 [特殊字符]
  • Langchain-Chatchat本地知识库问答系统实战:如何用GPU加速大模型推理
  • 深度剖析:群晖DS920+定制化引导镜像的构建奥秘
  • 【Open-AutoGLM实战排障系列】:从零搞定手机连接的6步标准化网络配置流程
  • Gymnasium环境版本控制实战:企业级强化学习复现性终极指南
  • 模型识别不准怎么办?资深工程师亲授Open-AutoGLM调优7大绝招
  • 权限拒绝频发?Open-AutoGLM授权失败的7种场景与应对策略
  • Open-AutoGLM配对总失败?别急,这4个网络设置你很可能没改对
  • AI+散热设计结合
  • 8个降AI率工具,专科生高效避坑指南
  • 5‘-Biotin Phosphoramidite,135137-87-0,实现目标分子的高效捕获
  • 【Open-AutoGLM中文乱码终极解决方案】:20年专家亲授输入修复三步法
  • 智能测试的并行化策略:加速高质量软件交付
  • FaceFusion与Node-RED物联网逻辑引擎集成设想
  • 5步掌握Windows高效屏幕录制:wcap工具完全指南
  • 求真AI打造全球最大百科知识门户,容量超维基百科6000倍 | 美通社头条
  • markdown-processor:一款使用 Python 编写的强大的 Markdown 处理工具,提供 Markdown 文本格式化和图片管理功能。
  • FaceFusion在智能家居控制界面中的个性化头像生成
  • 视觉驱动真的更稳定吗?Open-AutoGLM两大模式压测结果震惊业内
  • Accelerated C++:快速掌握C++编程核心技能的终极指南
  • WingetUI离线部署技术解析:企业环境下的高效解决方案
  • 【真人实测】Java企业级AI编码工具横评:效率狂升70%+,零安全漏洞落地验证
  • Open-AutoGLM启动卡在加载权重?,资深架构师教你4招快速恢复运行
  • 鲸鸿动能斩获2025 Morketing Awards 灵眸奖三项大奖
  • Rust跨平台编译终极指南:用cross实现嵌入式开发快速上手