手写Mybatis:第18章-一级缓存

news2024/11/28 4:05:30

文章目录

  • 一、目标:一级缓存
  • 二、设计:一级缓存
  • 三、实现:一级缓存
    • 3.1 工程结构
    • 3.2 一级缓存类图
    • 3.3 一级缓存实现
      • 3.3.1 定义缓存接口
      • 3.3.2 实现缓存接口
      • 3.3.3 创建缓存KEY
      • 3.3.4 NULL值缓存key
    • 3.4 定义缓存机制、占位符和修改配置文件
      • 3.4.1 定义缓存机制
      • 3.4.2 定义占位符符
      • 3.4.3 修改映射器语句类
      • 3.4.4 修改配置项
    • 3.5 在会话中缓存使用
      • 3.5.1 创建缓存KEY
      • 3.5.2 抽象执行器实现
    • 3.6 解析缓存机制
  • 四、测试:一级缓存
    • 4.1 修改XML配置文件
    • 4.2 单元测试
      • 4.2.1 两次查询
      • 4.2.2 提交会话
      • 4.2.3 关闭会话
  • 五、总结:一级缓存

一、目标:一级缓存

💡 什么是一级缓存?

  • 在数据库的一次会话中,有时候我们可能需要反复的执行完全相同查询语句。
    • 如果不采取一些优化的手段,那么每一次查询都会查询一次数据库,而在极短的会话周期内几乎反复查询出来的结果也是完全相同的
  • 与内存获取相比,相同的数据再查询数据库的代价是很大的,如果系统的调用量较大,那么这可能造成很大的资源浪费。、
    • 所以结合我们实现的 ORM 框架,在一个会话周期内添加缓存操作,当会话结束 commit/close/clear 时,则进行清空缓存。

二、设计:一级缓存

💡 一级缓存如何设计?

  • mybatis的一级缓存:主要在于一次 session 会话周期内,将相同的执行语句结果缓存起来,避免重复执行数据操作。
  • 当发生一切影响 session 会话的操作时,都会清空缓存,避免发生胀肚。

https://article-images.zsxq.com/Fq6qNr4inPGf3qav-TzvEntASm4n

  • MybatisXML 配置文件中,可以设置本地缓存的机制,如果不设置则是默认 SESSION 级别,也就是使用一级缓存保存会话生命周期内的数据。
    • 如果设置为 STATEMENT 则不使用一级缓存。
  • SqlSession 的工作主要交给 Executor 执行器完成,负责数据库的各种操作。
    • 当创建一个 SqlSession 对象时,Mybatis 会为这个 SqlSession 创建一个新的 Executor 执行器。
    • 而缓存的工具包也是在执行器的创建时构建出来的。
  • 基于缓存的创建,在会话周期内保存查询结果数据,当一个 session 会话内发生了改变数据的行为包括:insert/delete/update 则清空缓存。
  • 另外当主动执行 close、commit、clear 操作时,也要顺便把缓存数据清空。这样才能尽最大可能的提高查询效率的同时,降低发生脏读的可能。

三、实现:一级缓存

3.1 工程结构

mybatis-step-17
|-src
  |-main
  | |-java
  |   |-com.lino.mybatis
    |     |-annotations
    |     | |-Delete.java
  |     | |-Insert.java
  |     | |-Select.java
    |     | |-Update.java
    |     |-binding
    |     | |-MapperMethod.java
  |     | |-MapperProxy.java
  |     | |-MapperProxyFactory.java
    |     | |-MapperRegistry.java
    |     |-builder
    |     | |-annotations
    |     | | |-MapperAnnotationBuilder.java
    |     | |-xml
    |     | | |-XMLConfigBuilder.java
    |     | | |-XMLMapperBuilder.java
    |     | | |-XMLStatementBuilder.java
    |     | |-BaseBuilder.java
    |     | |-MapperBuilderAssistant.java
    |     | |-ParameterExpression.java
    |     | |-ResultMapResolver.java
    |     | |-SqlSourceBuilder.java
    |     | |-StaticSqlSource.java
    |     |-cache
    |     | |-impl
    |     | | |-PrepetualCache.java
    |     | |-Cache.java
    |     | |-CacheKey.java
    |     | |-NullCacheKey.java
  |     |-datasource
  |     | |-druid
  |     | | |-DruidDataSourceFacroty.java
  |     | |-pooled
  |     | | |-PooledConnection.java
  |     | | |-PooledDataSource.java
  |     | | |-PooledDataSourceFacroty.java
  |     | | |-PoolState.java
  |     | |-unpooled
  |     | | |-UnpooledDataSource.java
  |     | | |-UnpooledDataSourceFacroty.java
  |     | |-DataSourceFactory.java
  |     |-executor
  |     | |-keygen
  |     | | |-Jdbc3KeyGenerator.java
  |     | | |-KeyGenerator.java
  |     | | |-NoKeyGenerator.java
  |     | | |-SelectKeyGenerator.java
  |     | |-parameter
  |     | | |-ParameterHandler.java
  |     | |-result
  |     | | |-DefaultResultContext.java
  |     | | |-DefaultResultHandler.java
  |     | |-resultset
  |     | | |-DefaultResultSetHandler.java
  |     | | |-ResultSetHandler.java
  |     | | |-ResultSetWrapper.java
  |     | |-statement
  |     | | |-BaseStatementHandler.java
  |     | | |-PreparedStatementHandler.java
  |     | | |-SimpleStatementHandler.java
  |     | | |-StatementHandler.java
  |     | |-BaseExecutor.java
    |     | |-ExecutionPlaceholder.java
  |     | |-Executor.java
  |     | |-SimpleExecutor.java
    |     |-io
    |     | |-Resources.java
    |     |-mapping
    |     | |-BoundSql.java
    |     | |-Environment.java
    |     | |-MappedStatement.java
    |     | |-ParameterMapping.java
    |     | |-ResultFlag.java
    |     | |-ResultMap.java
    |     | |-ResultMapping.java
    |     | |-SqlCommandType.java
    |     | |-SqlSource.java
    |     |-parsing
    |     | |-GenericTokenParser.java
    |     | |-TokenHandler.java
    |     |-plugin
    |     | |-Interceptor.java
    |     | |-InterceptorChain.java
    |     | |-Intercepts.java
    |     | |-Invocation.java
    |     | |-Plugin.java
    |     | |-Signature.java
  |     |-reflection
  |     | |-factory
  |     | | |-DefaultObjectFactory.java
  |     | | |-ObjectFactory.java
  |     | |-invoker
  |     | | |-GetFieldInvoker.java
  |     | | |-Invoker.java
  |     | | |-MethodInvoker.java
  |     | | |-SetFieldInvoker.java
  |     | |-property
  |     | | |-PropertyNamer.java
  |     | | |-PropertyTokenizer.java
  |     | |-wrapper
  |     | | |-BaseWrapper.java
  |     | | |-BeanWrapper.java
  |     | | |-CollectionWrapper.java
  |     | | |-DefaultObjectWrapperFactory.java
  |     | | |-MapWrapper.java
  |     | | |-ObjectWrapper.java
  |     | | |-ObjectWrapperFactory.java
  |     | |-MetaClass.java
  |     | |-MetaObject.java
  |     | |-Reflector.java
  |     | |-SystemMetaObject.java
  |     |-scripting
  |     | |-defaults
  |     | | |-DefaultParameterHandler.java
  |     | | |-RawSqlSource.java
  |     | |-xmltags
  |     | | |-DynamicContext.java
  |     | | |-DynamicSqlSource.java
    |     | | |-ExpressionEvaluator.java
    |     | | |-IfSqlNode.java
  |     | | |-MixedSqlNode.java
    |     | | |-OgnlCache.java
    |     | | |-OgnlClassResolver.java
  |     | | |-SqlNode.java
    |     | | |-StaticTextSqlNode.java
  |     | | |-TextSqlNode.java
    |     | | |-TrimSqlNode.java
  |     | | |-XMLLanguageDriver.java
  |     | | |-XMLScriptBuilder.java
  |     | |-LanguageDriver.java
  |     | |-LanguageDriverRegistry.java
    |     |-session
    |     | |-defaults
    |     | | |-DefaultSqlSession.java
    |     | | |-DefaultSqlSessionFactory.java
    |     | |-Configuration.java
    |     | |-LocalCacheScope.java
    |     | |-ResultContext.java
    |     | |-ResultHandler.java
    |     | |-RowBounds.java
    |     | |-SqlSession.java
    |     | |-SqlSessionFactory.java
    |     | |-SqlSessionFactoryBuilder.java
    |     | |-TransactionIsolationLevel.java
    |     |-transaction
    |     | |-jdbc
    |     | | |-JdbcTransaction.java
    |     | | |-JdbcTransactionFactory.java
    |     | |-Transaction.java
    |     | |-TransactionFactory.java
    |     |-type
    |     | |-BaseTypeHandler.java
    |     | |-DateTypeHandler.java
    |     | |-IntegerTypeHandler.java
    |     | |-JdbcType.java
    |     | |-LongTypeHandler.java
    |     | |-SimpleTypeRegistry.java
    |     | |-StringTypeHandler.java
    |     | |-TypeAliasRegistry.java
    |     | |-TypeHandler.java
    |     | |-TypeHandlerRegistry.java
  |-test
    |-java
    | |-com.lino.mybatis.test
    | |-dao
    | | |-IActivityDao.java
    | |-plugin
    | | |-TestPlugin.java
    | |-po
    | | |-Activity.java
    | |-ApiTest.java
        |-resources
      |-mapper
      | |-Activity_Mapper.xml
          |-mybatis-config-datasource.xml

3.2 一级缓存类图

在这里插入图片描述

  • XMLConfigBuilder 配置构建器为入口,解析 XML 配置中关于缓存机制的配置。
    • 关闭一级缓存,可以通过 LocalCacheScope 配置的方式进行处理。
  • 接下来就是在会话周期内,以创建 SqlSession 构建出 Executor 执行器初始化缓存组件,在执行查询操作时存放数据。
    • 当会话周期内有执行 insert/update/delete 以及关闭、提交、清空等操作时,在缓存组件中删除相关的缓存数据。
  • 具体的缓存操作通过 PerpetualCache 以及永久缓存实现类,实现 Cache 缓存接口来完成,其中关于缓存的 ID 字段,使用 CacheKey 为查询操作创建索引。
    • 注意:一般缓存框架的数据结构基本上都是 Key-Value 方式存储,Mybatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment] 生成一个哈希码作为 Key 使用。

3.3 一级缓存实现

  • Mybatis 框架中有一个单独提供的 cache 包,用于处理数据的缓存操作,一级缓存、二级缓存的操作都是这个包下提供的服务。

3.3.1 定义缓存接口

Cache.java

package com.lino.mybatis.cache;

/**
 * @description: 缓存接口
 */
public interface Cache {

    /**
     * 获取ID,每个缓存都有唯一的ID标识
     *
     * @return ID
     */
    String getId();

    /**
     * 存入值
     *
     * @param key   键
     * @param value 值
     */
    void putObject(Object key, Object value);

    /**
     * 获取值
     *
     * @param key 键
     * @return 值
     */
    Object getObject(Object key);

    /**
     * 删除值
     *
     * @param key 键
     * @return 值
     */
    Object removeObject(Object key);

    /**
     * 清空
     */
    void clear();

    /**
     * 获取缓存大小
     *
     * @return 缓存大小
     */
    int getSize();
}
  • 缓存接口主要提供了数据的存放、获取、删除、清空,以及数量大小的获取。

3.3.2 实现缓存接口

PerpetualCache.java

package com.lino.mybatis.cache.impl;

import com.alibaba.fastjson.JSON;
import com.lino.mybatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: 一级缓存,在 Session 生命周期内一直保持,每创建新的 OpenSession 都会创建一个缓存器 PerpetualCache
 */
public class PerpetualCache implements Cache {

    private Logger logger = LoggerFactory.getLogger(PerpetualCache.class);

    private String id;
    /**
     * 使用HashMap存放一级缓存数据,session 生命周期较短,正常情况下数据不会一直在缓存存放
     */
    private Map<Object, Object> cache = new HashMap<>(16);

    public PerpetualCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        Object obj = cache.get(key);
        if (null != obj) {
            logger.info("一级缓存 \r\nkey:{} \r\nval:{}", key, JSON.toJSONString(obj));
        }
        return obj;
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public int getSize() {
        return cache.size();
    }
}
  • 一般缓存的实现类也叫永远缓存,使用 HashMap 存放数据,因为这个缓存是配合整个会话周期内使用和销毁。
    • 所以使用 HashMap 比较简单,不需要太多的容量和大小限制。
  • 基本这个类里的操作都是对 HashMap 的存放、删除和清空的基本操作。

3.3.3 创建缓存KEY

  • 通常使用 HashMap 的时候 Key 都是一个 String 的值,那么这里因为需要对查询的信息以及 SQL 做一个 ID 使用,但这样都拼装下来就太长了。
  • 所以在缓存 Key 的实现中,基于这些信息创建了一个新的 HashCode 作为 KEY 使用。

CacheKey.java

package com.lino.mybatis.cache;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

/**
 * @description: 缓存key,一般缓存框架的数据结构基本上都是 key-value 方式存储
 * Mybatis 对于其 key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
 */
public class CacheKey implements Cloneable, Serializable {

    private static final long serialVersionUID = 1146682552656046210L;

    public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

    private static final int DEFAULT_MULTIPLYEP = 37;
    private static final int DEFAULT_HASHCODE = 17;

    private int multiplier;
    private int hashcode;
    private long checksum;
    private int count;
    private List<Object> updateList;

    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFAULT_MULTIPLYEP;
        this.count = 0;
        this.updateList = new ArrayList<>();
    }

    public CacheKey(Object[] objects) {
        this();
        updateAll(objects);
    }

    public int getUpdateCount() {
        return updateList.size();
    }

    public void update(Object object) {
        if (object != null && object.getClass().isArray()) {
            int length = Array.getLength(object);
            for (int i = 0; i < length; i++) {
                Object element = Array.get(object, i);
                doUpdate(element);
            }
        } else {
            doUpdate(object);
        }
    }

    private void doUpdate(Object object) {
        // 计算hash值,校验码
        int baseHashCode = object == null ? 1 : object.hashCode();

        count++;
        checksum += baseHashCode;
        baseHashCode *= count;

        hashcode = multiplier + hashcode + baseHashCode;

        updateList.add(object);
    }

    public void updateAll(Object[] objects) {
        for (Object o : objects) {
            update(o);
        }
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof CacheKey)) {
            return false;
        }

        final CacheKey cachekey = (CacheKey) object;

        if (hashcode != cachekey.hashcode) {
            return false;
        }

        if (checksum != cachekey.checksum) {
            return false;
        }

        if (count != cachekey.count) {
            return false;
        }

        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cachekey.updateList.get(i);
            if (thisObject == null) {
                if (thatObject != null) {
                    return false;
                }
            } else {
                if (!thisObject.equals(thatObject)) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return hashcode;
    }

    @Override
    public String toString() {
        StringBuilder returnValue = new StringBuilder().append(hashcode).append(":").append(checksum);
        for (Object obj : updateList) {
            returnValue.append(":").append(obj);
        }
        return returnValue.toString();
    }

    @Override
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey) super.clone();
        clonedCacheKey.updateList = new ArrayList<>(updateList);
        return clonedCacheKey;
    }

}
  • doUpdate:哈希计算
    • Mybatis 对于其 Key 的生成规则采取规则为 [mappedStatementId + offset + limit + SQL + queryParams + environment] 生成一个哈希码。
    • doUpdate 方法的 object 入参对象就是用于拼装哈希值的具体操作。
  • equals:哈希 equal
    • 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存 Key 重写了 equals 对比方法。
    • 这也是为什么在 doUpdate 计算哈希方法时,把对象添加到 updateList.add(object) 集合中,就是用于这里的 equals 判断使用

3.3.4 NULL值缓存key

NullCacheKey.java

package com.lino.mybatis.cache;

/**
 * @description: NULL值缓存key
 */
public class NullCacheKey extends CacheKey {

    private static final long serialVersionUID = 3704229911977019465L;

    public NullCacheKey() {
        super();
    }
}

3.4 定义缓存机制、占位符和修改配置文件

  • Mybatis 框架中默认情况下以及缓存是开启使用的,但是也支持用户可以自主关闭以及缓存。
<settings>
    <!--缓存级别:SESSION/STATEMENT-->
    <setting name="localCacheScope" value="SESSION"/>
</settings>
  • localCacheScope 缓存机制的属性值有两个,SESSION、STATEMENT
    • STATEMENT:关闭一级缓存。

3.4.1 定义缓存机制

LocalCacheScope.java

package com.lino.mybatis.session;

/**
 * @description: 本地缓存机制
 */
public enum LocalCacheScope {
    /**
     * 本地缓存
     */
    SESSION, STATEMENT
}
  • LocalCacheScope 缓存机制是个枚举值配置,分别为:SESSION, STATEMENT
    • SESSION:为默认值,支持使用一级缓存。
    • STATEMENT:不支持使用一级缓存。

3.4.2 定义占位符符

ExecutionPlaceholder.java

package com.lino.mybatis.executor;

/**
 * @description: 占位符
 */
public enum ExecutionPlaceholder {
    /**
     * 占位符
     */
    EXECUTION_PLACEHOLDER
}

3.4.3 修改映射器语句类

MappedStatement.java

package com.lino.mybatis.mapping;

import com.lino.mybatis.executor.keygen.Jdbc3KeyGenerator;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.keygen.NoKeyGenerator;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.Collections;
import java.util.List;

/**
 * @description: 映射器语句类
 */
public class MappedStatement {

    private String resource;
    private Configuration configuration;
    private String id;
    private SqlCommandType sqlCommandType;
    private SqlSource sqlSource;
    Class<?> resultType;
    private LanguageDriver lang;
    private List<ResultMap> resultMaps;
    private boolean flushCacheRequired;

    ...

    public MappedStatement() {
    }

    ...

    public boolean isFlushCacheRequired() {
        return flushCacheRequired;
    }
}

3.4.4 修改配置项

Configuration.java

package com.lino.mybatis.session;

import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.datasource.pooled.PooledDataSourceFactory;
import com.lino.mybatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.SimpleExecutor;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.DefaultResultSetHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.executor.statement.PreparedStatementHandler;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.plugin.Interceptor;
import com.lino.mybatis.plugin.InterceptorChain;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.DefaultObjectFactory;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.reflection.wrapper.DefaultObjectWrapperFactory;
import com.lino.mybatis.reflection.wrapper.ObjectWrapperFactory;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.LanguageDriverRegistry;
import com.lino.mybatis.scripting.xmltags.XMLLanguageDriver;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @description: 配置项
 */
public class Configuration {

    /**
     * 环境
     */
    protected Environment environment;
    /**
     * 是否使用自动生成键值对
     */
    protected boolean useGeneratedKeys = false;
    /**
     * 缓存机制,默认不配置的情况是 SESSION
     */
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    ...

    public LocalCacheScope getLocalCacheScope() {
        return localCacheScope;
    }

    public void setLocalCacheScope(LocalCacheScope localCacheScope) {
        this.localCacheScope = localCacheScope;
    }

    ...

}
  • 添加 LocalCacheScope 缓存机制。

3.5 在会话中缓存使用

  • Mybatis 的会话创建 SqlSession 其实主要操作都集中在 Executor 执行器的抽象类中,通过抽象类的创建实例化 new PerpetualCache("LocalCache") 一级缓存,并在执行器中完成缓存存放、使用、删除等操作。

3.5.1 创建缓存KEY

Executor.java

package com.lino.mybatis.executor;

import com.lino.mybatis.cache.CacheKey;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import java.sql.SQLException;
import java.util.List;

/**
 * @description: 执行器
 */
public interface Executor {

    /**
     * 结果处理器
     */
    ResultHandler NO_RESULT_HANDLER = null;

    /**
     * 更新
     *
     * @param ms        映射器语句
     * @param parameter 参数
     * @return 返回的是受影响的行数
     * @throws SQLException SQL异常
     */
    int update(MappedStatement ms, Object parameter) throws SQLException;

    /**
     * 查询,含缓存
     *
     * @param ms            映射器语句
     * @param parameter     参数
     * @param rowBounds     分页记录限制
     * @param resultHandler 结果处理器
     * @param key           缓存key
     * @param boundSql      SQL对象
     * @param <E>           返回的类型
     * @return List<E>
     * @throws SQLException SQL异常
     */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException;

    /**
     * 查询
     *
     * @param ms            映射器语句
     * @param parameter     参数
     * @param rowBounds     分页记录限制
     * @param resultHandler 结果处理器
     * @param <E>           返回的类型
     * @return List<E>
     * @throws SQLException SQL异常
     */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

    /**
     * 获取事务
     *
     * @return 事务对象
     */
    Transaction getTransaction();

    /**
     * 提交
     *
     * @param required 是否请求执行
     * @throws SQLException SQL异常
     */
    void commit(boolean required) throws SQLException;

    /**
     * 回滚
     *
     * @param required 是否请求执行
     * @throws SQLException SQL异常
     */
    void rollback(boolean required) throws SQLException;

    /**
     * 关闭
     *
     * @param forceRollback 是否强制回滚
     */
    void close(boolean forceRollback);

    /**
     * 清理session缓存
     */
    void clearLocalCache();

    /**
     * 创建缓存key
     *
     * @param ms              映射器语句
     * @param parameterObject 参数对象
     * @param rowBounds       分页记录限制
     * @param boundSql        SQL对象
     * @return 缓存key
     */
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
}
  • Executor 执行器接口有2个查询方法,一个含有缓存 Key,另外一个不含有。
  • 含有缓存 Keyquery 方法,会被另外一个不含有缓存 Keyquery 方法调用。

3.5.2 抽象执行器实现

BaseExecutor

package com.lino.mybatis.executor;

import com.lino.mybatis.cache.CacheKey;
import com.lino.mybatis.cache.impl.PerpetualCache;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ParameterMapping;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.LocalCacheScope;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.type.TypeHandlerRegistry;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * @description: 执行器抽象基类
 */
public abstract class BaseExecutor implements Executor {

    private org.slf4j.Logger logger = LoggerFactory.getLogger(BaseExecutor.class);

    protected Configuration configuration;
    protected Transaction transaction;
    protected Executor wrapper;
    /**
     * 本地缓存
     */
    protected PerpetualCache localCache;

    private boolean closed;
    /**
     * 查询堆栈
     */
    protected int queryStack = 0;

    public BaseExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.wrapper = this;
        this.localCache = new PerpetualCache("LocalCache");
    }

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        clearLocalCache();
        return doUpdate(ms, parameter);
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        // 清理局部缓存,查询堆栈为0则清理。queryStack 避免递归调用清理
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            // 根据cacheKey从localCache中查询数据
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list == null) {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                clearLocalCache();
            }
        }
        return list;
    }

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                                          CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        // 存入缓存
        localCache.putObject(key, list);
        return list;
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 1.获取绑定SQL
        BoundSql boundSql = ms.getBoundSql(parameter);
        // 2.创建缓存key
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

    /**
     * 更新方法
     *
     * @param ms        映射器语句
     * @param parameter 参数
     * @return 返回的是受影响的行数
     * @throws SQLException SQL异常
     */
    protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

    /**
     * 查询方法
     *
     * @param ms            映射器语句
     * @param parameter     参数
     * @param rowBounds     分页记录限制
     * @param resultHandler 结果处理器
     * @param boundSql      SQL对象
     * @param <E>           返回的类型
     * @return List<E>
     * @throws SQLException SQL异常
     */
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

    @Override
    public Transaction getTransaction() {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        return transaction;
    }

    @Override
    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new RuntimeException("Cannot commit, transaction is already closed.");
        }
        clearLocalCache();
        if (required) {
            transaction.commit();
        }
    }

    @Override
    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            try {
                clearLocalCache();
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }

    @Override
    public void clearLocalCache() {
        if (!closed) {
            localCache.clear();
        }
    }

    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        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();
        for (ParameterMapping parameterMapping : parameterMappings) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            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 = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);
        }
        if (configuration.getEnvironment() != null) {
            cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }

    @Override
    public void close(boolean forceRollback) {
        try {
            try {
                rollback(forceRollback);
            } finally {
                transaction.close();
            }
        } catch (SQLException e) {
            logger.warn("Unexpected exception on closing transaction. Cause: " + e);
        } finally {
            transaction = null;
            closed = true;
        }
    }

    /**
     * 关闭语句
     *
     * @param statement 语句
     */
    protected void closeStatement(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException ignore) {

            }
        }
    }
}
  • query:查询,不含缓存方法
    • BaseExecutor#query 主要新增加了关于缓存 Key 的创建,创建后调用重载的另外一个含有缓存 Keyquery 方法。

创建缓存KEY

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new RuntimeException("Executor was closed.");
    }
    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();
    for (ParameterMapping parameterMapping : parameterMappings) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        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 = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
    }
    if (configuration.getEnvironment() != null) {
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}
  • 缓存 Key 的创建,需要依赖于:[mappedStatementId + offset + limit + SQL + queryParams + environment] 信息构建出一个哈希值。
  • 所以这里把这些对应的信息分别传递给 cacheKey#update 方法。

查询数据缓存

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    if (closed) {
        throw new RuntimeException("Executor was closed.");
    }
    // 清理局部缓存,查询堆栈为0则清理。queryStack 避免递归调用清理
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 根据cacheKey从localCache中查询数据
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list == null) {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}
  • query 查询操作中,判断 queryStack 是否为0且是否刷新请求,如果是则会操作清空缓存。
  • 接下来 queryStack 自增以后通过 localCache 获取缓存数据。
    • 首次查询这个数据是空的,这个时候会进入到 queryFromDatabase 方法从数据库查询数据并返回结果。
    • 此外在 XML 配置中的缓存机制 LocalCacheScope 会在这里判断,如果不是 SESSION 机制,则清空缓存。

存放缓存数据

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
    try {
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    // 存入缓存
    localCache.putObject(key, list);
    return list;
}
  • 在一个会话内,首次执行 query 查询的时候,会把数据库查询到的数据,使用 localCache.putObject(key, list) 存放缓存中。
  • 同时在存放前,是使用占位符占位,查询后先清空再存放数据。

删除缓存数据

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    if (closed) {
        throw new RuntimeException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

@Override
public void commit(boolean required) throws SQLException {
    if (closed) {
        throw new RuntimeException("Cannot commit, transaction is already closed.");
    }
    clearLocalCache();
    if (required) {
        transaction.commit();
    }
}

@Override
public void rollback(boolean required) throws SQLException {
    if (!closed) {
        try {
            clearLocalCache();
        } finally {
            if (required) {
                transaction.rollback();
            }
        }
    }
}

@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
    }
}

@Override
public void close(boolean forceRollback) {
    try {
        try {
            rollback(forceRollback);
        } finally {
            transaction.close();
        }
    } catch (SQLException e) {
        logger.warn("Unexpected exception on closing transaction. Cause: " + e);
    } finally {
        transaction = null;
        closed = true;
    }
}
  • 在前面,insert、delete、update,都是调用执行器的 update 方法进行处理的。
  • 这里的 update、commit、rollback、close,都会调用到 clearLocalCache 执行缓存清空。
  • 因为 clearLocalCache 也是对外的,所以你也可以在缓存机制为 SESSION 级别下,手动清空缓存操作。

3.6 解析缓存机制

XMLConfigBuilder.java

package com.lino.mybatis.builder.xml;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.datasource.DataSourceFactory;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.plugin.Interceptor;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.LocalCacheScope;
import com.lino.mybatis.transaction.TransactionFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Properties;

/**
 * @description: XML配置构建器,建造者模式,集成BaseBuilder
 */
public class XMLConfigBuilder extends BaseBuilder {

    ...

    /**
     * 解析配置:类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
     *
     * @return Configuration
     */
    public Configuration parse() {
        try {
            // 插件添加
            pluginElement(root.element("plugins"));
            // 设置
            settingElement(root.element("settings"));
            // 环境
            environmentsElement(root.element("environments"));
            // 解析映射器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    ...

    /**
     * <settings>
     * <!--缓存级别:SESSION/STATEMENT-->
     * <setting name="localCacheScope" value="SESSION"/>
     * </settings>
     */
    private void settingElement(Element context) {
        if (context == null) {
            return;
        }
        List<Element> elements = context.elements();
        Properties props = new Properties();
        for (Element element : elements) {
            props.setProperty(element.attributeValue("name"), element.attributeValue("value"));
        }
        configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope")));
    }

    ...

}
  • 基于 XMLConfigBuilder 配置构建器,解析配置在 XML 文件中的缓存机制,并把解析处理出来的内容放到 Configuration 配置项中。

四、测试:一级缓存

4.1 修改XML配置文件

mybatis-config-datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
        <plugin interceptor="com.lino.mybatis.test.plugin.TestPlugin">
            <property name="test00" value="100"/>
            <property name="test01" value="200"/>
        </plugin>
    </plugins>

    <settings>
        <!--缓存级别:SESSION/STATEMENT-->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--XML配置-->
        <mapper resource="mapper/Activity_Mapper.xml"/>
    </mappers>
</configuration>

4.2 单元测试

4.2.1 两次查询

ApiTest.java

    @Test
    public void test_queryActivityById() {
        // 1.获取映射器对象
        IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
        // 2.测试验证
        Activity activity = new Activity();
        activity.setActivityId(100001L);
        logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
        logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
    }
  • 在单元测试验证中,开启 session 后执行力两次相同的查询。验证缓存的使用

测试结果

14:16:03.602 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1610525991.
拦截SQLSELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?
14:16:03.617 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
14:16:03.617 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
14:16:03.617 [main] INFO  c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IActivityDao.queryActivityById parameter:{"activityId":100001}
14:16:03.617 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:activityId propertyType:class java.lang.Long
14:16:03.617 [main] INFO  c.l.m.cache.impl.PerpetualCache - 一级缓存
key:-1917147986:525044925:com.lino.mybatis.test.dao.IActivityDao.queryActivityById:0:2147483647:SELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?:100001:development
val:[{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}]
14:16:03.617 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}

在这里插入图片描述

  • 从测试结果缓存的日志打印和断点调试缓存查询中可以看到,执行第二次查询的时候,就可以通过缓存获取数据了,证明一级缓存生效了。

4.2.2 提交会话

ApiTest.java

@Test
public void test_queryActivityById() {
    // 1.获取映射器对象
    IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
    // 2.测试验证
    Activity activity = new Activity();
    activity.setActivityId(100001L);
    logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
	// 提交会话
    sqlSession.commit();
    logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
}

测试结果

14:19:50.865 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1610525991.
拦截SQLSELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?
14:19:50.880 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
14:19:50.880 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
14:19:50.880 [main] INFO  c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IActivityDao.queryActivityById parameter:{"activityId":100001}
14:19:50.880 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:activityId propertyType:class java.lang.Long
拦截SQLSELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?
14:19:50.880 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
14:19:50.880 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}

在这里插入图片描述

  • 添加 sqlSession.commit(),执行会话提交,则会清空缓存,此时的二次查询已经不会从缓存中获取数据,而是要去数据库读取。

4.2.3 关闭会话

ApiTest.java

@Test
public void test_queryActivityById() {
    // 1.获取映射器对象
    IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
    // 2.测试验证
    Activity activity = new Activity();
    activity.setActivityId(100001L);
    logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
	// 提交会话
    sqlSession.commit();
    // 关闭会话
    sqlSession.close();
    logger.info("测试结果:{}", JSON.toJSONString(dao.queryActivityById(activity)));
}

测试结果

14:22:22.512 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1610525991.
拦截SQLSELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
         where activity_id = ?
14:22:22.512 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
14:22:22.527 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
14:22:22.527 [main] INFO  c.l.m.d.pooled.PooledDataSource - Returned connection 1610525991 to pool.
14:22:22.527 [main] INFO  c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IActivityDao.queryActivityById parameter:{"activityId":100001}
14:22:22.527 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:activityId propertyType:class java.lang.Long

java.lang.RuntimeException: Executor was closed.

	at com.lino.mybatis.executor.BaseExecutor.createCacheKey(BaseExecutor.java:177)
	at com.lino.mybatis.executor.BaseExecutor.query(BaseExecutor.java:107)
	at com.lino.mybatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:54)
	at com.lino.mybatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:39)
	at com.lino.mybatis.binding.MapperMethod.execute(MapperMethod.java:50)
	at com.lino.mybatis.binding.MapperProxy.invoke(MapperProxy.java:36)
	at com.sun.proxy.$Proxy4.queryActivityById(Unknown Source)
	at com.lino.mybatis.test.ApiTest.test_queryActivityById(ApiTest.java:48)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

在这里插入图片描述

  • 新增 sqlSession.close(),当执行会话关闭后,调用 rollback(forceRollback) 方法,缓存会被清空,同时因为会话已经关闭,所以再执行的查询会报错:Executor was closed

五、总结:一级缓存

  • 一级缓存属于轻量级缓存,仅限于在一次 session 会话内完成,所以整个模型也可以简单的理解为:
    • 使用 HashMap 存放缓存数据,当有发生对数据库的操作,则进行缓存清空。
  • 通常如果说你的应用是独立的单体应用,或者开发体量较小的运营后台类应用,可能不会发生任何由于缓存所产生的脏读问题。但当你的应用是分布式部署,并且你的 session 会话过长,执行了大范围的 select 操作,那么要注意此时数据的有效性。
    • 如果都是类似这样的场景,你可能需要关闭一级缓存,或者在关键节点及时手动清空缓存。
  • 缓存的设计比较小巧,整个结构并不算复杂,但它的设计贯穿了整个 session 的生命周期,这也是提醒我们在设计一个业务流程时,要考虑全局的流程状态流转,避免一小部分的问题影响全局的结果。
    • 另外关于 CacheKey 缓存 Key 的哈希设计也可以借鉴,如果有大长字符串拼接需要作为 key 使用的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/975688.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jrebel与Xrebel教学

简介 JRebel和XRebel是两个非常有用的工具&#xff0c;可以显著提升Java开发人员的生产力和应用程序性能。 JRebel是一个强大的Java开发工具&#xff0c;它允许开发人员在不重新启动应用程序的情况下进行代码修改和调试。传统上&#xff0c;每次修改Java代码都需要重新编译和重…

bat批处理——统计当前文件夹下的所有文件名

一、在当前文件夹下建立XX.txt文件&#xff0c;将指令dir *.* /b/s>test.txt写到XX.txt文件中 测试文件夹目录及文件结构图&#xff1a; 指令说明&#xff1a; dir *.* /b/s>test.txt /*** 此部分为注释内容* dir 获取当前目录下的目录及文件* *.* 对文件进行筛选&…

ShopXO商城系统文件上传0Day代审历程

Git仓库&#xff1a; https://github.com/gongfuxiang/shopxo简介&#xff1a; 两天攻防中&#xff0c;某政局内网横向发现多网段服务器&#xff0c;该服务器搭建了ShopXO商城系统(后来发现是开发临时搭建的&#xff0c;准备做二开用的)。结果花了30来秒审了个垃圾Day拿下该服…

应用于激光雷达、 激光测距、 脉冲测量的高精度时间测量(TDC)电路MS1205N

MS1205N 是一款高精度时间测量 (TDC) 电路&#xff0c;具有四通 道、多脉冲的采样能力、高速 SPI 通讯、多种测量模式&#xff0c;适合 于激光雷达和激光测距。 主要特点 ◼ 单精度模式 60ps ◼ 双精度模式 30ps ◼ 非校准测量范围 3.5ns(0ns) 至 25μs ◼ 单…

【LeetCode】202. 快乐数 - hash表 / 快慢指针

目录 2023-9-5 09:56:15 202. 快乐数 2023-9-5 09:56:15 关键是怎么去判断循环&#xff1a; hash表&#xff1a; 每次生成链中的下一个数字时&#xff0c;我们都会检查它是否已经在哈希集合中。 如果它不在哈希集合中&#xff0c;我们应该添加它。如果它在哈希集合中&#x…

docker安装mysql、clickhouse、oracle等各种数据库汇总

1&#xff1a;docker 安装mongo数据库并使用 官网&#xff1a;https://www.mongodb.com/docs/manual/ 安装 &#xff1a;https://www.zhihu.com/question/54602953/answer/3047452434?utm_id0 安装2&#xff1a;https://www.duidaima.com/Group/Topic/ArchitecturedDesign/91…

MySQL 全局锁、表级锁、行锁详解

前言 MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类&#xff0c;全局锁和表级锁是在server层实现的。 全局锁 全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法&#xff0c;命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状…

华为产业链爆发,这次是系统级的革命

华为Mate60手机爆火&#xff0c;让产业链随之振奋。我们看到&#xff0c;这种改变将是持久的、系统的。 可能在未来20年&#xff0c;这种影响会持续。 01 华为产业链有多长 客观而言&#xff0c;华为产业链可以看作是由多个领域的企业和组织组成的网络&#xff0c;其中包括半…

Linux内核源码分析 (B.1)内核内存布局和堆管理

Linux内核源码分析 (B.1)内核内存布局和堆管理 文章目录 Linux内核源码分析 (B.1)内核内存布局和堆管理一、Linux内核内存布局二、堆管理 一、Linux内核内存布局 64位Linux一般使用48位来表示虚拟地址空间&#xff0c;45位表示物理地址。通过命令&#xff1a;cat/proc/cpuinfo。…

HTML5-4-表单

文章目录 表单属性表单标签输入元素文本域&#xff08;Text Fields&#xff09;密码字段单选按钮&#xff08;Radio Buttons&#xff09;复选框&#xff08;Checkboxes&#xff09;按钮&#xff08;button&#xff09;提交按钮(Submit)label标签 文本框&#xff08;textarea&am…

叉积方法,求点与线段的相对位置

叉积可以用来判断一个点在一条线段的哪个方向。 线段两个端点坐标为 A(x1, y1), B(x2, y2)&#xff0c; 假设点 P 的坐标为 (px, py)&#xff0c; 则向量 AP 和 BP 的坐标表示为&#xff1a; AP (px - x1, py - y1) BP (x2 - px, y2 - py) 叉积的计算公式为&#xff1a; (py…

基于jeecg-boot的flowable流程自定义业务退回撤回或驳回到发起人后的再次流程提交

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 主要…

神仙级python入门教程(非常详细),从0到精通,从看这篇开始!

毫无疑问&#xff0c;Python 是当下最火的编程语言之一。对于许多未曾涉足计算机编程的领域「小白」来说&#xff0c;深入地掌握 Python 看似是一件十分困难的事。其实&#xff0c;只要掌握了科学的学习方法并制定了合理的学习计划&#xff0c;Python 从 入门到精通只需要一个月…

骨传导和入耳式哪个危害大一点?入耳式和骨传导哪种好?

骨传导和入耳式这两种耳机虽然都存在一定的危害&#xff0c;但是入耳式耳机对人体的危害要更大一点。 入耳式耳机直接塞进耳朵这种佩戴方式&#xff0c;会阻塞外部声音的进入&#xff0c;长时间使用可能会导致耳道感染&#xff0c;还可能对听力造成损伤&#xff0c;而骨传导耳…

RecyclerView源码解析(二):结合LinearLayout分析绘制流程

RecyclerView源码解析&#xff08;二&#xff09;:结合LinearLayout分析绘制流程 封面&#xff1a; 导言 上篇文章中主要已经介绍了RecyclerView的View的三大工作流程&#xff08;onMeasure&#xff0c;onLayout&#xff0c;onDraw&#xff09;&#xff0c;实际上看了上篇文章…

数学思维导图怎么绘制?这个详细绘制方法了解一下

数学思维导图怎么绘制&#xff1f;数学思维导图是数学学习中的一种重要辅助工具。在复杂的数学问题中&#xff0c;思维导图可以帮助学生更好地理解和组织各个知识点&#xff0c;从而更好地解决问题。在绘制数学思维导图时&#xff0c;有很多工具可供选择&#xff0c;下面就给大…

港陆证券:政策累积效应催生A股普涨行情 北证50指数创最大单日涨幅

周一&#xff0c;在活泼资本商场政策继续推出、不断累积的布景下&#xff0c;A股商场迎来久别的普涨行情。金融、白酒与资源类板块集体发力&#xff0c;带动沪深主板指数高开高走。北交所商场体现更为亮眼&#xff0c;北证50指数收盘大涨5.92%&#xff0c;创该指数前史最大单日…

CSS中图片旋转超出父元素解决办法

下面的两种解决办法都会导致图片缩小&#xff0c;可以给图片进行初始化的宽高设置 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge">…

NIO原理浅析(三)

epoll 首先认识一下epoll的几个基础函数 int s socket(AF_INET, SOCK_STREAM, 0); bind(s, ...); listen(s, ...);int epfd epoll_create(...) epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中while(1) {int n epoll_wait(...);for(接受到数据的socket) {//处…

el-table 垂直表头

效果如下&#xff1a; 代码如下&#xff1a; <template><div class"vertical_head"><el-table style"width: 100%" :data"getTblData" :show-header"false"><el-table-columnv-for"(item, index) in getHe…