1. 前言

前面近一个月去写自己的mybatis框架了,对mybatis源码分析止步不前,此文继续前面的文章。开始分析mybatis一,二级缓存的实现。

附上自己的项目github地址:https://github.com/xbcrh/simple-ibatis

对mybatis感兴趣的同学可关注下,全手写的一个orm框架,实现了sql的基本功能和对象关系映射。

废话不说,开始解析mybatis缓存源码实现。

2. mybatis中缓存的实现方式

见mybatis源码包 org.apache.ibatis.cache

2.1 mybatis缓存实现接口类:cache

public interface Cache {

// 获取缓存的ID

String getId();

// 放入缓存

void putObject(Object key, Object value);

// 从缓存中获取

Object getObject(Object key);

// 移除缓存

Object removeObject(Object key);

// 清除缓存

void clear();

// 获取缓存大小

int getSize();

// 获取锁

ReadWriteLock getReadWriteLock();

}

mybatis自定义了缓存接口类,提供了基本的缓存增删改查的操作。在此基础上,提供了基础缓存实现类PerpetualCache。源码如下:

2.2 mybatis缓存基本实现类:PerpetualCache

public class PerpetualCache implements Cache {

// 缓存的ID

private String id;

// 使用HashMap充当缓存(老套路,缓存底层实现基本都是map)

private Map cache = new HashMap();

// 唯一构造方法(即缓存必须有ID)

public PerpetualCache(String id) {

this.id = id;

}

// 获取缓存的唯一ID

public StringgetId() {

returnid;

}

// 获取缓存的大小,实际就是hashmap的大小

public intgetSize() {

returncache.size();

}

// 放入缓存,实际就是放入hashmap

public void putObject(Object key, Object value) {

cache.put(key, value);

}

// 从缓存获取,实际就是从hashmap中获取

public Object getObject(Object key) {

returncache.get(key);

}

// 从缓存移除

public Object removeObject(Object key) {

returncache.remove(key);

}

// hashmap清除数据方法

public voidclear() {

cache.clear();

}

// 暂时没有其实现

public ReadWriteLockgetReadWriteLock() {

returnnull;

}

// 缓存是否相同

public boolean equals(Object o) {

if(getId() == null) throw new CacheException("Cache instances require an ID.");

if(this == o)returntrue; // 缓存本身,肯定相同

if(!(o instanceof Cache))returnfalse; // 没有实现cache类,直接返回false

Cache otherCache = (Cache) o; // 强制转换为cache

returngetId().equals(otherCache.getId()); // 直接比较ID是否相等

}

// 获取hashCode

public inthashCode() {

if(getId() == null) throw new CacheException("Cache instances require an ID.");

returngetId().hashCode();

}

}

PerpetualCache 类其实是对HashMap的封装,通过对map的put和get等操作实现缓存的存取等功能。mybatis中除了基本的缓存实现类外还提供了一系列的装饰类(此处是用到装饰者模式),此处拿较为重要的装饰类LruCache进行分析。

2.3 Lru淘汰策略实现分析

Lru是一种缓存淘汰策略,其核心思想是”如果数据最近被访问过,那么将来被访问的几率也更高“,LruCache 是基于LinkedHashMap实现,LinkedHashMap继承自HashMap,来分析下为什么LinkedHashMap可以当做Lru缓存实现。

public class LinkedHashMap

extends HashMap

implements Map

LinkedHashMap继承HashMap类,实际上就是对HashMap的一个封装。

// 内部维护了一个自定义的Entry,集成HashMap中的node类

static class Entry extends HashMap.Node {

// linkedHashmap用来连接节点的字段,根据这两个字段可查找按顺序插入的节点

Entry before, after;

Entry(inthash, K key, V value, Node next) {

super(hash, key, value, next);

}

}

构造方法见如下:

public LinkedHashMap(int initialCapacity,

floatloadFactor,

boolean accessOrder) {

// 调用HashMap的构造方法

super(initialCapacity, loadFactor);

// 访问顺序维护,默认false不开启

this.accessOrder = accessOrder;

}

引入两种图来理解HashMap与LinkedHashMap

 

以上是HashMap的结构,采用拉链法解决冲突。LinkedHashMap在HashMap基础上增加了一个双向链表来表示节点插入顺序。

 

如上,节点上多出的红色和蓝色箭头代表了Entry中的before和after。在put元素时,会自动在尾节点后加上该元素,维持双向链表。了解LinkedHashMap结构后,在看看究竟什么是维护节点的访问顺序。先说结论,当开启accessOrder后,在对元素进行get操作时,会将该元素放在双向链表的队尾节点。源码如下:

public V get(Object key) {

Node e;

// 调用HashMap的getNode方法,获取元素

if((e = getNode(hash(key), key)) == null)

returnnull;

// 默认为false,如果开启维护链表访问顺序,执行如下方法

if(accessOrder)

afterNodeAccess(e);

returne.value;

}

// 方法实现(将e放入尾节点处)

void afterNodeAccess(Node e) { // move node to last

LinkedHashMap.Entry last;

// 当节点不是双向链表的尾节点时

if(accessOrder && (last = tail) != e) {

LinkedHashMap.Entry p =

(LinkedHashMap.Entry)e, b = p.before, a = p.after; // 将待调整的e节点赋值给p

p.after = null;

if(b == null) // 说明e为头节点,将老e的下一节点值为头节点

head = a;

else

b.after = a;// 否则,e的上一节点直接指向e的下一节点

if(a != null)

a.before = b; // e的下一节点的上节点为e的上一节点

else

last = b;

if(last == null)

head = p;

else{

p.before = last;   // last和p互相连接

last.after = p;

}

tail = p;   // 将双向链表的尾节点指向p

++modCount; // 修改次数加以

}

}

代码很简单,如上面的图,我访问了节点值为3的节点,那木经过get操作后,结构变成如下:

 

经过如上分析我们知道,如果限制双向链表的长度,每次删除头节点的值,就变为一个lru的淘汰策略了。举个例子,我想限制双向链表的长度为3,依次put 1 2 3,链表为 1 -> 2 -> 3,访问元素2,链表变为 1 -> 3-> 2,然后put 4 ,发现链表长度超过3了,淘汰1,链表变为3 -> 2 ->4;

那木linkedHashMap是怎样知道自定义的限制策略,看代码,因为LinkedHashMap中没有提供自己的put方法,是直接调用的HashMap的put方法,查看hashMap代码如下:

// hashMap

final V putVal(inthash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node[] tab; Node p; int n, i;

if((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

if((p = tab[i = (n - 1) &hash]) == null)

tab[i] = newNode(hash, key, value, null);

else{

Node e; K k;

if(p.hash ==hash&&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

elseif(p instanceof TreeNode)

e = ((TreeNode)p).putTreeVal(this, tab,hash, key, value);

else{

for(int binCount = 0; ; ++binCount) {

if((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

if(binCount >= TREEIFY_THRESHOLD - 1) // -1for1st

treeifyBin(tab,hash);

break;

}

if(e.hash ==hash&&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

if(e != null) { // existing mappingforkey

V oldValue = e.value;

if(!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

returnoldValue;

}

}

++modCount;

if(++size > threshold)

resize();

// 看这个方法

afterNodeInsertion(evict);

returnnull;

}

// linkedHashMap重写了此方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest

LinkedHashMap.Entry first;

// removeEldestEntry默认返回fasle

if(evict && (first = head) != null && removeEldestEntry(first)) {

K key = first.key;

// 移除双向链表中的头指针元素

removeNode(hash(key), key, null,false,true);

}

}

原来只需要重新实现removeEldestEntry就可以自定义实现lru功能了。了解基本的lru原理后,开始分析LruCache。

2.4 缓存包装类 - LruCache

public class LruCache implements Cache {

// 被装饰的缓存类,即真实的缓存类,提供真正的缓存能力

private final Cache delegate;

// 内部维护的一个linkedHashMap,用来实现LRU功能

private Map keyMap;

// 待淘汰的缓存元素

private Object eldestKey;

// 唯一构造方法

public LruCache(Cache delegate) {

this.delegate = delegate; // 被装饰的缓存类

setSize(1024); // 设置缓存大小

}

....

}

经分析,LruCache还是个装饰类。内部除了维护真正的Cache外,还维护了一个LinkedHashMap,用来实现Lru功能,查看其构造方法。

// 唯一构造方法

public LruCache(Cache delegate) {

this.delegate = delegate; // 被装饰的缓存类

setSize(1024); // 设置缓存大小

}

// setSize()是构造方法中方法

public void setSize(final int size) {

// 初始化keyMap

keyMap = new LinkedHashMap(size, .75F,true) {

private static final long serialVersionUID = 4267176411845948333L;

// 什么时候自动删除缓存元素,此处是根据当缓存数量超过指定的数量,在LinkedHashMap内部删除元素

protected boolean removeEldestEntry(Map.Entry eldest) {

boolean tooBig = size() > size;

if(tooBig) {

// 将待删除元素赋值给eldestKey,后续会根据此值是否为空在真实缓存中删除

eldestKey = eldest.getKey();

}

returntooBig;

}

};

}

和上文分析一样,重写了removeEldestEntry方法。此方法返回一个boolean值,当缓存的大小超过自定义大小,返回true,此时linkedHashMap中会自动删除eldest元素。在真实缓存cache中也将此元素删除。保持真实cache和linkedHashMap元素一致。其实就是用linkedHashMap的lru特性来保证cache也具有此lru特性。

分析put方法和get方法验证此结论.。

@Override

public Object getObject(Object key) {

keyMap.get(key); // 触发linkedHashMap中get方法,将key对应的元素放入队尾

returndelegate.getObject(key); // 调用真实的缓存get方法

}

// 放入缓存时,除了在真实缓存中放一份外,还会在LinkedHashMap中放一份

@Override

public void putObject(Object key, Object value) {

delegate.putObject(key, value);

// 调用LinkedHashMap的方法

cycleKeyList(key);

}

private void cycleKeyList(Object key) {

// linkedHashMap中put,会触发removeEldestEntry方法,如果缓存大小超过指定大小,则将双向链表对头值赋值给eldestKey

keyMap.put(key, key);

// 检查eldestKey是否为空。不为空,则代表此元素是淘汰的元素了,需要在真实缓存中删除。

if(eldestKey != null) {

// 真实缓存中删除

delegate.removeObject(eldestKey);

eldestKey = null;

}

}

介绍完Cache基本实现后,开始分析mybatis中一级缓存

3. mybatis一级缓存使用源码分析

此处是仅介绍mybatis的实现,没有涉及到与Spring整合,先介绍mybatis最基本的sql执行语法。默认大家掌握了SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession用法。后面我会写一篇博客分析SQL在mybatis中执行的过程,会介绍到这些基础知识。

InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 构建字节流

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();   // 构建SqlSessionFactoryBuilder

SqlSessionFactory factory = builder.build(inputStream);  // 构建SqlSessionFactory

SqlSession sqlSession = factory.openSession(); // 生成SqlSession

List userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 执行SysUserMapper类的getSysUser方法

前文构建SqlSession的内容大家感兴趣可自行查看,此处仅分析执行过程。查看selectList方法,mybatis中sqlSession的默认实现为DefaultSqlSession

public  List selectList(String statement, Object parameter, RowBounds rowBounds) {

try {

// 每个mapper文件会解析生成一个MappedStatement

MappedStatement ms = configuration.getMappedStatement(statement);

// 调用真实的查询方法,此处是调用executor的方法。executor采用了装饰者模式,若该mapper文件未启用二级缓存,则默认为BaseExecutor。

// 若该mapper文件启用了二级缓存,则使用的是CachingExecutor

List result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

returnresult;

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error querying database.  Cause: "+ e, e);

} finally {

ErrorContext.instance().reset();

}

}

因为此处使用的是装饰者模式,BaseExecutor是最基础的执行器,使用了一级缓存,CachingExecutor是对BaseExecutor进行一次封装,若打开二级缓存开关,在使用一级缓存前,先使用二级缓存。后文介绍二级缓存会分析这两个Executor生成地方。先分析BaseExecutor的一级缓存实现。

// BaseExecutor.java

/**

* 查询,并创建好CacheKey对象

* @param ms Mapper.xml文件的select,delete,update,insert这些DML标签的封装类

* @param parameter 参数对象

* @param rowBounds Mybatis的分页对象

* @param resultHandler 结果处理器对象

*/

public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

BoundSql boundSql = ms.getBoundSql(parameter); // 获取boundSql对象

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  // 生成缓存KEY

returnquery(ms, parameter, rowBounds, resultHandler, key, boundSql); // 执行如下方法

}

@SuppressWarnings("unchecked")

public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

if(closed) throw new ExecutorException("Executor was closed.");

//如果将flushCacheRequired为true,则会在执行器执行之前就清空本地一级缓存

if(queryStack == 0 && ms.isFlushCacheRequired()) {

clearLocalCache();

}

List list;

try {

queryStack++; // 请求堆栈加一

// 如果此次查询的resultHandler为null(默认为null),则尝试从本地缓存中获取已经缓存的的结果

list = resultHandler == null ? (List) localCache.getObject(key) : null;

if(list != null) {

//如果查到localCache缓存,处理localOutputParameterCache,即对存储过程的sql进行特殊处理

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

}else{

// 从数据库中查询,并将结果放入到localCache

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

// 请求堆栈减一

queryStack--;

}

if(queryStack == 0) {

// 加载延迟加载List

for(DeferredLoad deferredLoad : deferredLoads) {

deferredLoad.load();

}

deferredLoads.clear(); // issue#601

if(configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

clearLocalCache(); // issue#482

}

}

returnlist;

}

private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

List list;

localCache.putObject(key, EXECUTION_PLACEHOLDER); // 先放置一个占位符

try {

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);  // 从数据库中查找

} finally {

localCache.removeObject(key); // 移除占位符

}

localCache.putObject(key, list); // 放入缓存

if(ms.getStatementType() == StatementType.CALLABLE) {

localOutputParameterCache.putObject(key, parameter);  // 若是存储过程,则放入存储过程缓存中

}

returnlist; // 返回查询结果

}

mybatis一级缓存很好理解,对于同一个SqlSession对象(即同一个Executor),执行同一条语句时,BaseExecutor会先从自己的缓存中查找,是否存在此条语句的结果,若能找到,则直接返回(暂且忽略存储过程处理)。若没有找到,则查询数据库,将结果放入此缓存,供下次使用。mybatis默认打开一级缓存。

4. mybatis二级缓存使用源码分析

4.1 配置方式

在全局配置文件中mybatis-config.xml中加入如下设置

在具体mapper.xml中配置<cache/>标签或者<cache-ref/>标签

<cache></cache>或者<cache-ref/>

或者采用注解配置方式,在mapper.java文件上配置注解

@CacheNamespace 或者 @CacheNamespaceRef

4.1 mybatis解析二级缓存标签

还是采用上面sqlSession方式代码来debug

InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 构建字节流

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();   // 构建SqlSessionFactoryBuilder

SqlSessionFactory factory = builder.build(inputStream);  // 构建SqlSessionFactory

进入查看builder.build()方法

// SqlSessionFactoryBuilder.java

/**根据流构建SqlSessionFactory*/

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {

/**构建XML文件解析器*/

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

/**开始解析mybatis-config.xml文件并构建全局变量Configuration*/

returnbuild(parser.parse());

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error building SqlSession.", e);

} finally {

ErrorContext.instance().reset();

try {

inputStream.close();

} catch (IOException e) {

// Intentionally ignore. Prefer previous error.

}

}

}

进入parser.parse()方法,,进一步分析

public Configurationparse() {

if(parsed) {

throw new BuilderException("Each XMLConfigBuilder can only be used once.");

}

parsed =true;

parseConfiguration(parser.evalNode("/configuration"));

returnconfiguration;

}

private void parseConfiguration(XNode root) {

try {

propertiesElement(root.evalNode("properties")); //issue#117 read properties first // 读取properties配置

typeAliasesElement(root.evalNode("typeAliases")); // 读取别名设置

pluginElement(root.evalNode("plugins")); // 读取插件设置

objectFactoryElement(root.evalNode("objectFactory")); // 读取对象工厂设置

objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 读取对象包装工厂设置

settingsElement(root.evalNode("settings")); // 读取setting设置

environmentsElement(root.evalNode("environments")); //readit after objectFactory and objectWrapperFactory issue#631 // 读取环境设置

databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 读取数据库ID提供信息

typeHandlerElement(root.evalNode("typeHandlers"));  // 读取类型转换处理器

mapperElement(root.evalNode("mappers"));  // 解析mapper文件

} catch (Exception e) {

throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e);

}

}

此处仅分析<cache/> 和 <cache-ref/>标签的解析,<cache/> 和 <cache-ref/>存在具体的mapper.xml文件中,分析mapperElement()方法。因为在mybatis-config.xml文件中关于<mapper>标签的值可配置package,resource,url,class等配置。如

分析mapperElement()方法

/**

* 映射文件支持四种配置,package,resource,url,class四种

* 如在mybatis-config.xml中配置

*

* */

private void mapperElement(XNode parent) throws Exception {

if(parent != null) {

for(XNode child : parent.getChildren()) {

if("package".equals(child.getName())) { // 若配置的是package,在讲package下的所有mapper文件进行解析

String mapperPackage = child.getStringAttribute("name");

configuration.addMappers(mapperPackage);

}else{

String resource = child.getStringAttribute("resource");

String url = child.getStringAttribute("url");

String mapperClass = child.getStringAttribute("class");

if(resource != null && url == null && mapperClass == null) {  // 若配置的是resource,在解析resource对应的mapper.xml

ErrorContext.instance().resource(resource);

InputStream inputStream = Resources.getResourceAsStream(resource); // 获取xml文件字节流

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 构建xml文件构造器

mapperParser.parse(); // 解析xml文件

}elseif(resource == null && url != null && mapperClass == null) { // 若配置的是url,在解析url对应的mapper.xml

ErrorContext.instance().resource(url);

InputStream inputStream = Resources.getUrlAsStream(url);

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());

mapperParser.parse();

}elseif(resource == null && url == null && mapperClass != null) { // 若配置的是class,在解析class对应的mapper文件

Class mapperInterface = Resources.classForName(mapperClass);

configuration.addMapper(mapperInterface); // 分析addMapper()方法

}else{

throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");

}

}

}

}

}

因为我采用的是class配置,所以分析configuration.addMapper()方法

// Configuration.java

public  void addMapper(Classtype) {

mapperRegistry.addMapper(type);

}

继续进入mapperRegistry.addMapper进行分析

// MapperRegistry.java

public  void addMapper(Classtype) {

if(type.isInterface()) { // mapper接口

if(hasMapper(type)) { // 若mapper已被注册

throw new BindingException("Type "+type+" is already known to the MapperRegistry.");

}

boolean loadCompleted =false;

try {

knownMappers.put(type, new MapperProxyFactory(type));  // 注册映射接口

// It's important that the type is added before the parser is run

// otherwise the binding may automatically be attempted by the

// mapper parser. If the type is already known, it won'

t try.

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config,type); // 生成注解构造器

parser.parse(); // 解析mapper上的注解

loadCompleted =true;

} finally {

if(!loadCompleted) {

knownMappers.remove(type);

}

}

}

}

knownMappers.put(type, new MapperProxyFactory<T>(type));这里很重要,是注册mapper文件代理对象。此处只做缓存的解释,不做注册详解,后面在分析sql执行流程时单独去分析。

parser.parse()是对mapper文件进行解析的关键,继续分析

// MapperAnnotationBuilder.java

// 解析配置文件

public voidparse() {

String resource = type.toString(); // 接口的全限定名 class com.test.userMapper

if(!configuration.isResourceLoaded(resource)) {  // 是否加载过

loadXmlResource(); // 在默认路径下(默认和mapper接口同个包下),加载xml文件

configuration.addLoadedResource(resource); // 设为该mapper配置文件已解析

assistant.setCurrentNamespace(type.getName()); // 设置构建助力器当前命名空间 com.test.userMapper

parseCache(); // 解析CacheNamespace注解,构建一个Cache对象,并保存到Mybatis全局配置信息中

parseCacheRef(); //解析CacheNamespace注解,引用CacheRef对应的Cache对象。

// 由此可知,当引入了和后,该命名空间的缓存对象变为了CacheRef引用的缓存对象

Method[] methods = type.getMethods(); // 获取方法

for(Method method : methods) {

try {

if(!method.isBridge()) { // issue#237 若该方法不是桥接方法

parseStatement(method); //构建MapperStatement对象,并添加到Mybatis全局配置信息中

}

} catch (IncompleteElementException e) {

//当出现未完成元素时,添加构建Method时抛出异常的MethodResolver实例,到下个Mapper的解析时再次尝试解析

configuration.addIncompleteMethod(new MethodResolver(this, method));

}

}

}

parsePendingMethods(); // 解析未完成解析的Method

}

通过上面的代码注释,可知,当解析mapper.java文件前,会先在同个文件夹下查看是否存在mapper.xml文件,若存在,则先解析mapper.xml文件。在解析mapper.xml文件时,若在mapper.xml中写了缓存<cache/>或<cache-ref>,也会生成二级缓存。若同时还在mapper.java文件里写了@CacheNamespace注解。则会进行报错,因为出现了两个缓存。此时我们根据注解配置去分析。去分析parseCache()和parseCacheRef(),看配置了注解@CacheNamespace和CacheNamespaceRef之后缓存具体怎样生成。

// MapperAnnotationBuilder.java

private voidparseCache() {

// 获取是否有@CacheNamespace 注解

CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);

if(cacheDomain != null) {

/*

* 构建一个缓存对象,具体分析

* */

assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), cacheDomain.flushInterval(), cacheDomain.size(), cacheDomain.readWrite(), null);

}

}

// mapperBuilderAssistant.java

public Cache useNewCache(Class typeClass, // 基本缓存类

Class evictionClass,  // 缓存装饰类

Long flushInterval, // 缓存刷新间隔

Integer size, // 缓存大小

boolean readWrite, // 缓存可读写

Properties props) {

typeClass = valueOrDefault(typeClass, PerpetualCache.class); // 没有设置则采用默认的PerpetualCache

evictionClass = valueOrDefault(evictionClass, LruCache.class); // 没有设置则采用默认的LruCache

Cache cache = new CacheBuilder(currentNamespace) // 命名空间作为缓存唯一ID

.implementation(typeClass)

.addDecorator(evictionClass)

.clearInterval(flushInterval)

.size(size)

.readWrite(readWrite)

.properties(props)

.build();

configuration.addCache(cache); // 加入到全局缓存

currentCache = cache; // 当前缓存设为cache,由此可知,缓存是mapper级别

returncache;

}

此处是生成了二级缓存的地方,并设置当前mapper文件的缓存为这个生成的二级缓存。若没有配置@CacheNamespaceRef,那木此mapper文件就使用了这个自己生成的二级缓存。那@CacheNamespaceRef是用来干嘛的?回到上面代码处进行分析。

// MapperAnnotationBuilder.java

private voidparseCacheRef() {

// @CacheNamespaceRef 相当于标签

CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);

if(cacheDomainRef != null) {

assistant.useCacheRef(cacheDomainRef.value().getName()); // 构建缓存引用,进入分析

}

}

public Cache useCacheRef(String namespace) {

if(namespace == null) {

throw new BuilderException("cache-ref element requires a namespace attribute.");

}

try {

unresolvedCacheRef =true;

Cache cache = configuration.getCache(namespace); // 获取被引用的缓存

if(cache == null) { //被引用的缓存是否存在

throw new IncompleteElementException("No cache for namespace '"+ namespace +"' could be found.");

}

currentCache = cache; // 设置当前缓存对象为被引用的缓存对象

unresolvedCacheRef =false; // 标志设置为false,代表有缓存引用。

returncache;

} catch (IllegalArgumentException e) {

throw new IncompleteElementException("No cache for namespace '"+ namespace +"' could be found.", e);

}

}

由上文可知,当配置了@CacheNamespaceRef和@CacheNamespace后,该mapper文件对应的缓存以@CacheNamespaceRef引用的缓存为准。这样可是使得不同的mapper文件有相同的缓存。

4.2 缓存具体使用场景

上文说了,开启二级缓存后,sqlSession中的Executor是CachingExecutor,查看生成CachingExecutor具体位置。继续从那段测试代码分析

SqlSession sqlSession = factory.openSession(); // 生成SqlSession

List userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 执行SysUserMapper类的getSysUser方法

debug进入DefaultSqlSessionfactory.openSession()方法

// DefaultSqlSessionfactory.java

public SqlSessionopenSession() {

returnopenSessionFromDataSource(configuration.getDefaultExecutorType(), null,false);

}

...

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null;

try {

final Environment environment = configuration.getEnvironment(); // 获取当前配置设置的环境,有事务工厂,数据源

final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 创建事务工厂

tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 事务类

final Executor executor = configuration.newExecutor(tx, execType); // 生成执行器

returnnew DefaultSqlSession(configuration, executor, autoCommit);

} catch (Exception e) {

closeTransaction(tx); // may have fetched a connection so lets call close()

throw ExceptionFactory.wrapException("Error opening session.  Cause: "+ e, e);

} finally {

ErrorContext.instance().reset();

}

}

....

分析Executor executor = configuration.newExecutor(tx, execType);此段代码

// Configuration.java

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

executorType = executorType == null ? defaultExecutorType : executorType; // 默认为SimpleExecutor

executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

Executor executor;

.......

if(cacheEnabled) {   // 若开启二级缓存,则生成CachingExecutor

executor = new CachingExecutor(executor);

}

.......

}

当执行查询语句时,会执行Executor的query()方法。分析CachingExecutor中query()方法究竟是怎样使用二级缓存。

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

throws SQLException {

// mapper.xml设置了或者mapper.java使用了二级缓存注解

Cache cache = ms.getCache();

if(cache != null) {

// 若该mapper文件中执行的上一条语句是更新语句(增删改),则会清空该mapper文件对应的二级缓存

flushCacheIfRequired(ms);

if(ms.isUseCache() && resultHandler == null) {

ensureNoOutParams(ms, parameterObject, boundSql);

@SuppressWarnings("unchecked")

List list = (List) tcm.getObject(cache, key); // 从二级缓存中获取

if(list == null) { // 若二级缓存中不存在

list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 调用后续的Executor执行语句,后续的Executor会继续使用一级缓存。

tcm.putObject(cache, key, list); // issue#578. Query must be not synchronized to prevent deadlocks  // 放入二级缓存中

}

returnlist;

}

}

returndelegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 若没开启二级缓存,则调用后续的Executor执行语句。后续的Executor会继续使用一级缓存。

}

// 此处的update包括增删改

public int update(MappedStatement ms, Object parameterObject) throws SQLException {

// 清空二级缓存

flushCacheIfRequired(ms);

returndelegate.update(ms, parameterObject);

}

通过上面分析可知,二级缓存的实现是mapper级别的。只要对这个mapper文件使用@CacheNamespace注解或对应的xml使用等标签,那木该mapper在生成时就会注册一个mapper级别的缓存。在后续

对这一mapper文件任何查询语句进程操作的时候,都会使用到这个二级缓存。二级缓存就相当于在一级缓存上在加入一个缓存。二级缓存Cache的实现是在LruCache上在封装了一层TransactionCache,为了防止脏数据的产生。感兴趣的可以自行去查看。以上便是关于mybatis缓存的内容。

5. 总结验证

我们知道,二级缓存是mapper级别的,在mybatis初始化时便生成了。当此mapper文件中有更新语句时,才会刷新二级缓存。举个例子,有MapperA.java和MapperB.java两个文件,并都开启了二级缓存,cacheA和cacheB。MapperA.java中有一条查询语句select1,此查询语句关联了B的表。在第一次执行MapperA.java中select1时,会从库中取出数据,并放入在cacheA中。当mapperB.java中如果有一条更新语句update2,执行update2,会刷新二级缓存cacheB。但不会刷新cacheA,因为update2并不在MapperA.java中。那此时cacheA中存在的数据便是脏数据了。

其实也有解决办法,即在MapperA.java中使用@CacheNamespaceRef = "mapperB.java".让两个文件公用同一个二级缓存。这样就OK啦

 
 

 

Mall电商实战项目发布重大更新,全面支持SpringBoot 2.3.0的更多相关文章

  1. 01-Flutter移动电商实战-项目学习记录

    一直想系统性的学习一下 Flutter,正好看到该课程<Flutter移动电商实战>的百度云资源,共 69 课时,由于怕自己坚持不下去(经常学着学着就不学了),故采用博客监督以记之. 1. ...

  2. 微服务电商项目发布重大更新,打造Spring Cloud最佳实践!

    Spring Cloud实战电商项目mall-swarm地址:转发+关注 私信我获取地址 系统架构图   系统架构图 项目组织结构 mall├── mall-common-- 工具类及通用代码模块├─ ...

  3. Java架构师系统培训高并发分布式电商实战activemq,netty,nginx,redis dubbo shiro jvm虚拟机视频教程下载

    15套java架构师.集群.高可用.高可扩 展.高性能.高并发.性能优化.Spring boot.Redis.ActiveMQ.Nginx.Mycat.Netty.Jvm大型分布 式项目实战视频教程 ...

  4. 微信小程序电商实战-首页(上)

    嗨,大家好!经过近两周的精心准备终于开始微信小程序电商实战之路喽.那么最终会做成什么样呢?当然可以肯定不会只做一个静态demo哦,先把我们小程序电商实战的整体架构发出来晒一下,请看下图:   架构图. ...

  5. Mall电商项目总结(一)——项目概述

    项目概述 此电商项目为本人学习项目,后端 使用nginx实现负载均衡转发请求到多台tomcat服务器,使用多台 redis服务器分布式 缓存用户登录信息. 项目已经部署到阿里云服务器,从阿里云linu ...

  6. L05 Laravel 教程 - 电商实战

    https://laravel-china.org/courses/laravel-shop https://laravel-china.org/topics/13206/laravel-shop-c ...

  7. laravel 5.5 《电商实战 》基础布局

    我们需要为我们的项目构建一个基础的页面布局,布局文件统一存放在 resources/views/layouts 文件夹中,布局涉及的文件如下: app.blade.php —— 主要布局文件,项目的所 ...

  8. Flutter移动电商实战 --(1)项目学习记录

    1.项目相关截图 2.项目知识点梳理图 Dio2.0: Dio是一个强大的 Dart Http 请求库,支持 Restful API.FormData.拦截器.请求取消等操作. Swiper: Swi ...

  9. 02-Flutter移动电商实战-建立项目和编写入口文件

    环境搭建请参考之前写的一篇文章:Flutter_初体验_创建第一个应用 1.创建项目 采用AndroidStudio构建本项目,FIle>New>New Flutter Project… ...

随机推荐

  1. Learning in Spiking Neural Networks by Reinforcement of Stochastic Synaptic Transmission

    郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! Summary 众所周知,化学突触传递是不可靠的过程,但是这种不可靠的函数仍然不清楚.在这里,我考虑这样一个假设,即大脑利用突触传递的随机 ...

  2. 从一个小需求感受Redis的独特魅力

    分享一个简单的小需求应该怎么设计实现以及有关Redis的使用 Redis在实际应用中使用的非常广泛,本篇文章就从一个简单的需求说起,为你讲述一个需求是如何从头到尾开始做的,又是如何一步步完善的. 需求 ...

  3. C++奇淫技巧

    一.关于:的妙用 如下代码 #include<cstdio> #include<iostream> typedef struct point{ int a; int b; po ...

  4. DevOps系列(2)-SonarQube之PostgreSQL

    上一篇从整体上讲了下DevOps大概涉及到了哪些内容,以及主要工作流程,接下来我们将分步详细介绍各个部分的使用. 概览 首先从 SonarQube 开始说起,SonarQube 是一个可用于检测重复代 ...

  5. ORB-SLAM: A Versatile and Accurate Monocular SLAM System 笔记(二)

    4. 自动地图初始化 地图初始化的目标是两个帧之间相对位姿来三角化一系列的点云(riangulate an initial set of map points),这个操作是独立与场景且不需要人为的干预 ...

  6. HP Probook 4230s 更换 CPU 过程

    HP Probook 4230s 更换 CPU 过程 原来使用的 CPU 是 i5-2540M 更换为 i7-2760QM    1.松开 硬盘线 硬盘线插头上有一片黑色的胶片,可以用来拔起插头 硬盘 ...

  7. TypeError 之 Cannot convert undefined or null to object

    分享一个今天遇到的一个bug , 希望对你也有用. 1.Object.keys()中传错了参数 2.由于undefined和null无法转成对象,所以如果它们做为Object.assign()的参数( ...

  8. Unity资源引用问题

    前几天做项目时,遇到一个奇怪的问题: 从一处复制了一个预制体,预制体上面还附有一个材质球,材质球上关联着另一张贴图. 将所有关联的东西,均Copy两份,然后关联成一个新的预制体,最后用项目内的读取内存 ...

  9. C# 发送邮件通知

    web.config <!-- 配置发邮件:邮件访问登录地址--> <add key="HttpsUrl" value="http://localhos ...

  10. 图像通道、RGB与色彩体系

    通道(Channels) 图像的通道指的是什么?是不是灰度图的通道数为1,彩色图的通道为3 ? 图像通道,在RGB色彩模式下就是指那单独的红色.绿色.蓝色部分.也就是说,一幅完整的图像,是由红色绿色蓝 ...