Mybatis-缓存

abstract

缓存的作用

对于变动不频繁的数据,我们通常使用缓存的方式来将数据存起来,从而减少程序对数据库的访问次数,提升查找效率。

Mybatis一级缓存

一级缓存是Sqlsession级别的。默认开启。
一个Sqlsession对象代表一次数据库会话。一次会话中有可能频繁执行相同查询语句,往往结果短时间内不会变动,对于这种不会变动的数据。Sqlsession会把每次查询的结果放在本地缓存(local cache),当发现相同查询时,直接从本地缓存中取。减少了对数据库的io和数据库的查询。

Mybatis一级缓存相关接口类

SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。
缓存信息也由Executor执行器维护。

MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。SqlSession、Executor、Cache之间的关系如下列类图所示:

  1. BaseExecutor类实现了Executor接口
  2. PerpetualCache类实现了Cache接口
  3. BaseExecutor类的queryFromDatabase等方法可以操纵PerpetualCache类。从而实现了对缓存的操控。

PerpetualCache实现原理其实很简单,其内部就是通过一个简单的HashMap<k,v> 来实现的,没有其他的任何限制。如下是PerpetualCache的实现代码:

MyBatis一级缓存工作流程

  1. 对于某个查询,根据statementId,params,rowBounds等来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
  2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
  3. 如果命中,则直接将缓存结果返回;
  4. 如果没命中:
    1. 去数据库中查询数据,得到查询结果;
    2. 将key和查询到的结果分别作为key,value对存储到Cache中;
    3. 将查询结果返回;

如何判断两次查询是相同的呢

结论

  • 判断[statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值]组成的CacheKey是否相同。

判断依据

  1. 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql;
  2. MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页;
  3. 由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。
  4. 上述的第3个条件正是要求保证传递给JDBC的SQL语句完全一致;第4条则是保证传递给JDBC的参数也完全一致;MyBatis会将上述的SQL中的#{} 转化成?,通过JDBC的PreparedStatement的参数值进行替换,如果参数值相同,则满足自四条

    Mybatis一级缓存的生命周期

    生成
  • 随Sqlsession的创建而创建

消亡

  • SqlSession的close()方法会释放缓存,缓存不可用
  • SqlSession的clearCache()方法会清空缓存中的内容
  • SqlSession执行update()、delete()、insert()方法时,会清空缓存中的内容

Mybatis一级缓存注意事项

对于数据更新频繁的SQL,应该注意Sqlsession的生存时间,或是再mapper文件中禁用一级缓存

Mybatis一级缓存扩展

PerpetualCache类

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
* Copyright 2009-2012 The MyBatis Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache.impl;


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

public class PerpetualCache implements Cache {

private String id;

private Map<Object, Object> cache = new HashMap<Object, Object>();

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

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

public String getId() {
return id;
}

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

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

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

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

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

public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}

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

Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}

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

}

BaseExecutor类的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;
}

Mybatis二级缓存

二级缓存是mapper级别的。