Mybatis源码之美:3.3.Mybatis中的缓存配置

931 阅读14分钟

Mybatis中的缓存配置

在不涉及源码的前提下了解缓存

mybatis为了提升查询的效率和降低数据库的访问压力,提供了较为强大的缓存机制。

mybatis中的缓存分为一级缓存二级缓存

  • 一级缓存默认开启,不可关闭,但是可以通过localCacheScope参数修改缓存的作用域。

    前面的文章中提到过localCacheScope参数对应着LocalCacheScope枚举对象,用于指定mybatis一级缓存的生命周期,他有两个取值:SESSION对应的生命周期为SqlSession,STATEMENT对应的生命周期为Statement

  • 二级缓存可以通过参数cacheEnabled全局地开启或关闭,除了mybatis提供的默认的二级缓存实现之外,我们也可以通过具体的cache元素来集成第三方的缓存实现,比如可以集成ehcache:

    pom.xml引入ehcache:

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.0</version>
    </dependency>
    

    *Mapper.xml使用ehcache:

    <mapper namespace="org.acme.FooMapper">
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
        <property name="timeToIdleSeconds" value="3600"/><!--1 hour-->
        <property name="timeToLiveSeconds" value="3600"/><!--1 hour-->
        <property name="maxEntriesLocalHeap" value="1000"/>
        <property name="maxEntriesLocalDisk" value="10000000"/>
        <property name="memoryStoreEvictionPolicy" value="LRU"/>
    </cache>
    ...
    </mapper>
    

了解二级缓存的配置

负责配置二级缓存的cache元素的DTD定义如下:

<!ELEMENT cache (property*)>
<!ATTLIST cache
type CDATA #IMPLIED
eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED

cache元素有六个非必填的属性:

  • type表示使用的缓存实例类型,默认使用PerpetualCache
  • eviction表示使用的缓存回收策略。
  • flushInterval表示缓存刷新的时间间隔,单位为毫秒ms
  • size表示缓存可用空间的大小,即可以缓存对象的个数。
  • readOnly表示缓存是否为只读
  • blocking表示缓存是否为阻塞性的,若是在缓存中无法获取指定Key对应的对象时,会一直阻塞,直到有对应的对象放入缓存。

除此之外,cache元素下可以配置零个或多个proerty元素,这些property元素集合用来配置用户对缓存的自定义配置。

解析cache元素

对于cache元素的解析工作,自XmlMapperBuilder中的configurationElement(XNode context)方法调用cacheElement(context.evalNode("cache"))开始。

  /**
     * 解析配置mapper节点
     *
     * @param context mapper节点
     */
    private void configurationElement(XNode context) {
        // ...
        // 解析缓存配置,并给当前命名空间配置一个缓存,默认情况下Mybatis使用PerpetualCache
        cacheElement(context.evalNode("cache"));
        // ...
    }

cacheElement(context.evalNode("cache"))方法会依次读取出cache元素的属性和property定义,并将其转换为所需的数据来配置缓存对象:

/**
    * 解析cache节点
    * 缓存配置
    */
private void cacheElement(XNode context) {
    if (context != null) {
        // 二级缓存,默认为PERPETUAL
        String type = context.getStringAttribute("type", "PERPETUAL");
        // 解析出缓存实现类
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 缓存回收策略
        //  LRU –  最近最少使用的:移除最长时间不被使用的对象。
        //
        //  FIFO –  先进先出:按对象进入缓存的顺序来移除它们。
        //
        //  SOFT –  软引用:移除基于垃圾回收器状态和软引用规则的对象。
        //
        //  WEAK –  弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
        // 解析出缓存策略
        String eviction = context.getStringAttribute("eviction", "LRU");
        // 解析出缓存策略类
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // 设置刷新间隔
        Long flushInterval = context.getLongAttribute("flushInterval");
        // 可用内存大小
        Integer size = context.getIntAttribute("size");
        // 是否为只读缓存,主要读取参数前面的'!'。
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        // 阻塞缓存
        boolean blocking = context.getBooleanAttribute("blocking", false);

        // 配置用户自定义参数
        Properties props = context.getChildrenAsProperties();

        // 使用一个新的缓存
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

取值操作比较简单,先通过typeevication属性来确定缓存类型以及缓存回收策略,typeevication两个属性都支持别名,在Configuration的构造方法为他们默认注册了一些实例:

public Configuration() {
    // ...
    // 注册永久缓存
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    // 注册先入先出回收的缓存
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    // 注册最近最少使用回收缓存
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    // 注册软引用缓存
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    // 注册弱引用缓存
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    // ...
  • cachetype属性默认值是PERPETUAL,对应着PerpetualCache缓存实现。

  • cacheevication属性默认值是LRU,对应着LruCache缓存回收策略。

在确定了缓存类型以及缓存回收策略之后,就开始依次读取一些参数性的属性。

首先获取定义缓存刷新时间间隔的配置flushInterval,之后获取定义缓存可用内存大小的size属性,然后再获取用于定义缓存类型(是否为只读)的属性readOnly(默认为flase),接着在读取用户配置缓存阻塞性质的属性blocking(默认为flase),最后读取cache下的参数集合property

需要注意的是对于readOnly属性的处理,前面加了一个非(!),因为该属性实际对应的是CacheBuilderreadWrite属性,后面会讲到CacheBuilder

在获取到这些属性之后,一窝蜂的交给了映射器构建助手MapperBuilderAssistantuseNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props)方法来完成缓存的配置工作:

public Cache useNewCache(
        Class<? extends Cache> typeClass,
        Class<? extends Cache> evictionClass,
        Long flushInterval,
        Integer size,
        boolean readWrite,
        boolean blocking,
        Properties props
        ) {

// 给当前的命名空间注册一个新的缓存
Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class)) /*配置缓存实例,默认PerpetualCache.class*/
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))/*配置缓存回收策略,默认LruCache.class*/
        .clearInterval(flushInterval)/*配置缓存清理时间间隔*/
        .size(size)/*配置缓存能够使用的内存大小*/
        .readWrite(readWrite)/*配置缓存的读写能力*/
        .blocking(blocking)/*配置缓存的阻塞性*/
        .properties(props)/*配置缓存对应的自定义配置*/
        .build();

// 添加全局缓存引用
configuration.addCache(cache);
currentCache = cache;
return cache;
}

useNewCache方法的实现比较简单,他借助于CacheBuilder对象将用户的配置转换为Cache对象实例,并将获取到的Cache实例注册到Configuration维护的缓存注册表caches中,并刷新currentCache属性的引用值。

ConfigurationaddCache()方法以Cacheid属性作为Key值,做了一个简单的赋值操作:

/**
    * 缓存对象注册表
    */
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");

public void addCache(Cache cache) {
    caches.put(cache.getId(), cache);
}

CacheBuilder对象

CacheBuilder是一个用于创建Cache对象的建造器,当然从名字上也能看出来这是一个建造者模式的实现。

CacheBuilder对象定义了下面几个属性:

/**
    * 缓存全局唯一的标志
    */
private final String id;
/**
    * 缓存实现类型
    */
private Class<? extends Cache> implementation;
/**
    * 缓存包装器
    */
private final List<Class<? extends Cache>> decorators;
/**
    * 缓存大小
    */
private Integer size;
/**
    * 缓存刷新间隔,单位为MS
    */
private Long clearInterval;
/**
    * 是否为可读可写缓存
    */
private boolean readWrite;
/**
    * 参数
    */
private Properties properties;
/**
    * 是否阻塞
    */
private boolean blocking;

解析cache元素所获得的参数值将会转换为这些属性用于控制被生成Cache对象的行为:

/ 给当前的命名空间注册一个新的缓存
Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class)) /*配置缓存实例,默认PerpetualCache.class*/
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))/*配置缓存回收策略,默认LruCache.class*/
        .clearInterval(flushInterval)/*配置缓存清理时间间隔*/
        .size(size)/*配置缓存能够使用的内存大小*/
        .readWrite(readWrite)/*配置缓存的读写能力*/
        .blocking(blocking)/*配置缓存的阻塞性*/
        .properties(props)/*配置缓存对应的自定义配置*/
        .build();

CacheBuilderbuild()的方法中,将会根据上述的属性值,来完成具体Cache对象的创建工作:

public Cache build() {
    // 配置缓存的默认实现类
    setDefaultImplementations();
    // 通过反射获取指定缓存的实例,并配置其唯一标志
    Cache cache = newBaseCacheInstance(implementation, id);
    // 通过反射配置用户对于缓存的自定义参数
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 添加缓存装饰器
        for (Class<? extends Cache> decorator : decorators) {
            // 缓存装饰器实例
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 为缓存添加标准的包装
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        // 如果缓存不具有日志功能,包装日志功能
        cache = new LoggingCache(cache);
    }
    return cache;
}

setDefaultImplementations()方法用来完成默认缓存实例和默认缓存回收策略的配置工作:

/**
    * 配置缓存的默认实现类
    */
private void setDefaultImplementations() {
    if (implementation == null) {
        implementation = PerpetualCache.class;
        if (decorators.isEmpty()) {
            // 添加默认缓存回收策略
            decorators.add(LruCache.class);
        }
    }
}

流程图:

@startuml
hide footbox
participant CacheBuilder as cb
[-> cb:build()
activate cb
cb -> cb ++ :setDefaultImplementations()
opt 未指定缓存实现类
    note left of cb
        默认使用PerpetualCache
    end note
    opt 未指定缓存回收策略
    note left of cb
        默认使用LruCache
    end note
    end
end
return
@enduml

完成默认缓存实例和默认缓存回收策略的配置工作

可以看到配置默认缓存回收策略的前提是用户未指定缓存实现类也没有指定缓存回收策略实现类。

配置了默认的缓存实现类之后,newBaseCacheInstance()方法负责通过反射来获取缓存实现类的实例,这里对于缓存实现类有一个要求:缓存实现类必须提供一个包含了String类型的单参构造函数。

这个String类型的参数,将会被赋值给Cache实例的id属性,他的取值实际上是当前cache元素所属Mapper文件的命名空间namespace,该参数用来唯一标志一个Cache缓存实例。

private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
    Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
    try {
        return cacheConstructor.newInstance(id);
    } catch (Exception e) {
        throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
    }
}

private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
    try {
        return cacheClass.getConstructor(String.class);
    } catch (Exception e) {
        throw new CacheException("Invalid base cache implementation (" + cacheClass + ").  " +
                "Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: " + e, e);
    }
}

调用链:

@startuml
hide footbox
participant CacheBuilder as cb
participant Class as Clazz
activate Clazz

[->cb++:newBaseCacheInstance();\n获取Cache对象实例
    cb->cb++:getBaseCacheConstructor();\n
        cb->Clazz++:getConstructor(String.class);\n获取Cache对象的`String`单参构造方法
        Clazz->Constructor ** :create
        return
    return
    cb->Constructor++:newInstance(id)
        Constructor->Cache**:create
    return
[<-cb:返回Cache对象

@endtuml

调用链

获取到Cache对象之后,setCacheProperties()方法负责通过反射将用户自定义的有效参数设置到缓存实例中,然后尝试执行Cache对象的初始化方法。

对于是否为有效参数的判断取决于对应的缓存实例中是否拥有用户定义的参数名称的setter方法。

设置有效参数的工作借助于MetaObjectsetValue()方法来完成,实现比较简单和枯燥:

if (properties != null) {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            String name = (String) entry.getKey();
            String value = (String) entry.getValue();
            if (metaCache.hasSetter(name)) {
                Class<?> type = metaCache.getSetterType(name);
                if (String.class == type) {
                    metaCache.setValue(name, value);
                } else if (int.class == type
                        || Integer.class == type) {
                    metaCache.setValue(name, Integer.valueOf(value));
                } else if (long.class == type
                        || Long.class == type) {
                    metaCache.setValue(name, Long.valueOf(value));
                } else if (short.class == type
                        || Short.class == type) {
                    metaCache.setValue(name, Short.valueOf(value));
                } else if (byte.class == type
                        || Byte.class == type) {
                    metaCache.setValue(name, Byte.valueOf(value));
                } else if (float.class == type
                        || Float.class == type) {
                    metaCache.setValue(name, Float.valueOf(value));
                } else if (boolean.class == type
                        || Boolean.class == type) {
                    metaCache.setValue(name, Boolean.valueOf(value));
                } else if (double.class == type
                        || Double.class == type) {
                    metaCache.setValue(name, Double.valueOf(value));
                } else {
                    throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
                }
            }
        }
    }

是否执行Cache对象的初始化方法,取决于Cache对象是否是InitializingObject接口的实例。

InitializingObject接口只定义了一个initialize()方法,该方法将在指定对象所有属性都被初始化后调用:

/**
 * 定义了具有初始化方法的接口
 * @since 3.4.2
 * @author Kazuki Shimizu
 */
public interface InitializingObject {

  /**
   * 初始化实例,该方法将会在所有属性都被设置之后调用。
   * </p>
   * @throws Exception 配置出错或者初始化失败
   */
  void initialize() throws Exception;

}

如果Cache对象实现了InitializingObject接口,在完成自定义属性的设值工作之后,将会调用Cache对象的initialize()方法,完成Cache对象的初始化工作。

整个setCacheProperties()方法的流程:

@startuml
hide footbox
participant CacheBuilder as cb
participant Cache as c

[->cb++:setCacheProperties()
opt 存在用户自定义参数
    cb->SystemMetaObject++:forObject()
    SystemMetaObject->MetaObject**: create
    return
    loop 存在未处理的用户自定义参数
        alt Cache中包含该参数对应的setter方法
            cb->MetaObject++:setValue();
            return
        else 抛出异常
            cb->Exception ** :throws
        end
    end
end
opt Cache对象实现了InitializingObject接口
    cb->c++:initialize()\n完成初始化工作
    return
end
[<-cb:完成

@enduml

setCacheProperties()方法的流程

到这里,我们就获得了一个基本可用的Cache对象。

接下来的工作就是根据用户的配置为Cache对象包装上一层层的装饰对象,这些装饰对象会增强Cache对象的原生方法,提供一些额外的能力,比如:日志记录,缓存清理等。 [图片上传失败...(image-b9ee95-1585211639605)]

Cache对象和包装他的装饰对象使用的是装饰器模式,这里需要注意区分装饰器模式代理模式的区别。

需要注意的是:

  • mybatis只会对PerpetualCache类型的缓存对象包装常用的缓存装饰对象

  • 针对于既不是PerpetualCache实例也不是LoggingCache实例的缓存对象,将会为其包装一层具有日志功能的装饰器。

这就意味着,cache元素的属性定义只会作用于PerpetualCache类型的缓存。

缓存包装器的处理:

if (PerpetualCache.class.equals(cache.getClass())) {
    // 添加缓存装饰器
    for (Class<? extends Cache> decorator : decorators) {
        // 缓存装饰器实例
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
    }
    // 为缓存添加标准的包装
    cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    // 如果缓存不具有日志功能,包装日志功能
    cache = new LoggingCache(cache);
}

在处理PerpetualCache类型的缓存对象时,除了会通过反射操作应用decorators中定义的缓存装饰器之外,还会调用setStandardDecorators()方法根据用户生成CacheBuilder时配置的参数处理缓存对象并为其添加相应的装饰器:

private Cache setStandardDecorators(Cache cache) {
    try {
        // 获取使用配置使用缓存的元数据
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            // 配置缓存允许使用的大小
            metaCache.setValue("size", size);
        }
        if (clearInterval != null) {
            // 配置清理缓存的时间间隔
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {
            // 配置为具有读写能力的缓存
            cache = new SerializedCache(cache);
        }
        // 包装日志功能
        cache = new LoggingCache(cache);
        // 包装同步锁功能
        cache = new SynchronizedCache(cache);
        if (blocking) {
            // 为缓存方法提供阻塞性
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}
  • 如果用户传入了size参数,同时缓存中拥有sizeSetter方法,那么将会设置缓存对象的size属性的值为用户传入的值。

  • 如果用户传入了clearInterval的参数,那么将会为缓存包装一层具有定时清理缓存功能的装饰器——ScheduledCache,清理缓存的时间间隔为clearInterval的值。

  • 如果用户配置了readWrite参数,那么将会为缓存包装一层具有序列化功能的装饰——SerializedCache

  • 直接为缓存包装一层提供日志功能的装饰——LoggingCache

  • 直接为缓存包装一层提供同步锁的功能——SynchronizedCache

  • 如果用户配置了blocking且值为true,那么将会为缓存的方法提供阻塞性——BlockingCache

在完成上诉包装流程之后,返回装饰后的PerpetualCache缓存实例。

@startuml
hide footbox
participant CacheBuilder as cb
[->cb++: 处理PerpetualCache类型的缓存对象
loop decorators中有未处理的装饰器定义
    cb->cb++:newCacheDecoratorInstance() \n使用装饰器包装Cache对象
    return
    cb->cb++:setCacheProperties()\n 处理装饰器的
        cb->cache++:通过反射将用户配置的自定义参数赋值到Cache对象中
        return
    return
end
cb->cb++:setStandardDecorators()\n根据用户配置添加额外的装饰器
opt 用户配置了size参数,且当前缓存拥有size属性
    cb->Cache++:配置缓存的大小
    return
end
opt 用户配置了clearInterval参数
    create participant ScheduledCache as sc
    cb -> sc ++ : 提供定时清除缓存的能力
        note left of sc
        ScheduledCache:
        具有清缓存的定时器功能的缓存装饰器
        end note
    return Cache
end
opt 用户配置了readWrite属性
    create participant SerializedCache as sec
    cb-> sec ++ : 提供序列化缓存对象的能力
        note left of sec
            SerializedCache
            具有序列化缓存对象功能的缓存装饰器
        end note
    return Cache
end
create participant LoggingCache as lc
cb->lc ++ : 为缓存提供日志记录功能
    note left of lc
        LoggingCache:
        可以记录和输出缓存命中率
    end note
return

create participant SynchronizedCache as syc
cb->syc ++ : 为缓存添加同步访问功能
    note left of syc
        SynchronizedCache:
        为缓存添加同步功能
    end note
return

opt 用户配置了blocking属性
    create participant BlockingCache as bc
    cb->bc++: 为缓存方法提供阻塞性
        note left of bc
            BlockingCache:
            当在缓存中找不到该元素时,它将锁定缓存key。
            直到其他线程填充对应的数据,而不是访问数据库。
        end note
    return
end
[<-cb:处理完成
@endtuml

装饰PerpetualCache缓存实例

针对于既不是PerpetualCache实例也不是LoggingCache实例的缓存对象的处理比较简单,直接包装日志装饰器即可:

hide footbox
participant CacheBuilder as cb
[->cb++: 处理既不是`PerpetualCache`实例也不是`LoggingCache`实例的缓存对象
create participant LoggingCache as lc
cb->lc ++ : 为缓存提供日志记录功能
    note left of lc
        LoggingCache:
        可以记录和输出缓存命中率
    end note
return
[<-cb:处理完成

包装日志装饰器

到这里,我们就完成了解析cache元素,配置Cache对象的工作。

关注我,一起学习更多知识

关注我