MyBatis 延迟加载、一级缓存、二级缓存(详解)

时间:2021-08-18 11:54:24

使用orm框架我们更多的是使用其查询功能,那么查询海量数据则又离不开性能,那么这篇中我们就看下mybatis高级应用之延迟加载一级缓存二级缓存。使用时需要注意延迟加载必须使用resultmap,resulttype不具有延迟加载功能。

一、延迟加载

延迟加载已经是老生常谈的问题,什么最大化利用数据库性能之类之类的,也懒的列举了,总是我一提到延迟加载脑子里就会想起来了hibernate get和load的区别。ok,废话少说,直接看代码。 先来修改配置项xml。

注意,编写mybatis.xml时需要注意配置节点的先后顺序,settings在最前面,否则会报错。

?
1
2
3
4
<settings>
  <setting name="lazyloadingenabled" value="true"/>
  <setting name="aggressivelazyloading" value="false"/>
 </settings>

MyBatis 延迟加载、一级缓存、二级缓存(详解)

前面提到延迟加载只能通过association、collection来实现,因为只有存在关联关系映射的业务场景里你才需要延迟加载,也叫懒加载,也就是常说的用的时候再去加载。ok,那么我们来配一个association来实现:

我来编写一个加载博客列表的同时加载出博客额作者, 主要功能点在id为blogauthorresumtmap这个resultmap上,其中使用了association,关键点是它的select属性,该属性也就是你需要懒加载调用的statment id。 当然需要懒加载的statement 返回值当然是resultmap

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<resultmap id="blogauthorresumtmap" type="blog">
  <id column="id" property="id"/>
  <result column="title" property="title"/>
  <result column="category" property="category"/>
  <result column="author_id" property="author_id"/>
  <!--使用assocition支持延迟加载功能,配置延迟加载关联关系-->
  <association property="author" javatype="author" select="selectauthorbyid" column="author_id"/>
 </resultmap>
 
 <!--要使用延迟记载的方法-->
 <select id="selectblogauthor" resultmap="blogauthorresumtmap">
  select id,title,category,author_id from t_blog
 </select>
 
 <!--延迟加载查询博客对应的作者方法-->
 <select id="selectauthorbyid" parametertype="int" resulttype="author">
  select id,name from t_author where id=#{value}
 </select>

ok,来看测试结果:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@test
 public void getblogauthorbylazyloading(){
  sqlsession sqlsession=null;
  try{
   sqlsession=sqlsessionfactory.opensession();
   list<blog> list = sqlsession.selectlist("com.autohome.mapper.author.selectblogauthor");
 
   for (blog blog:list) {
    system.out.println("id:"+blog.getid()+",title:"+blog.gettitle()+",category:"+blog.getcategory());
    system.out.println("author:"+blog.getauthor().getname());
   }
 
  }catch(exception e){
   e.printstacktrace();
  }finally {
   sqlsession.close();
  }
 }

  MyBatis 延迟加载、一级缓存、二级缓存(详解)

MyBatis 延迟加载、一级缓存、二级缓存(详解)

从图一中看出,执行selectblogauthor返回list<blog>对象时只执行了sql select id,title,category,author_id from t_blog,循环遍历时才去执行select id,name from t_author where id=?。

二、一级缓存

了解缓存前我们先看一张图片(图片来源于传智播客视频图片)。从图中可以了解一级缓存是sqlsession级别、二级缓存是mapper级别。在操作数据库时我们需要先构造sqlsession【默认实现是defaultsqlsession.java】,在对象中有一个数据结构【hashmap】来存储缓存数据。不同的sqlsession区域是互不影响的。 如果同一个sqlsession之间,如果多次查询之间执行了commit,则缓存失效,mybatis避免脏读。

MyBatis 延迟加载、一级缓存、二级缓存(详解)

ok,在看mybatis一级缓存时,我总是觉的一级缓存有点鸡肋,两个查询如果得到一样的数据,你还会执行第二次么,果断引用第一次的返回值了。 可能还没了解到一级缓存的奥妙之处。一级缓存默认是开启的,不需要额外设置,直接使用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void testcache(){
  sqlsession sqlsession=null;
  try{
   sqlsession=sqlsessionfactory.opensession();
 
   author author = sqlsession.selectone("com.autohome.mapper.author.selectauthorbyid",1);
   system.out.println("作者信息 id:"+author.getid()+",name:"+author.getname());
 
   author = sqlsession.selectone("com.autohome.mapper.author.selectauthorbyid",1);
   system.out.println("作者信息2 id:"+author.getid()+",name:"+author.getname());
 
  }catch(exception e){
   e.printstacktrace();
  }finally {
   sqlsession.close();
  }
 }

MyBatis 延迟加载、一级缓存、二级缓存(详解)

从debug截图来看,当我们第一次调用方法时执行了select id,name from t_author where id=? 此时缓存中还没有该数据,则执行数据库查询,当再次执行时直接从缓存中读取。

执行demo后我们来看下这个查询过程保存到缓存的源码,先看下defaultsqlsession.java。我们调用的selectone(),从代码中看它是直接调用selectlist()然后判断返回值size大小。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@override
 public <t> t selectone(string statement) {
 return this.<t>selectone(statement, null);
 }
 
 @override
 public <t> t selectone(string statement, object parameter) {
 // popular vote was to return null on 0 results and throw exception on too many.
 list<t> list = this.<t>selectlist(statement, parameter);
 if (list.size() == 1) {
  return list.get(0);
 } else if (list.size() > 1) {
  throw new toomanyresultsexception("expected one result (or null) to be returned by selectone(), but found: " + list.size());
 } else {
  return null;
 }
 }

再跟踪到selectlist方法,看到先构造mappedstatement对象,然后看到真正执行query()的是一个executor对象,在defaultsqlsession.java中executor是成员变量,再翻到org.apache.ibatis.executor包中看到executor实际是一个接口。ok,那么我们debug时发现其引用是cachingexecutor。再打开cachingexecutor.java

?
1
2
3
4
5
6
7
8
9
10
11
@override
 public <e> list<e> selectlist(string statement, object parameter, rowbounds rowbounds) {
 try {
  mappedstatement ms = configuration.getmappedstatement(statement);
  return executor.query(ms, wrapcollection(parameter), rowbounds, executor.no_result_handler);
 } catch (exception e) {
  throw exceptionfactory.wrapexception("error querying database. cause: " + e, e);
 } finally {
  errorcontext.instance().reset();
 }
 }

从cachingexecutor.java的两个query()可以看到先去构造cachekey 再调用抽象类baseexecutor.query(),这个也是最关键的一步。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//先创建cachekey
public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception {
 boundsql boundsql = ms.getboundsql(parameterobject);
 cachekey key = createcachekey(ms, parameterobject, rowbounds, boundsql);
 return query(ms, parameterobject, rowbounds, resulthandler, key, 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) {
  flushcacheifrequired(ms);
  if (ms.isusecache() && resulthandler == null) {
  ensurenooutparams(ms, parameterobject, boundsql);
  @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);
 }

baseexecutor.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 (closed) {
 throw new executorexception("executor was closed.");
 }
 if (querystack == 0 && ms.isflushcacherequired()) {
 clearlocalcache();
 }
 list<e> list;
 try {
 querystack++;
 list = resulthandler == null ? (list<e>) localcache.getobject(key) : null;
 if (list != null) {
  handlelocallycachedoutputparameters(ms, key, parameter, boundsql);
 } else {
  list = queryfromdatabase(ms, parameter, rowbounds, resulthandler, key, boundsql);
 }
 } finally {
 querystack--;
 }
 if (querystack == 0) {
 for (deferredload deferredload : deferredloads) {
  deferredload.load();
 }
 // issue #601
 deferredloads.clear();
 if (configuration.getlocalcachescope() == localcachescope.statement) {
  // issue #482
  clearlocalcache();
 }
 }
 return list;
}

再看其中关键代码queryfromdatabase

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private <e> list<e> queryfromdatabase(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception {
 list<e> 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);
 }
 return list;
 }

ok,看了一长串,终于是有点眉目了,我们看到finally中先删除当前key缓存,然后再调用localcache.putobject把最新的结果集存入hashmap中。

三、二级缓存

了解二级缓存之前先来看副图(图片来自传智播客视频,非本人编写),那么从图中我们可以看出,mybatis二级缓存是mapper级别,也就是说不同的sqlmapper共享不同的内存区域,不同的sqlsession共享同一个内存区域,用mapper的namespace区别内存区域。

MyBatis 延迟加载、一级缓存、二级缓存(详解)

开启mybatis二级缓存: 1、设置mybatis.xml,也就是说mybatis默认二级缓存是关闭的。

2、设置mapper。在mapper.xml内添加标签:<cache/>

3、pojo实现接口serializable。实现该接口后也就说明二级缓存不仅可以存入内存中,还可以存入磁盘。

ok,看一个二级缓存demo:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@test
 public void testcache2(){
  sqlsession sqlsession=null;
  sqlsession sqlsession2=null;
  try{
   sqlsession=sqlsessionfactory.opensession();
   sqlsession2=sqlsessionfactory.opensession();
 
   author author = sqlsession.selectone("com.autohome.mapper.author.selectauthorbyid",1);
   system.out.println("作者信息 id:"+author.getid()+",name:"+author.getname());
   sqlsession.close();
 
   author author2 = sqlsession2.selectone("com.autohome.mapper.author.selectauthorbyid",1);
   system.out.println("作者信息2 id:"+author2.getid()+",name:"+author2.getname());
   sqlsession2.close();
  }catch(exception e){
   e.printstacktrace();
  }finally {
 
  }
 }

运行demo可以看出二级缓存不同的地方在于cache hit ratio,发出sql查询时先看是否命中缓存,第一次则是0.0 ,再次查询时则直接读取缓存数据,命中率是0.5。当然数据结构还是hashmap。

如果数据实时性要求比较高,可以设置select 语句的 

 MyBatis 延迟加载、一级缓存、二级缓存(详解)

如果数据的查询实时性要求比较高,则设置select语句的usecache="false",则每次都直接执行sql。

?
1
2
3
<select id="selectblogauthor" resultmap="blogauthorresumtmap" usecache="false">
  select id,title,category,author_id from t_blog
 </select>

以上这篇mybatis 延迟加载、一级缓存、二级缓存(详解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/sword-successful/p/7400685.html