龙空技术网

用一个案例,带你看透MyBatis的一二级缓存

众星十一 159

前言:

而今小伙伴们对“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关闭缓存