helencoder 2011-05-04
iBATIS缓存实现分析[转]
为了提高应用程序性能,一种比较通用的方法是使用缓存技术来减少与数据库之间的交互。缓存技术是一种“以空间换时间”的设计理念,利用内存空间资源来提高数据检索速度的有效手段之一。
iBATIS以一种简单、易用、灵活的方式实现了数据缓存。下面,首先看一下iBATIS关于缓存部分的核心类图:
iBATIS缓存核心类图
关于这些类的用途,在注释中做了比较概括性的说明,下面就来仔细的讲一下这些类的用途以及它们是如何工作的。
在iBATIS中,可以配置多个缓存,每个cacheModel的配置对应一个CacheModel类的一个对象。其中包括id等配置信息。iBATIS通过这些配置信息来定义缓存管理的行为。
缓存的目的是为了能够实现数据的高速检索。在程序中,数据是用对象表示的;为了能够检索到以缓存的数据对象,每个数据对象必须拥有一个唯一标识,在iBATIS中,这个唯一标识用CacheKey来表示。
那么,缓存的数据保存到什么地方了呢?如何实现数据的快速检索呢?答案在CacheController的实现类中。每个CacheController中都有一个Map类型的属性cache来保存被缓存的数据,其中key为CacheKey类型,value为Object类型;需要关注的是CacheKey对象的hashCode的生成算法,每次调用CacheKey对象的update方法时,都会更新它的hashCode值,关于hashCode值的计算方法后续在给出详细说明。
在拥有了数据缓存区后,就可以向其中存放数据和检索数据了。在iBATIS中,有多种的缓存管理策略,也可以自定义缓存管理策略。
关于缓存的功能,主要有两种类型:一种是对外提供的功能:数据存储和数据检索;另外一种是内部管理的功能:缓存对象标识的生成,缓存区刷新,数据检索算法等。下面就逐一介绍这些功能的代码实现。
1.数据存储
首先看一下CacheModel中的putObject方法是如何实现的
publicvoidputObject(CacheKeykey,Objectvalue){
if(null==value)value=NULL_OBJECT;
//关于缓存的操作,需要互斥
synchronized(this){
if(serialize&&!readOnly&&value!=NULL_OBJECT){
//需要序列化,并且非只读,则需要将缓存对象序列化到内存,以供后续检索使用
//readOnly为false时,不能直接将对象引用直接返回个客户程序
try{
ByteArrayOutputStreambos=newByteArrayOutputStream();
ObjectOutputStreamoos=newObjectOutputStream(bos);
oos.writeObject(value);
oos.flush();
oos.close();
value=bos.toByteArray();
}catch(IOExceptione){
thrownewRuntimeException("Errorcachingserializableobject.Cause:"+e,e);
}
}
//如果执行了内存序列化,则保存的是它的字节数组
controller.putObject(this,key,value);
if(log.isDebugEnabled()){
log("storedobject",true,value);
}
}
}
因为真正缓存数据对象的地方是在CacheController中,所以CacheModel的putObject方法中会调用CacheController的putObject方法执行真正的数据存储。由于不同的CacheController实现的缓存管理方式不同,所以putObject实现也各不相同。下面分别介绍不同的CacheController实现的putObject方法
1)FifoCacheController
publicvoidputObject(CacheModelcacheModel,Objectkey,Objectvalue){
//保存到Map中
cache.put(key,value);
//保存key到keyList
keyList.add(key);
//如果当前key的数量大于缓存容量时,移除keyList和cache中的第一个元素,达到先进先出的目的
if(keyList.size()>cacheSize){
try{
ObjectoldestKey=keyList.remove(0);
cache.remove(oldestKey);
}catch(IndexOutOfBoundsExceptione){
//ignore
}
}
}
2)LruCacheController
publicvoidputObject(CacheModelcacheModel,Objectkey,Objectvalue){
cache.put(key,value);
keyList.add(key);
if(keyList.size()>cacheSize){
try{
//取得keyList中的第一个元素作为最近最少用的key,为什么呢?
//这个问题等到讲解它的getObject方法时别会知晓
ObjectoldestKey=keyList.remove(0);
cache.remove(oldestKey);
}catch(IndexOutOfBoundsExceptione){
//ignore
}
}
}
3)MemoryCacheController
publicvoidputObject(CacheModelcacheModel,Objectkey,Objectvalue){
Objectreference=null;
//根据配置创建响应的引用类型,此种缓存管理方式完全交给jvm的垃圾回收器来管理
//创建好引用后,将数据对象放入到引用中
if(referenceType.equals(MemoryCacheLevel.WEAK)){
reference=newWeakReference(value);
}elseif(referenceType.equals(MemoryCacheLevel.SOFT)){
reference=newSoftReference(value);
}elseif(referenceType.equals(MemoryCacheLevel.STRONG)){
reference=newStrongReference(value);
}
//在缓存中保存引用
cache.put(key,reference);
}
4)OSCacheController
这个缓存管理使用了OSCache来管理缓存,这里就不做仔细的介绍了。
2.数据检索
在数据被放置到缓存区中以后,程序需要根据一定的条件进行数据检索。首先看一下CacheModel类的getObject方法是如何检索数据的
publicObjectgetObject(CacheKeykey){
Objectvalue=null;
//互斥访问缓冲区
synchronized(this){
if(flushInterval!=NO_FLUSH_INTERVAL
&&System.currentTimeMillis()-lastFlush>flushInterval){
//如果到了定期刷新缓冲区时,则执行刷新
flush();
}
//根据key来从CacheController中取得数据对象
value=controller.getObject(this,key);
if(serialize&&!readOnly&&
(value!=NULL_OBJECT&&value!=null)){
//如果需要序列化,并且非只读,则从内存中序列化出一个数据对象的副本
try{
ByteArrayInputStreambis=newByteArrayInputStream((byte[])value);
ObjectInputStreamois=newObjectInputStream(bis);
value=ois.readObject();
ois.close();
}catch(Exceptione){
thrownewRuntimeException("Errorcachingserializableobject.Besureyou'renotattemptingtouse"+
"aserializedcacheforanobjectthatmaybetakingadvantageoflazyloading.Cause:"+e,e);
}
}
//下面的两个操作是用来计算缓存区数据检索的命中率的
//对于缓冲区的数据检索请求加一操作
requests++;
//如果检索到数据,则命中数加一
if(value!=null){
hits++;
}
if(log.isDebugEnabled()){
if(value!=null){
log("retrievedobject",true,value);
}
else{
log("cachemiss",false,null);
}
}
}
returnvalue;
}
真正的数据检索操作是在CacheController的实现类中进行的,下面就分别来看一下各个实现类是如何检索数据的。
1)FifoCacheController
publicObjectgetObject(CacheModelcacheModel,Objectkey){
//直接从Map中取得
returncache.get(key);
}
2)LruCacheController
publicObjectgetObject(CacheModelcacheModel,Objectkey){
Objectresult=cache.get(key);
//因为这个key被使用了,如果检索到了数据,则将其移除并重新放置到队尾
//这样的目的就是保持最近使用的key放在队尾,而对头为最近未使用的
//如果没有检索到对象,则直接将该key移除
keyList.remove(key);
if(result!=null){
keyList.add(key);
}
returnresult;
}
3)MemoryCacheController
publicObjectgetObject(CacheModelcacheModel,Objectkey){
Objectvalue=null;
//取得引用对象
Objectref=cache.get(key);
if(ref!=null){
//从引用对象中取得数据对象
if(refinstanceofStrongReference){
value=((StrongReference)ref).get();
}elseif(refinstanceofSoftReference){
value=((SoftReference)ref).get();
}elseif(refinstanceofWeakReference){
value=((WeakReference)ref).get();
}
}
returnvalue;
}
3唯一标识的生成
在iBATIS中,用CacheKey来标识一个缓存对象,而CacheKey通常是作为Map中的key存在,所以CacheKey的hashCode的计算方法异常重要。影响hashCode的值有很多方面的因素,对每一个影响hashCode的元素,都需要调用CacheKey的update方法来重新计算hashCode值。下面我们就来看一下CacheKey的创建以及计算的相关过程。
首先CacheKey是在BaseDataExchange类的getCacheKey方法中被创建的。
publicCacheKeygetCacheKey(StatementScopestatementScope,ParameterMapparameterMap,ObjectparameterObject){
CacheKeykey=newCacheKey();
//取得parameterObject中的数据,这个parameterObject就是客户端传递过来的参数对象
Object[]data=getData(statementScope,parameterMap,parameterObject);
//根据parameterObject中的数据去重计算hashCode
for(inti=0;i<data.length;i++){
if(data[i]!=null){
key.update(data[i]);
}
}
returnkey;
}
这个方法被MappedStatement中的getCacheKey调用
publicCacheKeygetCacheKey(StatementScopestatementScope,ObjectparameterObject){
Sqlsql=statementScope.getSql();
ParameterMappmap=sql.getParameterMap(statementScope,parameterObject);
CacheKeycacheKey=pmap.getCacheKey(statementScope,parameterObject);
//statementid对hashCode有影响
cacheKey.update(id);
cacheKey.update(baseCacheKey);
//sql语句对hashCode有影响
cacheKey.update(sql.getSql(statementScope,parameterObject));//Fixesbug953001
returncacheKey;
}
真正需要CacheKey对象的地方是在CacheStatement类中
publicCacheKeygetCacheKey(StatementScopestatementScope,ObjectparameterObject){
CacheKeykey=statement.getCacheKey(statementScope,parameterObject);
//如果不可读并且不被序列化,那么当前的SessionScope也对hashCode有影响
//而真正起作用的是SessionScope的id属性
//也就是说这个缓存与调用线程的会话有关,当前线程所存储的数据不能被其他线程使用
if(!cacheModel.isReadOnly()&&!cacheModel.isSerialize()){
key.update(statementScope.getSession());
}
returnkey;
}
经过上述一系列的getCacheKey调用,将对CacheKey有影响的因素施加给了hashCode。其中对CacheKey的hashCode起影响作用的因素主要有:baseCacheKey,sql语句,参数值,statementid。可能产生影响的因素是sessionid。
现在我们知道了决定CacheKey的相关因素,也就知道了iBATIS是如何唯一的确定一个缓存对象。
经过以上的代码分析,可以掌握iBatis如何生成CacheKey对象和计算其hashCode值,以及存储和检索数据对象。这些正是iBATIS缓存的基础,掌握了这些实现原理,有助于我们更高效的使用iBATIS缓存功能,或者是开发自己的缓存系统。
原文http://www.cnblogs.com/lvpei/archive/2011/03/14/1984138.html
如:对于sql语句order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id"。