- 浏览: 169889 次
- 性别:
- 来自: 南京
最新评论
-
lvzhou_31:
msgpack-0.5.1-devel.jar 有么
msgpack -
lvzhou_31:
lvzhou_31 写道能不能把msgpack-0.5.1-d ...
msgpack -
lvzhou_31:
能不能把msgpack-0.5.1-devel.jar给下。m ...
msgpack -
dxqrr:
mark下
MyBatis缓存加载机制/自定义二级缓存 -
wulixiaoxue:
好东西,看看哈
mongodb使用 java
缓存概述
正如大多数持久层框架一样,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 过程中线程安全问题。
源码剖解
接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。
执行过程:
[点击查看原始大小图片]
① 通常情况下,我们需要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操作,上述根据产品 ID 获取 Product 对象。
② 当调用 ProductMapper 时中的方法时,其实这里所调用的是 MapperProxy 中的方法,并且 MapperProxy已经将将所有方法拦截,其具体原理及分析,参考 MyBatis+Spring基于接口编程的原理分析,其 invoke 方法代码为:
Java代码 收藏代码
//当调用 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;
}
③其中的 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) {
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代码 收藏代码
<cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />
启动Memcached:
Shell代码 收藏代码
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");
}
Memcached Loging:
看上去没什么问题~ OK了。
转载: http://denger.iteye.com/blog/1126423
正如大多数持久层框架一样,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 过程中线程安全问题。
源码剖解
接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。
执行过程:
[点击查看原始大小图片]
① 通常情况下,我们需要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操作,上述根据产品 ID 获取 Product 对象。
② 当调用 ProductMapper 时中的方法时,其实这里所调用的是 MapperProxy 中的方法,并且 MapperProxy已经将将所有方法拦截,其具体原理及分析,参考 MyBatis+Spring基于接口编程的原理分析,其 invoke 方法代码为:
Java代码 收藏代码
//当调用 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;
}
③其中的 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) {
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代码 收藏代码
<cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />
启动Memcached:
Shell代码 收藏代码
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");
}
Memcached Loging:
看上去没什么问题~ OK了。
转载: http://denger.iteye.com/blog/1126423
发表评论
-
J2EE项目中修改jsessionid属性
2014-06-08 17:53 6989J2EE项目中,默认会生成JSESSIONID,用户保存S ... -
HTTP协议--cookie、session、缓存与代理
2013-06-21 10:37 31071 Cookie和 Session Cookie和 Ses ... -
HTTP协议详情
2013-06-18 16:30 1573HTTP是一个属于应用层的面 向对象的协议,由于其简捷、快速 ... -
什么是HTTP Headers?
2013-06-18 16:22 2332什么是HTTP Headers HTTP是“Hype ... -
maven多环境下打包
2012-11-30 17:41 55341. 修改pom文件增加 <build> ... -
Java 动态代理和Cglib代理(一)
2012-10-07 16:35 1338Cglib(Code Generation Libra ... -
对Mybatis数据库数据查询乱码处理
2012-04-25 20:47 10815Java代码 public class StringTyp ... -
三种方法实现URL重写
2011-08-28 17:11 3259URL重写,就把动态URL静态化,便于搜索引擎爬虫抓取你的动态 ... -
msgpack简介(续) 复杂的数据类型
2011-08-28 10:08 2545复杂的类型 JavaBean里包括 List 和Bean类型 ... -
SSO 网站跨站点单点登录
2011-07-06 21:30 1173昨天和几位朋友探讨到了这个话题,发现虽然单点 ... -
mongodb安装
2011-06-28 19:38 12611. 解压 tar zxvf mongodb-linux-x8 ... -
msgpack
2011-06-28 19:38 20632msgpack简介 MessagePack是一个基于二进制高效 ... -
mongodb使用 java
2011-06-28 19:35 2378mongodb使用 1. 下载 mongo-2.6.3.j ...
相关推荐
MyBatis的二级缓存机制允许我们将执行过的SQL查询结果存储在内存中,以供后续相同的查询直接使用,从而避免了重复的数据库访问。二级缓存可以配置在Mapper级别或全局级别,通过XML配置文件或注解进行设置。在实现二...
开启二级缓存需要在 MyBatis 配置文件中设置 `<cache/>`,并在映射文件中启用 `<cache/>`。二级缓存的生命周期更长,但需要注意并发控制问题,以防止数据不一致。 例如,开启二级缓存配置: ```xml ...
MyBatis支持自定义二级缓存实现,可以将经常访问的数据存储在内存中,提高查询效率。 **Redis** 是一个高性能的键值对存储系统,常用于实现缓存和消息队列等场景。在本项目中,Redis被用作二级缓存的存储介质,由于...
4. **缓存机制**:MyBatis 提供了本地缓存和二级缓存,合理利用可以提高查询性能。不过要注意缓存更新策略,防止数据不一致。 5. **延迟加载**:对于一对多、多对多关联查询,可以开启延迟加载,减少一次性加载大量...
5. **缓存机制**:为了提高性能,MyBatis提供了二级缓存机制,能够在一定程度上减少数据库的访问次数。 6. **易于集成**:MyBatis可以很容易地与Spring等其他Java框架进行集成,便于构建大型应用系统。 #### 三、...
13. **缓存**:MyBatis支持本地缓存和二级缓存,可以提高数据读取速度,减少数据库压力。 通过对MyBatis 3.2.6源码的学习,开发者可以更好地理解其工作原理,从而优化性能、解决问题,甚至进行扩展和定制。例如,你...
第三天的内容聚焦于MyBatis的缓存机制,分为一级缓存(SqlSession级别的缓存)和二级缓存(Mapper级别的缓存),并讨论了缓存的优缺点以及如何管理缓存。另外,还介绍了MyBatis的插件机制,通过自定义插件可以对...
2. **MyBatis缓存机制**:MyBatis的缓存分为一级缓存和二级缓存。一级缓存是默认开启的,位于SqlSession级别,同一SqlSession内的多次相同查询会复用结果。二级缓存是可选的,存在于Mapper的命名空间内,跨...
MyBatis 支持一级缓存和二级缓存。一级缓存是 SqlSession 级别的缓存,默认开启;二级缓存是 SqlSessionFactory 级别的缓存,需要手动开启。合理的缓存策略能够显著提升系统性能。 #### 4. 事务管理 MyBatis 支持...
5. **Mybatis缓存**:Mybatis 提供了本地缓存和二级缓存机制,可以有效地提高查询效率。开发者可以根据需求选择开启或关闭,以及定制缓存策略。 6. **SqlSession与Executor**:`SqlSession`是执行SQL操作的会话对象...
- **二级缓存**:可以全局配置的二级缓存,用于不同SqlSession间的查询结果缓存。 #### 十一、插件扩展 - **插件机制**:MyBatis支持插件扩展,可以自定义拦截器来增强或改变默认的行为,如日志记录、性能监控等。 ...
缓存分为一级缓存和二级缓存。一级缓存是SqlSession级别的,即同一个SqlSession内的多次查询,如果数据没有发生变化,会直接从缓存中获取,避免了重复的数据库访问。一级缓存默认开启,存储在内存中。二级缓存则跨...
另外,文档会涵盖MyBatis的缓存机制,包括一级缓存(基于SqlSession)和二级缓存(基于namespace),以及如何自定义缓存实现。还有关于MyBatis的插件机制,通过拦截器实现对SQL执行过程的拦截和修改,如性能分析插件...
教程中还会讲解MyBatis的缓存机制,包括一级缓存和二级缓存,以及如何自定义和配置缓存。 MyBatis的另一个关键特性是它的注解支持,可以在Java接口和实体类上使用注解,代替XML文件进行配置。这种方式更加简洁,...
MyBatis支持一级缓存和二级缓存,以减少数据库访问次数,提升性能。 - **使用自定义缓存**:允许开发者自定义缓存实现。 - **参照缓存**:使用外部缓存如EHCache。 #### 动态SQL MyBatis支持动态SQL,允许根据...
- MyBatis的缓存机制:MyBatis提供了本地缓存和二级缓存,理解它们的工作原理并能灵活运用。 - MyBatis与Spring的整合:学习如何在Spring框架中集成MyBatis,实现依赖注入,简化配置。 - MyBatis的扩展:如自定义...
12. **缓存(Cache)**:MyBatis内置了两级缓存,一级缓存是SqlSession级别的,二级缓存是Mapper级别的,可以跨多个SqlSession共享数据,提高数据读取速度。 通过对MyBatis 3.4.5源码的学习,我们可以深入了解其...
- **二级缓存**:二级缓存是基于`namespace`级别的缓存机制。MyBatis默认不会开启二级缓存,需要在对应的Mapper配置文件中添加`<cache />`元素来开启。二级缓存可以在多个`SqlSession`实例之间共享,因此对于同一`...
6. 缓存机制:MyBatis内置了两种级别的缓存,一级缓存是SqlSession级别的,二级缓存是全局应用级别的。合理使用缓存能够提高查询速度,减少对数据库的访问。 7. 易于调试:MyBatis的异常体系结构清晰,当出现错误时...
6. **缓存机制**: MyBatis提供了本地缓存和二级缓存,可以提高数据访问效率。开发者可以根据需求选择开启或关闭缓存,并自定义缓存实现。 7. **结果映射**: 结果映射是MyBatis将数据库查询结果自动映射到Java对象的...