相信大家都已经很熟悉Mybatis框架了,它可以说是持久层框架的一个代表,拥有极高的灵活性MyBatis框架是支持缓存的,这样能够优化搜索效率,其默认为我们开启一级缓存,本篇文章我们就以一个案例来验证一下mybatis的一二级缓存,并且我们也会从源码上分析其一二级缓存是如何实现的,下面我们就来说一说关于mybatis二级缓存存在的意义?我们一起去了解并探讨一下这个问题吧!
mybatis二级缓存存在的意义
一、案例相信大家都已经很熟悉Mybatis框架了,它可以说是持久层框架的一个代表,拥有极高的灵活性。MyBatis框架是支持缓存的,这样能够优化搜索效率,其默认为我们开启一级缓存,本篇文章我们就以一个案例来验证一下mybatis的一二级缓存,并且我们也会从源码上分析其一二级缓存是如何实现的。
首先我们需要构建一个集成了MyBatis的SpringBoot项目,并且需要准备装有MySQL、Redis数据库的服务器一台,因为我们需要分别验证一级与二级缓存,二级缓存我们就以redis缓存为例介绍一下。
1. 依赖导入首先我们创建一个SpringBoot项目(使用官方脚手架创建,或者通过Maven简单项目手动构建均可),然后我们在pom文件加入以下依赖(主要就是SpringBoot、SpringBoot-Web、Mybatis、MySQL、Redis,这里我们也用到了lombok)。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>Spring-boot</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
导入依赖项后,构建如下图结构的项目,进行验证
3. 配置首先是配置文件的内容,如下:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://aliyun:3306/test?characterEncoding=utf8&useSSL=true
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
redis:
host: aliyun
port: 6379
timeout: 1000
password:
database: 0
mybatis:
mapper-locations: classpath*:mapper/*Mapper.xml
configuration:
# sql 打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 二级缓存开启、关闭
cache-enabled: true
# 一级缓存的作用域
local-cache-scope: session
然后是集成二级缓存需要的config文件
SpringBoot集成redis的config,如下
package com.zhj.demo.config;
import com.fasterxml.jackson.annotation.jsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Autowired
private LettuceConnectionFactory connectionFactory;
@Bean
public RedisTemplate<String,Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, connectionFactory);
return redisTemplate;
}
/**
* 设置数据存入 redis 的序列化方式
* @param template
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> template,LettuceConnectionFactory factory) {
// 定义 key 的序列化方式为 string
// 需要注意这里Key使用了StringRedisSerializer,那么Key只能是String类型的,不能为其它类型,否则会报错抛异常。
StringRedisSerializer redisSerializer = new StringRedisSerializer();
template.setKeySerializer(redisSerializer);
// 定义 value 的序列化方式为 json
@SuppressWarnings({"rawtypes", "unchecked"})
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash结构的key和value序列化方式
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setEnableTransactionSupport(true);
template.setConnectionFactory(factory);
}
}
Spring的上下文扩展,如下
package com.zhj.demo.config;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
public void setApplicationContext(ApplicationContext applicationContext) {
ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR
}
/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
checkApplicationContext();
return applicationContext;
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
checkApplicationContext();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz) {
checkApplicationContext();
return (T) applicationContext.getBeansOfType(clazz);
}
/**
* 清除applicationContext静态变量.
*/
public static void cleanApplicationContext() {
applicationContext = null;
}
private static void checkApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder");
}
}
}
MyBatis的二级缓存如下:
package com.zhj.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class MybatisRedisCache implements Cache {
private String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public MybatisRedisCache(String id) {
this.id = id;
}
private RedisTemplate<Object, Object> getRedisTemplate(){
return ApplicationContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.boundHashOps(getId()).put(key, value);
log.info("[结果放入到缓存中: " key "=" value " ]");
}
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).get(key);
log.info("[从缓存中获取了: " key "=" value " ]");
return value;
}
@Override
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).delete(key);
log.info("[从缓存删除了: " key "=" value " ]");
return value;
}
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(getId());
log.info("清空缓存!!!");
}
@Override
public int getSize() {
RedisTemplate redisTemplate = getRedisTemplate();
Long size = redisTemplate.boundHashOps(getId()).size();
return size == null ? 0 : size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
这样我们就创建好了一个简单的,可以支持Mybatis二级缓存的SpringBoot项目,其中controller->service->mapper的代码这里不会粘贴出来,大家自行书写。
二、一级缓存1. 介绍首先,我们测试一级缓存,这时我们需要关闭二级缓存(将配置中的开关关闭即可)
MyBatis的一级缓存默认是开启的,并且其作用域默认是session级别的,还有一种作用域是statement
下面我们来测试一下一级缓存(session),首先我们不开启事务测试一下
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public User get(long id) {
userMapper.get(id);
userMapper.get(id);
userMapper.update(id);
userMapper.get(id);
return userMapper.get(id);
}
}
结果如下,我们可以看到不开启事务时,每一次查询都会创建一个新的SqlSession,这时一级缓存是没什么用的。
JDBC Connection [HikariProxyConnection@153304455 wrapping com.mysql.jdbc.JDBC4Connection@66899143] will not be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3745598c]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f5de1] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1484518905 wrapping com.mysql.jdbc.JDBC4Connection@66899143] will not be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f5de1]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b29d61a] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1679169739 wrapping com.mysql.jdbc.JDBC4Connection@66899143] will not be managed by Spring
==> Preparing: update user set age = 22 where id = ?
==> Parameters: 1(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b29d61a]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f45d686] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@994135013 wrapping com.mysql.jdbc.JDBC4Connection@66899143] will not be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f45d686]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@476b5f6c] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@115036385 wrapping com.mysql.jdbc.JDBC4Connection@66899143] will not be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@476b5f6c]
下面我们在来开启事务测试一下
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Transactional
public User get(long id) {
userMapper.get(id);
userMapper.get(id);
userMapper.update(id);
userMapper.get(id);
return userMapper.get(id);
}
}
结果如下,我们可以看到在第二次查询,明显的使用的一级缓存,并且多次操作都是基于同一个SqlSession,在所有操作都完成,才去提交事务,并释放SqlSession,当代码中出现UPDATE、DELETE、INSERTU等操作也会去删除缓存。
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
JDBC Connection [HikariProxyConnection@2067395888 wrapping com.mysql.jdbc.JDBC4Connection@37a9bada] will be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
==> Preparing: update user set age = 22 where id = ?
==> Parameters: 1(Long)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
下面我们在来开启事务测试一下
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Transactional
public User get(long id) {
userMapper.get(id);
userMapper.get(id);
userMapper.update(id);
userMapper.get(id);
return userMapper.get(id);
}
}
结果如下,我们可以看到在第二次查询,明显的使用的一级缓存,并且多次操作都是基于同一个SqlSession,在所有操作都完成,才去提交事务,并释放SqlSession,当代码中出现UPDATE、DELETE、INSERTU等操作也会去删除缓存。
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
JDBC Connection [HikariProxyConnection@2067395888 wrapping com.mysql.jdbc.JDBC4Connection@37a9bada] will be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
==> Preparing: update user set age = 22 where id = ?
==> Parameters: 1(Long)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]
MyBatis的一级缓存主要是绑定在SqlSession上的,并且是通过Executor进行设置的,在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,创建Executor代码如下所示:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new ReuseExecutor(this, transaction);
}
// 是否开启二级缓存,开启二级缓存将Executor包装成CachingExecutor
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
其中BatchExecutor、ReuseExecutor、ReuseExecutor都是BaseExecutor的子类,一级缓存主要是在父类BaseExecutor来实现的,BaseExecutor是Executor接口的抽象实现,并提供了三个抽象方法doUpdate、doFlushStatements和doQuery,是典型的模板类。
其中update/insert/delete都会调用update方法,调用后会清理缓存,query是查询的时候调用会生成缓存,这里需要注意的是在query最后会进行一级缓存作用域的判断,如果LocalCacheScope设置为STATEMENT则会清理掉缓存,致使一级缓存无法在SqlSession级别使用。
其中缓存是由成员变量PerpetualCache localCache来进行存储的
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<BaseExecutor.DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
public Transaction getTransaction() {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
return this.transaction;
}
}
public void close(boolean forceRollback) {
try {
try {
this.rollback(forceRollback);
} finally {
if (this.transaction != null) {
this.transaction.close();
}
}
} catch (SQLException var11) {
log.warn("Unexpected exception on closing transaction. Cause: " var11);
} finally {
this.transaction = null;
this.deferredLoads = null;
this.localCache = null;
this.localOutputParameterCache = null;
this.closed = true;
}
}
public boolean isClosed() {
return this.closed;
}
// update/insert/delete会调用此方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
// 先清缓存,再更新,更新由子类实现
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
public List<BatchResult> flushStatements() throws SQLException {
return this.flushStatements(false);
}
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
return this.doFlushStatements(isRollBack);
}
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
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 (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
this.queryStack;
// 根据cachekey从localCache去查
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
// 如果查到缓存,处理localOutputParameterCache
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果没有查到缓存,从数据库查
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// 清空堆栈
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
// 如果LocalCacheScope设置为STATEMENT则会清理掉缓存
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return this.doQueryCursor(ms, parameter, rowBounds, boundSql);
}
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
BaseExecutor.DeferredLoad deferredLoad = new BaseExecutor.DeferredLoad(resultObject, property, key, this.localCache, this.configuration, targetType);
if (deferredLoad.canLoad()) {
deferredLoad.load();
} else {
this.deferredLoads.add(new BaseExecutor.DeferredLoad(resultObject, property, key, this.localCache, this.configuration, targetType));
}
}
}
// 创建缓存的key
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
Iterator var8 = parameterMappings.iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
public boolean isCached(MappedStatement ms, CacheKey key) {
return this.localCache.getObject(key) != null;
}
public void commit(boolean required) throws SQLException {
if (this.closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
} else {
this.clearLocalCache();
this.flushStatements();
if (required) {
this.transaction.commit();
}
}
}
public void rollback(boolean required) throws SQLException {
if (!this.closed) {
try {
this.clearLocalCache();
this.flushStatements(true);
} finally {
if (required) {
this.transaction.rollback();
}
}
}
}
public void clearLocalCache() {
if (!this.closed) {
this.localCache.clear();
this.localOutputParameterCache.clear();
}
}
protected abstract int doUpdate(MappedStatement var1, Object var2) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean var1) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, BoundSql var5) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4) throws SQLException;
protected void closeStatement(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException var3) {
}
}
}
protected void applyTransactionTimeout(Statement statement) throws SQLException {
StatementUtil.applyTransactionTimeout(statement, statement.getQueryTimeout(), this.transaction.getTimeout());
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
Object cachedParameter = this.localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
MetaObject metaCachedParameter = this.configuration.newMetaObject(cachedParameter);
MetaObject metaParameter = this.configuration.newMetaObject(parameter);
Iterator var8 = boundSql.getParameterMappings().iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.IN) {
String parameterName = parameterMapping.getProperty();
Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = this.transaction.getConnection();
return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection;
}
public void setExecutorWrapper(Executor wrapper) {
this.wrapper = wrapper;
}
// 延迟加载
private static class DeferredLoad {
private final MetaObject resultObject;
private final String property;
private final Class<?> targetType;
private final CacheKey key;
private final PerpetualCache localCache;
private final ObjectFactory objectFactory;
private final ResultExtractor resultExtractor;
public DeferredLoad(MetaObject resultObject, String property, CacheKey key, PerpetualCache localCache, Configuration configuration, Class<?> targetType) {
this.resultObject = resultObject;
this.property = property;
this.key = key;
this.localCache = localCache;
this.objectFactory = configuration.getObjectFactory();
this.resultExtractor = new ResultExtractor(configuration, this.objectFactory);
this.targetType = targetType;
}
// 缓存中找到,且不为占位符,可以加载
public boolean canLoad() {
return this.localCache.getObject(this.key) != null && this.localCache.getObject(this.key) != ExecutionPlaceholder.EXECUTION_PLACEHOLDER;
}
public void load() {
List<Object> list = (List)this.localCache.getObject(this.key);
Object value = this.resultExtractor.extractObjectFromList(list, this.targetType);
this.resultObject.setValue(this.property, value);
}
}
}
缓存PerpetualCache 的实现,内部维护了一个HashMap来做缓存。
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public int getSize() {
return this.cache.size();
}
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
public Object removeObject(Object key) {
return this.cache.remove(key);
}
public void clear() {
this.cache.clear();
}
public boolean equals(Object o) {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else if (this == o) {
return true;
} else if (!(o instanceof Cache)) {
return false;
} else {
Cache otherCache = (Cache)o;
return this.getId().equals(otherCache.getId());
}
}
public int hashCode() {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else {
return this.getId().hashCode();
}
}
}
1. 介绍测试二级缓存,我们需要配置开启二级缓存(将配置中的开关开启即可),本案例我们采用Redis存储二级缓存的数据
MyBatis的二级缓存默认是关闭的,并且开启后支持第三方缓存中间件,它的作用域是namespace,也就是说它是支持分布式的,也是多个SqlSession间共享的,开启后会优先读取二级缓存的数据,然后是一级缓存,最后才是查库。
二级缓存开启需要调整cache-enabled为true,并且需要注意的是需要在xml中指定
<cache type="com.zhj.demo.config.MybatisRedisCache"></cache>
# 事例
<cache type="PERPETUAL" eviction="LRU" size="60" blocking="false" flushInterval="24" readOnly="false"/>
# cache-ref 是指相互关联的namespace,当其他触发update等操作时,也需要删除缓存,尽可能保证缓存的一致性
<cache-ref namespace="com.zhj.demo.mapper.StudentMapper"/>
其中cache标签的各个参数的意义如下:
下面我们来测试一下一级缓存(session),首先我们不开启事务测试一下,与上述测试一级缓存一样,我们可以看到每次查询都能通过缓存获取到,当系统发生数据更新,缓存中的数据也会被清除掉,然后重新查库,再放入缓存。开启事务后,结果依然如此。
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1dbd6e3b] was not registered for synchronization because synchronization is not active
2022-05-29 16:05:43.236 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : [从缓存中获取了: 303202628:-828304:com.zhj.demo.mapper.UserMapper.get:0:2147483647:select * from user where id = ?:1:SqlSessionFactoryBean=[User(id=1, name=小明, sex=1, age=22)] ]
Cache Hit Ratio [com.zhj.demo.mapper.UserMapper]: 1.0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1dbd6e3b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d737ab] was not registered for synchronization because synchronization is not active
2022-05-29 16:05:43.251 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : [从缓存中获取了: 303202628:-828304:com.zhj.demo.mapper.UserMapper.get:0:2147483647:select * from user where id = ?:1:SqlSessionFactoryBean=[User(id=1, name=小明, sex=1, age=22)] ]
Cache Hit Ratio [com.zhj.demo.mapper.UserMapper]: 1.0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d737ab]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d347521] was not registered for synchronization because synchronization is not active
2022-05-29 16:05:43.257 INFO 24596 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-05-29 16:05:43.546 INFO 24596 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@2026474916 wrapping com.mysql.jdbc.JDBC4Connection@66c8d1de] will not be managed by Spring
==> Preparing: update user set age = 22 where id = ?
==> Parameters: 1(Long)
<== Updates: 1
2022-05-29 16:05:43.611 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : 清空缓存!!!
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d347521]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238f411] was not registered for synchronization because synchronization is not active
2022-05-29 16:05:43.627 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : [从缓存中获取了: 303202628:-828304:com.zhj.demo.mapper.UserMapper.get:0:2147483647:select * from user where id = ?:1:SqlSessionFactoryBean=null ]
Cache Hit Ratio [com.zhj.demo.mapper.UserMapper]: 0.6666666666666666
JDBC Connection [HikariProxyConnection@1955671286 wrapping com.mysql.jdbc.JDBC4Connection@66c8d1de] will not be managed by Spring
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, name, sex, age
<== Row: 1, 小明, 1, 22
<== Total: 1
2022-05-29 16:05:43.677 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : [结果放入到缓存中: 303202628:-828304:com.zhj.demo.mapper.UserMapper.get:0:2147483647:select * from user where id = ?:1:SqlSessionFactoryBean=[User(id=1, name=小明, sex=1, age=22)] ]
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238f411]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@23a605c0] was not registered for synchronization because synchronization is not active
2022-05-29 16:05:43.692 INFO 24596 --- [nio-8080-exec-1] com.zhj.demo.config.MybatisRedisCache : [从缓存中获取了: 303202628:-828304:com.zhj.demo.mapper.UserMapper.get:0:2147483647:select * from user where id = ?:1:SqlSessionFactoryBean=[User(id=1, name=小明, sex=1, age=22)] ]
Cache Hit Ratio [com.zhj.demo.mapper.UserMapper]: 0.75
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@23a605c0]
我们看一下Redis中是如何存储的,我们可以看到其key还是一个比较复杂的json,value就是我们的查询结果。
3. 源码分析MyBatis的二级缓存也是通过Executor进行设置的,我们可以看到在创建Executor时,判断是否开启二级缓存,如果开启二级缓存,会将Executor包装成CachingExecutor,创建Executor代码如下所示:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new ReuseExecutor(this, transaction);
}
// 是否开启二级缓存,开启二级缓存将Executor包装成CachingExecutor
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
复制代码
我们再来看以下CachingExecutor,这里我们还得以query方法下手,看看其如何操作缓存,我们可以看到ms.getCache();
需要注意的是CachingExecutor会使用TransactionalCacheManager tcm包装初始生成的Cache,如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
最终使用的Cache就是在<cache type="com.zhj.demo.config.MybatisRedisCache"></cache>指定的类型,这段是在xml解析时读取的,这里也是会使用装饰者模式去装饰原始的PerpetualCache对象。
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
public Transaction getTransaction() {
return this.delegate.getTransaction();
}
public void close(boolean forceRollback) {
try {
if (forceRollback) {
this.tcm.rollback();
} else {
this.tcm.commit();
}
} finally {
this.delegate.close(forceRollback);
}
}
public boolean isClosed() {
return this.delegate.isClosed();
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
this.flushCacheIfRequired(ms);
return this.delegate.update(ms, parameterObject);
}
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
this.flushCacheIfRequired(ms);
return this.delegate.queryCursor(ms, parameter, rowBounds);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.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 {
// 获取MappedStatement 上的缓存对象
Cache cache = ms.getCache();
if (cache != null) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
// 通过CacheKey 作为key去缓存里搜索
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
// 如果缓存value不存在,那么借助delegate执行查询,然后存入Cache
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public List<BatchResult> flushStatements() throws SQLException {
return this.delegate.flushStatements();
}
public void commit(boolean required) throws SQLException {
this.delegate.commit(required);
this.tcm.commit();
}
public void rollback(boolean required) throws SQLException {
try {
this.delegate.rollback(required);
} finally {
if (required) {
this.tcm.rollback();
}
}
}
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
Iterator var3 = boundSql.getParameterMappings().iterator();
while(var3.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var3.next();
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " ms.getId() " statement.");
}
}
}
}
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return this.delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
public boolean isCached(MappedStatement ms, CacheKey key) {
return this.delegate.isCached(ms, key);
}
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
this.delegate.deferLoad(ms, resultObject, property, key, targetType);
}
public void clearLocalCache() {
this.delegate.clearLocalCache();
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
this.tcm.clear(cache);
}
}
public void setExecutorWrapper(Executor executor) {
throw new UnsupportedOperationException("This method should not be called");
}
}
如果感觉有所帮助,点赞,收藏走一走
原文:http://juejin.cn/post/7103306529633206303