- 浏览: 100291 次
- 性别:
- 来自: 深圳
文章分类
- 全部博客 (209)
- http://docs.jpush.io/server/java_sdk/ (1)
- SpingMVC ModelAndView (1)
- Model (1)
- Control以及参数传递 (1)
- https://www.alipay.com/ (1)
- 检查指定的字符串列表是否不为空。 (1)
- 转自http://my.oschina.net/rpgmakervx/blog/505434 (1)
- 压缩js (1)
- 含包含 字母数字校验 (1)
- 判断浏览器是ie (1)
- fixbox (0)
- 转自http://www.cnblogs.com/lanzi/archive/2010/10/26/1861338.html (1)
- http://dl2.iteye.com/upload/attachment/0113/2406/afbd8d53-dcad-3afc-8d78-41c1591 (0)
- IDEA (0)
- maven搭建 (0)
- http://www.jeasyuicn.com/api/docTtml/index.htm (1)
- 给tomcat添加运行内存: (1)
- JPUSH推送 (1)
- requestScope (0)
- oracle中plsql安装client10连接数据库 (1)
- 不需要安装oracle11g (1)
- tuikuan (1)
- http://www.hjxmall.com/index.php (1)
- java (1)
- 参考 (1)
- xml签名验签模拟程序 (1)
- 技术网站收集分享 (1)
- Java NIO提供了与标准IO不同的IO工作方式 ------来源于 http://www.iteye.com/magazines/132-Java-NIO (1)
- oracle表查询语句优化 (0)
- oracle (5)
- a (1)
- TenpayHttpClient (2)
- mongodb (1)
- http://www.qcloud.com/product/cmongo.html?utm_source=pcsem1&utm_medium=bdgj46&utm_campaign=baidu (1)
- SpringMVC整合MongoDB开发 (0)
- SpringMVC整合MongoDB开发 https://www.mongodb.org (1)
- Java 语言中常见问题总结 (1)
- 数据库SQL优化大总结 (1)
- 拦截器与过滤器的区别 (1)
- Struts2中拦截器与过滤器的区别及执行顺序 (1)
- Spring声明式事务管理与配置 (0)
- oracle OPERTION (1)
- java 高并发多线程开发 (1)
- Collection 与 map 接口相关集合 (1)
- 多线程开发实践 (1)
- JVM调优总结 转自 http://www.importnew.com/18694.html (1)
- redis 五种数据类型的使用场景 转自于 http://blog.csdn.net/gaogaoshan/article/details/41039581 (1)
- HttpWatch http基础 来自于http://blog.csdn.net/gaogaoshan/article/details/21237555 (1)
- maven 远程仓库 http://blog.csdn.net/gaogaoshan/article/details/40266779 (1)
- 生成Webservice客户端的4中方法 http://blog.csdn.net/gaogaoshan/article/details/8980775 (1)
- http://fgh2011.iteye.com/blog/1564283 (1)
- sleep和wait有什么区别 http://xiaowei2002.iteye.com/blog/2174188 (1)
- JDK中常用包及其类 常见的几种RuntimeException (1)
- Java的运行原理 (1)
- mybatis缓存的使用及理解 http://my.oschina.net/dxqr/blog/123163 (1)
- 软件架构设计分析 (1)
- redis技术总结 (3)
- java面试总结知识点 (1)
- ZooKeeper技术 (1)
- Hadoop (1)
- sso单点登录 (1)
- SpringIOC (1)
- 书签ssssssssssssssssssssss (1)
- spring事务的隔离级别 http://www.cnblogs.com/yangy608/archive/2011/06/29/2093478.html (1)
- 秒杀系统架构分析与实战 http://www.importnew.com/18920.html (1)
- oracle 连接plsql配置 (1)
- maven工程集成springmvc http://blog.csdn.net/fox_lht/article/details/16952683 (1)
- java类序列化与反序列化版本唯一号serialVersionUID (1)
- spring注解用法总结 (1)
- eclipse导入maven项目找不到资源文件方法 (1)
- dubbo (0)
- socket 网络编程 服务器与客户端 编程 (1)
- Thread与Runnable实现线程利用线程插队实现求和操作 (1)
- 生产者与消费者模式 (1)
- JAXB注解 java 关于xml的注解,自动生成xml文件 - @XML*** (1)
- xml 与JAVAbean转换 (1)
- MAP (2)
- decimalToString (1)
- 反编译插件 (0)
- 反编译插件 https://sourceforge.net/projects/jadclipse/?source=typ_redirect (1)
- AWX (1)
- 官网地址Dinp (1)
- public interface ExecutorService extends Executor (1)
- MAIN (1)
- 转自于 http://blog.csdn.net/lufeng20/article/details/24314381 (1)
- JaxbUtil (1)
- HttpXmlClient (0)
- Http post 请求 (1)
- URLDecoder (1)
- spdb (0)
- 订单号生成规则 (1)
- 距离0点毫秒数 (1)
- MyBatis存储过程调用java (1)
- Banks (1)
- 学习网址 (1)
- hots配置oracle数据库配置修改 (1)
- 支付宝文档 (1)
- Jadclipse (1)
- filter (0)
- Filter过滤器 (1)
- 工具类 fixbox (1)
- java quartz (0)
- java quartz 实现定时任务,每个礼拜一到礼拜五定时执行 (1)
- Thread (4)
- getResourceAsStream (1)
- BigData (1)
- 开源架构 (17)
- 架构 (0)
- 文件操作 (2)
- tools (20)
- 测试工具 (1)
- vm (1)
- mq (1)
- blog (1)
- 开源架构搭建技术 (6)
- JAVA细节技术点总结 (1)
- 优化技术 (1)
- LINUX命令使用 (1)
- spring (2)
- 大数据 (1)
- struts2 (1)
- python (1)
- pay (1)
- linux (1)
- 压力测试 (1)
- 代码检查插件 (1)
- 游戏端开发 (1)
- 微信开发相关 (1)
- 搜索引擎 (1)
- 网络技术 (1)
- 分布式部署 (1)
- 堆栈信息 (1)
最新评论
和hibernate一样,mybatis也有缓存机制
一级缓存是基于 PerpetualCache(mybatis自带)的 HashMap 本地缓存,作用范围为session,所以当session commit或close后,缓存就会被清空
二级缓存默认也是基于 PerpetualCache,但是可以为其制定存储源,比如ehcache
一级缓存缓存的是SQL语句,而二级缓存缓存的是结果对象,看如下例子(mybatis的日志级别设为debug)
?
1
2
3
4
5
6
7
8
List<User> users = sqlSession.selectList("com.my.mapper.UserMapper.getUser", "jack");
System.out.println(users);
//sqlSession.commit();①
List<User> users2 = sqlSession.selectList("com.my.mapper.UserMapper.getUser", "jack");//②admin
System.out.println(users);
结果是只发起一次SQL语句,如果我们把②出的参数jack改为admin,发现还是只发起一次SQL语句,但是会设置不同参数
如果把①处去掉注释,会发现不会有缓存了
下面就来启用二级缓存
在配置文件中启用二级缓存
?
1
<setting name="cacheEnabled" value="true" />
在需要进行缓存的mapper文件UserMapper.xml中加上
?
1
<cache readOnly="true"></cache>
注意这里的readOnly设为true,默认是false,表示结果集对象需要被序列化
我们打开①处注释,②处仍然使用jack,我们发现结果只执行了一次SQL语句
但是如果把②处改为admin,执行了2次SQL语句,这说明二级缓存是缓存结果集对象的
下面我们来使用ehcache
在classpath下添加ehcache.xml
在UserMapper.xml中添加:
?
1
2
<!-- <cache readOnly="true" type="org.mybatis.caches.ehcache.LoggingEhcache"/> -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
用上面那个会输出更加详细的日志,下面的不会
需要用到ehcache.jar,下载地址:http://sourceforge.net/projects/ehcache/files/ehcache/ehcache-2.7.0/ehcache-2.7.0-distribution.tar.gz/download
mybatis-ehcache.jar下载地址:http://code.google.com/p/mybatis/downloads/detail?name=mybatis-ehcache-1.0.2-SNAPSHOT-bundle.zip&can=3&q=Product%3DCache
--尽量避免使用二级缓存技术,多表操作 的业务场景下
一、创建Cache的完整过程
我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
然后是:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
看parser.parse()方法:
parseConfiguration(parser.evalNode("/configuration"));
看处理Mapper.xml文件的位置:
mapperElement(root.evalNode("mappers"));
看处理Mapper.xml的XMLMapperBuilder:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration,
resource, configuration.getSqlFragments());
mapperParser.parse();
继续看parse方法:
configurationElement(parser.evalNode("/mapper"));
到这里:
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
从这里看到namespace就是xml中<mapper>元素的属性。然后下面是先后处理的cache-ref和cache,后面的cache会覆盖前面的cache-ref,但是如果一开始cache-ref没有找到引用的cache,他就不会被覆盖,会一直到最后处理完成为止,最后如果存在cache,反而会被cache-ref覆盖。这里是不是看着有点晕、有点乱?所以千万别同时配置这两个,实际上也很少有人会这么做。
看看MyBatis如何处理<cache/>:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
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);
}
}
从源码可以看到MyBatis读取了那些属性,而且很容易可以到这些属性的默认值。
创建Java的cache对象方法为builderAssistant.useNewCache,我们看看这段代码:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
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)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
从调用该方法的地方,我们可以看到并没有使用返回值cache,在后面的过程中创建MappedStatement的时候使用了currentCache。
二、使用Cache过程
在系统中,使用Cache的地方在CachingExecutor中:
@Override
public <E> List<E> query(
MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
获取cache后,先判断是否有二级缓存。
只有通过<cache/>,<cache-ref/>或@CacheNamespace,@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。
if (cache != null) {
如果cache存在,那么会根据sql配置(<insert>,<select>,<update>,<delete>的flushCache属性来确定是否清空缓存。
flushCacheIfRequired(ms);
然后根据xml配置的属性useCache来判断是否使用缓存(resultHandler一般使用的默认值,很少会null)。
if (ms.isUseCache() && resultHandler == null) {
确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错。
ensureNoOutParams(ms, parameterObject, boundSql);
没有问题后,就会从cache中根据key来取值:
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
如果没有缓存,就会执行查询,并且将查询结果放到缓存中。
if (list == null) {
list = delegate.<E>query(ms, parameterObject,
rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
返回结果
return list;
}
}
没有缓存时,直接执行查询
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在上面的代码中tcm.putObject(cache, key, list);这句代码是缓存了结果。但是实际上直到sqlsession关闭,MyBatis才以序列化的形式保存到了一个Map(默认的缓存配置)中。
三、Cache使用时的注意事项
1. 只能在【只有单表操作】的表上使用缓存
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。
2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
这一点不需要多说,所有人都应该清楚。记住,这一点需要保证在1的前提下才可以!
四、避免使用二级缓存
可能会有很多人不理解这里,二级缓存带来的好处远远比不上他所隐藏的危害。
缓存是以namespace为单位的,不同namespace下的操作互不影响。
insert,update,delete操作会清空所在namespace下的全部缓存。
通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。
为什么避免使用二级缓存
在符合【Cache使用时的注意事项】的要求时,并没有什么危害。
其他情况就会有很多危害了。
针对一个表的某些操作不在他独立的namespace下进行。
例如在UserMapper.xml中有大多数针对user表的操作。但是在一个XXXMapper.xml中,还有针对user单表的操作。
这会导致user在两个命名空间下的数据不一致。如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单表查询,使用缓存的结果可能会不正确。
更危险的情况是在XXXMapper.xml做了insert,update,delete操作时,会导致UserMapper.xml中的各种操作充满未知和风险。
有关这样单表的操作可能不常见。但是你也许想到了一种常见的情况。
多表操作一定不能使用缓存
为什么不能?
首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。
例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO">
select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>
1
2
3
像上面这个查询,你会写到那个xml中呢??
不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
这点应该很容易理解。
在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。
如果你让他们都使用同一个namespace(通过<cache-ref>)来避免脏数据,那就失去了缓存的意义。
看到这里,实际上就是说,二级缓存不能用。整篇文章介绍这么多也没什么用了。
五、挽救二级缓存?
想更高效率的使用二级缓存是解决不了了。
但是解决多表操作避免脏数据还是有法解决的。解决思路就是通过拦截器判断执行的sql涉及到那些表(可以用jsqlparser解析),然后把相关表的缓存自动清空。但是这种方式对缓存的使用效率是很低的。
设计这样一个插件是相当复杂的,既然我没想着去实现,就不废话了。
最后还是建议,放弃二级缓存,在业务层使用可控制的缓存代替更好。
如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)
时间 2014-12-09 11:31:00 CSDN博客
原文
http://blog.csdn.net/luanlouis/article/details/41800511
主题 MyBatis 数据库
本文如下组织结构:
一个关于MyBatis的二级缓存的实际问题
当前MyBatis二级缓存的工作机制
mybatis-enhanced-cache插件的设计和工作原理
mybatis-enhanced-cache 插件的使用实例
1.一个关于MyBatis的二级缓存的实际问题
网友 chanfish 给我抛出的问题
现有 AMapper.xml 中定义了对数据库表 ATable 的CRUD操作,BMapper定义了对数据库表 BTable 的CRUD操作;
假设MyBatis的二级缓存开启,并且 AMapper 中使用了二级缓存, AMapper 对应的二级缓存为 ACache ;
除此之外, AMapper 中还定义了一个跟 BTable 有关的查询语句,类似如下所述:
<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">
select * from ATable left join BTable on ....
</select>
执行以下操作:
1. 执行 AMapper 中的" selectATableWithJoin " 操作,此时会将查询到的结果放置到 AMapper 对应的二级缓存 ACache 中;
2. 执行 BMapper 中对 BTable 的更新操作( update、delete、insert )后, BTable 的数据更新;
3. 再执行1完全相同的查询,这时候会直接从 AMapper 二级缓存 ACache 中取值,将 ACache 中的值直接返回;
好,问题就出现在第3步上:
由于AMapper的“ selectATableWithJoin ” 对应的SQL语句需要和 BTable 进行join查找,而在第 2 步 BTable 的数据已经更新了,但是第 3 步查询的值是第 1 步的缓存值,已经极有可能跟真实数据库结果不一样,即 ACache 中缓存数据过期了!
总结来看,就是:
对于某些使用了 join连接的查询,如果其关联的表数据发生了更新,join连接的查询由于先前缓存的原因,导致查询结果和真实数据不同步;
从MyBatis的角度来看,这个问题可以这样表述:
对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
当前的MyBatis的缓存机制不能很好地处理这一问题,下面我们将从当前的MyBatis的缓存机制入手,分析这一问题:
2. 当前MyBatis二级缓存的工作机制:
当前MyBatis二级缓存的工作机制:
MyBatis二级缓存的一个重要特点:即松散的Cache缓存管理和维护。
一个Mapper中定义的增删改查操作只能影响到自己关联的Cache对象。如上图所示的Mapper namespace1中定义的若干CRUD语句,产生的缓存只会被放置到相应关联的Cache1中,即Mapper namespace2,namespace3,namespace4 中的CRUD的语句不会影响到Cache1。
可以看出,Mapper之间的缓存关系比较松散,相互关联的程度比较弱。
现在再回到上面描述的问题,如果我们将AMapper和BMapper共用一个Cache对象,那么,当BMapper执行更新操作时,可以清空对应Cache中的所有的缓存数据,这样的话,数据不是也可以保持最新吗?
确实这个也是一种解决方案,不过,它会使缓存的使用效率变的很低!AMapper和BMapper的任意的更新操作都会将共用的Cache清空,会频繁地清空Cache,导致Cache实际的命中率和使用率就变得很低了,所以这种策略实际情况下是不可取的。
最理想的解决方案就是:
对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
这样,就是以很细的粒度管理MyBatis内部的缓存,使得缓存的使用率和准确率都能大大地提升。
基于这个思路,我写了一个对应的mybatis-enhanced-cache 缓存插件,可以很好地支持上述的功能。
对于上述的例子中,该插件可以实现:当BMapper对BTable执行了更新操作时,指定清除与BTable相关联的selectATableWithJoin查询语句在ACache中产生的缓存。
接下来就来看看这个mybatis-enhanced-cache插件的设计原理吧:
3. mybatis-enhanced-cache插件的设计和工作原理
mybatis-enhanced-cache插件的设计和工作原理
该插件主要由两个构件组成: EnhancedCachingExecutor 和 EnhancedCachingManager 。
EnhancedCachingExecutor 是针对于Executor的拦截器,拦截Executor的几个关键的方法;
EnhancedCachingExecutor 主要做以下几件事:
1. 每当有Executor执行query操作时,
1.1 记录下该查询StatementId和CacheKey,然后将其添加到 EnhancedCachingManager 中;
1.2 记录下该查询StatementId 和此StatementId所属Mapper内的Cache缓存对象引用,添加到 EnhancedCachingManager 中;
2. 每当Executor执行了update操作时,将此 update操作的StatementId传递给 EnhancedCachingManager ,让 EnhancedCachingManager 根据此update的StatementId的配置,去清空指定的查询语句所产生的缓存;
另一个构件: EnhancedCachingManager ,它也是本插件的核心,它维护着以下几样东西:
1. 整个MyBatis的所有查询所产生的CacheKey集合(以statementId分类);
2. 所有的使用过了的查询的statementId 及其对应的Cache缓存对象的引用;
3. update类型的StatementId和查询StatementId集合的映射,用于当Update类型的语句执行时,根据此映射决定应该清空哪些查询语句产生的缓存;
如下图所示:
工作原理:
原理很简单,就是 当执行了某个update操作时,根据配置信息去清空指定的查询语句在Cache中所产生的缓存数据。
如何获取mybatis-enhanced-cache插件源码
1. 源码和jar包2合一压缩包
2. github 地址,直接fork即可:
https://github.com/LuanLouis/mybatis-enhanced-cache
4. mybatis-enhanced-cache 插件的使用实例:
1. 下载 mybatis-enhanced-cache.rar压缩包 ,解压,将其内的mybatis-enhanced-cache-0.0.1-SNAPSHOT.jar添加到项目的classpath下;
2. 配置MyBatis配置文件如下:
<plugins>
<plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">
<property name="dependency" value="dependencys.xml"/>
<property name="cacheEnabled" value="true"/>
</plugin>
</plugins>
其中,<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。
3. 配置StatementId之间的依赖关系
<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
<statements>
<statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">
<observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />
</statement>
</statements>
</dependencies>
<statement>节点配置的是更新语句的statementId,其内的子节点<observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点<observer>可以有多个。
如上的配置,则说明,如果"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。
4. 配置DepartmentsMapper.xml 和EmployeesMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" >
<cache></cache>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >
<id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />
<result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />
<result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />
<result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />
</resultMap>
<sql id="Base_Column_List" >
DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID
</sql>
<update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >
update HR.DEPARTMENTS
set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},
MANAGER_ID = #{managerId,jdbcType=DECIMAL},
LOCATION_ID = #{locationId,jdbcType=DECIMAL}
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</update>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from HR.DEPARTMENTS
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.louis.mybatis.dao.EmployeesMapper">
<cache eviction="LRU" flushInterval="100000" size="10000"/>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">
<id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />
<result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />
<result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />
<result column="EMAIL" jdbcType="VARCHAR" property="email" />
<result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />
<result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />
<result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />
<result column="SALARY" jdbcType="DECIMAL" property="salary" />
<result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />
<result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />
<result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />
</resultMap>
<sql id="Base_Column_List">
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,
COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID
</sql>
<select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
select
*
from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID
where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}
</select>
</mapper>
5. 测试代码:
package com.louis.mybatis.test;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import com.louis.mybatis.model.Department;
import com.louis.mybatis.model.Employee;
/**
* SqlSession 简单查询演示类
* @author louluan
*/
public class SelectDemo3 {
private static final Logger loger = Logger.getLogger(SelectDemo3.class);
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
SqlSession sqlSession = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
//3.使用SqlSession查询
Map<String,Object> params = new HashMap<String,Object>();
params.put("employeeId",10);
//a.查询工资低于10000的员工
Date first = new Date();
//第一次查询
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 11);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 12);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 13);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10);
department.setDepartmentName("updated");
sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department);
sqlSession.commit();
checkCacheStatus(sqlSession);
}
public static void checkCacheStatus(SqlSession sqlSession)
{
loger.info("------------Cache Status------------");
Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator();
while(iter.hasNext())
{
String it = iter.next();
loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize());
}
loger.info("------------------------------------");
}
}
结果输出:
结果分析:
从上述的结果可以看出,前四次执行了“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句,EmployeesMapper对应的Cache缓存中存储的结果缓存有1个增加到4个。
当执行了"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"后,EmployeeMapper对应的缓存Cache结果被清空了,即"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"更新语句引起了EmployeeMapper中的" com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments "缓存的清空。
作者的话
该插件的实现周期比较短,尚未经过性能方面的测试,如果果您对此插件有任何意见或者看法,可以留言一起交流和探讨。
该插件源码已经放到了Github上,可供大家自由修改,github地址:
https://github.com/LuanLouis/mybatis-enhanced-cache
yBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。本文将全面分析MyBatis的二级缓存的设计原理。
1.MyBatis的缓存机制整体设计以及二级缓存的工作模式
如上图所示,当开一个会话时,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作, MyBatis 的二级缓存机制的关键就是对这个 Executor 对象做文章。如果用户配置了" cacheEnabled=true ",那么 MyBatis 在为 SqlSession 对象创建 Executor 对象时,会对 Executor 对象加上一个装饰者: CachingExecutor ,这时 SqlSession 使用 CachingExecutor 对象来完成操作请求。 CachingExecutor 对于查询请求,会先判断该查询请求在 Application 级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的 Executor 对象来完成查询操作,之后 CachingExecutor 会将真正 Executor 返回的查询结果放置到缓存中,然后在返回给用户。
CachingExecutor 是 Executor 的装饰者,以增强 Executor 的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,
CachingExecutor 和 Executor 的接口的关系如下类图所示:
2 . MyBatis二级缓存的划分
MyBatis 并不是简单地对整个 Application 就只有一个 Cache 缓存对象,它将缓存划分的更细,即是 Mapper 级别的,即每一个Mapper都可以拥有一个 Cache 对象,具体如下:
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);
b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置)
MyBatis 将 Application 级别的二级缓存细分到 Mapper 级别,即对于每一个 Mapper.xml ,如果在其中使用了 <cache> 节点,则 MyBatis 会为这个 Mapper 创建一个 Cache 缓存对象,如下图所示:
注: 上述的每一个Cache对象,都会有一个自己所属的namespace命名空间,并且会将Mapper的 namespace作为它们的ID;
b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置)
如果你想让多个 Mapper 公用一个 Cache 的话,你可以使用 <cache-ref namespace=""> 节点,来指定你的这个 Mapper 使用到了哪一个 Mapper 的 Cache 缓存。
3. 使用二级缓存,必须要具备的条件
MyBatis 对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在 Mapper 中配置了 <cache> ,并且为此 Mapper 分配了 Cache 对象,这并不表示我们使用 Mapper 中定义的查询语句查到的结果都会放置到 Cache 对象之中,我们必须指定 Mapper 中的某条选择语句是否支持缓存,即如下所示,在 <select> 节点中配置 useCache="true" , Mapper 才会对此 Select 的查询支持缓存特性,否则,不会对此 Select 查询,不会经过 Cache 缓存。如下所示, Select 语句配置了 useCache="true" ,则表明这条 Select 语句的查询会使用二级缓存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
总之,要想使某条 Select 查询支持二级缓存,你需要保证:
1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效
3. 该select语句的参数 useCache=true
4. 一级缓存和二级缓存的使用顺序
请注意,如果你的 MyBatis 使用了二级缓存,并且你的 Mapper 和 select 语句也配置使用了二级缓存,那么在执行 select 查询的时候, MyBatis 会先从二级缓存中取输入,其次才是一级缓存,即 MyBatis 查询数据的顺序是:
二级缓存 ———> 一级缓存——> 数据库
5. 二级缓存实现的选择
MyBatis 对二级缓存的设计非常灵活,它自己内部实现了一系列的 Cache 缓存实现类,并提供了各种缓存刷新策略如 LRU,FIFO 等等;另外, MyBatis 还允许用户自定义 Cache 接口实现,用户是需要实现 org.apache.ibatis.cache.Cache 接口,然后将 Cache 实现类配置在 <cache type=""> 节点的 type 属性上即可;除此之外, MyBatis 还支持跟第三方内存缓存库如 Memecached 的集成,总之,使用 MyBatis 的二级缓存有三个选择:
1.MyBatis自身提供的缓存实现;
2. 用户自定义的Cache接口实现;
3.跟第三方内存缓存库的集成;
6. MyBatis自身提供的二级缓存的实现
MyBatis 自身提供了丰富的,并且功能强大的二级缓存的实现,它拥有一系列的 Cache 接口装饰者,可以满足各种对缓存操作和更新的策略。
MyBatis 定义了大量的 Cache 的装饰器来增强 Cache 缓存的功能,如下类图所示。
对于每个 Cache 而言,都有一个容量限制, MyBatis 各供了各种策略来对 Cache 缓存的容量进行控制,以及对 Cache 中的数据进行刷新和置换。 MyBatis 主要提供了以下几个刷新和置换策略:
LRU:(Least Recently Used) ,最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近做少被使用的缓存记录清除掉,然后添加新的记录;
FIFO:(First in first out) ,先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉;
Scheduled :指定时间间隔清空算法,该算法会以指定的某一个时间间隔将 Cache 缓存中的数据清空;
6. 写在后面(关于涉及到的设计模式)
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。关于装饰者模式,读者可以阅读相关资料,我的另外一篇博文 Java 设计模式 装饰者模式 供读者参考。
本文只是讲述MyBatis二级缓存的基本原理,关于自定义二级缓存和与第三方内存库的集成,将在后续的文章中再做讨论,敬请关注!
作者的话
本文是《深入理解mybatis原理》系列的其中一篇,如果您有兴趣,请关注该系列的其他文章~
一级缓存是基于 PerpetualCache(mybatis自带)的 HashMap 本地缓存,作用范围为session,所以当session commit或close后,缓存就会被清空
二级缓存默认也是基于 PerpetualCache,但是可以为其制定存储源,比如ehcache
一级缓存缓存的是SQL语句,而二级缓存缓存的是结果对象,看如下例子(mybatis的日志级别设为debug)
?
1
2
3
4
5
6
7
8
List<User> users = sqlSession.selectList("com.my.mapper.UserMapper.getUser", "jack");
System.out.println(users);
//sqlSession.commit();①
List<User> users2 = sqlSession.selectList("com.my.mapper.UserMapper.getUser", "jack");//②admin
System.out.println(users);
结果是只发起一次SQL语句,如果我们把②出的参数jack改为admin,发现还是只发起一次SQL语句,但是会设置不同参数
如果把①处去掉注释,会发现不会有缓存了
下面就来启用二级缓存
在配置文件中启用二级缓存
?
1
<setting name="cacheEnabled" value="true" />
在需要进行缓存的mapper文件UserMapper.xml中加上
?
1
<cache readOnly="true"></cache>
注意这里的readOnly设为true,默认是false,表示结果集对象需要被序列化
我们打开①处注释,②处仍然使用jack,我们发现结果只执行了一次SQL语句
但是如果把②处改为admin,执行了2次SQL语句,这说明二级缓存是缓存结果集对象的
下面我们来使用ehcache
在classpath下添加ehcache.xml
在UserMapper.xml中添加:
?
1
2
<!-- <cache readOnly="true" type="org.mybatis.caches.ehcache.LoggingEhcache"/> -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
用上面那个会输出更加详细的日志,下面的不会
需要用到ehcache.jar,下载地址:http://sourceforge.net/projects/ehcache/files/ehcache/ehcache-2.7.0/ehcache-2.7.0-distribution.tar.gz/download
mybatis-ehcache.jar下载地址:http://code.google.com/p/mybatis/downloads/detail?name=mybatis-ehcache-1.0.2-SNAPSHOT-bundle.zip&can=3&q=Product%3DCache
--尽量避免使用二级缓存技术,多表操作 的业务场景下
一、创建Cache的完整过程
我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
然后是:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
看parser.parse()方法:
parseConfiguration(parser.evalNode("/configuration"));
看处理Mapper.xml文件的位置:
mapperElement(root.evalNode("mappers"));
看处理Mapper.xml的XMLMapperBuilder:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration,
resource, configuration.getSqlFragments());
mapperParser.parse();
继续看parse方法:
configurationElement(parser.evalNode("/mapper"));
到这里:
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
从这里看到namespace就是xml中<mapper>元素的属性。然后下面是先后处理的cache-ref和cache,后面的cache会覆盖前面的cache-ref,但是如果一开始cache-ref没有找到引用的cache,他就不会被覆盖,会一直到最后处理完成为止,最后如果存在cache,反而会被cache-ref覆盖。这里是不是看着有点晕、有点乱?所以千万别同时配置这两个,实际上也很少有人会这么做。
看看MyBatis如何处理<cache/>:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
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);
}
}
从源码可以看到MyBatis读取了那些属性,而且很容易可以到这些属性的默认值。
创建Java的cache对象方法为builderAssistant.useNewCache,我们看看这段代码:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
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)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
从调用该方法的地方,我们可以看到并没有使用返回值cache,在后面的过程中创建MappedStatement的时候使用了currentCache。
二、使用Cache过程
在系统中,使用Cache的地方在CachingExecutor中:
@Override
public <E> List<E> query(
MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
获取cache后,先判断是否有二级缓存。
只有通过<cache/>,<cache-ref/>或@CacheNamespace,@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。
if (cache != null) {
如果cache存在,那么会根据sql配置(<insert>,<select>,<update>,<delete>的flushCache属性来确定是否清空缓存。
flushCacheIfRequired(ms);
然后根据xml配置的属性useCache来判断是否使用缓存(resultHandler一般使用的默认值,很少会null)。
if (ms.isUseCache() && resultHandler == null) {
确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错。
ensureNoOutParams(ms, parameterObject, boundSql);
没有问题后,就会从cache中根据key来取值:
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
如果没有缓存,就会执行查询,并且将查询结果放到缓存中。
if (list == null) {
list = delegate.<E>query(ms, parameterObject,
rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
返回结果
return list;
}
}
没有缓存时,直接执行查询
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在上面的代码中tcm.putObject(cache, key, list);这句代码是缓存了结果。但是实际上直到sqlsession关闭,MyBatis才以序列化的形式保存到了一个Map(默认的缓存配置)中。
三、Cache使用时的注意事项
1. 只能在【只有单表操作】的表上使用缓存
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。
2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
这一点不需要多说,所有人都应该清楚。记住,这一点需要保证在1的前提下才可以!
四、避免使用二级缓存
可能会有很多人不理解这里,二级缓存带来的好处远远比不上他所隐藏的危害。
缓存是以namespace为单位的,不同namespace下的操作互不影响。
insert,update,delete操作会清空所在namespace下的全部缓存。
通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。
为什么避免使用二级缓存
在符合【Cache使用时的注意事项】的要求时,并没有什么危害。
其他情况就会有很多危害了。
针对一个表的某些操作不在他独立的namespace下进行。
例如在UserMapper.xml中有大多数针对user表的操作。但是在一个XXXMapper.xml中,还有针对user单表的操作。
这会导致user在两个命名空间下的数据不一致。如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单表查询,使用缓存的结果可能会不正确。
更危险的情况是在XXXMapper.xml做了insert,update,delete操作时,会导致UserMapper.xml中的各种操作充满未知和风险。
有关这样单表的操作可能不常见。但是你也许想到了一种常见的情况。
多表操作一定不能使用缓存
为什么不能?
首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。
例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO">
select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>
1
2
3
像上面这个查询,你会写到那个xml中呢??
不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
这点应该很容易理解。
在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。
如果你让他们都使用同一个namespace(通过<cache-ref>)来避免脏数据,那就失去了缓存的意义。
看到这里,实际上就是说,二级缓存不能用。整篇文章介绍这么多也没什么用了。
五、挽救二级缓存?
想更高效率的使用二级缓存是解决不了了。
但是解决多表操作避免脏数据还是有法解决的。解决思路就是通过拦截器判断执行的sql涉及到那些表(可以用jsqlparser解析),然后把相关表的缓存自动清空。但是这种方式对缓存的使用效率是很低的。
设计这样一个插件是相当复杂的,既然我没想着去实现,就不废话了。
最后还是建议,放弃二级缓存,在业务层使用可控制的缓存代替更好。
如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)
时间 2014-12-09 11:31:00 CSDN博客
原文
http://blog.csdn.net/luanlouis/article/details/41800511
主题 MyBatis 数据库
本文如下组织结构:
一个关于MyBatis的二级缓存的实际问题
当前MyBatis二级缓存的工作机制
mybatis-enhanced-cache插件的设计和工作原理
mybatis-enhanced-cache 插件的使用实例
1.一个关于MyBatis的二级缓存的实际问题
网友 chanfish 给我抛出的问题
现有 AMapper.xml 中定义了对数据库表 ATable 的CRUD操作,BMapper定义了对数据库表 BTable 的CRUD操作;
假设MyBatis的二级缓存开启,并且 AMapper 中使用了二级缓存, AMapper 对应的二级缓存为 ACache ;
除此之外, AMapper 中还定义了一个跟 BTable 有关的查询语句,类似如下所述:
<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">
select * from ATable left join BTable on ....
</select>
执行以下操作:
1. 执行 AMapper 中的" selectATableWithJoin " 操作,此时会将查询到的结果放置到 AMapper 对应的二级缓存 ACache 中;
2. 执行 BMapper 中对 BTable 的更新操作( update、delete、insert )后, BTable 的数据更新;
3. 再执行1完全相同的查询,这时候会直接从 AMapper 二级缓存 ACache 中取值,将 ACache 中的值直接返回;
好,问题就出现在第3步上:
由于AMapper的“ selectATableWithJoin ” 对应的SQL语句需要和 BTable 进行join查找,而在第 2 步 BTable 的数据已经更新了,但是第 3 步查询的值是第 1 步的缓存值,已经极有可能跟真实数据库结果不一样,即 ACache 中缓存数据过期了!
总结来看,就是:
对于某些使用了 join连接的查询,如果其关联的表数据发生了更新,join连接的查询由于先前缓存的原因,导致查询结果和真实数据不同步;
从MyBatis的角度来看,这个问题可以这样表述:
对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
当前的MyBatis的缓存机制不能很好地处理这一问题,下面我们将从当前的MyBatis的缓存机制入手,分析这一问题:
2. 当前MyBatis二级缓存的工作机制:
当前MyBatis二级缓存的工作机制:
MyBatis二级缓存的一个重要特点:即松散的Cache缓存管理和维护。
一个Mapper中定义的增删改查操作只能影响到自己关联的Cache对象。如上图所示的Mapper namespace1中定义的若干CRUD语句,产生的缓存只会被放置到相应关联的Cache1中,即Mapper namespace2,namespace3,namespace4 中的CRUD的语句不会影响到Cache1。
可以看出,Mapper之间的缓存关系比较松散,相互关联的程度比较弱。
现在再回到上面描述的问题,如果我们将AMapper和BMapper共用一个Cache对象,那么,当BMapper执行更新操作时,可以清空对应Cache中的所有的缓存数据,这样的话,数据不是也可以保持最新吗?
确实这个也是一种解决方案,不过,它会使缓存的使用效率变的很低!AMapper和BMapper的任意的更新操作都会将共用的Cache清空,会频繁地清空Cache,导致Cache实际的命中率和使用率就变得很低了,所以这种策略实际情况下是不可取的。
最理想的解决方案就是:
对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
这样,就是以很细的粒度管理MyBatis内部的缓存,使得缓存的使用率和准确率都能大大地提升。
基于这个思路,我写了一个对应的mybatis-enhanced-cache 缓存插件,可以很好地支持上述的功能。
对于上述的例子中,该插件可以实现:当BMapper对BTable执行了更新操作时,指定清除与BTable相关联的selectATableWithJoin查询语句在ACache中产生的缓存。
接下来就来看看这个mybatis-enhanced-cache插件的设计原理吧:
3. mybatis-enhanced-cache插件的设计和工作原理
mybatis-enhanced-cache插件的设计和工作原理
该插件主要由两个构件组成: EnhancedCachingExecutor 和 EnhancedCachingManager 。
EnhancedCachingExecutor 是针对于Executor的拦截器,拦截Executor的几个关键的方法;
EnhancedCachingExecutor 主要做以下几件事:
1. 每当有Executor执行query操作时,
1.1 记录下该查询StatementId和CacheKey,然后将其添加到 EnhancedCachingManager 中;
1.2 记录下该查询StatementId 和此StatementId所属Mapper内的Cache缓存对象引用,添加到 EnhancedCachingManager 中;
2. 每当Executor执行了update操作时,将此 update操作的StatementId传递给 EnhancedCachingManager ,让 EnhancedCachingManager 根据此update的StatementId的配置,去清空指定的查询语句所产生的缓存;
另一个构件: EnhancedCachingManager ,它也是本插件的核心,它维护着以下几样东西:
1. 整个MyBatis的所有查询所产生的CacheKey集合(以statementId分类);
2. 所有的使用过了的查询的statementId 及其对应的Cache缓存对象的引用;
3. update类型的StatementId和查询StatementId集合的映射,用于当Update类型的语句执行时,根据此映射决定应该清空哪些查询语句产生的缓存;
如下图所示:
工作原理:
原理很简单,就是 当执行了某个update操作时,根据配置信息去清空指定的查询语句在Cache中所产生的缓存数据。
如何获取mybatis-enhanced-cache插件源码
1. 源码和jar包2合一压缩包
2. github 地址,直接fork即可:
https://github.com/LuanLouis/mybatis-enhanced-cache
4. mybatis-enhanced-cache 插件的使用实例:
1. 下载 mybatis-enhanced-cache.rar压缩包 ,解压,将其内的mybatis-enhanced-cache-0.0.1-SNAPSHOT.jar添加到项目的classpath下;
2. 配置MyBatis配置文件如下:
<plugins>
<plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">
<property name="dependency" value="dependencys.xml"/>
<property name="cacheEnabled" value="true"/>
</plugin>
</plugins>
其中,<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。
3. 配置StatementId之间的依赖关系
<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
<statements>
<statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">
<observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />
</statement>
</statements>
</dependencies>
<statement>节点配置的是更新语句的statementId,其内的子节点<observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点<observer>可以有多个。
如上的配置,则说明,如果"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。
4. 配置DepartmentsMapper.xml 和EmployeesMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" >
<cache></cache>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >
<id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />
<result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />
<result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />
<result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />
</resultMap>
<sql id="Base_Column_List" >
DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID
</sql>
<update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >
update HR.DEPARTMENTS
set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},
MANAGER_ID = #{managerId,jdbcType=DECIMAL},
LOCATION_ID = #{locationId,jdbcType=DECIMAL}
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</update>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from HR.DEPARTMENTS
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.louis.mybatis.dao.EmployeesMapper">
<cache eviction="LRU" flushInterval="100000" size="10000"/>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">
<id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />
<result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />
<result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />
<result column="EMAIL" jdbcType="VARCHAR" property="email" />
<result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />
<result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />
<result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />
<result column="SALARY" jdbcType="DECIMAL" property="salary" />
<result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />
<result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />
<result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />
</resultMap>
<sql id="Base_Column_List">
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,
COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID
</sql>
<select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
select
*
from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID
where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}
</select>
</mapper>
5. 测试代码:
package com.louis.mybatis.test;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import com.louis.mybatis.model.Department;
import com.louis.mybatis.model.Employee;
/**
* SqlSession 简单查询演示类
* @author louluan
*/
public class SelectDemo3 {
private static final Logger loger = Logger.getLogger(SelectDemo3.class);
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
SqlSession sqlSession = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
//3.使用SqlSession查询
Map<String,Object> params = new HashMap<String,Object>();
params.put("employeeId",10);
//a.查询工资低于10000的员工
Date first = new Date();
//第一次查询
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 11);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 12);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 13);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10);
department.setDepartmentName("updated");
sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department);
sqlSession.commit();
checkCacheStatus(sqlSession);
}
public static void checkCacheStatus(SqlSession sqlSession)
{
loger.info("------------Cache Status------------");
Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator();
while(iter.hasNext())
{
String it = iter.next();
loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize());
}
loger.info("------------------------------------");
}
}
结果输出:
结果分析:
从上述的结果可以看出,前四次执行了“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句,EmployeesMapper对应的Cache缓存中存储的结果缓存有1个增加到4个。
当执行了"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"后,EmployeeMapper对应的缓存Cache结果被清空了,即"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"更新语句引起了EmployeeMapper中的" com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments "缓存的清空。
作者的话
该插件的实现周期比较短,尚未经过性能方面的测试,如果果您对此插件有任何意见或者看法,可以留言一起交流和探讨。
该插件源码已经放到了Github上,可供大家自由修改,github地址:
https://github.com/LuanLouis/mybatis-enhanced-cache
yBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。本文将全面分析MyBatis的二级缓存的设计原理。
1.MyBatis的缓存机制整体设计以及二级缓存的工作模式
如上图所示,当开一个会话时,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作, MyBatis 的二级缓存机制的关键就是对这个 Executor 对象做文章。如果用户配置了" cacheEnabled=true ",那么 MyBatis 在为 SqlSession 对象创建 Executor 对象时,会对 Executor 对象加上一个装饰者: CachingExecutor ,这时 SqlSession 使用 CachingExecutor 对象来完成操作请求。 CachingExecutor 对于查询请求,会先判断该查询请求在 Application 级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的 Executor 对象来完成查询操作,之后 CachingExecutor 会将真正 Executor 返回的查询结果放置到缓存中,然后在返回给用户。
CachingExecutor 是 Executor 的装饰者,以增强 Executor 的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,
CachingExecutor 和 Executor 的接口的关系如下类图所示:
2 . MyBatis二级缓存的划分
MyBatis 并不是简单地对整个 Application 就只有一个 Cache 缓存对象,它将缓存划分的更细,即是 Mapper 级别的,即每一个Mapper都可以拥有一个 Cache 对象,具体如下:
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);
b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置)
MyBatis 将 Application 级别的二级缓存细分到 Mapper 级别,即对于每一个 Mapper.xml ,如果在其中使用了 <cache> 节点,则 MyBatis 会为这个 Mapper 创建一个 Cache 缓存对象,如下图所示:
注: 上述的每一个Cache对象,都会有一个自己所属的namespace命名空间,并且会将Mapper的 namespace作为它们的ID;
b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置)
如果你想让多个 Mapper 公用一个 Cache 的话,你可以使用 <cache-ref namespace=""> 节点,来指定你的这个 Mapper 使用到了哪一个 Mapper 的 Cache 缓存。
3. 使用二级缓存,必须要具备的条件
MyBatis 对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在 Mapper 中配置了 <cache> ,并且为此 Mapper 分配了 Cache 对象,这并不表示我们使用 Mapper 中定义的查询语句查到的结果都会放置到 Cache 对象之中,我们必须指定 Mapper 中的某条选择语句是否支持缓存,即如下所示,在 <select> 节点中配置 useCache="true" , Mapper 才会对此 Select 的查询支持缓存特性,否则,不会对此 Select 查询,不会经过 Cache 缓存。如下所示, Select 语句配置了 useCache="true" ,则表明这条 Select 语句的查询会使用二级缓存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
总之,要想使某条 Select 查询支持二级缓存,你需要保证:
1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效
3. 该select语句的参数 useCache=true
4. 一级缓存和二级缓存的使用顺序
请注意,如果你的 MyBatis 使用了二级缓存,并且你的 Mapper 和 select 语句也配置使用了二级缓存,那么在执行 select 查询的时候, MyBatis 会先从二级缓存中取输入,其次才是一级缓存,即 MyBatis 查询数据的顺序是:
二级缓存 ———> 一级缓存——> 数据库
5. 二级缓存实现的选择
MyBatis 对二级缓存的设计非常灵活,它自己内部实现了一系列的 Cache 缓存实现类,并提供了各种缓存刷新策略如 LRU,FIFO 等等;另外, MyBatis 还允许用户自定义 Cache 接口实现,用户是需要实现 org.apache.ibatis.cache.Cache 接口,然后将 Cache 实现类配置在 <cache type=""> 节点的 type 属性上即可;除此之外, MyBatis 还支持跟第三方内存缓存库如 Memecached 的集成,总之,使用 MyBatis 的二级缓存有三个选择:
1.MyBatis自身提供的缓存实现;
2. 用户自定义的Cache接口实现;
3.跟第三方内存缓存库的集成;
6. MyBatis自身提供的二级缓存的实现
MyBatis 自身提供了丰富的,并且功能强大的二级缓存的实现,它拥有一系列的 Cache 接口装饰者,可以满足各种对缓存操作和更新的策略。
MyBatis 定义了大量的 Cache 的装饰器来增强 Cache 缓存的功能,如下类图所示。
对于每个 Cache 而言,都有一个容量限制, MyBatis 各供了各种策略来对 Cache 缓存的容量进行控制,以及对 Cache 中的数据进行刷新和置换。 MyBatis 主要提供了以下几个刷新和置换策略:
LRU:(Least Recently Used) ,最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近做少被使用的缓存记录清除掉,然后添加新的记录;
FIFO:(First in first out) ,先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉;
Scheduled :指定时间间隔清空算法,该算法会以指定的某一个时间间隔将 Cache 缓存中的数据清空;
6. 写在后面(关于涉及到的设计模式)
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。关于装饰者模式,读者可以阅读相关资料,我的另外一篇博文 Java 设计模式 装饰者模式 供读者参考。
本文只是讲述MyBatis二级缓存的基本原理,关于自定义二级缓存和与第三方内存库的集成,将在后续的文章中再做讨论,敬请关注!
作者的话
本文是《深入理解mybatis原理》系列的其中一篇,如果您有兴趣,请关注该系列的其他文章~
相关推荐
在使用Mybatis缓存时,需要注意以下几点: 1. 数据一致性:由于缓存的存在,可能会导致数据的延迟更新,因此在处理实时性要求高的业务时,需要谨慎使用缓存。 2. 缓存命中率:合理的缓存策略可以提高缓存的利用率,...
"深入理解MyBatis中的一级缓存与二级缓存" MyBatis是一种流行的持久层框架,它提供了缓存机制来提高应用程序的性能。在MyBatis中,有两种类型的缓存:一级缓存和二级缓存。下面我们将深入了解MyBatis中的一级缓存和...
Mybatis 是一款流行的Java持久层框架,它简化了数据库操作,通过XML或注解的方式将SQL与Java代码...通过这个测试示例,开发者不仅可以学习到Mybatis缓存的基本使用,还能了解到如何在实际项目中对其进行测试和调优。
总的来说,《Mybatis缓存开源架构源码2021》这份文档很可能会涵盖 MyBatis 缓存体系的各个方面,包括一级缓存和二级缓存的使用、配置、源码解析以及实际开发中的注意事项。对于想深入了解 MyBatis 缓存机制的开发者...
Mybatis 是一款流行的Java持久层框架,它简化了数据库操作,通过XML或注解的方式将SQL...本教程的17页内容将会详尽地讲解Mybatis缓存的各个方面,包括配置、使用示例以及最佳实践,帮助开发者更好地掌握这一关键特性。
首先,我们要理解MyBatis的缓存分为一级缓存和二级缓存。一级缓存是SqlSession级别的,也被称为本地缓存,它存储在SqlSession对象内部。当我们在同一个SqlSession内执行相同的SQL语句时,MyBatis会优先从一级缓存中...
至于"工具"标签,可能是指使用一些辅助工具来管理和查看MyBatis的缓存,比如通过日志监控、性能分析工具等,这些可以帮助我们更好地理解和调整缓存的性能。 在文件列表"mybatis-04"中,可能包含了MyBatis的配置文件...
首先,理解MyBatis的缓存机制。MyBatis提供了两级缓存:一级缓存是SqlSession级别的,存在于SqlSessionFactory内部,而二级缓存是全局的,可以在多个SqlSession之间共享。默认情况下,一级缓存是开启的,但二级缓存...
4. **在Mapper接口和XML中使用缓存**:在MyBatis的Mapper接口中,使用`@CacheNamespace`注解标记整个接口,以启用缓存。然后在具体的查询方法上使用`@CacheNamespaceRef`注解,指定使用哪个缓存区域。在对应的Mapper...
MyBatis 是一款深受开发者喜爱的 Java 持久层框架,它简化了数据库操作,提供了 SQL 映射功能和对象关系映射。...通过 `mybatis-test` 相关的测试代码,开发者可以深入理解 MyBatis 缓存机制并优化其使用。
本篇笔记将深入探讨MyBatis的缓存机制,包括一级缓存和二级缓存的概念、工作原理、配置与使用。 一级缓存是SqlSession级别的缓存,每当执行一个SQL查询时,如果结果不在缓存中,MyBatis会将其放入一级缓存。当同一...
标题中的“mybatis二级缓存”指的是MyBatis框架中的一个重要特性,它是MyBatis缓存机制的一部分,用于提升数据库查询效率。MyBatis一级缓存是SqlSession级别的,而二级缓存则是在整个Mapper配置范围内的全局缓存,...
MyBatis 是一款优秀的持久层框架,它支持...理解并合理运用 MyBatis 的缓存机制,能显著提高应用程序的性能,减少对数据库的访问压力。在实际开发中,要根据项目需求和环境来调整缓存策略,确保系统的稳定性和高效性。
总结,MyBatis二级缓存是一项实用的性能优化技术,但使用时需注意其工作原理和潜在的问题,合理配置和管理才能发挥最大效能。通过理解和实践,我们可以更好地掌握这一工具,提高应用的运行效率。
通过对MyBatis缓存机制的深入理解,开发者可以更有效地利用缓存提升应用性能,同时避免并发控制和数据一致性的问题。通过自定义缓存策略,还可以根据项目需求调整缓存的行为,比如使用更复杂的排除算法或调整缓存...
在使用MyBatis时,我们需要理解其核心组件和工作原理,以便更好地进行数据库操作。 一、MyBatis核心配置文件 MyBatis的核心配置文件通常命名为`mybatis-config.xml`,它是整个MyBatis系统的总配置文件,包含了数据...
6. **缓存策略**: Hibernate的二级缓存可以用于提高数据访问速度,而MyBatis可以通过插件实现自己的缓存机制,两者结合可以构建更高效的缓存体系。 7. **错误调试**: MyBatis的SQL日志记录功能可以帮助开发者快速...
**一、MyBatis缓存概念** MyBatis的缓存分为一级缓存和二级缓存。一级缓存是SqlSession级别的,同一个SqlSession内的相同SQL语句不会重复执行,而是直接从缓存中获取结果。然而,一级缓存存在生命周期问题,当...
总之,Mybatis的一级缓存和二级缓存是提高数据访问效率的有效手段,但正确理解和使用它们至关重要,避免因不当配置引发的问题。通过深入理解缓存机制并结合实际业务需求,我们可以合理地利用缓存,提升系统的整体...
开发者可以通过阅读和运行这个项目,来理解和学习如何在实际项目中实现MyBatis和Redis的二级缓存功能。 总结来说,"mybatis+redis实现二级缓存"项目展示了如何利用Redis的高性能缓存特性增强MyBatis的二级缓存,...