美文网首页JavaJava 程序员
用一个案例,带你看透MyBatis的一二级缓存

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

作者: 马小莫QAQ | 来源:发表于2022-05-30 15:35 被阅读0次

相信大家都已经很熟悉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]

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的话,极易读取到脏数据

作者:CoderJie
链接:https://juejin.cn/post/7103306529633206303
来源:稀土掘金

相关文章

网友评论

    本文标题:用一个案例,带你看透MyBatis的一二级缓存

    本文链接:https://www.haomeiwen.com/subject/ofvoprtx.html