Mybatis源码解析之一级缓存

前言:

    像Mybatis、Hibernate这样的ORM框架,封装了JDBC的大部分操作,极大的简化了我们对数据库的操作。

    当然,其还有一些优势功能,比如缓存就是这样的一个优势功能。

    在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询,直接返回了结果,实际这种情况我们就可以称为缓存。

    框架针对这种查询做了一定了优化,那么缓存有几种类型?具体是如何优化的呢?能否从源码角度来分析一下这种优化是如何做的?

1.Mybatis的缓存级别

    网上有很多这种概念的描述,笔者直接拿来

    * 一级缓存(SqlSession级别的缓存,对于相同的查询,会直接从缓存中返回结果而不是查询数据库)

    * 二级缓存(Mapper级别缓存,定义在Mapper文件的<cache>标签并需要开启此缓存,多个Mapper文件可以共用一个缓存,依赖<cache-ref>标签配置)

2.Mybatis一级缓存的使用

    一级缓存默认是开启的,这里笔者主要将个人测试的配置信息描述一下

    1)maven引入(笔者依据SpringBoot来开发的)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
</parent>		
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

    2)创建实体类、配置文件

    * Blog.java

package jdbc;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Blog implements Serializable{
	private static final long serialVersionUID = 1L;
	
	private int id;
	private String name;
	private String url;
}

    * BlogMapper.java

package mybatis;

import jdbc.Blog;

public interface BlogMapper {

	public Blog queryById(int id);
    public void updateBlog(Blog blog);
}

    * src/main/resources/config/下创建blog.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="mybatis.BlogMapper">
	<select id="queryById" parameterType="int" resultType="jdbc.Blog">
		select * from blog where id = #{id}
    </select>
    <update id="updateBlog" parameterType="jdbc.Blog">
    	update Blog set name = #{name},url = #{url} where id=#{id}
    </update>
</mapper>

    * src/main/resources/config/下创建configure.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false" />
				<property name="username" value="root" />
				<property name="password" value="xxx" />
			</dataSource>
		</environment>
	</environments>

	<mappers>
		<mapper resource="config/blog.xml" />
	</mappers>
</configuration>

    3)创建测试类

public class Test {
	private static SqlSessionFactory sqlSessionFactory;
	private static Reader reader;

	static {
		try {
            // 1.读取mybatis配置文件,并生成SQLSessionFactory
			reader = Resources.getResourceAsReader("config/configure.xml");
			sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	public static SqlSessionFactory getSession() {
		return sqlSessionFactory;
	}
	
	public static void main(String[] args) {
		// 2.获取session,主要的CRUD操作均在SqlSession中提供
		SqlSession session = sqlSessionFactory.openSession();
		SqlSession session1 = sqlSessionFactory.openSession();
		try {
			
			Blog blog = (Blog)session.selectOne("queryById",17);
			// session.commit();
			Blog blog2 = (Blog)session1.selectOne("queryById",17);
			
		} finally {
			session.close();
		}
	}
}

3.一级缓存的测试与结论

    1)同一个session查询

	public static void main(String[] args) {
		SqlSession session = sqlSessionFactory.openSession();
		try {
			Blog blog = (Blog)session.selectOne("queryById",17);
			Blog blog2 = (Blog)session.selectOne("queryById",17);
		} finally {
			session.close();
		}
	}

    结论:只有一个DB查询

    2)两个session分别查询

	public static void main(String[] args) {
		// 2.获取session,主要的CRUD操作均在SqlSession中提供
		SqlSession session = sqlSessionFactory.openSession();
		SqlSession session1 = sqlSessionFactory.openSession();
		try {
			
			Blog blog = (Blog)session.selectOne("queryById",17);
			Blog blog2 = (Blog)session1.selectOne("queryById",17);
			
		} finally {
			session.close();
		}
	}

    结论:进行了两次DB查询

    3)同一个session,进行update之后再次查询

public static void main(String[] args) {
		SqlSession session = sqlSessionFactory.openSession();
		try {
			Blog blog = (Blog)session.selectOne("queryById",17);

			blog.setName("llll");
			session.update("updateBlog",blog);
            
			Blog blog2 = (Blog)session.selectOne("queryById",17);
		} finally {
			session.close();
		}
	}

    结论:进行了两次DB查询

    总结:在一级缓存中,同一个SqlSession下,查询语句相同的SQL会被缓存,如果执行增删改操作之后,该缓存就会被删除

4.一级缓存源码分析

    既然所有的操作都在session.selectOne()之中,那么我们就来看下这个操作有何特殊之处

    SqlSession的默认实现为DefaultSqlSession

    1)DefaultSqlSession.selectOne()

@Override
public <T> T selectOne(String statement, Object parameter) {
    // 直接委托给selectList
    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;
    }
}

// DefaultSqlSession.selectList()
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 委托给executor执行
        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();
    }
}

    2)executor.query()executor默认实现为CachingExecutor

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 转换SQL,并获取CacheKey
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    
    // 执行查询
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

//
@Override
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;
        }
    }
    
    // 直接来到这里
    // 实现为BaseExecutor.query()
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

    3)BaseExecutor.query()

@SuppressWarnings("unchecked")
@Override
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中获取对应CacheKey的结果值
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
        // 如果缓存中没有值,则从DB中查询
            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;
}

    4)BaseExecutor.queryFromDatabase()

    我们先来看下这种缓存中没有值的情况,看一下查询后的结果是如何被放置到缓存中的

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 {
        // 1.执行查询,获取list
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    // 2.将查询后的结果放置到localCache中,key就是我们刚才封装的CacheKey,value就是从DB中查询到的list
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

    5)localCache的结构与操作

// 在BaseExecutor中我们看到 localCache如下所示:
protected PerpetualCache localCache;

// PerpetualCache.java
public class PerpetualCache implements Cache {

  private String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

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

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

    总结:可以看到localCache本质上就是一个Map,key为我们的CacheKey,value为我们的结果值

    6)SqlSession.update()是如何清除缓存的呢

    还是按照刚才分析SqlSession.selectOne()的方式来一步步跟踪,我们跟踪到BaseExecutor.update()方法

// BaseExecutor.update()
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // 看这里,执行了一个清理操作
    clearLocalCache();
    return doUpdate(ms, parameter);
}

// clearLocalCache();
@Override
public void clearLocalCache() {
    if (!closed) {
        // 直接将Map清空
        localCache.clear();
        localOutputParameterCache.clear();
    }
}

总结:

    针对于一级缓存还是比较简单的

    主要就是在查询的时候,将结果值放入到一个Map中,map的key为查询条件的一系列封装,同一个session在执行相同查询的时候,则先从缓存中获取,缓存中没有,再去数据库中查询;

    针对于SqlSession的增删改操作,会清空该缓存Map

发布了124 篇原创文章 · 获赞 126 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/85681037