MyBatis缓存机制深度解析:从本地缓存到分布式方案

作为MyBatis的核心特性之一,缓存机制直接影响着应用性能表现。本文将系统剖析MyBatis的一级缓存、二级缓存实现原理,并深入探讨企业级应用中如何安全高效地使用自定义缓存。

一、一级缓存:SqlSession级别的本地缓存

1.1 基本特性

一级缓存是MyBatis默认开启的本地缓存,其生命周期与SqlSession绑定:

try (SqlSession session = sqlSessionFactory.openSession()) {
    // 第一次查询,访问数据库
    User user1 = session.selectOne("org.example.mapper.UserMapper.findById", 1L);
    
    // 第二次查询相同SQL,直接从缓存获取
    User user2 = session.selectOne("org.example.mapper.UserMapper.findById", 1L);
    
    System.out.println(user1 == user2); // 输出true,同一对象引用
}

1.2 缓存失效场景

以下操作会清空当前SqlSession的一级缓存:

  • DML操作:执行任意insert/update/delete语句
  • 手动清除:调用sqlSession.clearCache()
  • 配置关闭:设置localCacheScope=STATEMENT(默认SESSION)

图1

1.3 实践建议

  • 适合读多写少的场景,如配置数据、基础信息查询
  • 注意长会话可能导致内存泄漏(建议及时关闭SqlSession)
  • 跨方法调用时注意缓存一致性(可考虑@Transactional统一管理)

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

2.1 启用配置

需在Mapper XML中显式声明(注意实体类必须实现Serializable):

<!-- UserMapper.xml -->
<cache/>

或通过注解配置:

@CacheNamespace
public interface UserMapper {
    //...
}

2.2 跨会话共享机制

二级缓存存储在SqlSessionFactory级别,不同SqlSession可共享:

// 会话1
try (SqlSession session1 = sqlSessionFactory.openSession()) {
    User user1 = session1.selectOne("UserMapper.findById", 1L);
    session1.commit(); // 必须提交才会写入二级缓存
}

// 会话2
try (SqlSession session2 = sqlSessionFactory.openSession()) {
    User user2 = session2.selectOne("UserMapper.findById", 1L); // 命中二级缓存
}

2.3 缓存策略与风险

特性说明风险点
跨会话共享提升查询性能脏读风险(需合理设置缓存刷新策略)
序列化存储要求实体类实现Serializable影响性能,大对象慎用
作用域控制可配置flushInterval等参数配置不当会导致数据不一致

2.4 实践建议

  • 严格只读数据适合使用(如省份字典、系统参数)
  • 高并发写入场景建议禁用(可通过<cache-ref/>引用空缓存)
  • 分布式环境需要配合集中式缓存(如Redis)解决一致性问题

三、自定义缓存:集成第三方缓存方案

3.1 Redis集成示例

  1. 添加依赖:

    <dependency>
     <groupId>org.mybatis.caches</groupId>
     <artifactId>mybatis-redis</artifactId>
     <version>1.0.0-beta2</version>
    </dependency>
  2. 配置Mapper:

    <cache type="org.mybatis.caches.redis.RedisCache" 
        eviction="LRU"
        flushInterval="60000"
        size="1024"/>
  3. 配置redis.properties:

    redis.host=127.0.0.1
    redis.port=6379
    redis.password=
    redis.maxTotal=100

3.2 自定义缓存实现

实现org.apache.ibatis.cache.Cache接口:

public class CustomRedisCache implements Cache {
    private final String id;
    private final RedisTemplate<String, Object> redisTemplate;

    public CustomRedisCache(String id) {
        this.id = id;
        this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        redisTemplate.opsForValue().set(key.toString(), value);
    }

    @Override
    public Object getObject(Object key) {
        return redisTemplate.opsForValue().get(key.toString());
    }
    
    // 其他方法实现...
}

3.3 缓存方案选型对比

方案优点缺点适用场景
本地缓存零网络开销,速度最快单机有效,容量有限单体应用高频只读数据
Redis分布式支持,数据结构丰富网络延迟,需要序列化分布式系统,需要数据共享
Ehcache内存+磁盘二级缓存集群支持较弱大数据量本地缓存
Caffeine高性能,现代缓存算法无持久化功能高性能本地缓存

四、缓存最佳实践

  1. 监控与统计:实现Cache接口的统计方法,监控缓存命中率

    @Override
    public int getSize() {
     Long size = redisTemplate.execute(
         (RedisCallback<Long>) connection -> connection.dbSize());
     return size != null ? size.intValue() : 0;
    }
  2. 多级缓存策略

图2

  1. 缓存雪崩防护

    // 使用Redis的SETNX实现互斥锁
    public Object getObjectWithLock(Object key) {
     Object value = getObject(key);
     if (value == null) {
         String lockKey = key.toString() + "_lock";
         if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) {
             try {
                 // 查询数据库
                 value = queryFromDB(key);
                 putObject(key, value);
             } finally {
                 redisTemplate.delete(lockKey);
             }
         } else {
             // 等待重试或返回旧数据
             Thread.sleep(100);
             return getObject(key);
         }
     }
     return value;
    }

总结

MyBatis缓存机制提供了从本地到全局的多层次缓存方案,合理使用可以显著提升系统性能。但在实际应用中需要特别注意:

  • 一级缓存适合短生命周期的重复查询
  • 二级缓存在分布式环境下需要额外的一致性保障
  • 生产环境推荐采用Redis等集中式缓存作为二级缓存实现
  • 必须建立完善的监控和失效机制保障数据一致性

通过理解这些缓存特性及适用场景,开发者可以在性能和数据一致性之间找到最佳平衡点。

添加新评论