mybatis缓存

缓存是互联网系统常常用到的,其特点是将数据保存在内存中。目前流行的缓存服务器有MongoDB、Redis、Ehcache等。缓存是在计算机内存上保存的数据,在读取的时候无需再从磁盘读入,因此具备快速读取和使用的特点,如果缓存命中率高,那么可以极大地提高系统的性能。如果缓存命中率很低,那么缓存就不存在使用的意义了,所以使用缓存的关键在于存储内容访问的命中率。

一、mybatis缓存案例
1.1 一级缓存

MyBatis对缓存提供支持,但是在没有配置的默认的情况下,它只开启一级缓存(一级缓存只是相对于同一个SqlSession而言)。
所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,因为使用SqlSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,执行增删改操作或者手动清空了缓存,并且缓存没超时的情况下,SqlSession都只会取出当前缓存的数据,而不会再次发送SQL到数据库

SqlSession session = sqlSessionFactory.openSession();
PersonMapper mapper = session.getMapper(PersonMapper.class);
mapper.getOne("1");//执行一次数据库查询
mapper.getOne("1");//从缓存拿数据
session.close();

执行结果如下:

DEBUG [main] - ==>  Preparing: select * from person where id = ? 
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <==      Total: 1

但是如果你使用的是不同的SqlSesion对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper、参数和方法,它还是会再次发送SQL到数据库去执行,返回结果。

SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
mapper1.getOne("1");//执行一次数据库查询
session1.close();

SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper mapper2 = session2.getMapper(PersonMapper.class);
mapper2.getOne("1");//执行一次数据库查询
session2.close();

执行结果如下:

DEBUG [main] - ==>  Preparing: select * from person where id = ? 
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <==      Total: 1
DEBUG [main] - ==>  Preparing: select * from person where id = ? 
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <==      Total: 1

从上面的执行结果我们发现第一次查询和第二次查询一样,那我们可不可以使第二次查询从缓存中取出呢? 为了克服这个问题,我们往往需要配置二级缓存,使得缓存在SqlSessionFactory层面上能够提供给各个SqlSession对象共享。

1.2 二级缓存

mybatis二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。

二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的,也就是要求实现Serializable接口,配置的方法很简单,只需要在映射XML文件配置就可以开启缓存了。

  • 在MyBatis的配置文件中加入
<settings>  
   <!--开启二级缓存 这个可以不加,因为cacheEnabled默认值就为true,但是防止MyBatis将来改变其默认值为false(我们升级了版本),还是加上好的。-->  
    <setting name="cacheEnabled" value="true"/>  
</settings> 
  • 在需要开启二级缓存的mapper.xml中加入caceh标签**
<cache/>
  • 让使用二级缓存的POJO类实现Serializable接口
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
mapper1.getOne("1");
session1.commit();//必须有这个,否则二级缓存无作用
session1.close();

SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper mapper2 = session2.getMapper(PersonMapper.class);
mapper2.getOne("1");
session2.commit();
session2.close();

执行结果如下:

DEBUG [main] - Cache Hit Ratio [com.clyu.mapper.PersonMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from person where id = ? 
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Cache Hit Ratio [com.clyu.mapper.PersonMapper]: 0.5
1.2 二级缓存策略配置

从上面我们可以看出,二级缓存配置我们可以通过配置cache标签来实现

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

eviction属性表示可用的清除策略,其值如下

LRU – 最近最少使用:移除最长时间不被使用的对象。这个是默认清除策略
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

flushInterval属性表示刷新间隔

属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size表示引用数目

属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly表示缓存是否是只读

属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

type自定义缓存策略

除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

这个示例展示了如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。

定义单个sql语句的缓存策略

请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache 将其设置为 fasle 后,本条查询语句会禁用二级缓存:默认true

insert,update,delete 是没有useCache属性的

1.3 二级缓存的脏读

二级缓存脏读产生原因

Mybatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都有自己的二级缓存,不同的mapper的二级缓存互不影响。这样的设计一不注意就会引起脏读,从而导致数据一致性的问题。引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。

解决方案
使用cache-ref,实现多个命名空间中共享相同的缓存配置和实例。使用 cache-ref 元素来引用另一个缓存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

脏读的避免

mapper中的操作以单表操作为主,避免在关联操作中使用mapper
在关联操作的mapper中使用参照缓存

二 缓存源码分析
public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //缓存key值生成器CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  
 @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //从MappedStatement对象中获取其,二级缓存cache。这样说明了二级缓存是namespace级别的
    Cache cache = ms.getCache();
    if (cache != null) {
      //判断要不要刷新缓存
      flushCacheIfRequired(ms);
      //是否使用二级缓存,默认true
      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;
      }
    }
    //进入BaseExecutor的query方法
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
}


/*****************************分界线********************************************/
public abstract class BaseExecutor implements Executor {

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //${}是在这里解析的
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  //MappedStatement是全局共享的
  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //从一级缓存中拿
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //对于储存过程有输出资源的处理
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //,一级缓存没有。查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
}

结论:如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库

三 spring整合mybatis后,mybatis一级缓存失效的原因

spring对mybatis的sqlsession的使用是由template控制的,sqlSessionTemplate又被spring当作resource放在当前线程的上下文里
同一线程里面两次查询同一数据所使用的sqlsession是不相同的,所以,给人的印象就是结合spring后,mybatis的一级缓存失效了。

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   // 每次执行前都创建一个新的sqlSession
     SqlSession sqlSession = getSqlSession(
     SqlSessionTemplate.this.sqlSessionFactory,
     SqlSessionTemplate.this.executorType,
     SqlSessionTemplate.this.exceptionTranslator);
   try {
   // 执行方法
    Object result = method.invoke(sqlSession, args);
    if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
     // force commit even on non-dirty sessions because some databases require
     // a commit/rollback before calling close()
     sqlSession.commit(true);
    }
    return result;
   } catch (Throwable t) {
    Throwable unwrapped = unwrapThrowable(t);
    if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
     // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
     sqlSession = null;
     Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
     if (translated != null) {
      unwrapped = translated;
     }
    }
    throw unwrapped;
   } finally {
    if (sqlSession != null) {
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    }
   }
  }
 }

因为每次都进行创建,所以就用不上sqlSession的缓存了.

对于开启了事务为什么可以用上呢, 跟入getSqlSession方法

如下:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  // 首先从SqlSessionHolder里取出session
  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
   return session;
  }
  if (LOGGER.isDebugEnabled()) {
   LOGGER.debug("Creating a new SqlSession");
  }
  session = sessionFactory.openSession(executorType);
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
  return session;
 }

在里面维护了个SqlSessionHolder,关联了事务与session,如果存在则直接取出,否则则新建个session,所以在有事务的里,每个session都是同一个,故能用上缓存了

四 Mybatis缓存架构

他底层是利用装饰者模式设计的,这样设计的好处是:我们这样灵活的装配缓存的功能
Mybatis缓存接口是:org.apache.ibatis.cache

public interface Cache {
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  default ReadWriteLock getReadWriteLock() {return null;}
}

其有如下类,注意他们的子类都是平级的

BlockingCache
FifoCache
LoggingCache://打印日志
LruCache //
PerpetualCache: //他是真正的缓存类,底层有个map
ScheduledCache
SerializedCache://就是用来序列化数据的
SoftCache
SynchronizedCache ://这个类就是保证线程安全。所以他的方法基本上是加上synchronized来保证线程安全的
TransactionalCache
WeakCache
4.1 LruCache

如果数据最近被访问过,那么将来被访问的几率也更高,同理:如果数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小

public class LruCache implements Cache {

  //原缓存对象
  private final Cache delegate;
  private Map<Object, Object> keyMap;
  //最长时间未使用
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

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

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

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

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}
4.2 TransactionalCache

缓存穿透:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大

缓存击穿:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //真正的缓存
  private final Cache delegate;
  //fasle 事物还没有提交
  private boolean clearOnCommit;
  //所有待提交的缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  //未命中的缓存集合,防止缓存穿透。防止一直访问一个为null key导致一直查询数据库
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }


  @Override
  public Object getObject(Object key) {
    // 根据key从缓存中获取数据
    Object object = delegate.getObject(key);
    if (object == null) {
      //数据为null,就把这个key放到未命中的缓存集合中
      entriesMissedInCache.add(key);
    }//如果提交了,返回null
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    //如果事物已经提交了
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      //put到真实缓存
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      //也把未命中的put进去
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        //清除
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值