课程标题《mybatis框架源码解读一级、二级缓存源码解读》
课程内容:
1.什么是一级、二级缓存
2.一级缓存特点?源码解读、优缺点
3.如何禁止使用一级缓存
4.spring中底层如何使用一级缓存
5.什么是二级缓存?如何开启二级缓存
6.二级缓存的优缺点有哪些
mybatis一级、二级缓存概述
缓存基本越小 查询速度越快、缓存内容越少
缓存基本越大 查询速度越慢 缓存非常多内容
多级缓存概念
之前学习到多级缓存查询方式
先查询一级、一级缓存如果没有
在查询二级 二级缓存没有在查询数据库
在mybatis中反过来
先查询二级、二级如果没有在查询一级、一级如果没有在查询
数据库。
BaseExecutor 属于一级缓存执行器
CachingExecutor 属于二级缓存执行器
缓存 缓存key 、缓存value
1.Mybatis 中有一级缓存和二级缓存,采用装饰设计模式;
2.默认情况下一级缓存是开启的,而且是不能关闭的 ,一级缓存是指 SqlSession 级别的缓存,当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条 SQL。
3.二级缓存是指可以跨 SqlSession 的缓存。 是 mapper 级别的缓存,对于 mapper 级别的缓存不同的sqlsession 是可以共享的,需要额外整合到第三方缓存 例如Redis、MongoDB、oscache、ehcache等。
一级、二级缓存 采用装饰模式设计封装。
mybatis一级缓存源码解读
一级缓存特点
一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个SqlSession中,多个不同的SqlSession是无效的。
MyBatis中一级缓存是默认开启的,不需要任何额外配置
一级缓存效果演示
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1.定义mybatis-config.xml
String resource = "mybatis-config.xml";
// 2.读取mybatis-config.xml
InputStream inputStream = Resources.getResourceAsStream(resource);
//3. 创建SqlSessionFactoryBuilder.build(inputStream) 建造者 键盘快捷键ctrl+点击方法
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//4.获取到session
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println("第一次查询:");
List<UserEntity> userList1 = sqlSession.selectList("com.mayikt.mapper.UserMapper.getByUsers2",
UserEntity.class);
System.out.println("userList1:" + userList1);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
System.out.println("第二次查询:");
List<UserEntity> userList2 = sqlSession2.selectList("com.mayikt.mapper.UserMapper.getByUsers2",
UserEntity.class);
System.out.println("userList2:" + userList2);
如果是在同一个sqlSession 第二次在查询 则会走一级缓存 不会查询db。
通过日志可以得出第二次没有查询db 走的是 一级缓存返回数据:
一级缓存源码解读
第一次发出一个查询 sql, sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个Map集合。
key: MapperID+offset+limit+Sql+所有的入参
value:缓存的数据
同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询, 从数据库查询到再写入缓存。
1.默认是开启了二级缓存,则应该将二级缓存关闭
在mayikt-config.xml 设置 关闭二级缓存
<settings>
<setting name="cacheEnabled" value="false"/>
<!-- 打印sql日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
2.执行到org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)
3.执行到org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 拼接缓存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
CacheKey主要是由以下6部分组成
1、将Statement中的id添加到CacheKey对象中的updateList属性
2、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0)
3、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE)
4、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性
5、循环用户传入的参数,并将每个参数添加到CacheKey对象中的updateList属性
6、如果有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性
4.一级缓存核心代码
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是为null 则调用 localCache.getObject(key) 是为一级缓存如果
// 如果一级缓存不为null 则直接返回一级缓存数据
// 否则查询db
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;
}
PerpetualCache 为一级缓存 底层采用Map集合实现
/**
* Copyright 2009-2017 the original author or authors.
*
* 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 org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@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);
}
@Override
public void clear() {
cache.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
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());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
一级缓存没有数据 则查询db,如果db中有数据 则将db中数据返回给一级缓存。
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;
}
当我们一级缓存有数据之后 在同一个sqlsession 则会使用一级缓存中数据,不会查询db。
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--;
}
第二次查询时,判断如果一级缓存有数据 则 list返回数据 就是 从一级缓存中查询的数据
一级缓存优缺点
1. Mybatis的一级缓存存放在SqlSession的生命周期,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。
如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在改键值时,则会返回缓存中的对象。(一个SqlSession连续两次查询 得到的是同一个java对象)
任何的insert update delete操作都会清空一级缓存(增删改任何记录都会清空当前SqlSession的缓存)。
2.如果服务器集群的时候,每个sqlSession有自己独立的缓存相互之间不存在共享,所以在服务器集群的时候容易产生查询数据冲突问题。
一级缓存依赖jvm--而与jvm没有解耦
如果缓存数据过多的情况下 容易造成内存溢出的问题
如何禁止使用mybatis一级缓存
方案1 在sql语句上 随机生成 不同的参数 存在缺点:map集合可能爆 内存溢出的问题
方案2 开启二级缓存(共享 依赖Redis实现)
方案3 使用sqlSession强制清除缓存
方案4 创建新的sqlSession连接。
2022年7月16日22:04开始
一级缓存在什么时候清除?
1.提交事务的时候
sqlSession.commit();//提交事务 把一级缓存中数据给清除掉的
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();// 一级缓存对应的 map集合中数据 清空
localOutputParameterCache.clear();
}
2.执行 insert、delete、update语句的时候
Spring整合Mybatis的时候一级缓存的问题
Spring中底层有一个模板 SqlSessionTemplate 封装 mybatis sql session
1.在未开启事务的情况之下,每次查询spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的
2.在开启事务的情况之下,spring模板使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的
spring中sql session 被代理 SqlSessionInterceptor invoke
sql session.selectList方法()
场景1:
发送多次请求
场景2:2
知道 sqlsession.反射机制 调用selectList方法
mybatis二级缓存源码解读
mybatis二级缓存特点
1.二级缓存的范围是 mapper 级别( mapper 同一个命名空间), mapper 以命名空间为单位创建缓存数据结构,结构是 map, mybatis 的二级缓存是通过 CacheExecutor 实现的。 CacheExecutor其实是 Executor 的代理对象,所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库,该过程采用装饰模式设计
key: MapperID+offset+limit+Sql+所有的入参
具体使用需要配置:
Mybatis 全局配置中启用二级缓存配置
在对应的 Mapper.xml 中配置 cache 节点
在对应的 select 查询节点中添加 useCache=true
开启mybatis二级缓存
1.mybatis 配置文件 默认开启了二级缓存配置
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="cacheEnabled" value="true"/>
</settings>
2.MybatisRedisCache(先启动Redis)
package com.mayikt.cache;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.mayikt.utils.SerializeUtil;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class MybatisRedisCache implements Cache {
private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private Jedis redisClient = createReids();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
this.id = id;
}
public String getId() {
return this.id;
}
public int getSize() {
return Integer.valueOf(redisClient.dbSize().toString());
}
public void putObject(Object key, Object value) {
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value);
redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
}
public Object getObject(Object key) {
Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value);
return value;
}
public Object removeObject(Object key) {
return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
}
public void clear() {
redisClient.flushDB();
}
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
protected static Jedis createReids() {
JedisPool pool = new JedisPool("127.0.0.1", 6379);
return pool.getResource();
}
}
3.UserMapper 中配置:
<cache eviction="LRU" type="com.mayikt.cache.MybatisRedisCache"/>
mybatis源码解读
1、创建一级缓存的CacheKey
2、获取二级缓存
3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。
4、查询到结果之后将结果进行缓存。
1.先执行org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
@Override
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);
}
执行到二级缓存org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 如果开启了二级缓存 则cache不为null
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);
// 在将一级缓存数据存入到map集合中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存底层也是采用 HashMap集合实现
/**
* Copyright 2009-2015 the original author or authors.
*
* 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;
import java.util.HashMap;
import java.util.Map;
import org.apache.ibatis.cache.decorators.TransactionalCache;
/**
* @author Clinton Begin
*/
public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
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) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 回调到我们自定义的 将数据存入到redis中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
笔记
@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++;
// 先查询我们的一级缓存 一级返回如果没有数据
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;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在查询db之前 该缓存的内容 占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 开始查询db
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除占位符
localCache.removeObject(key);
}
// 在将我们db中数据返回到一级缓存中存放
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
PerpetualCache 一级缓存 底层就算一个map集合
key
value
一级缓存默认就是开启
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//判断是否配置了我们二级缓存 如果配置了cache != null
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
// 先查询二级缓存---(redis)
List<E> list = (List<E>) tcm.getObject(cache, key);
// 如果二级缓存中没有该数据 则查询 简单执行器
if (list == null) {
// 简单执行器---base执行器 调用一级缓存查询-----装饰模式
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);
}
源码解读:
二级缓存在什么时候存放数据到redis中呢?
1.先将一级缓存中数据 先放入到 二级缓存临时 map集合中
tcm.putObject(cache, key, list);
entriesToAddOnCommit.put(key, object);
2.当我们调用commit 将二级缓存临时 map集合 中数据 遍历 写入到redis中
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
// 将数据写入到redis中缓存中
delegate.putObject(entry, null);
}
}
}
相关代码
一级、二级缓存源码解读.rar