java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis一二级缓存

MyBatis一二级缓存机制全解析

作者:佛祖让我来巡山

理解MyBatis的一二级缓存不仅有助于优化应用性能,还能避免因缓存不当导致的数据一致性问题,本文将从基础概念到高级原理,全方位解析MyBatis缓存机制,感兴趣的朋友跟随小编一起看看吧

引言

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

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

1.1 缓存的价值

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

1.2 缓存的经济学原理

二、一级缓存: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
└─────────────────────────┘

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

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 生活化比喻

一级缓存 = 私人对话记忆

二级缓存 = 公司公告栏

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="缓存大小">

可用策略:

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

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

到此这篇关于MyBatis一二级缓存机制全解析的文章就介绍到这了,更多相关MyBatis一二级缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文