阜阳市网站建设_网站建设公司_HTTPS_seo优化
2025/12/18 19:45:42 网站建设 项目流程

引言

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询