前言:
而今小伙伴们对“apache关闭缓存”大致比较关怀,朋友们都想要学习一些“apache关闭缓存”的相关内容。那么小编同时在网上网罗了一些有关“apache关闭缓存””的相关资讯,希望小伙伴们能喜欢,咱们快快来学习一下吧!相信大家都已经很熟悉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>2. 项目结构
导入依赖项后,构建如下图结构的项目,进行验证
3. 配置
首先是配置文件的内容,如下:
server: port: 8080spring: 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: 0mybatis: 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;@Configurationpublic 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;@Componentpublic 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;@Slf4jpublic 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就是指的我们常说的SqlSession,也就是一次数据库会话,也可以理解为一个事务中statement仅针对于一次查询,相当于关闭一级缓存2. 案例测试
下面我们来测试一下一级缓存(session),首先我们不开启事务测试一下
@Servicepublic 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3745598c]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f5de1] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f5de1]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b29d61a] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b29d61a]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f45d686] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f45d686]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@476b5f6c] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@476b5f6c]
下面我们在来开启事务测试一下
@Servicepublic 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 SqlSessionRegistering 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: 1Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transactionReleasing 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: 1Releasing 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: 1Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transactionReleasing 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]
下面我们在来开启事务测试一下
@Servicepublic 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 SqlSessionRegistering 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: 1Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transactionReleasing 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: 1Releasing 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: 1Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb]Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ae5fdcb] from current transactionReleasing 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]3 源码分析
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(); } }}4. 总结一级缓存默认作用域session级别session级别的一级缓存生命周期与SqlSession一致分布式情况下,可能会造成脏数据,不同SqlSession无法互相干预,如果对数据一致性要求特别高的可将其设置为statement一级缓存使用简单的HashMap做缓存,没有大小限制,没有淘汰策略,可能在部分极限情况对系统有不利影响三、二级缓存
测试二级缓存,我们需要配置开启二级缓存(将配置中的开关开启即可),本案例我们采用Redis存储二级缓存的数据
1. 介绍
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标签的各个参数的意义如下:
type:cache使用的类型,默认是PerpetualCache,一级缓存使用的就是PerpetualCacheeviction: 定义缓存淘汰策略,常见的有FIFO,LRUflushInterval: 配置自动刷新缓存的时间,单位是毫秒size:最多缓存对象的个数readOnly:是否只读,若配置可读写,则需要对应的实体类能够序列化blocking:如果缓存中找不到对应的key,是否会一直阻塞,直到有对应的数据进入缓存2. 案例测试
下面我们来测试一下一级缓存(session),首先我们不开启事务测试一下,与上述测试一级缓存一样,我们可以看到每次查询都能通过缓存获取到,当系统发生数据更新,缓存中的数据也会被清除掉,然后重新查库,再放入缓存。开启事务后,结果依然如此。
Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1dbd6e3b] was not registered for synchronization because synchronization is not active2022-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.0Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1dbd6e3b]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d737ab] was not registered for synchronization because synchronization is not active2022-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.0Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d737ab]Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d347521] was not registered for synchronization because synchronization is not active2022-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: 12022-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 SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238f411] was not registered for synchronization because synchronization is not active2022-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.6666666666666666JDBC 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: 12022-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 SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@23a605c0] was not registered for synchronization because synchronization is not active2022-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.75Closing 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"); }}4. 总结二级缓存作用域namespace级别的二级缓存可以做到多个SqlSession的共享二级缓存支持多种Cache的方式,并且可以接入外部分布式缓存中间件,缓存的功能更为丰富二级缓存极易出现脏数据,如果是分布式环境,使用本地存储,必然会造成脏数据,所以一定要使用分布式缓存中间件避免二级缓存在多表操作时,或者namespace设置不合理时,基于出现脏数据,而且排查起来困难,多表查询可能跨越多个namespace,如果没有指定cache-ref的话,极易读取到脏数据
如果感觉有所帮助,点赞,收藏走一走
原文:
标签: #apache关闭缓存