Mybatis 缓存

135 阅读5分钟

缓存

程序与数据库交互时,是造成性能瓶颈的一个主要原因。数据是存储在磁盘上的,存储大量数据不利于查询。还有要进行网络通信,IO操作等。所以一种解决方案是在程序与数据库之间加入一层就是缓存。
首先缓存是内存,内存是有限的,不可能像使用磁盘一样使用内存,所以内存通常是有限的,这就决定了我们无法把所有的数据放在内存中,一是大小限制,二十数据不安全,放内存中容易丢失。
内存又可以分为两种,一种是外部的,比如redis,memcache等,也可以是在jvm中的一块空间,各有优劣。外部的缓存空间更加充分,但是需要进行网络通信,jvm由于各种限制,内存空间不如外部的,但是读取快。

代码实现

基于缓存的原理,数据结构可选map实现,当然也可以使用redis等存储数据。
在实现的过程中,会发现逻辑有些是重复的

public User queryUserById(){  
   //对缓存的操作   
   // ---> Redis 获取 取到 直接返回   
   // 没取到   访问数据库 执行JDBC  
   JDBC ----> DB  
}

考虑后续要修改的问题,所以每个查询方法都实现这块逻辑是有问题的。所以要把这部分逻辑抽出去,可以使用代理模式。

Mybatis Cache

image.png

image.png

核心的实现是impl包中的PerpetualCache类,底层是HashMap实现的。 decorators包中的是装饰器增强的Cache类,为了让PerpetualCache拥有其他的一些功能,功能增强。 内存换出策略:由于内存空间的有限性,当内存空间不够时,需要一定的策略来释放内存。
FIFO: 先入先出
LRU: 最少使用
LoggingCache: Cache增加日志功能
BlockingCache: 保证同一时间只有一个线程查询key
ScheduledCache: 设置时间间隔清空缓存
SerializedCache: 自动完成key,value的序列化和反序列化
TransactionalCache: 只在事务操作成功时,把对应的数据放置在缓存中

装饰器设计模式

mybatis中使用了大量装饰器模式 装饰器模式的作用是为目标扩展功能(本职功能、核心功能)
代理设计模式是为目标增加额外功能
装饰器和代理模式的类图是一样的
本质区别就是 装饰器:增加核心功能,和被装饰的对象完成的是同一件事情 代理模式:增加额外功能,和被代理对象做的是不同的事情

一级缓存

mybatis默认开启了一级缓存
注意:一级缓存对同一个SqlSession生效,换SqlSession不能查询原有的SqlSesison缓存中的数据。
每一请求或者每个线程使用的Connection、SqlSession都应该是独立的,因为其中会涉及到事务的处理,不能共用一个,如果共用会造成事务的混乱,比如事务的提交时机。

查询有些情况下也需要事务

  1. 加悲观锁
  2. 二级缓存

image.png 这里使用了适配器设计模式
适配器设计模式:在实现一个接口的过程中,只想或者只能实现其部分方法,考虑使用适配器设计模式
一级缓存功能主要体现在BaseExecutor中的PerpetualCache localCache

二级缓存

完成二级缓存的类是CachingExecutor,是SimpleExecutor,ReuseExecutor的装饰器,增强了核心的Cache功能。

// CachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
   // 对应了<cache/>
  Cache cache = ms.getCache();
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

CachingExecutor创建时机

mybatis解析配置文件封装成Configuration类时创建,newExecutor(),CachingExecutor ce = new CachingExecutor(simpleExecutor),默认创建的Executor就是CachingExecutor。

创建过程

解析配置文件中的cache标签时进行创建,useNewCache,构建者模式

image.png

  1. 声明了默认缓存
  2. 创建了新的实现
Cache cache = newBaseCacheInstance(implementation, id);
  1. 读取 整合<cache 增加额外的参数(内置缓存不用,自定义缓存 Redis OsCache Ehcache)
  2. 增加装饰器 
<cache/> PerpetualCache LruCache  
<cache blocking="" readOnly="" size="" flushInterval=""/>
evication="FIFO" 表示将默认的LRUCache替换为FIFOCache

创建好的Cache最终存在MappedStatement中,Configuration二级缓存操作从MappedStatement中获取Cache缓存

二级缓存、一级缓存的查询顺序

CachingExecutor -> BaseExecutor -> SimpleExecutor Configuration默认创建的是CachingExecutor,所以是先查询二级缓存的,然后在CachingExecutor中装饰了BaseExecutor,在BaseExector中查询了一级缓存

cache-ref标签

mybatis的二级缓存当有数据更新时,整个Cache都会被清空,防止脏数据的产生。
实际上每个Mapper正常都有自己的cache对象,那就会产生一个问题,当有两个表A、B关联操作时,假如存在了AMapper的Cache中,那么当更新B的数据时,其实是不会清空AMapper的Cache数据的,这时就会造成B表的脏数据。所以要在BMapper中使用cache-ref标签使AMapper合BMapper使用同一个Cache,就解决了脏数据的问题。
所以AB无论是谁更新了数据都会清空这个共同的Cache,所以要想办法根据CacheKey的规律去清空Cache,需要去自定义清空规则,mybatis中是修改不了的,必须自己定义。
可以考虑使用缓存中间件redis、memcache,或者使用jvm级别的ehcache,oschache等