相信大家都已经很熟悉Mybatis框架了,它可以说是持久层框架的一个代表,拥有极高的灵活性MyBatis框架是支持缓存的,这样能够优化搜索效率,其默认为我们开启一级缓存,本篇文章我们就以一个案例来验证一下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>

2. 项目结构

导入依赖项后,构建如下图结构的项目,进行验证

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就是指的我们常说的SqlSession,也就是一次数据库会话,也可以理解为一个事务中

  • statement仅针对于一次查询,相当于关闭一级缓存

    2. 案例测试

    下面我们来测试一下一级缓存(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]

    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,一级缓存使用的就是PerpetualCache

  • eviction: 定义缓存淘汰策略,常见的有FIFO,LRU

  • flushInterval: 配置自动刷新缓存的时间,单位是毫秒

  • size:最多缓存对象的个数

  • readOnly:是否只读,若配置可读写,则需要对应的实体类能够序列化

  • blocking:如果缓存中找不到对应的key,是否会一直阻塞,直到有对应的数据进入缓存

    2. 案例测试

    下面我们来测试一下一级缓存(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"); } } ​

    4. 总结
  • 二级缓存作用域namespace级别的

  • 二级缓存可以做到多个SqlSession的共享

  • 二级缓存支持多种Cache的方式,并且可以接入外部分布式缓存中间件,缓存的功能更为丰富

  • 二级缓存极易出现脏数据,如果是分布式环境,使用本地存储,必然会造成脏数据,所以一定要使用分布式缓存中间件避免

  • 二级缓存在多表操作时,或者namespace设置不合理时,基于出现脏数据,而且排查起来困难,多表查询可能跨越多个namespace,如果没有指定cache-ref的话,极易读取到脏数据

    如果感觉有所帮助,点赞,收藏走一走

    原文:http://juejin.cn/post/7103306529633206303