找回密码
 立即注册
首页 业界区 安全 深入理解MyBatis缓存机制:一二级缓存全解析 ...

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

戎玉珂 昨天 20:50
引言

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

1.1 缓存的价值

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


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

2.1 什么是SqlSession?

在深入一级缓存前,需要先理解SqlSession。SqlSession不是数据库连接(Connection),而是一次数据库对话的抽象:
  1. // SqlSession相当于一次完整对话,不是一通电话
  2. SqlSession session = sqlSessionFactory.openSession();
  3. try {
  4. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 对话中的多次查询
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  userMapper.getUser(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 第一次查询
  6. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  orderMapper.getOrders(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 第二次查询
  7. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  accountMapper.getBalance(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 第三次查询
  8. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  session.commit();<cache size="1024"/><cache flushInterval="1800000"/>  // 确认对话内容
  9. } finally {
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  session.close();<cache size="1024"/><cache flushInterval="1800000"/>  // 结束对话
  11. }
复制代码
2.2 一级缓存的核心特性

作用范围:SqlSession内部(一次对话)
默认状态:自动开启,无法关闭
生命周期:随SqlSession创建而创建,随其关闭而销毁
2.3 一级缓存的工作原理
  1. // 示例代码展示一级缓存行为
  2. public void demonstrateLevel1Cache() {
  3. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SqlSession session = sqlSessionFactory.openSession();
  4. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UserMapper mapper = session.getMapper(UserMapper.class);
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  6. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("第一次查询用户1:");
  7. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user1 = mapper.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 发SQL:SELECT * FROM user WHERE id=1
  8. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("第二次查询用户1:");
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user2 = mapper.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 不发SQL!从一级缓存读取
  11. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  12. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("查询用户2:");
  13. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user3 = mapper.selectById(2);<cache size="1024"/><cache flushInterval="1800000"/>  // 发SQL:参数不同,缓存未命中
  14. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  15. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("修改用户1:");
  16. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  mapper.updateUser(user1);<cache size="1024"/><cache flushInterval="1800000"/>  // 清空一级缓存
  17. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  18. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("再次查询用户1:");
  19. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user4 = mapper.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 发SQL:缓存被清空
  20. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  21. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  session.close();
  22. }
复制代码
2.4 一级缓存的数据结构

一级缓存的实现非常简单直接:
  1. // 一级缓存的核心实现类
  2. public class PerpetualCache implements Cache {
  3. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 核心:就是一个ConcurrentHashMap!
  4. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  private final Map<Object, Object> cache = new ConcurrentHashMap<>();
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  6. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override
  7. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public void putObject(Object key, Object value) {
  8. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  cache.put(key, value);<cache size="1024"/><cache flushInterval="1800000"/>  // 简单的Map.put()
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  11. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override
  12. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public Object getObject(Object key) {
  13. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return cache.get(key);<cache size="1024"/><cache flushInterval="1800000"/>  // 简单的Map.get()
  14. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  15. }
复制代码
缓存Key的生成规则
  1. // CacheKey包含以下要素,决定两个查询是否"相同"
  2. // 1. Mapper Id(namespace + method)
  3. // 2. 分页参数(offset, limit)
  4. // 3. SQL语句
  5. // 4. 参数值
  6. // 5. 环境Id
  7. // 这意味着:即使SQL相同,参数不同,也会生成不同的CacheKey
复制代码
2.5 一级缓存的失效场景


  • 执行任何UPDATE/INSERT/DELETE操作
  • 手动调用clearCache()
  • 设置flushCache="true"
  • SqlSession关闭
  • 查询参数变化(因为CacheKey不同)
三、二级缓存:Mapper级别的全局缓存

3.1 二级缓存的核心特性

作用范围:Mapper级别(跨SqlSession共享)
默认状态:默认关闭,需要手动开启
生命周期:随应用运行而存在
3.2 二级缓存的配置
  1. <settings>
  2. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <setting name="cacheEnabled" value="true"/>
  3. </settings>
  4. <mapper namespace="com.example.UserMapper">
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  6. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache/>
  7. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  8. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  eviction="LRU"<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   
  11. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  flushInterval="60000"<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  12. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  size="1024"<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  13. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  readOnly="true"<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  14. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  blocking="false"/><cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   
  15. </mapper>
  16. <select id="selectById" resultType="User" useCache="true">
  17. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SELECT * FROM user WHERE id = #{id}
  18. </select>
  19. <update id="updateUser" flushCache="true">
  20. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UPDATE user SET name = #{name} WHERE id = #{id}
  21. </update>
复制代码
3.3 二级缓存的数据结构

二级缓存不像一级缓存那么简单,它采用了装饰器模式
  1. 二级缓存装饰器链(层层包装):
  2. ┌─────────────────────────┐
  3. │<cache size="1024"/><cache flushInterval="1800000"/>  SerializedCache<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  │ ← 序列化存储
  4. │<cache size="1024"/><cache flushInterval="1800000"/>  LoggingCache<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   │ ← 日志统计
  5. │<cache size="1024"/><cache flushInterval="1800000"/>  SynchronizedCache<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  │ ← 线程安全
  6. │<cache size="1024"/><cache flushInterval="1800000"/>  LruCache<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   │ ← LRU淘汰
  7. │<cache size="1024"/><cache flushInterval="1800000"/>  PerpetualCache<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   │ ← 基础HashMap
  8. └─────────────────────────┘
复制代码
每个装饰器都有特定功能:

  • PerpetualCache:基础存储,使用HashMap
  • LruCache:最近最少使用淘汰
  • SynchronizedCache:保证线程安全
  • LoggingCache:记录命中率
  • SerializedCache:序列化对象,防止修改
3.4 二级缓存的工作流程
  1. public void demonstrateLevel2Cache() {
  2. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 用户A查询(第一个访问者)
  3. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SqlSession sessionA = sqlSessionFactory.openSession();
  4. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UserMapper mapperA = sessionA.getMapper(UserMapper.class);
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user1 = mapperA.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 查询数据库
  6. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  sessionA.close();<cache size="1024"/><cache flushInterval="1800000"/>  // 关键:关闭时才会写入二级缓存
  7. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  8. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 用户B查询(不同SqlSession)
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SqlSession sessionB = sqlSessionFactory.openSession();
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UserMapper mapperB = sessionB.getMapper(UserMapper.class);
  11. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user2 = mapperB.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 从二级缓存读取,不发SQL
  12. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  13. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 管理员更新数据
  14. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SqlSession sessionC = sqlSessionFactory.openSession();
  15. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UserMapper mapperC = sessionC.getMapper(UserMapper.class);
  16. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  mapperC.updateUser(user1);<cache size="1024"/><cache flushInterval="1800000"/>  // 清空相关二级缓存
  17. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  sessionC.commit();
  18. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  sessionC.close();
  19. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  20. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 用户D再次查询
  21. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SqlSession sessionD = sqlSessionFactory.openSession();
  22. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  UserMapper mapperD = sessionD.getMapper(UserMapper.class);
  23. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user3 = mapperD.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 缓存被清,重新查询数据库
  24. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  sessionD.close();
  25. }
复制代码
3.5 二级缓存的同步机制

二级缓存有一个重要特性:事务提交后才更新。这意味着:
  1. // 场景:事务内查询,事务提交前其他会话看不到更新
  2. SqlSession session1 = sqlSessionFactory.openSession();
  3. UserMapper mapper1 = session1.getMapper(UserMapper.class);
  4. // 修改数据,但未提交
  5. mapper1.updateUser(user);
  6. // 此时二级缓存还未更新
  7. // 另一个会话查询
  8. SqlSession session2 = sqlSessionFactory.openSession();
  9. UserMapper mapper2 = session2.getMapper(UserMapper.class);
  10. User user2 = mapper2.selectById(1);<cache size="1024"/><cache flushInterval="1800000"/>  // 可能读到旧数据!
  11. session1.commit();<cache size="1024"/><cache flushInterval="1800000"/>  // 提交后,二级缓存才会更新
  12. // 之后的新查询才会看到新数据
复制代码
四、一二级缓存的对比与选择

4.1 核心差异对比

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

一级缓存 = 私人对话记忆

  • 你和朋友的聊天内容,只有你们两人知道
  • 聊天结束(SqlSession关闭),记忆逐渐模糊
二级缓存 = 公司公告栏

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

适合一级缓存的场景:
  1. // 场景1:方法内多次查询相同数据
  2. public void processOrder(Long orderId) {
  3. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  Order order1 = validateOrder(orderId);<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 第一次查数据库
  4. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  Order order2 = calculateDiscount(orderId);<cache size="1024"/><cache flushInterval="1800000"/>  // 走一级缓存
  5. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  Order order3 = generateInvoice(orderId);<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 走一级缓存
  6. }
  7. // 场景2:循环内查询
  8. for (int i = 0; i < 100; i++) {
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  Config config = configMapper.getConfig("system_timeout");
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 只有第一次查数据库,后续99次走缓存
  11. }
复制代码
适合二级缓存的场景:
  1. // 场景1:读多写少的配置数据
  2. SystemConfig config = configMapper.getConfig("app_settings");
  3. // 多个用户频繁读取,很少修改
  4. // 场景2:热门商品信息
  5. Product product = productMapper.getHotProduct(666);
  6. // 商品详情页,大量用户访问同一商品
  7. // 场景3:静态字典数据
  8. List<City> cities = addressMapper.getAllCities();
  9. // 城市列表,很少变化
复制代码
不适合缓存的场景:
  1. // 场景1:实时性要求高的数据
  2. Stock stock = stockMapper.getRealTimeStock(productId);
  3. // 库存信息,需要实时准确
  4. // 场景2:频繁更新的数据
  5. UserBalance balance = accountMapper.getBalance(userId);
  6. // 用户余额,每次交易都变化
  7. // 场景3:大数据量查询
  8. List<Log> logs = logMapper.getTodayLogs();
  9. // 数据量大,缓存占用内存过多
复制代码
五、缓存的高级特性与原理

5.1 缓存淘汰策略

MyBatis提供了多种淘汰策略:
  1. [/code]可用策略:
  2. [list]
  3. [*][b]LRU[/b](Least Recently Used):最近最少使用(默认)
  4. [*][b]FIFO[/b](First In First Out):先进先出
  5. [*][b]SOFT[/b]:软引用,内存不足时被GC回收
  6. [*][b]WEAK[/b]:弱引用,GC时立即回收
  7. [/list][size=4]5.2 LRU缓存的实现原理[/size]
  8. [code]public class LruCache implements Cache {
  9. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  private final Cache delegate;
  10. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 使用LinkedHashMap实现LRU
  11. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  private Map<Object, Object> keyMap;
  12. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  private Object eldestKey;
  13. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  14. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public void setSize(final int size) {
  15. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
  16. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override
  17. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
  18. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  boolean tooBig = size() > size;
  19. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  if (tooBig) {
  20. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  eldestKey = eldest.getKey();
  21. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  22. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return tooBig;
  23. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  24. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  };
  25. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  26. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
  27. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override
  28. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public Object getObject(Object key) {
  29. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 访问时更新顺序
  30. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  keyMap.get(key);
  31. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return delegate.getObject(key);
  32. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }
  33. }
复制代码
5.3 缓存查询的完整流程
  1. 查询执行流程:
  2. 1. 请求到达CachingExecutor(二级缓存入口)
  3. 2. 生成CacheKey(包含SQL、参数等信息)
  4. 3. 查询二级缓存
  5. <cache size="1024"/><cache flushInterval="1800000"/>   └─ 命中 → 返回结果
  6. <cache size="1024"/><cache flushInterval="1800000"/>   └─ 未命中 → 继续
  7. 4. 查询一级缓存
  8. <cache size="1024"/><cache flushInterval="1800000"/>   └─ 命中 → 返回结果,并放入二级缓存(事务提交时)
  9. <cache size="1024"/><cache flushInterval="1800000"/>   └─ 未命中 → 继续
  10. 5. 查询数据库
  11. 6. 结果存入一级缓存
  12. 7. 事务提交时,一级缓存刷入二级缓存
  13. 8. 返回结果
复制代码
六、缓存的最佳实践与避坑指南

6.1 最佳实践

1. 合理配置缓存大小
  1. <cache size="1024"/><cache flushInterval="1800000"/>  
复制代码
2. 设置合理的刷新间隔
  1. <cache size="1024"/><cache flushInterval="1800000"/>  
复制代码
3. 选择性使用缓存
  1. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SELECT * FROM realtime_table<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  SELECT * FROM important_table
复制代码
4. 关联查询的缓存策略
  1. <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  
复制代码
6.2 常见问题与解决方案

问题1:脏读问题

场景:一个会话修改数据但未提交,另一个会话从二级缓存读取到旧数据。
解决方案
  1. // 设置事务隔离级别@Transactional(isolation = Isolation.READ_COMMITTED)public void updateUser(User user) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  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内存不足。
解决方案

  • 设置合理的缓存大小和淘汰策略
  • 使用软引用/弱引用缓存
  • 定期清理不活跃的缓存
问题3:分布式环境缓存不一致

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

  • 使用集中式缓存(Redis、Memcached)替代默认二级缓存
  • 实现自定义Cache接口:
  1. public class RedisCache implements Cache {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  private JedisPool jedisPool;<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public void putObject(Object key, Object value) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  try (Jedis jedis = jedisPool.getResource()) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  jedis.set(serialize(key), serialize(value));<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  @Override<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  public Object getObject(Object key) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  try (Jedis jedis = jedisPool.getResource()) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  byte[] value = jedis.get(serialize(key));<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return deserialize(value);<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }}
复制代码
问题4:缓存穿透

场景:查询不存在的数据,每次都查数据库。
解决方案
  1. // 缓存空对象public User getUser(Long id) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  User user = userMapper.selectById(id);<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  if (user == null) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  // 缓存空值,设置短过期时间<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  cacheNullValue(id);<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return null;<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  }<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  return user;}
复制代码
6.3 监控与调试

开启缓存日志
  1. # 查看缓存命中情况
  2. logging.level.org.mybatis=DEBUG
  3. logging.level.com.example.mapper=TRACE
复制代码
监控缓存命中率
  1. // 获取缓存统计信息Cache cache = sqlSession.getConfiguration()<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  .getCache("com.example.UserMapper");if (cache instanceof LoggingCache) {<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  LoggingCache loggingCache = (LoggingCache) cache;<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("命中次数: " + loggingCache.getHitCount());<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("未命中次数: " + loggingCache.getMissCount());<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  System.out.println("命中率: " +<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>   (loggingCache.getHitCount() * 100.0 /<cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  <cache size="1024"/><cache flushInterval="1800000"/>  (loggingCache.getHitCount() + loggingCache.getMissCount())) + "%");}
复制代码
七、总结与思考

7.1 核心要点回顾


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

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

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

在实际项目中:

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

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

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

MyBatis缓存机制是一个看似简单实则精妙的设计。理解它不仅能帮助我们优化应用性能,还能加深对缓存设计模式的理解。记住,缓存是提升性能的利器,但也可能成为数据一致的陷阱。合理使用、谨慎配置、持续监控,才能让缓存真正为应用赋能。
缓存不是银弹,而是需要精心调校的利器。 在实际开发中,应根据业务特点、数据特性和访问模式,选择最合适的缓存策略,在性能与一致性之间找到最佳平衡点。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册