了解了MyBatis缓存的使用后,我们再来学习MyBatis缓存的实现原理。MyBatis的缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。MyBatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
getId():该方法用于获取缓存的Id,通常情况下缓存的Id为Mapper的命名空间名称。
putObject():该方法用于将一个Java对象添加到缓存中,该方法有两个参数,第一个参数为缓存的Key,即CacheKey的实例;第二个参数为需要缓存的对象。
getObject():该方法用于获取缓存Key对应的缓存对象。removeObject():该方法用于将一个对象从缓存中移除。
clear():该方法用于清空缓存。
removeObject():该方法用于将一个对象从缓存中移除。
clear():该方法用于清空缓存。
getReadWriteLock():该方法返回一个ReadWriteLock对象,该方法在3.2.6版本后已经不再使用。
MyBatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的Id相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象的Id作为因子生成hashCode。
除了基础的PerpetualCache类之外,MyBatis中为了对PerpetualCache类的功能进行增强,提供了一些缓存的装饰器类,如图所示。
BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。
LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节可参考LruCache类的源码。
ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
下面是PerpetualCache类及MyBatis提供了缓存装饰类的使用案例:
public void testCache() {
final int N = 100000;
Cache cache = new PerpetualCache("default");
cache = new LruCache(cache);
cache = new FifoCache(cache);
cache = new SoftCache(cache);
cache = new WeakCache(cache);
cache = new ScheduledCache(cache);
cache = new SerializedCache(cache);
cache = new SynchronizedCache(cache);
cache = new TransactionalCache(cache);
for (int i = 0; i < N; i++) {
cache.putObject(i, i);
((TransactionalCache) cache).commit();
}
System.out.println(cache.getSize());
}
如上面的代码所示,我们可以使用MyBatis提供的缓存装饰器类对基础的PerpetualCache类的功能进行增强,使用不同的装饰器后,缓存对象则拥有对应的功能。
另外,MyBatis提供了一个CacheBuilder类,通过生成器模式创建缓存对象。下面是使用CacheBuilder构造缓存对象的案例:
@Test
public void testCacheBuilder() {
final int N = 100000;
Cache cache = new CacheBuilder("com.xxx.mybatis.example.mapper.UserMapper")
.implementation( PerpetualCache.class)
.addDecorator(LruCache.class)
.clearInterval(10 * 60L)
.size(1024)
.readWrite(false)
.blocking(false)
.properties(null)
.build();
for (int i = 0; i < N; i++) {
cache.putObject(i, i);
}
System.out.println(cache.getSize());
}