缓存概述
- 正如大多数持久层框架一样,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 方法代码为:
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- if (!OBJECT_METHODS.contains(method.getName())) {
- final Class declaringInterface = findDeclaringInterface(proxy, method);
-
- final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession);
-
- final Object result = mapperMethod.execute(args);
- ....
- return result;
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return null;
- }
③其中的 mapperMethod 中的 execute 方法代码如下:
- public Object execute(Object[] args) throws SQLException {
- Object result;
-
- 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 方法:
- 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);
-
- 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 方法如下:
- 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++;
-
- CacheKey key = createCacheKey(ms, parameter, rowBounds);
-
- final List cachedList = (List) localCache.getObject(key);
-
- if (cachedList != null) {
- list = cachedList;
- } else {
-
- localCache.putObject(key, EXECUTION_PLACEHOLDER);
- try {
-
- list = doQuery(ms, parameter, rowBounds, resultHandler);
- } finally {
-
- localCache.removeObject(key);
- }
-
- localCache.putObject(key, list);
- }
- } finally {
- queryStack--;
- }
-
- 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 代码如下:
- 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);
-
- cache.getReadWriteLock().readLock().lock();
- try {
-
- if (ms.isUseCache()) {
-
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds);
- final List cachedList = (List) cache.getObject(key);
-
- if (cachedList != null) {
- return cachedList;
- } else {
-
- List list = delegate.query(ms, parameterObject, rowBounds, resultHandler);
- tcm.putObject(cache, key, list);
- return list;
- }
- } else {
- return delegate.query(ms, parameterObject, rowBounds, resultHandler);
- }
- } finally {
-
- cache.getReadWriteLock().readLock().unlock();
- }
- }
- }
- return delegate.query(ms, parameterObject, rowBounds, resultHandler);
- }
至此,已经完完了整个缓存执行器的整个流程分析,接下来是对缓存的 缓存数据管理实例进行分析,也就是其 Cache 接口,用于对缓存数据 put 、get及remove的实例对象。
Cache 委托链构建:
正如最开始的缓存概述所描述道,其缓存类的设计采用 装饰模式,基于委托的调用机制。
缓存实例构建:
缓存实例的构建 ,Mybatis 在解析其 Mapper 配置文件时就已经将该实现初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中可以看到:
- 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 方法实现:
- 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 cache = new CacheBuilder(currentNamespace)
-
- .implementation(typeClass)
- .addDecorator(evictionClass)
- .clearInterval(flushInterval)
- .size(size)
- .readWrite(readWrite)
- .properties(props)
-
- .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);
- }
-
- cache = setStandardDecorators(cache);
-
- return cache;
- }
最终生成后的缓存实例对象结构:
可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。
Cache 实例解剖:
实例类: SynchronizedCache
说 明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。
解 剖:
对于 Lock 机制来说,其分为 Read 和 Write 锁,其 Read 锁允许多个线程同时持有,而 Write 锁,一次能被一个线程持有,如果当 Write 锁没有释放,其它需要 Write 的线程只能等待其释放才能去持有。
其代码实现:
- public void putObject(Object key, Object object) {
- acquireWriteLock();
- try {
- delegate.putObject(key, object);
- } finally {
- releaseWriteLock();
- }
- }
对于 Read 数据来说,也是如此,不同的是 Read 锁允许多线程同时持有 :
- public Object getObject(Object key) {
- acquireReadLock();
- try {
- return delegate.getObject(key);
- } finally {
- releaseReadLock();
- }
- }
其具体原理可以看看 jdk concurrent 中的 ReadWriteLock 实现。
实例类: LoggingCache
说 明:用于日志记录处理,主要输出缓存命中率信息。
解 剖:
说到缓存命中信息的统计,只有在 get 的时候才需要统计命中率:
- public Object getObject(Object key) {
- requests++;
- final Object value = delegate.getObject(key);
- if (value != null) {
- hits++;
- }
- if (log.isDebugEnabled()) {
-
- log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
- }
- return value;
- }
实例类: SerializedCache
说 明:向缓存中 put 或 get 数据时的序列化及反序列化处理。
解 剖:
序列化在Java里面已经是最基础的东西了,这里也没有什么特殊之处:
- public void putObject(Object key, Object object) {
-
- 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 个元素:
- public LruCache(Cache delegate) {
- this.delegate = delegate;
- setSize(1024);
- }
- public void setSize(final int size) {
-
- keyMap = new LinkedHashMap(size, .75F, true) {
-
-
- protected boolean removeEldestEntry(Map.Entry eldest) {
- boolean tooBig = size() > size;
- if (tooBig) {
- eldestKey = eldest.getKey();
- }
- return tooBig;
- }
- };
- }
-
- public void putObject(Object key, Object value) {
- delegate.putObject(key, value);
- cycleKeyList(key);
- }
-
- private void cycleKeyList(Object key) {
- keyMap.put(key, key);
- if (eldestKey != null) {
- delegate.removeObject(eldestKey);
- eldestKey = null;
- }
- }
-
- public Object getObject(Object key) {
- keyMap.get(key);
- return delegate.getObject(key);
- }
实例类: PerpetualCache
说 明:这个比较简单,直接通过一个 HashMap 来存储缓存数据。所以没什么说的,直接看下面的 MemcachedCache 吧。
自定义二级缓存/Memcached
其自定义二级缓存也较为简单,它本身默认提供了对 Ehcache 及 Hazelcast 的缓存支持: Mybatis-Cache,我这里参考它们的实现,自定义了针对 Memcached 的缓存支持,其代码如下:
在 ProductMapper 中增加配置:
- <cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />
启动Memcached:
- memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v
执行Mapper 中的查询、修改等操作,Test:
- @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了。