文章目录
- 一、目标:一级缓存
- 二、设计:一级缓存
- 三、实现:一级缓存
- 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
时,则进行清空缓存。
- 所以结合我们实现的 ORM 框架,在一个会话周期内添加缓存操作,当会话结束
二、设计:一级缓存
💡 一级缓存如何设计?
- mybatis的一级缓存:主要在于一次 session 会话周期内,将相同的执行语句结果缓存起来,避免重复执行数据操作。
- 当发生一切影响 session 会话的操作时,都会清空缓存,避免发生胀肚。
https://article-images.zsxq.com/Fq6qNr4inPGf3qav-TzvEntASm4n
- 在 Mybatis 的 XML 配置文件中,可以设置本地缓存的机制,如果不设置则是默认
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
入参对象就是用于拼装哈希值的具体操作。
- Mybatis 对于其 Key 的生成规则采取规则为
equals
:哈希 equal- 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存 Key 重写了
equals
对比方法。 - 这也是为什么在
doUpdate
计算哈希方法时,把对象添加到updateList.add(object)
集合中,就是用于这里的equals
判断使用
- 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存 Key 重写了
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,另外一个不含有。
- 含有缓存 Key 的
query
方法,会被另外一个不含有缓存 Key 的query
方法调用。
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 的创建,创建后调用重载的另外一个含有缓存 Key 的query
方法。
- 在
创建缓存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&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.
拦截SQL:SELECT 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.
拦截SQL:SELECT 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
拦截SQL:SELECT 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.
拦截SQL:SELECT 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 使用的场景。