Mybatis缓存机制

Mybatis缓存机制深刻解析
正如大多数持久层框架同样,MyBatis 一样提供了一级缓存和二级缓存的支持;
一级缓存基于 PerpetualCache 的 HashMap 本地缓存,其存储做用域为 Session,当 Session flush 或 close 以后,该Session中的全部 Cache 就将清空。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不一样在于其存储做用域为 Mapper(Namespace),而且可自定义存储源,如 Ehcache、Hazelcast等。
对于缓存数据更新机制,当某一个做用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操做后,默认该做用域下全部 select 中的缓存将被clear。
MyBatis 的缓存采用了delegate机制 及 装饰器模式设计,当put、get、remove时,其中会通过多层 delegate cache 处理,其Cache类别有:BaseCache(基础缓 存)、EvictionCache(排除算法缓存) 、DecoratorCache(装饰器缓存)、BaseCache:为缓存数据最终存储的处理类,默认为 PerpetualCache,基于Map存储;可自定义存储处理,如基于EhCache、Memcached等;
EvictionCache :当缓存数量达到必定大小后,将经过算法对缓存数据进行清除。默认采用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等;
DecoratorCache:缓存put/get处理先后的装饰器,如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache的数据 put或get 进行序列化 及反序列化处理、当设置flushInterval(默认1/h)后,则使用 ScheduledCache 对缓存数据进行定时刷新等。
通常缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 Key 的生成采起规则为:[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。
对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock,使用ReentrantReadWriteLock 的实现,从而经过 Lock 机制防止在并发 Write Cache 过程当中线程安全问题。
源码剖解 :java

接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。

执行过程: 算法

① 一般状况下,咱们须要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操做,上述根据产品 ID 获取 Product 对象。
② 当调用 ProductMapper 时中的方法时,其实这里所调用的是 MapperProxy 中的方法,而且 MapperProxy已经将将全部方法拦截,其具体原理及分析,参考 MyBatis+Spring基于接口编程的原理分析,其 invoke 方法代码为:
[Java] 纯文本查看 复制代码
?sql

//当调用 Mapper 全部的方法时,将都交由Proxy 中的 invoke 处理:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {数据库

try {  
  if (!OBJECT_METHODS.contains(method.getName())) {  
    final Class declaringInterface = findDeclaringInterface(proxy, method);  
    // 最终交由 MapperMethod 类处理数据库操做,初始化 MapperMethod 对象  
    final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession);  
    // 执行 mapper method,返回执行结果   
    final Object result = mapperMethod.execute(args);  
    ....  
    return result;  
  }  
} catch (SQLException e) {  
  e.printStackTrace();  
}  
return null;

}apache

③其中的 mapperMethod 中的 execute 方法代码以下:
[Java] 纯文本查看 复制代码
?编程

public Object execute(Object[] args) throws SQLException {缓存

Object result;  
// 根据不一样的操做类别,调用 DefaultSqlSession 中的执行处理  
if (SqlCommandType.INSERT == type) {  
  Object param = getParam(args);  
  result = sqlSession.insert(commandName, param);  
} else if (SqlCommandType.UPDATE == type) {  
  Object param = getParam(args);  
  result = sqlSession.update(commandName, param);  
} else if (SqlCommandType.DELETE == type) {  
  Object param = getParam(args);  
  result = sqlSession.delete(commandName, param);  
} else if (SqlCommandType.SELECT == type) {  
  if (returnsList) {  
    result = executeForList(args);  
  } else {  
    Object param = getParam(args);  
    result = sqlSession.selectOne(commandName, param);  
  }  
} else {  
  throw new BindingException("Unkown execution method for: " + commandName);  
}  
return result;

}安全

因为这里是根据 ID 进行查询,因此最终调用为 sqlSession.selectOne函数。也就是接下来的的 DefaultSqlSession.selectOne 执行;
④ ⑤ 能够在 DefaultSqlSession 看到,其 selectOne 调用了 selectList 方法:
[Java] 纯文本查看 复制代码
?数据结构

public Object selectOne(String statement, Object parameter) {mybatis

List list = selectList(statement, parameter);  
if (list.size() == 1) {  
  return list.get(0);  
}   
...

}

public List selectList(String statement, Object parameter, RowBounds rowBounds) {

try {  
  MappedStatement ms = configuration.getMappedStatement(statement);  
  // 若是启动用了Cache 才调用 CachingExecutor.query,反之则使用 BaseExcutor.query 进行数据库查询   
  return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
} catch (Exception e) {  
  throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
} finally {  
  ErrorContext.instance().reset();  
}

}

⑥到这里,已经执行到具体数据查询的流程,在分析 CachingExcutor.query 前,先看看 MyBatis 中 Executor 的结构及构建过程。

执行器(Executor):
Executor: 执行器接口。也是最终执行数据获取及更新的实例。其类结构以下:

BaseExecutor: 基础执行器抽象类。实现一些通用方法,如createCacheKey 之类。而且采用 模板模式 将具体的数据库操做逻辑(doUpdate、doQuery)交由子类实现。另外,能够看到变量 localCache: PerpetualCache,在该类采用 PerpetualCache 实现基于 Map 存储的一级缓存,其 query 方法以下:
[Java] 纯文本查看 复制代码
?

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());  
// 执行器已关闭  
if (closed) throw new ExecutorException("Executor was closed.");  
List list;  
try {  
  queryStack++;   
  // 建立缓存Key  
  CacheKey key = createCacheKey(ms, parameter, rowBounds);   
  // 从本地缓存在中获取该 key 所对应 的结果集  
  final List cachedList = (List) localCache.getObject(key);   
  // 在缓存中找到数据  
  if (cachedList != null) {   
    list = cachedList;  
  } else { // 未从本地缓存中找到数据,开始调用数据库查询  
    //为该 key 添加一个占位标记  
    localCache.putObject(key, EXECUTION_PLACEHOLDER);   
    try {  
      // 执行子类所实现的数据库查询 操做  
      list = doQuery(ms, parameter, rowBounds, resultHandler);   
    } finally {  
      // 删除该 key 的占位标记  
      localCache.removeObject(key);  
    }  
    // 将db中的数据添加至本地缓存中  
    localCache.putObject(key, list);  
  }  
} finally {  
  queryStack--;  
}  
// 刷新当前队列中的全部 DeferredLoad实例,更新 MateObject  
if (queryStack == 0) {   
  for (DeferredLoad deferredLoad : deferredLoads) {  
    deferredLoad.load();  
  }  
}  
return list;

}

BatchExcutor、ReuseExcutor、SimpleExcutor: 这几个就没什么好说的了,继承了 BaseExcutor 的实现其 doQuery、doUpdate 等方法,一样都是采用 JDBC 对数据库进行操做;三者区别在于,批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在Mybatis 的文档上都有详细说明。

CachingExecutor: 二级缓存执行器。我的以为这里设计的不错,灵活地使用 delegate机制。其委托执行的类是 BaseExcutor。 当没法从二级缓存获取数据时,一样须要从 DB 中进行查询,因而在这里能够直接委托给 BaseExcutor 进行查询。其大概流程为:

流程为: 从二级缓存中进行查询 -> [若是缓存中没有,委托给 BaseExecutor] -> 进入一级缓存中查询 -> [若是也没有] -> 则执行 JDBC 查询,其 query 代码以下:
[Java] 纯文本查看 复制代码
?

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

if (ms != null) {  
  // 获取二级缓存实例  
  Cache cache = ms.getCache();  
  if (cache != null) {  
    flushCacheIfRequired(ms);  
    // 获取 读锁( Read锁可由多个Read线程同时保持)  
    cache.getReadWriteLock().readLock().lock();  
    try {  
      // 当前 Statement 是否启用了二级缓存  
      if (ms.isUseCache()) {  
        // 将建立 cache key 委托给 BaseExecutor 建立  
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds);  
        final List cachedList = (List) cache.getObject(key);  
        // 从二级缓存中找到缓存数据  
        if (cachedList != null) {  
          return cachedList;  
        } else {  
          // 未找到缓存,很委托给 BaseExecutor 执行查询  
          List list = delegate.query(ms, parameterObject, rowBounds, resultHandler);  
          tcm.putObject(cache, key, list);  
          return list;  
        }  
      } else { // 没有启动用二级缓存,直接委托给 BaseExecutor 执行查询   
        return delegate.query(ms, parameterObject, rowBounds, resultHandler);  
      }  
    } finally {  
      // 当前线程释放 Read 锁  
      cache.getReadWriteLock().readLock().unlock();  
    }  
  }  
}  
return delegate.query(ms, parameterObject, rowBounds, resultHandler);

}

至此,已经完完了整个缓存执行器的整个流程分析,接下来是对缓存的 缓存数据管理实例进行分析,也就是其 Cache 接口,用于对缓存数据 put 、get及remove的实例对象。

Cache 委托链构建:
正如最开始的缓存概述所描述道,其缓存类的设计采用 装饰模式,基于委托的调用机制。
缓存实例构建:
缓存实例的构建 ,Mybatis 在解析其 Mapper 配置文件时就已经将该实现初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中能够看到:
[Java] 纯文本查看 复制代码
?

private void cacheElement(XNode context) throws Exception {

if (context != null) {  
  // 基础缓存类型  
  String type = context.getStringAttribute("type", "PERPETUAL");  
  Class typeClass = typeAliasRegistry.resolveAlias(type);  
  // 排除算法缓存类型  
  String eviction = context.getStringAttribute("eviction", "LRU");  
  Class evictionClass = typeAliasRegistry.resolveAlias(eviction);  
  // 缓存自动刷新时间  
  Long flushInterval = context.getLongAttribute("flushInterval");  
  // 缓存存储实例引用的大小  
  Integer size = context.getIntAttribute("size");  
  // 是不是只读缓存  
  boolean readWrite = !context.getBooleanAttribute("readOnly", false);  
  Properties props = context.getChildrenAsProperties();  
  // 初始化缓存实现  
  builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);  
}

}

如下是 useNewCache 方法实现:
[Java] 纯文本查看 复制代码
?

public Cache useNewCache(Class typeClass,

Class evictionClass,  
                       Long flushInterval,  
                       Integer size,  
                       boolean readWrite,  
                       Properties props) {  
typeClass = valueOrDefault(typeClass, PerpetualCache.class);  
evictionClass = valueOrDefault(evictionClass, LruCache.class);  
// 这里构建 Cache 实例采用 Builder 模式,每个 Namespace 生成一个  Cache 实例  
Cache cache = new CacheBuilder(currentNamespace)  
    // Builder 前设置一些从XML中解析过来的参数  
    .implementation(typeClass)  
    .addDecorator(evictionClass)  
    .clearInterval(flushInterval)  
    .size(size)  
    .readWrite(readWrite)  
    .properties(props)  
    // 再看下面的 build 方法实现  
    .build();  
configuration.addCache(cache);  
currentCache = cache;  
return cache;

}

public Cache build() {

setDefaultImplementations();  
// 建立基础缓存实例  
Cache cache = newBaseCacheInstance(implementation, id);  
setCacheProperties(cache);  
// 缓存排除算法初始化,并将其委托至基础缓存中  
for (Class<? extends Cache> decorator : decorators) {  
  cache = newCacheDecoratorInstance(decorator, cache);  
  setCacheProperties(cache);  
}  
// 标准装饰器缓存设置,如LoggingCache之类,一样将其委托至基础缓存中  
cache = setStandardDecorators(cache);  
// 返回最终缓存的责任链对象  
return cache;

}

最终生成后的缓存实例对象结构:

可见,全部构建的缓存实例已经经过责任链方式将其串连在一块儿,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。

Cache 实例解剖:
实例类:SynchronizedCache
说 明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。
解 剖:
对于 Lock 机制来讲,其分为 Read 和 Write 锁,其 Read 锁容许多个线程同时持有,而 Write 锁,一次能被一个线程持有,若是当 Write 锁没有释放,其它须要 Write 的线程只能等待其释放才能去持有。
其代码实现:
[Java] 纯文本查看 复制代码
?

public void putObject(Object key, Object object) {

acquireWriteLock();  // 获取 Write 锁  
try {  
  delegate.putObject(key, object); // 委托给下一个 Cache 执行 put 操做  
} finally {  
  releaseWriteLock(); // 释放 Write 锁  
}

}

对于 Read 数据来讲,也是如此,不一样的是 Read 锁容许多线程同时持有 :
[Java] 纯文本查看 复制代码
?

public Object getObject(Object key) {

acquireReadLock();  
try {  
  return delegate.getObject(key);  
} finally {  
  releaseReadLock();  
}

}

其具体原理能够看看 jdk concurrent 中的 ReadWriteLock 实现。

实例类:LoggingCache
说 明:用于日志记录处理,主要输出缓存命中率信息。
解 剖:
说到缓存命中信息的统计,只有在 get 的时候才须要统计命中率:
[Java] 纯文本查看 复制代码
?

public Object getObject(Object key) {

requests++; // 每调用一次该方法,则获取次数+1  
final Object value = delegate.getObject(key);  
if (value != null) {  // 命中! 命中+1  
  hits++;  
}  
if (log.isDebugEnabled()) {  
  // 输出命中率。计算方法为: hits / requets 则为命中率  
  log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  
}  
return value;

}

实例类:SerializedCache
说 明:向缓存中 put 或 get 数据时的序列化及反序列化处理。
解 剖:
序列化在Java里面已是最基础的东西了,这里也没有什么特殊之处:
[Java] 纯文本查看 复制代码
?

public void putObject(Object key, Object object) {

// PO 类须要实现 Serializable 接口  
if (object == null || object instanceof Serializable) {  
  delegate.putObject(key, serialize((Serializable) object));   
} else {  
  throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);  
}

}

public Object getObject(Object key) {

Object object = delegate.getObject(key);  
// 获取数据时对 二进制数据进行反序列化  
return object == null ? null : deserialize((byte[]) object);

}

其 serialize 及 deserialize 代码就没必要要贴了。

实例类:LruCache
说 明:最近最少使用的:移除最长时间不被使用的对象,基于LRU算法。
解 剖:
这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好象以前看过 XMemcached 的 LRU 算法也是这样实现的。
初始化 LinkedHashMap,默认为大小为 1024 个元素:
[Java] 纯文本查看 复制代码
?

public LruCache(Cache delegate) {

this.delegate = delegate;  
setSize(1024); // 设置 map 默认大小

}
public void setSize(final int size) {

// 设置其 capacity 为size, 其 factor 为.75F  
keyMap = new LinkedHashMap(size, .75F, true) {  
  // 覆盖该方法,当每次往该map 中put 时数据时,如该方法返回 True,便移除该map中使用最少的Entry  
  // 其参数  eldest 为当前最老的  Entry  
  protected boolean removeEldestEntry(Map.Entry eldest) {  
    boolean tooBig = size() > size;  
    if (tooBig) {  
      eldestKey = eldest.getKey(); //记录当前最老的缓存数据的 Key 值,由于要委托给下一个 Cache 实现删除  
    }  
    return tooBig;  
  }  
};

}

public void putObject(Object key, Object value) {

delegate.putObject(key, value);  
cycleKeyList(key);  // 每次 put 后,调用移除最老的 key

}
// 看看当前实现是否有 eldestKey, 有的话就调用 removeObject ,将该key从cache中移除
private void cycleKeyList(Object key) {

keyMap.put(key, key); // 存储当前 put 到cache中的 key 值  
if (eldestKey != null) {  
  delegate.removeObject(eldestKey);  
  eldestKey = null;  
}

}

public Object getObject(Object key) {

keyMap.get(key); // 便于 该 Map 统计 get该key的次数  
return delegate.getObject(key);

}

实例类:PerpetualCache
说 明:这个比较简单,直接经过一个 HashMap 来存储缓存数据。因此没什么说的,直接看下面的 MemcachedCache 吧。

自定义二级缓存/Memcached
其自定义二级缓存也较为简单,它自己默认提供了对 Ehcache 及 Hazelcast 的缓存支持:Mybatis-Cache,我这里参考它们的实现,自定义了针对 Memcached 的缓存支持,其代码以下:
[Java] 纯文本查看 复制代码
?

package com.xx.core.plugin.mybatis;

import java.util.LinkedList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xx.core.memcached.JMemcachedClientAdapter;
import com.xx.core.memcached.service.CacheService;
import com.xx.core.memcached.service.MemcachedService;

/**

  • Cache adapter for Memcached.
  • @author denger

*/
public class MemcachedCache implements Cache {

// Sf4j logger reference  
private static Logger logger = LoggerFactory.getLogger(MemcachedCache.class);  

/** The cache service reference. */ 
protected static final CacheService CACHE_SERVICE = createMemcachedService();  

/** The ReadWriteLock. */ 
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  

private String id;  
private LinkedList<String> cacheKeys = new LinkedList<String>();  

public MemcachedCache(String id) {  
    this.id = id;  
}  
// 建立缓存服务类,基于java-memcached-client  
protected static CacheService createMemcachedService() {  
    JMemcachedClientAdapter memcachedAdapter;  

    try {  
        memcachedAdapter = new JMemcachedClientAdapter();  
    } catch (Exception e) {  
        String msg = "Initial the JMmemcachedClientAdapter Error.";  
        logger.error(msg, e);  
        throw new RuntimeException(msg);  
    }  
    return new MemcachedService(memcachedAdapter);  
}  

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

// 根据 key 从缓存中获取数据  
@Override 
public Object getObject(Object key) {  
    String cacheKey = String.valueOf(key.hashCode());  
    Object value = CACHE_SERVICE.get(cacheKey);  
    if (!cacheKeys.contains(cacheKey)){  
        cacheKeys.add(cacheKey);  
    }  
    return value;  
}  

@Override 
public ReadWriteLock getReadWriteLock() {  
    return this.readWriteLock;  
}  

// 设置数据至缓存中  
@Override 
public void putObject(Object key, Object value) {  
    String cacheKey = String.valueOf(key.hashCode());  

    if (!cacheKeys.contains(cacheKey)){  
        cacheKeys.add(cacheKey);  
    }  
    CACHE_SERVICE.put(cacheKey, value);  
}  
// 从缓存中删除指定 key 数据  
@Override 
public Object removeObject(Object key) {  
    String cacheKey = String.valueOf(key.hashCode());  

    cacheKeys.remove(cacheKey);  
    return CACHE_SERVICE.delete(cacheKey);  
}  
//清空当前 Cache 实例中的全部缓存数据  
@Override 
public void clear() {  
    for (int i = 0; i < cacheKeys.size(); i++){  
        String cacheKey = cacheKeys.get(i);  
        CACHE_SERVICE.delete(cacheKey);  
    }  
    cacheKeys.clear();  
}  

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

}

在 ProductMapper 中增长配置:
[XML] 纯文本查看 复制代码
?
1
<cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />

启动Memcached:
[Shell] 纯文本查看 复制代码
?
1
memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v

执行Mapper 中的查询、修改等操做,Test:
[Java] 纯文本查看 复制代码
?

@Test

public void testSelectById() {  
    Long pid = 100L;  

    Product dbProduct = productMapper.selectByID(pid);  
    Assert.assertNotNull(dbProduct);  

    Product cacheProduct = productMapper.selectByID(pid);  
    Assert.assertNotNull(cacheProduct);  

    productMapper.updateName("IPad", pid);  

    Product product = productMapper.selectByID(pid);  
    Assert.assertEquals(product.getName(), "IPad");  
}
相关文章
相关标签/搜索