MyBatis源码分析--一级缓存、二级缓存原理

前言:

有点项目经验的朋友都知道缓存的重要性是不言而喻的,不仅仅我们在开发项目业务功能的时候使用了各种缓存,框架在设计的时候也有框架层面的缓存,尤其在查询多的场景下,缓存可以大大的减少数据库访问,提升系统效率,Mybatis 也提供了缓存,分别为一级缓存和二级缓存,默认的情况下,Mybatis 只开启一级缓存。

Mybatis 相关知识传送门

初识 MyBatis 【MyBatis 核心概念】

MyBatis 源码分析–SqlSessionFactory

MyBatis 源码分析–获取SqlSession

MyBatis 源码分析-- getMapper(获取Mapper)

MyBatis 源码分析-- SQL请求执行流程( Mapper 接口方法的执行的过程)

MyBatis 源码分析-- 插件(拦截器)原理

MyBatis 插件(拦截器)实战(自定义实现拦截器)

一级缓存

什么是一级缓存?

一级缓存是 SqlSession 级别的缓存,在操作数据库时需要构造 SqlSession 对象,在对象中有一个 Map(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存是互相不影响的,即同一个 SqlSession 对象, 在参数和 SQL 完全一样的情况下,多次查询只执行一次 SQL 语句,因为第一次查询后,MyBatis 会将其放在缓存中,后面再次查询的时候,如果没有声明需要刷新,且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,不会再次发送 SQL 到数据库进行查询,这就是一级缓存。

一级缓存编码验证:

代码示例:

@Transactional(rollbackFor = Exception.class)
@Override
public void queryById(Long id) {
	selectById(id);
	log.info("第一次查询");
	selectById(id);
	log.info("第二次查询");
}

演示结果:

在这里插入图片描述
通过输出日志可以知道,两次查询只执行了一次查询数据库的操作,表名一级缓存生效了(Mybatis 默认开启一级缓存)。

注意:如果想要使用 Mybatis 的一级缓存,需要保证是在同一个事务中,只有在同一个事务中,才能获取到同一个 SqlSession。

一级缓存失效的场景:

  • 使用不同的 SqlSession。
  • 两次查询的查询条件不一致。
  • 两次查询之间有增删改操作。
  • 两次查询之间手动清除了一级缓存。

一级缓存源码分析

一级缓存的原理其实在前文分支 SQL 的执行过程的时候已经分析过了,这里我们在回忆一下,在 BaseExecutor#query 方法中会判断一级缓存中是否有数据,有就返回,没有才会去查询数据库。

//org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> 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 (this.closed) {
		throw new ExecutorException("Executor was closed.");
	} else {
		//queryStack 查询堆栈 防止递归查询重复处理缓存  是否刷新缓存 flushCacheRequired 是 true 清除一级缓存
		if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
			//清除一级缓存
			this.clearLocalCache();
		}

		List list;
		try {
			//查询堆栈 +1
			++this.queryStack;
			//从一级缓存中获取数据
			list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
			if (list != null) {
				//从一级缓存中获取数据
				this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
			} else {
				//从数据库查询数据
				list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
			}
		} finally {
			//查询堆栈-1
			--this.queryStack;
		}

		if (this.queryStack == 0) {
			Iterator i$ = this.deferredLoads.iterator();

			while(i$.hasNext()) {
				BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
				deferredLoad.load();
			}

			this.deferredLoads.clear();
			if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
				this.clearLocalCache();
			}
		}

		return list;
	}
}

一级缓存的存入时机就是每次查询数据库的时候,只要查询了数据库,结果都会缓存到一级缓存中,这也就是一级缓存默认打开的原因。

//org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	//一级缓存 存储一个占位符
	this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

	List list;
	try {
		//执行数据库查询
		list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
	} finally {
		//清除占位符缓存
		this.localCache.removeObject(key);
	}
	//存入一级缓存
	this.localCache.putObject(key, list);
	if (ms.getStatementType() == StatementType.CALLABLE) {
		this.localOutputParameterCache.putObject(key, parameter);
	}

	return list;
}

我们知道 Mybatis 的 INSERT、UPDATE、DELETE 最终执行的都是 update 方法,在 update 方法中我们看到了清除一级缓存的代码,这就是为什么两次查询之间有 INSERT、UPDATE、DELETE 操作时候,就无法使用一级缓存的原因。

//org.apache.ibatis.executor.BaseExecutor#update
public int update(MappedStatement ms, Object parameter) throws SQLException {
	ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
	if (this.closed) {
		throw new ExecutorException("Executor was closed.");
	} else {
		//清除一级缓存
		this.clearLocalCache();
		return this.doUpdate(ms, parameter);
	}
}

一级缓存源码结构

一级缓存的源码结构很简单,就是一个简单的 Map 结构,提供了一些常用的 put、get、remove、clear 方法,本质就是内存中的一个 Map。

public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public boolean equals(Object o) {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if (this == o) {
            return true;
        } else if (!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}

缓存 key 的生成方法 createCacheKey 源码分析

从源码中我们可以知道 cacheKey 是根据 MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter值 + Environment id 来确定唯一性的。

//org.apache.ibatis.executor.BaseExecutor#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
	//判断执行器是否关闭
	if (this.closed) {
		throw new ExecutorException("Executor was closed.");
	} else {
		//创建 缓存key 对象
		CacheKey cacheKey = new CacheKey();
		//根据接口的全限定类名+方法名更新 cacheKey
		cacheKey.update(ms.getId());
		//根据查询数据的偏移量更新 cacheKey
		cacheKey.update(rowBounds.getOffset());
		//根据查询数据的条数更新 cacheKey
		cacheKey.update(rowBounds.getLimit());
		//根据sql 语句更新 cacheKey
		cacheKey.update(boundSql.getSql());
		//其实是从查询 sql 占位符中解析出的参数元数据信息 然后进行遍历 获取每个参数名 并从查询接口传入的参数中获取相应的参数值
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		//获取类型处理器注册表
		TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
		//循环遍历入参 
		for(int i = 0; i < parameterMappings.size(); ++i) {
			//获取参数映射
			ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
			//参数类型判断判断
			if (parameterMapping.getMode() != ParameterMode.OUT) {
				//获取参数属性名称
				String propertyName = parameterMapping.getProperty();
				//参数值
				Object value;
				//是否有附加参数
				if (boundSql.hasAdditionalParameter(propertyName)) {
					//根据参数名称获取参数值
					value = boundSql.getAdditionalParameter(propertyName);
				} else if (parameterObject == null) {
					//参数为空
					value = null;
				} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
					//参数类型注册表中有参数类型 
					value = parameterObject;
				} else {
					//MetaObject提供了一些便捷的方法 可以根据 parameterObject 类型 获取相应 propertyName 值的方法 不需要直接使用反射操作
					MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
					//使用  metaObject 获取参数值
					value = metaObject.getValue(propertyName);
				}
				
				//根据环境id更新 cacheKey
				if (this.configuration.getEnvironment() != null) {
					cacheKey.update(this.configuration.getEnvironment().getId());
				}
				//根据环境 value 更新 cacheKey
				cacheKey.update(value);
			}
		}

		return cacheKey;
	}
}

CacheKey#update 方法源码分析

CacheKey#update 会把生产的唯一 CacheKey 进行 HashCode,并更新到 updateList 中,判断两个 CacheKey 对象相等的充分必要条件是两个对象代表的组合序列中的每个元素必须都相等,为了避免每次比较都要进行一次循环(遍历组合List),CacheKey 采用 hashCode–>checksum–>count–>updateList 的顺序比较,只要有一个不相等,则视为两个 CacheKey 对象不相等。

//org.apache.ibatis.cache.CacheKey#update
public void update(Object object) {
	//如果 object 为空 hashcode 就为1 不为 null  就获取 hashcode
	int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
	//count ++
	++this.count;
	//checksum 等于 checksum +object的 hashcode
	this.checksum += (long)baseHashCode;
	//baseHashCode 等于 baseHashCode 乘以 count
	baseHashCode *= this.count;
	//重新计算 hashcode
	this.hashcode = this.multiplier * this.hashcode + baseHashCode;
	//更新到列表中
	this.updateList.add(object);
}

二级缓存

什么是二级缓存?

二级缓存是存户在 SqlSessionFactory 中,二级缓存和名称空间绑定,也就是说通常所说的二级缓存是 Mapper 级别的缓存,即多个 SqlSession 共享同一个 Mapper 命名空间下的缓存,它的作用是缓存 Mapper 执行的结果,避免频繁地访问数据库,提高系统的性能。

配置二级缓存

Mybatis 二级缓存默认是关闭的,二级缓存有全局开关、局部开关和在某个查询 SQL 上配置使用缓存,可以根据自己的需求灵活配置。

全局开启二级缓存:

在 mybatis-config.xml 中如下配置。

<settings>
	<setting name="cacheEnabled" value="true"/>
</settings>

局部开启二级缓存:

在 Mapper.xml 文件中开启二级缓存。

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

标签解释:

  • eviction:清除缓存的策略,FIFO 表示先进先出的策略,默认是 LRU 最近最少使用。
  • flushInterval:缓存刷新时间间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值。
  • size:缓存存放多少个元素。
  • readOnly:是否只读,true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
  • type:指定自定义缓存的全类名,自己实现Cache 接口即可。
  • blocking:缓存中找不到对应的key,是否会一直 blocking,直到有对应的数据进入缓存。

eviction 缓存回收策略

  • FIFO :先进先出,按照缓存进入的顺序来移除。
  • LRU:最近最少使用,移除最长时间不被使用的对象。
  • SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK:弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象。

在某个查询 SQL 上配置使用缓存,如下:

<select id="selectUser" resultType="com.my.study.User" useCache="true" flushCache="true">
    select * from user where id = #{id}
</select>

二级缓存的创建

二级缓存的创建,其实就是解析 标签,这个解析动作是在构建 SqlSessionFactory 的过程中做的,前文在分析 SqlSessionFactory 的创建过程中有详细讲解,这里只分析 解析 标签的源码,如下:

SqlSession 创建传送门

//org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
	try {
		//获取 namespace
		String namespace = context.getStringAttribute("namespace");
		if (namespace != null && !namespace.isEmpty()) {
			//设置 namespace
			this.builderAssistant.setCurrentNamespace(namespace);
			//解析 cache-ref 标签
			this.cacheRefElement(context.evalNode("cache-ref"));
			//解析 cache 标签
			this.cacheElement(context.evalNode("cache"));
			//解析映射参数 parameterMap 
			this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
			//解析结果集 resultMap
			this.resultMapElements(context.evalNodes("/mapper/resultMap"));
			//解析 sql
			this.sqlElement(context.evalNodes("/mapper/sql"));
			//构建 crud 语句 
			this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
		} else {
			throw new BuilderException("Mapper's namespace cannot be empty");
		}
	} catch (Exception var3) {
		throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
	}
}

XMLMapperBuilder#cacheElement 源码分析

XMLMapperBuilder#cacheElement 源码就很简单了,其实就是对 Cache 标签的解析,解析 Cache 标签的各个属性,然后构建一个 Cache 对象返回。

//org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
private void cacheElement(XNode context) {
	if (context != null) {
		//获取缓存类名 这里如果我们定义了<cache/>中的 type 就使用自定义的Cache类 否则使用和一级缓存相同的PerpetualCache
		String type = context.getStringAttribute("type", "PERPETUAL");
		//从类型别名注册表中获取缓存类名的 class
		Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);
		//缓存淘汰策略 默认 LRU
		String eviction = context.getStringAttribute("eviction", "LRU");
		//从类型别名注册表中获取缓存类名的 class
		Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);
		//获取缓存刷新时间间隔
		Long flushInterval = context.getLongAttribute("flushInterval");
		//缓存存放的元素个数
		Integer size = context.getIntAttribute("size");
		//只读属性 默认 false
		boolean readWrite = !context.getBooleanAttribute("readOnly", false);
		//缓存中找不到对应的key 是否会一直 blocking 直到有对应的数据进入缓存 默认 false
		boolean blocking = context.getBooleanAttribute("blocking", false);
		Properties props = context.getChildrenAsProperties();
		//构建一个新的二级缓存对象
		this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
	}

}


//org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
	//创建 cache 对象
	Cache cache = (new CacheBuilder(this.currentNamespace)).implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class)).addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();
	//添加到 configuration 中
	this.configuration.addCache(cache);
	//将 cache 赋值给 MapperBuilderAssistant.currentCache
	this.currentCache = cache;
	return cache;
}

二级缓存在查询过程中的使用源码分析

二级缓存的使用再 CachingExecutor#query 方法中有体现,如下:

//CachingExecutor#query 方法会从 MappedStatement 中获取 SQL 信息,创建缓存 key 并执行查询,Configuration 中 cacheEnabled 属性值默认为 true,因此会执行 CachingExecutor 的 query方法。
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	//获取 SQL 基本信息
	BoundSql boundSql = ms.getBoundSql(parameterObject);
	//创建缓存 key
	CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
	//执行查询
	return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}


//CachingExecutor#query 会判断是否使用了缓存,如果允许使用缓存会先从二级缓存查询,二级缓存中查询不到才会去查询一级缓存或者数据库,如果二级缓存为空或者不允许使用缓存就会直接去查询一级缓存或者数据库。
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	//获取 二级缓存
	Cache cache = ms.getCache();
	//为空判断
	if (cache != null) {
		//是否刷新缓存
		//select 时候 flushCacheRequired 默认是false 不清除二级缓存 
		//insert update delete 时候 flushCacheRequired 是 true 会清除二级缓存
		this.flushCacheIfRequired(ms);
		//如果使用了缓存  ResultHandler 不为空 
		if (ms.isUseCache() && resultHandler == null) {
			//对存储过程的处理 确保没有存储过程 如果有存储过程 就报错
			this.ensureNoOutParams(ms, parameterObject, boundSql);
			//从二级缓存中查询数据 TransactionalCacheManager
			List<E> list = (List)this.tcm.getObject(cache, key);
			//结果为空 判断
			if (list == null) {
				//为空 BaseExexutor 执行查询数据库
				list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
				//加入二级缓存
				this.tcm.putObject(cache, key, list);
			}

			return list;
		}
	}
	//缓存为空 BaseExexutor 直接查询数据库
	return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

TransactionalCacheManager 源码分析

二级缓存 Cache 对象是从 MappedStatement 中获取的,一个 Mapper 对应一个 MapperStatment,MappedStatement(SQL语句信息) 对象是存在全局配置中,多个 CachingExecutor 多可以获取到,这样就可以在多个线程之间共用了,会存在线程安全问题,同样多个 SqlSession 使用同一个 Cache 也会出现数据脏读问题,因此使用了事务缓存管理器 TransactionalCacheManager 来管理二级缓存,TransactionalCacheManager 使用一个 Map 维护了 Cache 和 TransactionalCache 的关系,并提供了管理缓存的方法,但真正管理缓存的时候 TransactionalCache。

package org.apache.ibatis.cache;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.ibatis.cache.decorators.TransactionalCache;

public class TransactionalCacheManager {
	//维护 Cache ransactionalCache 之间的关系
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap();

    public TransactionalCacheManager() {
    }
	
	//清除缓存
    public void clear(Cache cache) {
        this.getTransactionalCache(cache).clear();
    }
	
	//获取缓存
    public Object getObject(Cache cache, CacheKey key) {
        return this.getTransactionalCache(cache).getObject(key);
    }

	//存入缓存
    public void putObject(Cache cache, CacheKey key, Object value) {
        this.getTransactionalCache(cache).putObject(key, value);
    }

    public void commit() {
        Iterator var1 = this.transactionalCaches.values().iterator();

        while(var1.hasNext()) {
            TransactionalCache txCache = (TransactionalCache)var1.next();
            txCache.commit();
        }

    }

    public void rollback() {
        Iterator var1 = this.transactionalCaches.values().iterator();

        while(var1.hasNext()) {
            TransactionalCache txCache = (TransactionalCache)var1.next();
            txCache.rollback();
        }

    }
	
	//获取 TransactionalCache
    private TransactionalCache getTransactionalCache(Cache cache) {
        return (TransactionalCache)this.transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
    }
}

TransactionalCache 源码分析

TransactionalCache 是事务缓存装饰器,可以为 Cache 增加事务功能,真正操作缓存的方法都在 TransactionalCache 中,TransactionalCache 中真正的二级缓存存在在 delegate 中,读取缓存从 delegate 中读取,存入缓存的时候会先存入到 entriesToAddOnCommit 中,只有正真提交事务的时候,才会把缓存加入到 delegate 真正的二级缓存中。

package org.apache.ibatis.cache.decorators;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

public class TransactionalCache implements Cache {
    private static final Log log = LogFactory.getLog(TransactionalCache.class);
	//二级缓存对象
    private final Cache delegate;
	//在commit的时候是否清除数据 标记位
    private boolean clearOnCommit;
	//存放缓存中没有的对象 只有在 commit 的时候 才会真正加入到缓存中
    private final Map<Object, Object> entriesToAddOnCommit;
	//事务提交前 在二级缓存中没有找到这个对象就加入 entriesMissedInCache
    private final Set<Object> entriesMissedInCache;

	//构造方法
    public TransactionalCache(Cache delegate) {
        this.delegate = delegate;
        this.clearOnCommit = false;
        this.entriesToAddOnCommit = new HashMap();
        this.entriesMissedInCache = new HashSet();
    }

    public String getId() {
        return this.delegate.getId();
    }

    public int getSize() {
        return this.delegate.getSize();
    }

	//获取缓存
    public Object getObject(Object key) {
		//从二级缓存中获取对象
        Object object = this.delegate.getObject(key);
		//为空
        if (object == null) {
			//key 加入到 entriesMissedInCache
            this.entriesMissedInCache.add(key);
        }
		//更新二级缓存的时候 不会直接清除二级缓存 而是会将 clearOnCommit 改为true
		//clearOnCommit 为true 返回 null  否则返回查询到的缓存对象
        return this.clearOnCommit ? null : object;
    }
	
	//存入缓存
    public void putObject(Object key, Object object) {
		//这里只是加入到 entriesToAddOnCommit 中
        this.entriesToAddOnCommit.put(key, object);
    }
	
    public Object removeObject(Object key) {
        return null;
    }
	
    public void clear() {
        this.clearOnCommit = true;
        this.entriesToAddOnCommit.clear();
    }
	
	//提交事务
    public void commit() {
        if (this.clearOnCommit) {
			//如果提交时候清除缓存 就执行清空操作
            this.delegate.clear();
        }
		//将 entriesToAddOnCommit 加入二级缓存
        this.flushPendingEntries();
		//加入完成之后 执行重置操作 重置 entriesToAddOnCommit entriesMissedInCache 
        this.reset();
    }
	//回滚
    public void rollback() {
		//清除二级缓存
        this.unlockMissedEntries();
		//重置操作  重置 entriesToAddOnCommit entriesMissedInCache 
        this.reset();
    }
	
	//重置操作
    private void reset() {
        this.clearOnCommit = false;
        this.entriesToAddOnCommit.clear();
        this.entriesMissedInCache.clear();
    }
	
	//刷新待处理的数据 其实就是加入二级缓存
    private void flushPendingEntries() {
		//迭代遍历 entriesToAddOnCommit 
        Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();
		
        while(var1.hasNext()) {
            Entry<Object, Object> entry = (Entry)var1.next();
			//加入二级缓存
            this.delegate.putObject(entry.getKey(), entry.getValue());
        }
		//迭代遍历 entriesMissedInCache
        var1 = this.entriesMissedInCache.iterator();
		
        while(var1.hasNext()) {
            Object entry = var1.next();
            if (!this.entriesToAddOnCommit.containsKey(entry)) {
				//entriesToAddOnCommit 中不存在 则加入二级缓存 value 为  null
                this.delegate.putObject(entry, (Object)null);
            }
        }

    }
	
	//如果回滚就执行该防范
    private void unlockMissedEntries() {
		//迭代遍历 entriesMissedInCache
        Iterator var1 = this.entriesMissedInCache.iterator();

        while(var1.hasNext()) {
            Object entry = var1.next();

            try {
				//从二级缓存中移出对象
                this.delegate.removeObject(entry);
            } catch (Exception var4) {
                log.warn("Unexpected exception while notifiying a rollback to the cache adapter. Consider upgrading your cache adapter to the latest version. Cause: " + var4);
            }
        }

    }
}

二级缓存生效的时机

我们根据 DefaultSqlSession#commit 方法的源码跟进去,发现最后调用了 TransactionalCache#commit 方法,也就是二级缓存的生效时机是在 SqlSession 提交之后,这样就避免了单机情况下的脏读问题。

//org.apache.ibatis.session.defaults.DefaultSqlSession#commit()
public void commit() {
	this.commit(false);
}


//org.apache.ibatis.session.defaults.DefaultSqlSession#commit(boolean)
public void commit(boolean force) {
	try {
		this.executor.commit(this.isCommitOrRollbackRequired(force));
		this.dirty = false;
	} catch (Exception var6) {
		throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
	} finally {
		ErrorContext.instance().reset();
	}

}

//org.apache.ibatis.executor.CachingExecutor#commit
public void commit(boolean required) throws SQLException {
	this.delegate.commit(required);
	this.tcm.commit();
}


//org.apache.ibatis.cache.TransactionalCacheManager#commit
public void commit() {
	Iterator var1 = this.transactionalCaches.values().iterator();

	while(var1.hasNext()) {
		TransactionalCache txCache = (TransactionalCache)var1.next();
		txCache.commit();
	}

}

//org.apache.ibatis.cache.decorators.TransactionalCache#commit
public void commit() {
	if (this.clearOnCommit) {
		this.delegate.clear();
	}

	this.flushPendingEntries();
	this.reset();
}

二级缓存更新时机

同样从 DefaultSqlSession#update 方法入手分析,发现最终调用了 TransactionalCache#clear 方法,TransactionalCache#clear 方法并没有正在的更新或者清除二级缓存,而是将 clearOnCommit 提交事务时候是否清除缓存改为 true(默认是false),事务提交时候会进行判断,如果 clearOnCommit 为 true,就会清除二级缓存,同样在查询二级缓存的时候也会判断 clearOnCommit 属性,如果 clearOnCommit 为 true,则直接返回 null,来保证数据的准确性。

//org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String)
public int update(String statement) {
	return this.update(statement, (Object)null);
}

//org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String, java.lang.Object)
public int update(String statement, Object parameter) {
	int var4;
	try {
		this.dirty = true;
		MappedStatement ms = this.configuration.getMappedStatement(statement);
		var4 = this.executor.update(ms, this.wrapCollection(parameter));
	} catch (Exception var8) {
		throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
	} finally {
		ErrorContext.instance().reset();
	}

	return var4;
}


//org.apache.ibatis.executor.CachingExecutor#update
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
	this.flushCacheIfRequired(ms);
	return this.delegate.update(ms, parameterObject);
}


//org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {
	Cache cache = ms.getCache();
	if (cache != null && ms.isFlushCacheRequired()) {
		this.tcm.clear(cache);
	}

}


//org.apache.ibatis.cache.TransactionalCacheManager#clear
public void clear(Cache cache) {
	this.getTransactionalCache(cache).clear();
}

//org.apache.ibatis.cache.decorators.TransactionalCache#clear
public void clear() {
	this.clearOnCommit = true;
	this.entriesToAddOnCommit.clear();
}

总结:二级缓存实现了 Sqlsession 之间的缓存数据共享,属于 namespace/Mapper 级别的缓存,具有丰富的缓存淘汰策略,二级缓存使用 TransactionalCache 来解决脏读的问题,只有事务提交的时候,对应的数据才会放入到二级缓存中,来避免脏读问题,同时需要注意的是默认的情况下,Mybatis将一二级缓存都存储到本地缓存中,因此在分布式情况下,二级缓存还是会出现脏读问题,分布式情况下不建议使用二级缓存。

欢迎提出建议及对错误的地方指出纠正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/748716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

valgrind调试c/c++内存问题:非法地址访问_内存泄漏_越界访问

1.valgrind命令 调试内存问题: valgrind --leak-checkfull 更新详细的显示: valgrind --leak-checkfull --show-leak-kindsall valgrind提示信息汇总 内存泄漏 lost in loss record 丢失记录 , 内存泄漏实例[[#2.内存泄漏–不完全释放内存|实例链接]]段错误 Process termina…

vue3-cropperjs图片裁剪工具-用户上传图片截取-(含预览视频)

效果图 上传图片弹窗预览 对于这个上传图片样式可以参考 官方原代码 官网传送入口 Upload 上传 | Element Plus (element-plus.org) <template><el-uploadclass"upload-demo"dragaction"https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6…

吴恩达LangChain教程:Embedding与文档解析

当前有很多应用想要实现根据文档或者文本内容实现用户问答&#xff0c;或者实现多轮会话能力&#xff0c;这时候就会使用到Embedding的能力。 01 | 使用类介绍 想要依据Embedding实现文本检索&#xff0c;需要引入如下的依赖。 其中&#xff0c;RetrievalQA的作用是对一些文档…

一天跌20%,多只可转债“腰斩”,近百只跌破面值,“退可守”的香饽饽为何破防?

专业人士指出&#xff0c;近期部分可转债大跌原因主要有两点&#xff1a;一方面&#xff0c;转债市场与权益市场联动性强。另一方面&#xff0c;近期公布的宏观经济数据稳中趋缓&#xff0c;“供强需弱”特征依然明显&#xff0c;证监会主席吴清发言及“科创板八条”新规延续了…

Python 基础 (标准库):heapq (堆)

1. 官方文档 heapq --- 堆队列算法 — Python 3.12.4 文档 2. 相关概念 堆 heap 是一种具体的数据结构&#xff08;concrete data structures&#xff09;&#xff1b;优先级队列 priority queue 是一种抽象的数据结构&#xff08;abstract data structures&#xff09;&…

秘籍来啦!手机找回删除的文件,掌握这3种方法

你是否曾经不小心删除了手机上的重要文件&#xff0c;如照片、视频、文档等&#xff0c;然后束手无策&#xff0c;不知道该如何恢复&#xff1f;这时候&#xff0c;找回删除的文件就显得尤为重要。别担心&#xff0c;本文将为大家揭秘3种方法&#xff0c;让你轻松找回那些被删除…

Redis Stream Redisson Stream

目录 一、Redis Stream1.1 场景1&#xff1a;多个客户端可以同时接收到消息1.1.1 XADD - 向stream添加Entry&#xff08;发消息 &#xff09;1.1.2 XREAD - 从stream中读取Entry&#xff08;收消息&#xff09;1.1.3 XRANGE - 从stream指定区间读取Entry&#xff08;收消息&…

徐徐拉开的帷幕:拜登与特朗普的辩论大戏 日元跌破160大关!创1986年以来最低纪录

北京时间6月27日&#xff08;本周五&#xff09;上午9:00&#xff0c;拜登和特朗普将参加2024年总统候选人电视辩论。作为参考&#xff0c;2016年大选辩论期间&#xff0c;美元汇率对辩论结果的反应相对温和&#xff0c;希拉里胜选预期增强在一定程度上支撑了美元。 时间逐渐临…

AI产品打造全攻略:看我是如何预测用户流失,搞定AI产品全流程的

前言 对于任何互联网公司而言&#xff0c;用户流失无疑是一个不容忽视的问题。在本文中&#xff0c;我将通过一个真实的预测用户流失的项目案例&#xff0c;带领大家深入了解AI产品从筹备到上线的整个流程。这个过程将展现AI产品经理的工作全貌&#xff0c;包括各个环节的角色…

钉钉在MAKE 2024大会上宣布开放AI生态;NBC将用AI主播播报巴黎奥运会内容

&#x1f680; 钉钉在MAKE 2024大会上宣布开放AI生态 摘要&#xff1a;钉钉总裁叶军在MAKE 2024生态大会上宣布&#xff0c;钉钉将对所有大模型厂商开放&#xff0c;构建“国内最开放AI生态”。目前已有六家大模型厂商接入钉钉&#xff0c;用户可直接使用七家大模型产品。未来…

下拉选择输入框(基于elment-ui)

最近在需求中&#xff0c;需要有一个下拉选择功能&#xff0c;又得可以输入&#xff0c;在 element-ui 官网找了&#xff0c;发现没有适合的&#xff0c;然后在修炼 cv 大法的我&#xff0c;也在网上看了一下&#xff0c;但是也都感觉不合适&#xff0c;所以就自己写了一个&…

R语言数据分析案例37-旅游景点聚类分析

一、研究背景 近年来&#xff0c;随着旅游业的迅猛发展&#xff0c;旅游景点的竞争日益激烈。如何在众多景点中脱颖而出&#xff0c;吸引更多游客&#xff0c;成为各大景点管理者关注的焦点。通过对旅游景点进行深入的数据分析&#xff0c;可以帮助管理者更好地了解景点的优势…

C#1.0-11.0所有历史版本主要特性总结

文章目录 前言名词解释主要版本一览表各版本主要特性一句话总结 C# 1.0 (Visual Studio 2002, .Net Framework 1.0)C# 2.0 (Visual Studio 2005, .Net Framework 2.0)C# 3.0 (Visual Studio 2008, .Net Framework 3.0)C# 4.0 (Visual Studio 2010, .Net Framework 4)C# 5.0 (V…

赏金猎人src挖掘入门

文章目录 1. 什么是漏洞2. OWASP Top 103. 利用的漏洞来源4. SRC安全应急响应中心5. Burpsuite简介6. 浏览器代理插件6.1 firefox浏览器代理插件6.2 edge浏览器代理插件3.chrome浏览器代理插件&#xff08;需要科学上网&#xff09; 1. 什么是漏洞 漏洞是指一个系统存在的弱点或…

2024广东省职业技能大赛云计算赛项实战——构建CICD

构建CI/CD 前言 题目如下&#xff1a; 构建CI/CD 编写流水线脚本.gitlab-ci.yml触发自动构建&#xff0c;具体要求如下&#xff1a; &#xff08;1&#xff09;基于镜像maven:3.6-jdk-8构建项目的drone分支&#xff1b; &#xff08;2&#xff09;构建镜像的名称&#xff1a…

C# VTK 自定义封装 vtkwPipeline 多边形管道建模

vtkwPipeline 简介 public vtkwPipeline(vtkLineSource lineSource, double outR, double inR, int sides) vtkwPipeline 是我自定义封装的C# 类 用于对管道壁建模&#xff0c;有内半径&#xff0c;外半径设置&#xff0c; 以及多边形边数设置。 参数 1. vtkLineSource li…

EI CCIE学习笔记-SDAccess之一:SDAccess解决方案

Chapter 1 SD-Access Solution Proposal 1.1 概念引入 SDN三要素&#xff1a;集中控制、转控分离、可编程 DNA DNA:Digital Network Architecture数字网络架构 思科提出的跨园区&#xff0c;分支机构&#xff0c;WAN和扩展企业的企业网络架构它提供了一种开放&#xff0c;可扩…

win10 C:\Users\Administrator

win10 C:\Users\Administrator C:\Users\Administrator\Documents\ C:\Users\Administrator\Pictures C:\Users\Administrator\Favorites C:\Users\Administrator\Links C:\Users\Administrator\Videos

Shopee API接口——获取商家店铺商品列表

一、引言 在跨境电商领域&#xff0c;Shopee作为东南亚地区领先的电商平台&#xff0c;为众多商家提供了广阔的市场和丰富的销售机会。本文将详细介绍如何通过Shopee API获取商家店铺商品列表&#xff0c;并探讨其应用场景。 二、核心功能介绍 Shopee API获取商家店铺商品列…

数据结构(Java):ArrayList的应用

1、引言 上一篇博客&#xff0c;已经为大家讲解了集合类ArrayList。 这篇博客&#xff0c;就来帮助大家学会使用ArrayList。 2、题1&#xff1a; 删除字符&#xff08;热身题&#xff09; 题目&#xff1a;给出str1和str2两个字符串&#xff0c;删除str1中出现的所有的str2…