MyBatis源码分析(4)—— Cache构建以及应用
@(MyBatis)[Cache]
MyBatis源码分析——Cache构建以及应用
SqlSession使用缓存流程
如果开启了二级缓存,而Executor会使用CachingExecutor来装饰,添加缓存功能,该CachingExecutor会从MappedStatement中获取对应的Cache来使用。(注:MappedStatement中有保存相关联的Cache)
在使用SqlSession向DB查询数据时,如果开启了二级缓存,则会优先从二级缓存中获取数据,没有命中的话才会去查询一级缓存,此时,一级缓存也没有命中,则才会真正的去数据库查询数据。
没有命中缓存
下图为开启了二级缓存的查询数据时序图,其中忽略了二级缓存事务的处理(见下面二级缓存详细说明)。
命中二级缓存
命中一级缓存
缓存键,CacheKey
下面为CacheKey的主要核心代码,省略了部分代码。在MyBatis中,是通过几个条件来判断是否同一条Sql的。
判断条件:
- Statement ID
- 结果范围
- Sql
- 所有的入参
在上面的条件中,对于需要使用JDBC查询出相同结果的来说,需要是同一条Sql以及该Sql的入参条件。
在查询数据之前,会先创建CacheKey,在BaseExecutor.createCacheKey
中实现:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
// StatementId, 即用于映射Mapper中的具体Sql的ID
cacheKey.update(ms.getId());
// 结果集范围,在数据库查询出来的结果中进行过滤,并非是物理分页。
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 具体执行的Sql
cacheKey.update(boundSql.getSql());
// 入参变量值
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
return cacheKey;
}
CacheKey的实现:
这里将所有需要判断相等的条件都放入List中,并且更新这些条件计算出校验值和hashCode,这是为了加快比较的速度。因为只有在校验值以及HashCode相等的情况下,才会去逐一地判断每个条件是否相等。
public class CacheKey implements Cloneable, Serializable {
// 默认扩展因子
private static final int DEFAULT_MULTIPLYER = 37;
// 默认HashCdoe基值
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
// 如果对象为数组,则根据每个数组的元素来进行计算
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
// 计算HashCode和checksum
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
// 扩展因子*当前的哈希值 + 对象的哈希值*扩大倍数
hashcode = multiplier * hashcode + baseHashCode;
// 添加到对比条件中
updateList.add(object);
}
public boolean equals(Object object) {
if (this == object)
return true;
if (!(object instanceof CacheKey))
return false;
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode)
return false;
if (checksum != cacheKey.checksum)
return false;
if (count != cacheKey.count)
return false;
// 只有上面的检验条件都相等的情况下,才对每个条件逐一对比
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null)
return false;
} else {
if (!thisObject.equals(thatObject))
return false;
}
}
return true;
}
}
一级缓存
一级缓存直接采用PerpetualCache
来实现,默认为SESSION范围
刷新时机
- SESSION范围缓存失效时刻:
- SqlSession关闭,则会释放缓存
- 提交或者回滚的时候会清空对应的一级缓存。
- 在更新操作的时候,则直接清空对应的一级缓存
- 手动调用清空缓存操作
- STATEMENT范围刷新缓存
无论是查询还是更新,在执行完Sql的时候都会清空对应的一级缓存。
二级缓存
在MyBatis中,Cache都通过CachingExecutor内的TransactionalCacheManager来管理Cache,每个Cache都会使用TransactionalCache来装饰,即缓存是事务性质的,需要手动通过commit或者SqlSession的close来实现真正的将执行结果反应到Cache中,因为二级缓存是属于全局的,会有可能涉及到多个Cache的添加或者删除操作。
构建二级缓存
MapperBuilderAssistant.useNewCache
调用构造CacheBuilder
来构建Cache,并且将构造出来的cache注入到MappedStatement
中。CacheBuilder
以Builder设计模式实现,而缓存的功能添加则是通过装饰者模式来实现。
下面为CacheBuilder
构建Cache的部分代码:
public Cache build() {
// 设置默认底层实现Cache,默认如果没有提供则为PerpetualCache
setDefaultImplementations();
// 创建基类,用于最底层的Cache实现
Cache cache = newBaseCacheInstance(implementation, id);
// 设置Cache属性
setCacheProperties(cache);
// 只有PerpetualCache才使用装饰类添加功能,自定义的Cache不添加
if (PerpetualCache.class.equals(cache.getClass())) {
// 使用装饰类包装
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 设置给定的装饰类
cache = setStandardDecorators(cache);
}
return cache;
}
// 根据给定的Cache以及待装饰实例,创建装饰类
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果开启了定时,则使用ScheduledCache装饰
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);
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
二级缓存刷新时机示例
配置:
关闭一级cache,仅仅开启二级Cache
<setting name="localCacheScope" value="STATEMENT"/>
不手动commit
public static void main(String args[]) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(true);
ProductMapper productMapper = session.getMapper(ProductMapper.class);
productMapper.queryAll();
productMapper.queryAll();
}
输出结果:
可以看到,当没有手动提交,并且是同一个session时,前一次执行的结果并没有刷到缓存,两次缓存的命中率均为0
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
手动commit
比上面多了一步,手动commit,刷新到缓存。
// 注:此处关闭了一级cache,仅仅开启了二级cache
public static void main(String args[]) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 此处设置了自动提交,但是那是JDBC中Connection的自动提交
SqlSession session = sqlSessionFactory.openSession(true);
ProductMapper productMapper = session.getMapper(ProductMapper.class);
List<Product> list = productMapper.queryAll();
// 这里比上面多操作一步,手动提交
session.commit();
productMapper.queryAll();
productMapper.queryAll();
}
输出结果:
可以看到下面的二级Cache命中率,第一次没有数据,故为0,第二次命中,变为0.5
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.5
MyBatis缓存使用注意点
在使用缓存的时候,需要注意如果数据缓存在本地,另一个系统修改数据库时,会出现脏数据问题。
一级缓存
Myatis的一级缓存默认为SESSION,而且由于底层采用PerpetualCache
来实现,该类直接使用HashMap
,并没有进行一些限制处理。
- 在MyBatis看来,SqlSession一般都是生命周期比较短的,当关闭的时候会释放缓存,但是如果使用SqlSession多次进行查询大量的数据时,会将数据缓存,那么有可能会导致OOM内存溢出。
二级缓存
MyBatis虽然全局配置开启缓存,但是还是取决于是否使用了<cache>
标签,如果使用了二级缓存,需要注意:
- 每个
<cache>
代表一个单独的二级缓存,如果多个Mapper需要共享同一个二级缓存,则需要使用<cache-ref>
- 如果一个Mapper中查询数据时,使用了多表联查,则,当另一个Mapper更新相关数据时,如果没有共享一个Cache,那么下一次该Mapper查询时,就会出现读到脏数据。
MyBatis源码分析(4)—— Cache构建以及应用的更多相关文章
- Mybatis源码分析之Cache二级缓存原理 (五)
一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...
- Mybatis源码分析之Cache一级缓存原理(四)
之前的文章我已经基本讲解到了SqlSessionFactory.SqlSession.Excutor以及Mpper执行SQL过程,下面我来了解下myabtis的缓存, 它的缓存分为一级缓存和二级缓存, ...
- MyBatis源码分析(3)—— Cache接口以及实现
@(MyBatis)[Cache] MyBatis源码分析--Cache接口以及实现 Cache接口 MyBatis中的Cache以SPI实现,给需要集成其它Cache或者自定义Cache提供了接口. ...
- MyBatis源码分析-SQL语句执行的完整流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- 【MyBatis源码分析】select源码分析及小结
示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是 ...
- MyBatis 源码分析 - 缓存原理
1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...
- MyBatis 源码分析 - 映射文件解析过程
1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...
- Mybatis源码分析
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- MyBatis 源码分析
MyBatis 运行过程 传统的 JDBC 编程查询数据库的代码和过程总结. 加载驱动. 创建连接,Connection 对象. 根据 Connection 创建 Statement 或者 Prepa ...
随机推荐
- 怎么样在Myeclipse中配置JDK?
1.首先电脑上安装JDK 2.打开Myeclipse >> Window >> Preferences 如图1: 图1 2.Preferences >> ...
- phpStudy2016 配置多个域名期间遇到的问题
第一步 在C:\Windows\System32\drivers\etc下的hosts文件下添加 第二步 找到Apache 下的httpd.conf 文件 打开,去掉171行前边的# 第 ...
- 自己解决虚拟机Ubuntu开机黑屏
Virtual Box+Ubuntu 64bit,之前都能好好用,但昨天一打开,过了开始的一个选择界面(有什么恢复模式那个)就黑了,左上角的光标不闪,一直卡在那里,后来发现原因了. 1.先下载LeoM ...
- 用 JSP 实现对文件的相关操作
前段时间一直忙着作业,实验,动手的时间真是少之又少,今天终于可以继续和大家分享关于 JSP 的学习心得. 简单总结一下吧: JSP 理论性很强,感觉就是纯语法. 我更偏向于实际编写代码,这样更容易理解 ...
- Js函数function基础理解
正文:我们知道,在js中,函数实际上是一个对象,每个函数都是Function类型的实例,并且都与其他引用类型一样具有属性和方法.因此,函数名实际上是指向函数对象的指针,不与某个函数绑定.在常见的两种定 ...
- 新手学跨域之iframe
https://segmentfault.com/a/1190000000702539 页面嵌套iframe是比较常见的,比如QQ相关业务页面的登录框一般都是iframe的.使用ifrmae跨域要满足 ...
- Openjudge 1.13-23:区间内的真素数(每日一水)
总时间限制: 1000ms 内存限制: 65536kB 描述 找出正整数 M 和 N 之间(N 不小于 M)的所有真素数.真素数的定义:如果一个正整数 P 为素数,且其反序也为素数,那么 P 就为 ...
- 如何在ASP.NET Core中使用Redis
注:本文提到的代码示例下载地址> https://code.msdn.microsoft.com/How-to-use-Redis-in-ASPNET-0d826418 Redis是一个开源的内 ...
- Maven学习笔记
1.Maven安装 Maven和ant同为apache出版的构建工具,与gradle是一类东西,与C语言中的make是同一类产品.从apache官网上下载maven的zip安装包,解压即可使用,需要把 ...
- neo4j关闭和开启密码访问权限
本例:neo4j-enterprise-2.3.1版本 neo4j默认安装是开启访问密码验证 可以发现,在conf/下的neo4j-server.properties配置文件 # Require (o ...