当前位置: 首页 > news >正文

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

文章目录

  • 一、目标:一级缓存
  • 二、设计:一级缓存
  • 三、实现:一级缓存
    • 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;}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {Object obj = cache.get(key);if (null != obj) {logger.info("一级缓存 \r\nkey:{} \r\nval:{}", key, JSON.toJSONString(obj));}return obj;}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic 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);}}@Overridepublic 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;}@Overridepublic int hashCode() {return hashcode;}@Overridepublic String toString() {StringBuilder returnValue = new StringBuilder().append(hashcode).append(":").append(checksum);for (Object obj : updateList) {returnValue.append(":").append(obj);}return returnValue.toString();}@Overridepublic 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");}@Overridepublic int update(MappedStatement ms, Object parameter) throws SQLException {if (closed) {throw new RuntimeException("Executor was closed.");}clearLocalCache();return doUpdate(ms, parameter);}@Overridepublic <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;}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 1.获取绑定SQLBoundSql boundSql = ms.getBoundSql(parameter);// 2.创建缓存keyCacheKey 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;@Overridepublic Transaction getTransaction() {if (closed) {throw new RuntimeException("Executor was closed.");}return transaction;}@Overridepublic void commit(boolean required) throws SQLException {if (closed) {throw new RuntimeException("Cannot commit, transaction is already closed.");}clearLocalCache();if (required) {transaction.commit();}}@Overridepublic void rollback(boolean required) throws SQLException {if (!closed) {try {clearLocalCache();} finally {if (required) {transaction.rollback();}}}}@Overridepublic void clearLocalCache() {if (!closed) {localCache.clear();}}@Overridepublic 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;}@Overridepublic 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

    @Testpublic 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_timeFROM activitywhere 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_timeFROM activitywhere 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_timeFROM activitywhere 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_timeFROM activitywhere 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_timeFROM activitywhere 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.Longjava.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 使用的场景。

相关文章:

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

文章目录 一、目标&#xff1a;一级缓存二、设计&#xff1a;一级缓存三、实现&#xff1a;一级缓存3.1 工程结构3.2 一级缓存类图3.3 一级缓存实现3.3.1 定义缓存接口3.3.2 实现缓存接口3.3.3 创建缓存KEY3.3.4 NULL值缓存key 3.4 定义缓存机制、占位符和修改配置文件3.4.1 定…...

哈夫曼编码实现文件的压缩和解压

程序示例精选 哈夫曼编码实现文件的压缩和解压 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《哈夫曼编码实现文件的压缩和解压》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0…...

解决六大痛点促进企业更好使用生成式AI,亚马逊云科技顾凡采访分享可用方案

亚马逊云科技大中华区战略业务发展部总经理顾凡在接受21世纪经济报道记者专访时表示&#xff0c;生成式人工智能将从四个方面为企业带来机遇&#xff1a;第一是创造全新的客户体验&#xff1b;第二是提高企业内部员工的生产力&#xff1b;第三是帮助企业提升业务运营效率&#…...

Qt 定时器放在线程中执行,支持随时开始和停止定时器。

前言&#xff1a;因为项目需要定时检查网络中设备是否能连通&#xff0c;需要定时去做ping操作&#xff0c;若是网络不通&#xff0c;则ping花费时间比较久&#xff08;局域网大概4秒钟才能结束&#xff0c;当然如果设置超时时间啥的&#xff0c;也能很快返回&#xff0c;就是会…...

java 过滤器 接口(API)验证入参,验签(sign) Demo

java 过滤器 接口&#xff08;API&#xff09;验证入参&#xff0c;验签&#xff08;sign&#xff09; Demo 一、思路 1、配置yml文件; 2、创建加载配置文件类; 3、继承 OncePerRequestFilter 重写方法 doFilterInternal; 4、注册自定义过滤器; 二、步骤 1、配置yml文件; ###系…...

独家!微信正在灰测一款全新消金产品

来源 | 镭射财经&#xff08;leishecaijing&#xff09; 「镭射财经」独家获悉&#xff0c;微信将推出一款名为“微信分期”的新消费信贷产品&#xff0c;目前该产品正处于小范围灰测阶段&#xff0c;还未正式上线。上线后&#xff0c;微信将运营微信分付和微信分期两款自营消…...

阿秀C++笔记-学习记录

81.C中的组合和继承相比的优缺点 在C中组合一对象系用描述对象包对象系组一个拥对象例其变合类的含的现。这的量类当有员被创建。 以下一个示例&#xff0c;展示了在C中如何实现组合关系&#xff1a; class Engine {// Engine class definition... };class Car {Engine engi…...

前端入门到入土?

文章目录 前言http和https的区别&#xff0c;https加密的原理是&#xff1f;区别https的加密原理 TCP为什么要三次握手&#xff1f;proxy代理的原理&#xff1f;内存泄漏&#xff1f;什么是内存泄漏&#xff1f;为什么会有内存泄漏&#xff1f;内存泄漏的情况&#xff1f;如何防…...

架构设计基础设施保障IaaS之网络

目录 1 DNS运用1.1 DNS功能作用1.2 DNS配置实践 2 DNS生产最佳实践方案2.1 全球加速功能2.2 不同运营商的加速方案2.3 全球业务高可用方案2.4 跨地域负载均衡 3 DNS域名劫持解决方案4 CDN剖析4.1 CDN原理4.2 缓存过期配置处理流程4.3 缓存配置规则 5 CDN运用6 CDN最佳实践方案6…...

zabbix安装部署

前期准备&#xff1a;安装mysql数据库和nginx 一、下载zabbix rpm -Uvh https://repo.zabbix.com/zabbix/4.4/rhel/7/x86_64/zabbix-release-4.4-1.el7.noarch.rpm yum-config-manager --enable rhel-7-server-optional-rpms yum install epel-release numactl yum install…...

零碎的C++

构造函数和析构函数 构造函数不能是虚函数&#xff0c;而析构函数可以是虚函数。原因如下&#xff1a; 构造函数不能是虚函数&#xff0c;因为在执行构造函数时&#xff0c;对象还没有完全创建&#xff0c;还没有分配内存空间&#xff0c;也没有初始化虚函数表指针。如果构造…...

模糊测试面面观 | 模糊测试是如何发现异常情况的?

协议模糊测试是一种用于评估通信协议、文件格式和API实现系统安全性和稳定性的关键技术。在模糊测试过程中&#xff0c;监视器扮演着关键角色&#xff0c;它们能够捕获异常情况、错误响应、资源利用等&#xff0c;为测试人员提供有价值的信息&#xff0c;有助于发现潜在漏洞和问…...

C#备份数据库文件

c#备份数据库文件完整代码 sqlServer 存储过程&#xff1a; USE [PSIDBase] GO /****** Object: StoredProcedure [dbo].[sp_BackupDB] Script Date: 2023/8/31 16:49:02 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GOALTER procedure [dbo].[sp_BackupDB]…...

行军遇到各种复杂地形怎么处理?

行军遇到各种复杂地形怎么处理&#xff1f; 【安志强趣讲《孙子兵法》第30讲】 【原文】 凡军好高而恶下&#xff0c;贵阳而贱阴&#xff0c;养生而处实&#xff0c;军无百疾&#xff0c;是谓必胜。 【注释】 阳&#xff0c;太阳能照到的地方。阴&#xff0c;太阳照不到的地方。…...

Python Number(数字).............................................

Python Number 数据类型用于存储数值。 数据类型是不允许改变的,这就意味着如果改变 Number 数据类型的值&#xff0c;将重新分配内存空间。 以下实例在变量赋值时 Number 对象将被创建&#xff1a; var1 1 var2 10您也可以使用del语句删除一些 Number 对象引用。 del语句…...

设置 Hue Server 与 Hue Web 界面之间的会话超时时间

设置 Hue Server 与 Hue Web 界面之间的会话超时时间 在 CDH 的 Hue 中&#xff0c;Auto Logout Timeout 参数表示用户在不活动一段时间后将自动注销&#xff08;登出&#xff09;的超时时间。当用户在 Hue 中处于不活动状态超过该设定时间时&#xff0c;系统将自动注销用户&am…...

openGauss学习笔记-57 openGauss 高级特性-并行查询

文章目录 openGauss学习笔记-57 openGauss 高级特性-并行查询57.1 适用场景与限制57.2 资源对SMP性能的影响57.3 其他因素对SMP性能的影响57.4 配置步骤 openGauss学习笔记-57 openGauss 高级特性-并行查询 openGauss的SMP并行技术是一种利用计算机多核CPU架构来实现多线程并行…...

软考(1)-面向对象的概念

目录 一. 软考基本信息 1. 软考时间&#xff1a; 2. 软考科目&#xff1a; 3.专业知识介绍 -- 综合知识考点分布 4. 专业介绍 -- 软件设计考点分布 二. 面向对象概念 1. 封装 考点一&#xff1a;对象 考点二&#xff1a;封装private 2. 继承 考点三&#xff1a;类 考…...

深度学习推荐系统(四)WideDeep模型及其在Criteo数据集上的应用

深度学习推荐系统(四)Wide&Deep模型及其在Criteo数据集上的应用 在2016年&#xff0c; 随着微软的Deep Crossing&#xff0c; 谷歌的Wide&Deep以及FNN、PNN等一大批优秀的深度学习模型被提出&#xff0c; 推荐系统全面进入了深度学习时代&#xff0c; 时至今日&#x…...

第十二章 YOLO的部署实战篇(中篇)

cuda教程目录 第一章 指针篇 第二章 CUDA原理篇 第三章 CUDA编译器环境配置篇 第四章 kernel函数基础篇 第五章 kernel索引(index)篇 第六章 kenel矩阵计算实战篇 第七章 kenel实战强化篇 第八章 CUDA内存应用与性能优化篇 第九章 CUDA原子(atomic)实战篇 第十章 CUDA流(strea…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

Xen Server服务器释放磁盘空间

disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...

AI病理诊断七剑下天山,医疗未来触手可及

一、病理诊断困局&#xff1a;刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断"&#xff0c;医生需通过显微镜观察组织切片&#xff0c;在细胞迷宫中捕捉癌变信号。某省病理质控报告显示&#xff0c;基层医院误诊率达12%-15%&#xff0c;专家会诊…...

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要&#xff1a; 近期&#xff0c;在使用较新版本的OpenSSH客户端连接老旧SSH服务器时&#xff0c;会遇到 "no matching key exchange method found"​, "n…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...

ubuntu22.04有线网络无法连接,图标也没了

今天突然无法有线网络无法连接任何设备&#xff0c;并且图标都没了 错误案例 往上一顿搜索&#xff0c;试了很多博客都不行&#xff0c;比如 Ubuntu22.04右上角网络图标消失 最后解决的办法 下载网卡驱动&#xff0c;重新安装 操作步骤 查看自己网卡的型号 lspci | gre…...

文件上传漏洞防御全攻略

要全面防范文件上传漏洞&#xff0c;需构建多层防御体系&#xff0c;结合技术验证、存储隔离与权限控制&#xff1a; &#x1f512; 一、基础防护层 前端校验&#xff08;仅辅助&#xff09; 通过JavaScript限制文件后缀名&#xff08;白名单&#xff09;和大小&#xff0c;提…...