Mybatis一级缓存

文章介绍了Mybatis的缓存机制,包括一级缓存和二级缓存的作用,一级缓存的SqlSession级别工作流程,以及Cache接口和其默认实现PerpetualCache。还提到了一些Cache的装饰器类,如FifoCache和LruCache等,用于优化缓存性能。

缓存目的

缓存的目的是为了解决速度不匹配问题。内存的访问速度远远超过磁盘的访问速度,所以为了减少磁盘的IO次数所带来的时间消耗,很多场景下都会选择在内存中开辟一块比较小的区域充当缓存。

Mybatis缓存

Mybatis缓存机制是在执行一条查询SQL语句后,会将查询到的结果集以键值对的方式存储起来,等到下一次执行相同的查询SQL时,会先尝试从缓存中读取数据,如果从缓存中读取到了对应的结果,则直接返回;否则查询数据库,并且将查询到的结果放置在缓存中。

Mybatis缓存分为一级缓存和二级缓存:

一级缓存:SQLSession级别缓存,每次MyBatis开启一次和数据库的会话,就会创建出一个SqlSession对象表示一次数据库会话。所以一个一级缓存对应一个SqlSession。

二级缓存:Mapper级别缓存。对于二级缓存的开启条件之一就是需要在XXXMapper.xml中书写<cache/>标签。所以一个二级缓存对应一个或多个XXXMapper.xml文件。(通过<cache-ref/>标签设置多个XXXMapper.xml文件共用同一个二级缓存)

Mybatis一级缓存概述

Mybatis一个SqlSession对象中创建一个本地缓存localCache,在二级缓存不命中情况下,对于每一次查询,都会尝试去本地缓存中查找当前查询Sql的结果数据,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。

Mybatis查询数据顺序:二级缓存 ---> 一级缓存 ---> 数据库

一级缓存是SQLSession级别的,每一个SqlSession会创建一个一级缓存,两个不同的SqlSession执行同一个查询Sql也是查询各自的一级缓存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JVImWjG6-1684509044298)(res/Mybatis一级缓存/image-20230514101637248.png)]

一级缓存工作流程

下图是查询一级缓存的流程,重点看一下Cache接口:

  1. 根据SQLSession执行查询Sql,Executor会根据Sql语句、查询参数等内容创建一个key值,根据key值查询一级缓存;

    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    
  2. 根据key值就会从Cache(PerpetualCache)中获取对应的缓存结果;

    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    
  3. 如果命中,直接将缓存结果返回;

  4. 如果不命中,则会查询数据库,并且将查询到的数据存放在缓存中,最后返回结果;

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      List<E> list;
      //占位符
      localCache.putObject(key, EXECUTION_PLACEHOLDER);
      try {
        //查询数据库
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
      } finally {
        //清除占位符
        localCache.removeObject(key);
      }
      //将从数据库中的数据存放在缓存中
      localCache.putObject(key, list);
      //执行Sql都是采用PreparedStatement,可以忽略下面的if语句块
      if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
      }
      //返回结果
      return list;
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDxCXUQ1-1684509044301)(res/Mybatis一级缓存/image-20230514103749342.png)]

Cache接口

public interface Cache {
  //Cache的id值,用于唯一标识Cache
  String getId();
  //向缓存中添加数据,键值对结构的
  void putObject(Object key, Object value);
  //根据key,获取缓存对应的数据
  Object getObject(Object key);
  //根据key,删除对应的缓存数据
  Object removeObject(Object key);
  //清空缓存 
  void clear();
  //缓存大小
  int getSize();
  
  //读写锁,不用该方法,Cache接口的实现类也没有实现该方法
  ReadWriteLock getReadWriteLock();

}

PerpetualCache

对于Cache接口,Mybatis提供了很多的实现类,其中除了perpetualCache,其他的实现类都属于对Cache的装饰器类。一级缓存也是通过PerpetualCache实现的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xI5O0Jgl-1684509044305)(res/Mybatis一级缓存/image-20230514105049559.png)]

进入到PerpetualCache中,也只是采用HashMap充当缓存:

public class PerpetualCache implements Cache {

  private final String id;

  //通过Map充当缓存,key和value都是Object类型
  private Map<Object, Object> cache = new HashMap<Object, Object>();

  //通过构造方法唯一标识Cache的id
  public PerpetualCache(String id) {
    this.id = id;
  }

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

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  //并没有实现获取读写锁方法
  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

这里采用HashMap充当一级缓存,没有限制Map的大小,是因为SqlSession的生存时间比较短,不会向Map中存放过多的缓存数据,当SqlSession消亡时,对应的一级缓存也会被清空;另外执行insert|update|delete修改操作时也会清空一级缓存。

Cache的装饰器

Cache接口的实现类中,除了PerpetualCache类,其他的实现类都属于装饰器类:

  • FifoCache:先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉

  • LruCache:最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近最少被使用的缓存记录清除掉

  • LoggingCache:为Cache增加日志功能,调用getObject(Object key)方法时打印日志

  • ScheduledCache:指定的某一个时间间隔将Cache缓存中的数据清空

  • SerializedCache:添加序列化功能,在调用getObject(Object key)和putObject(Object key, Object value)方法时对key和value进行序列化和反序列化功能。

    MybatisRedis整合时,Redis重写了Cache接口中的方法,其中putObject和getObject就对key和value进行了序列化和反序列化操作。虽然Redis中的key一般都是String类型,但是Mybatis缓存中的key是一个CacheKey对象,并不是一个字符串。而且对key和value进行序列化,也可以节省缓存空间。
    
  • SynchronizedCache:为Cache中的方法加上了Synchronized关键字,实现了不同线程对Cache的同步

  • TransactionalCache:针对二级缓存,只有在事务提交后,才会将结果存放在二级缓存中。

这些装饰器可以对指定的Cache进行增强,例如:

//核心Cache
PerpetualCache perpetualCache = new PerpetualCache("123");
//通过装饰器模式,为Cache增加功能
//增加日志功能
LoggingCache loggingCache = new LoggingCache(perpetualCache);
//增加LRU算法
LruCache lruCache = new LruCache(loggingCache);
### MyBatis 一级缓存的工作原理 MyBatis一级缓存是基于 `SqlSession` 实现的,默认情况下,每个 `SqlSession` 都有一个独立的一级缓存区域[^3]。这意味着在同一 `SqlSession` 范围内执行相同的 SQL 查询时,如果查询参数相同,则不会再次访问数据库,而是直接从缓存中返回结果。 #### 缓存存储位置 一级缓存的数据存储在内存中的一个 Map 结构里,键为查询语句及其参数的唯一标识符,值为查询的结果集。这种设计使得重复查询能够快速命中缓存并减少对数据库的压力[^1]。 #### 失效场景 尽管一级缓存能显著提升性能,但它并非始终有效。以下情况会导致一级缓存失效: - 当前 `SqlSession` 执行了任何修改操作(如 INSERT、UPDATE 或 DELETE),因为这些操作可能改变底层数据的状态。 - 显式调用了 `clearCache()` 方法手动清除了当前 `SqlSession` 的缓存。 - 不同的 `SqlSession` 对象之间无法共享缓存内容,即使它们执行的是完全一致的查询逻辑[^3]。 #### 使用场景 为了充分利用 MyBatis 一级缓存带来的优势,建议将其应用于如下场景: - **短生命周期事务**:在一个较短时间内完成的操作流程中频繁读取同一份数据,此时可以通过开启单个 `SqlSession` 来利用其内置的一级缓存功能。 - **只读模式下的批量处理**:对于只需要检索而无需更新的情况,比如报表生成或者数据分析任务,可以考虑通过保持同一个 `SqlSession` 提高效率[^4]。 以下是展示如何正确使用 MyBatis 一级缓存的一个简单例子: ```java // 创建 SqlSession 并保持它在整个过程中可用 try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 第一次查询触发实际SQL请求到DB User userFirstQuery = userMapper.getUserById(1); System.out.println(userFirstQuery.getName()); // 假设没有其他影响此记录状态的变化发生... // 下面第二次查询应该会命中缓存而不是再发新的SQL给DB User userSecondQuery = userMapper.getUserById(1); System.out.println(userSecondQuery.getName()); } catch(Exception e){ throw new RuntimeException(e.getMessage(),e); } ``` ### 注意事项 需要注意的是,由于一级缓存仅限于单个 `SqlSession` 生命周期之内生效,所以在分布式环境下跨多个服务实例间协作时并不能依赖于此特性来同步最新版本的信息[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值