你们好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何将咱们的xml配置文件构建为其内部的Configuration对象和MappedStatement对象的,而后在第二篇咱们说了构建完成后MyBatis是如何一步一步地执行咱们的SQL语句而且对结果集进行封装的。html
那么这篇做为MyBatis系列的最后一篇,天然是要来聊聊MyBatis中的一个不可忽视的功能,一级缓存和二级缓存。java
虽然这篇说的是MyBatis的缓存,可是我但愿正在学习计算机的小伙伴即便尚未使用过MyBatis框架也能看明白今天这篇文章。sql
缓存是什么?我来讲说我的的理解,最后再上比较官方的概念。数据库
缓存(Cache),顾名思义,有临时存储的意思。计算机中的缓存,咱们能够直接理解为,存储在内存中的数据的容器,这与物理存储是有差异的,因为内存的读写速度比物理存储高出几个数量级,因此程序直接从内存中取数据和从物理硬盘中取数据的效率是不一样的,因此有一些常常须要读取的数据,设计师们一般会将其放在缓存中,以便于程序对其进行读取。缓存
可是,缓存是有代价的,刚才咱们说过,缓存就是在内存中的数据的容器,一条64G的内存条,一般能够买3-4块1T-2T的机械硬盘了,因此缓存不能无节制地使用,这样成本会剧增,因此通常缓存中的数据都是须要频繁查询,可是又不常修改的数据。数据结构
而在通常业务中,查询一般会通过以下步骤。mybatis
读操做 --> 查询缓存中已经存在数据 -->若是不存在则查询数据库,若是存在则直接查询缓存-->数据库查询返回数据的同时,写入缓存中。app
写操做 --> 清空缓存数据 -->写入数据库框架
缓存流程ide
比较官方的概念:
☞ 缓存就是数据交换的缓冲区(称做:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。因为缓存的数据比内存快的多,因此缓存的做用就是帮助硬件更快的运行。
☞ 缓存每每使用的是RAM(断电既掉的非永久存储),因此在用完后仍是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。
☞ 高速缓存是用来协调CPU与主存之间存取速度的差别而设置的。通常CPU工做速度高,但内存的工做速度相对较低,为了解决这个问题,一般使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段常常访问的内容存在高速缓存,这样就在必定程度上缓解了因为主存速度低形成的CPU“停工待料”的状况。
☞ 缓存就是把一些外存上的数据保存在内存上而已,为何保存在内存上,咱们运行的全部程序里面的变量都是存放在内存中的,因此若是想将值放入内存上,能够经过变量的方式存储。在JAVA中一些缓存通常都是经过Map集合来实现的。
在说MyBatis的缓存以前,先了解一下Java中的缓存通常都是怎么实现的,咱们一般会使用Java中的Map,来实现缓存,因此在以后的缓存这个概念,就能够把它直接理解为一个Map,存的就是键值对。
MyBatis中的一级缓存,是默认开启且没法关闭的,一级缓存默认的做用域是一个SqlSession,解释一下,就是当SqlSession被构建了以后,缓存就存在了,只要这个SqlSession不关闭,这个缓存就会一直存在,换言之,只要SqlSession不关闭,那么这个SqlSession处理的同一条SQL就不会被调用两次,只有当会话结束了以后,这个缓存才会一并被释放。
虽然说咱们不能关闭一级缓存,可是做用域是能够修改的,好比能够修改成某个Mapper。
一级缓存的生命周期:
一、若是SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
二、若是SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,可是该对象仍可以使用。
三、SqlSession中执行了任何一个update操做(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,可是该对象能够继续使用。
节选自:https://www.cnblogs.com/happyflyingpig/p/7739749.html
MyBatis一级缓存简单示意图
MyBatis的二级缓存是默认关闭的,若是要开启有两种方式:
1.在mybatis-config.xml中加入以下配置片断
<!-- 全局配置参数,须要时再设置 -->
<settings>
<!-- 开启二级缓存 默认值为true -->
<setting name="cacheEnabled" value="true"/>
</settings>
2.在mapper.xml中开启
<!--开启本mapper的namespace下的二级缓存-->
<!--
eviction:表明的是缓存回收策略,目前MyBatis提供如下策略。
(1) LRU,最近最少使用的,一处最长时间不用的对象
(2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
(3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
(4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。
这里采用的是LRU, 移除最长时间不用的对形象
flushInterval:刷新间隔时间,单位为毫秒,若是你不配置它,那么当
SQL被执行的时候才会去刷新缓存。
size:引用数目,一个正整数,表明缓存最多能够存储多少个对象,不宜设置过大。设置过大会致使内存溢出。
这里配置的是1024个对象
readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是咱们能够快速读取缓存,缺点是咱们没有
办法修改缓存,他的默认值是false,不容许咱们修改
-->
<cache eviction="回收策略" type="缓存类"/>
二级缓存的做用域与一级缓存不一样,一级缓存的做用域是一个SqlSession,可是二级缓存的做用域是一个namespace,什么意思呢,你能够把它理解为一个mapper,在这个mapper中操做的全部SqlSession均可以共享这个二级缓存。可是假设有两条相同的SQL,写在不一样的namespace下,那这个SQL就会被执行两次,而且产生两份value相同的缓存。
依旧是用前两篇的测试用例,咱们从源码的角度看看缓存是如何执行的。
public static void main(String[] args) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//从调用者角度来说 与数据库打交道的对象 SqlSession
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","2121");
//执行这个方法实际上会走到invoke
System.out.println(mapper.selectAll(map));
sqlSession.close();
sqlSession.commit();
}
这里会执行到query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//二级缓存的Cache,经过MappedStatement获取
Cache cache = ms.getCache();
if (cache != null) {
//是否须要刷新缓存
//在<select>标签中也能够配置flushCache属性来设置是否查询前要刷新缓存,默认增删改刷新缓存查询不刷新
flushCacheIfRequired(ms);
//判断这个mapper是否开启了二级缓存
if (ms.isUseCache() && resultHandler == null) {
//无论
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//先从缓存拿
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//若是缓存等于空,那么查询一级缓存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查询完毕后将数据放入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回
return list;
}
}
//若是二级缓存为null,那么直接查询一级缓存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
能够看到首先MyBatis在查询数据时会先看看这个mapper是否开启了二级缓存,若是开启了,会先查询二级缓存,若是缓存中存在咱们须要的数据,那么直接就从缓存返回数据,若是不存在,则继续往下走查询逻辑。
接着往下走,若是二级缓存不存在,那么就直接查询数据了吗?答案是否认的,二级缓存若是不存在,MyBatis会再查询一次一级缓存,接着往下看。
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++;
//查询一级缓存(localCache)
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);
/**这个是queryFromDatabase的逻辑
* //先往缓存中put一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//往一级缓存中put真实数据
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
*/
}
} 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;
}
一级缓存和二级缓存的查询逻辑其实差很少,都是先查询缓存,若是没有则进行下一步查询,只不过一级缓存中若是没有结果,那么就直接查询数据库,而后回写一级缓存。
讲到这里其实一级缓存和二级缓存的执行流程就说完了,缓存的逻辑其实都差很少,MyBatis的缓存是先查询一级缓存再查询二级缓存。
可是文章到这里并无结束,还有一些缓存相关的问题能够聊。
不知道这个问题你们有没有想过,假设有这么一个场景,这里用二级缓存举例,由于二级缓存是跨事务的。
假设咱们在查询以前开启了事务,而且进行数据库操做:
1.往数据库中插入一条数据(INSERT)
2.在同一个事务内查询数据(SELECT)
3.提交事务(COMMIT)
4.提交事务失败(ROLLBACK)
咱们来分析一下这个场景,首先SqlSession先执行了一个INSERT操做,很显然,在咱们刚才分析的逻辑基础上,此时缓存必定会被清空,而后在同一个事务下查询数据,数据又从数据库中被加载到了缓存中,此时提交事务,而后事务提交失败了。
考虑一下此时会出现什么状况,相信已经有人想到了,事务提交失败以后,事务会进行回滚,那么执行INSERT插入的这条数据就被回滚了,可是咱们在插入以后进行了一次查询,这个数据已经放到了缓存中,下一次查询必然是直接查询缓存而不会再去查询数据库了,但是此时缓存和数据库之间已经存在了数据不一致的问题。
问题的根本缘由就在于,数据库提交事务失败了能够进行回滚,可是缓存不能进行回滚。
咱们来看看MyBatis是如何解决这个问题的。
这个类是MyBatis用于缓存事务管理的类,咱们能够看看其数据结构。
public class TransactionalCacheManager {
//事务缓存
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCacheManager中封装了一个Map,用于将事务缓存对象缓存起来,这个Map的Key是咱们的二级缓存对象,而Value是一个叫作TransactionalCache,顾名思义,这个缓存就是事务缓存,咱们来看看其内部的实现。
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//真实缓存对象
private final Cache delegate;
//是否须要清空提交空间的标识
private boolean clearOnCommit;
//全部待提交的缓存
private final Map<Object, Object> entriesToAddOnCommit;
//未命中的缓存集合,防止击穿缓存,而且若是查询到的数据为null,说明要经过数据库查询,有可能存在数据不一致,都记录到这个地方
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
//若是取出的是空,那么放到未命中缓存,而且在查询数据库以后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存
entriesMissedInCache.add(key);
}
//若是不为空
// issue #146
//查看缓存清空标识是否为false,若是事务提交了就为true,事务提交了会更新缓存,因此返回null。
if (clearOnCommit) {
return null;
} else {
//若是事务没有提交,那么返回原先缓存中的数据,
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//若是返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中
//原本应该put到缓存中,如今put到待提交事务的缓存中去。
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
//若是事务提交了,那么将清空缓存提交标识设置为true
clearOnCommit = true;
//清空entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
//若是为true,那么就清空缓存。
delegate.clear();
}
//把本地缓存刷新到真实缓存。
flushPendingEntries();
//而后将全部值复位。
reset();
}
public void rollback() {
//事务回滚
unlockMissedEntries();
reset();
}
private void reset() {
//复位操做。
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
//遍历事务管理器中待提交的缓存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//写入到真实的缓存中。
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//把未命中的一块儿put
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
//清空真实缓存区中未命中的缓存。
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
在TransactionalCache中有一个真实缓存对象Cache,这个真实缓存对象就是咱们真正的二级缓存,还有一个 entriesToAddOnCommit,这个Map对象中存放的是全部待提交事务的缓存。
咱们在二级缓存执行的代码中,看到在缓存中get或者put结果时,都是叫tcm的对象调用了getObject()方法和putObject()方法,这个对象实际上就是TransactionalCacheManager的实体对象,而这个对象其实是调用了TransactionalCache的方法,咱们来看看这两个方法是如何实现的。
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
//若是取出的是空,那么放到未命中缓存,而且在查询数据库以后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存
entriesMissedInCache.add(key);
}
//若是不为空
// issue #146
//查看缓存清空标识是否为false,若是事务提交了就为true,事务提交了会更新缓存,因此返回null。
if (clearOnCommit) {
return null;
} else {
//若是事务没有提交,那么返回原先缓存中的数据,
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//若是返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中
//原本应该put到缓存中,如今put到待提交事务的缓存中去。
entriesToAddOnCommit.put(key, object);
}
在getObject()方法中存在两个分支:
若是发现缓存中取出的数据为null,那么会把这个key放到entriesMissedInCache中,这个对象的主要做用就是将咱们未命中的key全都保存下来,防止缓存被击穿,而且当咱们在缓存中没法查询到数据,那么就有可能到一级缓存和数据库中查询,那么查询事后会调用putObject()方法,这个方法本应该将咱们查询到的数据put到真是缓存中,可是如今因为存在事务,因此暂时先放到entriesToAddOnCommit中。
若是发现缓存中取出的数据不为null,那么会查看事务提交标识(clearOnCommit)是否为true,若是为true,表明事务已经提交了,以后缓存会被清空,因此返回null,若是为false,那么因为事务尚未被提交,因此返回当前缓存中存的数据。
那么当事务提交成功或提交失败,又会是什么情况呢?不妨看看commit和rollback方法。
public void commit() {
if (clearOnCommit) {
//若是为true,那么就清空缓存。
delegate.clear();
}
//把本地缓存刷新到真实缓存。
flushPendingEntries();
//而后将全部值复位。
reset();
}
public void rollback() {
//事务回滚
unlockMissedEntries();
reset();
}
先分析事务提交成功的状况,若是事务正常提交了,那么会有这么几步操做:
清空真实缓存。
将本地缓存(未提交的事务缓存 entriesToAddOnCommit)刷新到真实缓存。
将全部值复位。
咱们来看看代码是如何实现的:
private void flushPendingEntries() {
//遍历事务管理器中待提交的缓存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//写入到真实的缓存中。
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//把未命中的一块儿put
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
//复位操做。
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
public void clear() {
//若是事务提交了,那么将清空缓存提交标识设置为true
clearOnCommit = true;
//清空事务提交缓存
entriesToAddOnCommit.clear();
}
清空真实缓存就不说了,就是Map调用clear方法,清空全部的键值对。
将未提交事务缓存刷新到真实缓存,首先会遍历entriesToAddOnCommit,而后调用真实缓存的putObject方法,将entriesToAddOnCommit中的键值对put到真实缓存中,这步完成后,还会将未命中缓存中的数据一块儿put进去,值设置为null。
最后进行复位,将提交事务标识设为false,未命中缓存、未提交事务缓存中的全部数据全都清空。
若是事务没有正常提交,那么就会发生回滚,再来看看回滚是什么流程:
清空真实缓存中未命中的缓存。
将全部值复位
public void rollback() {
//事务回滚
unlockMissedEntries();
reset();
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
//清空真实缓存区中未命中的缓存。
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
因为凡是在缓存中未命中的key,都会被记录到entriesMissedInCache这个缓存中,因此这个缓存中包含了全部查询数据库的key,因此最终只须要在真实缓存中把这部分key和对应的value给删除便可。
简而言之,缓存事务的控制主要是经过TransactionalCacheManager控制TransactionCache完成的,关键就在于TransactionCache中的entriesToAddCommit和entriesMissedInCache这两个对象,entriesToAddCommit在事务开启到提交期间做为真实缓存的替代品,将从数据库中查询到的数据先放到这个Map中,待事务提交后,再将这个对象中的数据刷新到真实缓存中,若是事务提交失败了,则清空这个缓存中的数据便可,并不会影响到真实的缓存。
entriesMissedInCache主要是用来保存在查询过程当中在缓存中没有命中的key,因为没有命中,说明须要到数据库中查询,那么查询事后会保存到entriesToAddCommit中,那么假设在事务提交过程当中失败了,而此时entriesToAddCommit的数据又都刷新到缓存中了,那么此时调用rollback就会经过entriesMissedInCache中保存的key,来清理真实缓存,这样就能够保证在事务中缓存数据与数据库的数据保持一致。
缓存事务
因为二级缓存的影响范围不是SqlSession而是namespace,因此二级缓存会在你的应用启动时一直存在直到应用关闭,因此二级缓存中不能存在随着时间数据量愈来愈大的数据,这样有可能会形成内存空间被占满。
因为二级缓存的做用域为namespace,那么就能够假设这么一个场景,有两个namespace操做一张表,第一个namespace查询该表并回写到内存中,第二个namespace往表中插一条数据,那么第一个namespace的二级缓存是不会清空这个缓存的内容的,在下一次查询中,还会经过缓存去查询,这样会形成数据的不一致。
因此当项目里有多个命名空间操做同一张表的时候,最好不要用二级缓存,或者使用二级缓存时避免用两个namespace操做一张表。
一级缓存的做用域是SqlSession,而使用者能够自定义SqlSession何时出现何时销毁,在这段期间一级缓存都是存在的。
当使用者调用close()方法以后,就会销毁一级缓存。
可是,咱们在和Spring整合以后,Spring帮咱们跳过了SqlSessionFactory这一步,咱们能够直接调用Mapper,致使在操做完数据库以后,Spring就将SqlSession就销毁了,一级缓存就随之销毁了,因此一级缓存就失效了。
那么怎么能让缓存生效呢?
开启事务,由于一旦开启事务,Spring就不会在执行完SQL以后就销毁SqlSession,由于SqlSession一旦关闭,事务就没了,一旦咱们开启事务,在事务期间内,缓存会一直存在。
使用二级缓存。
Hello world.
原文出处:https://www.cnblogs.com/javazhiyin/p/12357397.html