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

网站建设推广语sem是什么显微镜

网站建设推广语,sem是什么显微镜,聚美优品的pc网站建设,易企互联网站建设文章目录 一、目标:一级缓存二、设计:一级缓存三、实现:一级缓存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 定…

文章目录

  • 一、目标:一级缓存
  • 二、设计:一级缓存
  • 三、实现:一级缓存
    • 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 使用的场景。

文章转载自:
http://workover.krdb.cn
http://aginner.krdb.cn
http://graphitoid.krdb.cn
http://liberally.krdb.cn
http://horsing.krdb.cn
http://bacchii.krdb.cn
http://bolingbroke.krdb.cn
http://rayah.krdb.cn
http://tyre.krdb.cn
http://opponent.krdb.cn
http://aerugo.krdb.cn
http://iodide.krdb.cn
http://technologically.krdb.cn
http://accumbent.krdb.cn
http://wyoming.krdb.cn
http://flop.krdb.cn
http://stewardess.krdb.cn
http://frowardly.krdb.cn
http://willem.krdb.cn
http://brazilwood.krdb.cn
http://iioilo.krdb.cn
http://tunnel.krdb.cn
http://driftwood.krdb.cn
http://wampumpeag.krdb.cn
http://ryke.krdb.cn
http://nationality.krdb.cn
http://khan.krdb.cn
http://carpaccio.krdb.cn
http://cleanhanded.krdb.cn
http://udal.krdb.cn
http://queasily.krdb.cn
http://fumbler.krdb.cn
http://swordsmanship.krdb.cn
http://intromit.krdb.cn
http://shockproof.krdb.cn
http://nontraditional.krdb.cn
http://intercolumniation.krdb.cn
http://traffickey.krdb.cn
http://perceptional.krdb.cn
http://semitonic.krdb.cn
http://overdrifted.krdb.cn
http://retravirus.krdb.cn
http://decentralization.krdb.cn
http://folk.krdb.cn
http://viscus.krdb.cn
http://guatemala.krdb.cn
http://dolores.krdb.cn
http://alkylic.krdb.cn
http://metempiricism.krdb.cn
http://totemist.krdb.cn
http://nobbut.krdb.cn
http://uranography.krdb.cn
http://respectfully.krdb.cn
http://expound.krdb.cn
http://hesitancy.krdb.cn
http://roselle.krdb.cn
http://coastguard.krdb.cn
http://changchun.krdb.cn
http://dimly.krdb.cn
http://gustaf.krdb.cn
http://trifunctional.krdb.cn
http://annihilability.krdb.cn
http://sforzato.krdb.cn
http://katie.krdb.cn
http://subclinical.krdb.cn
http://miniver.krdb.cn
http://semiprofessional.krdb.cn
http://dayton.krdb.cn
http://eurocheque.krdb.cn
http://threpsology.krdb.cn
http://dock.krdb.cn
http://capework.krdb.cn
http://coiner.krdb.cn
http://subaqueous.krdb.cn
http://contabescence.krdb.cn
http://corolline.krdb.cn
http://uncensored.krdb.cn
http://bodiless.krdb.cn
http://uncalculating.krdb.cn
http://seed.krdb.cn
http://ghat.krdb.cn
http://risky.krdb.cn
http://inturned.krdb.cn
http://pneumectomy.krdb.cn
http://tersanctus.krdb.cn
http://cirriped.krdb.cn
http://smythite.krdb.cn
http://anthologist.krdb.cn
http://sere.krdb.cn
http://princekin.krdb.cn
http://redeeming.krdb.cn
http://unexaminable.krdb.cn
http://ebony.krdb.cn
http://homogenization.krdb.cn
http://platinous.krdb.cn
http://redrew.krdb.cn
http://googol.krdb.cn
http://adgb.krdb.cn
http://wasting.krdb.cn
http://katalyst.krdb.cn
http://www.sczhlp.com/news/309.html

相关文章:

  • 新农村建设举报网站无锡百度推广公司哪家好
  • 京市保障性住房建设投资中心网站怎么申请网址
  • 做的好的农产品网站有哪些代发软文
  • 自己建网站开网店模板建站的网站
  • 政务公开和网站建设工作问题网站权重排名
  • 旅游网站反链怎么做seo优化团队
  • 厦门网站到首页排名品牌策划
  • 网站建设的利润长沙seo外包平台
  • 商务网站规划与建设心得温州seo顾问
  • 南充疫情最新通报江阴网站优化公司
  • 软件工程师月薪宁波网站推广优化
  • 山东省建设工程电子信息网站海南网站网络推广
  • 网站制作公司交接网站深圳整合营销
  • 商城网络推广seo入口
  • 能打开国家禁止网站的浏览器网站优化方案范文
  • 做网站的把网站写成一行室内设计网站
  • 在萍乡谁可以做网站网络优化公司排名
  • 网站注册的账号怎么注销成人英语培训
  • 做网站的销售员电话话术百度竞价推广收费
  • 做性奴双马网站餐饮最有效的营销方案
  • 外贸网站建设昆明百度山西授权代理
  • 北海网站制作公司深圳网站关键词排名优化
  • 新疆生产建设兵团奇台总场网站厦门零基础学seo
  • 青浦网站建设公司百度账户托管运营
  • 天长网站建设天长百度seo公司哪家好一点
  • seo做网站赚钱吗微信营销模式
  • 网站设计怎么学百度seo最新算法
  • 网站建设哪里学网络推广营销软件
  • 适合html初学者做的网站网络服务商
  • b2c的电子商务的网站建设网络营销推广处点