1.实现目的
这一节的目的主要是实现SqlSession级别的缓存,也就是一级缓存,首先看下图一,用户可以通过设置来进行是否开启一级缓存,不设置的化默认开启一级缓存,localCacheScope=SESSION为要设置一级缓存,localCacheScope=STATEMENT为不要设置一级缓存,(所以后面在清理缓存时会进行判断,如果是STATEMENT就删除缓存)。
2.简单说明
然后我们就需要解析下缓存设置,拿到缓存级别,执行Sql语句前,将按Mybatis的规定处理缓存key(id,参数,Sql语句,环境等等,生成对应hash值),然后判断缓存中是否有当前key的结果数据,有的化结果数据直接返回,没有的话就去执行数据库查询,然后将查询的结果存储到一级缓存Map中,然后判断缓存级别是否是STATEMENT,是的话代表不进行缓存操作,那此时删除,下次进来还是继续查询库,不是的话就不删除,留着继续执行同一SqlSession会话使用。
其实不算难,还是很简单,也很有调理,这就是设计的魅力,把每个类与属性现实化,那对应需要改动哪里,就都很清晰。
3.XML图
XML类图就是对应上面所说的逻辑的实现。
1.XMLConfigBuilder里专门解析设置操作,Configuration则是缓存级别的存储,此时需要用到LocalCacheScope枚举类,
2.Executor类里query方法添加一个参数缓存Key参数(CacheKey),然后在BaseExecutor里用没有缓存key参数的quey方法来获取缓存key的操作(这里获取就是生成,将对应需要的参数传到CacheKey类里处理Hash),得到后调用有缓存Key参数的query方法,这时此方法根据当前key去缓存查询是否有数据,有的话缓存拿出,没有执行queryFromDatabase方法,从数据库获取,获取完毕存储缓存里(存储Cache接口PerpetualCache类的map里)
4.代码实现
4.1 解析缓存设置
XMLConfigBuilder:解析配置XML构建器,这里需要添加私有方法settingsElement,主要解析缓存设置,解析完毕将缓存级别存储到configuration的LocalCacheScope枚举类下。
public class XMLConfigBuilder extends BaseBuilder {
// 省略其他方法
public Configuration parse() {
// STEP-17 添加设置
settingsElement(root.element("settings"));
}
/**
* 解析配置在 XML 文件中的缓存机制。并把解析出来的内容存放到 Configuration 配置项中。
* <settings>
* <!--缓存级别:SESSION/STATEMENT-->
* <setting name="localCacheScope" value="SESSION"/>
* </settings>
*/
private void settingsElement(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")));
}
}
Configuration:Configuration类里就针对缓存级别进行赋值操作。
public class Configuration {
// 缓存机制,默认不配置的情况是 SESSION
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
public LocalCacheScope getLocalCacheScope() {
return localCacheScope;
}
public void setLocalCacheScope(LocalCacheScope localCacheScope) {
this.localCacheScope = localCacheScope;
}
}
LocalCacheScope:缓存级别枚举,
/**
* @Author df
* @Description: 本地缓存机制;
* SESSION 默认值,缓存一个会话中执行的所有查询
* STATEMENT 不支持使用一级缓存,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不做数据共享
* @Date 2024/1/9 10:10
*/
// step 17添加
public enum LocalCacheScope {
SESSION,
STATEMENT
}
4.2 执行器缓存处理
Executor:
1.Executor类的修改是关于query方法时新添加缓存Key(CacheKey)操作,
2.添加了清除一级缓存方法定义以及创建缓存Key的方法定义
然后在BaseExecutor执行query时执行缓存存储,并根据参数生成缓存key,调用createCacheKey方法,增删改时删除缓存操作调用clearLocalCache方法。
public interface Executor {
// step-17 添加CacheKey参数
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException;
// step-17添加清理Session缓存
void clearLocalCache();
// step-17添加创建缓存 Key
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
}
BaseExecutor:
1.update方法:添加了调用此方法就会触发清除缓存操作clearLocalCache();,当然Mybaties的操作增删改全部调用update方法。
2.query定义的有两个方法,所以没有缓存Key的那个需要先生成缓存Key,传给有缓存Key参数的query方法。
3.有缓存Key参数的query方法,先判断queryStack是否为0,是否isFlushCacheRequired强制刷新如果都是则清除下当前缓存,然后根据缓存Key去调用一级缓存(PerpetualCache类)Map里是否存储了数据,没有的话去调用queryFromDatabase方法(查询库拿到数据结果),缓存有的话直接返回就可,最后判断下缓存级别是否STATEMENT,是的话删除缓存,不是的话不操作。
4.queryFromDatabase(),这是添加的私有方法,主要是查询库结果数据,查询到的结果存储到一级缓存中。
5.commit方法的更改就是事务提交就清除缓存,所以是SqlSession级别的吗,rollback方法也是调用此方法就清除缓存。
6.clearLocalCache方法,此方法清除一级缓存。
7.createCacheKey方法, 创建缓存Key的业务处理,按照MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 Key 使用,所以依次把参数传给CacheKey的update的方法。
public abstract class BaseExecutor implements Executor {
// 省略其他属性,方法
// 本地缓存
protected PerpetualCache localCache;
private boolean closed;
// 查询堆栈
protected int queryStack = 0;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.configuration = configuration;
this.transaction = transaction;
this.wrapper = this;
this.localCache = new PerpetualCache("LocalCache");
}
// update添加清除缓存操作
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
// step-17添加-----------------------------start
if (closed) {
throw new RuntimeException("Executor was closed.");
}
clearLocalCache();
// step-17添加------------------------------end
return doUpdate(ms, parameter);
}
// query查询时创建缓存Key,将Mybatis规定的参数传入进去。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// step-17 创建缓存Key,用这些参数组成缓存使用的Key。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// step-17添加
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) {
// 如果是STATEMENT证明不用缓存,所以此处清理缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
// step-17添加-----------end
}
// step-17添加,去数据库查询数据,查询结果将存储到一级缓存中
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 void commit(boolean required) throws SQLException {
if (closed) {
throw new RuntimeException("Cannot commit, transaction is already closed");
}
// step-17添加
clearLocalCache();
if (required) {
transaction.commit();
}
}
// 事务回滚完毕清理缓存
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
// step-17添加
try {
clearLocalCache();
} finally {
if (required) {
transaction.rollback();
}
}
}
}
// step-17添加,清理一级缓存
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
}
}
/**
* 创建缓存Key的hash,MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 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();
// sql的id
cacheKey.update(ms.getId());
// offset
cacheKey.update(rowBounds.getOffset());
// limit
cacheKey.update(rowBounds.getLimit());
// SQL
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);
}
// queryParams
cacheKey.update(value);
}
// 环境id
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
}
4.3 缓存操作
包:package cn.bugstack.mybatis.cache;
Cache接口:缓存的接口,这里提供了保存缓存、删除缓存、查询缓存、清除缓存、获取缓存长度等定义的方法。
/**
* @Author df
* @Description: SPI(Service Provider Interface) for cache providers. 缓存接口
* 缓存接口主要提供了数据的存放、获取、删除、情况,以及数量大小的获取。这样的实现方式和我们通常做业务开发时,定义的数据存放都是相似的。
* @Date 2024/1/9 10:24
*/
// step 17添加
public interface Cache {
/**
* 获取ID,每个缓存都有唯一ID标识
*/
String getId();
/**
* 存入值
*/
void putObject(Object key, Object value);
/**
* 获取值
*/
Object getObject(Object key);
/**
* 删除值
*/
Object removeObject(Object key);
/**
* 清空
*/
void clear();
/**
* 获取缓存大小
*/
int getSize();
}
包名:package cn.bugstack.mybatis.cache.impl;
PerpetualCache:一级缓存类,实现Cache的接口,把缓存放入到Map中就可以其他操作拉。
/**
* @Author df
* @Description: 一级缓存,在 Session 生命周期内一直保持,每创建新的 OpenSession 都会创建一个缓存器 PerpetualCache,
* 一级缓存实现类也叫永久缓存
* @Date 2024/1/9 10:26
*/
// step 17添加
public class PerpetualCache implements Cache {
private Logger logger = LoggerFactory.getLogger(PerpetualCache.class);
private String id;
// 使用HashMap存放一级缓存数据,session 生命周期较短,正常情况下数据不会一直在缓存存放
private Map<Object, Object> cache = new HashMap<>();
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();
}
}
CacheKey:缓存Key操作类,这个方法主要是BaseExecutor里的query方法调用,主要是doUpdate()方法,这里边的代码都有说明,这个方法主要是将传过来的参数处理成hash,并把原参数放入updateList(后边equal对比使用)。
这里重写了equals和hashcode等方式,如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。
/**
* @Author df
* @Description: 缓存 Key,一般缓存框架的数据结构基本上都是 Key->Value 方式存储
* MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
* @Date 2024/1/9 10:34
*/
// step 17添加
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_MULTIPLYER = 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_MULTIPLYER;
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);
}
}
/**
* 1.根据参数计算hash码值
* 2.为了保证不重复处理计算最终的码值
* 3.并将对象放入updateList集合中
*/
private void doUpdate(Object object) {
// 确保hashcode一直都是有的。
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);
}
}
/**
* 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。这也就为什么在 doUpdate
* 计算哈希方法时,把对象添加到 updateList.add(object); 集合中,就是用于这里的 equal 判断使用。
* */
// 重写对象的equals方法,用于对象判断
@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;
}
}
NullCacheKey:NULL值操作。
/**
* @Author df
* @Description: NULL值缓存Key
* @Date 2024/1/9 10:36
*/
// step 17添加
public class NullCacheKey extends CacheKey{
private static final long serialVersionUID = 3704229911977019465L;
public NullCacheKey() {
super();
}
}
4.4 其他更改
为了能够兼容使用,其他地方的更改。
ExecutionPlaceholder:ExecutionPlaceholder类,这个是在存储结果数据集前先存储此占位符。
包名:package cn.bugstack.mybatis.executor;
/**
* @Author df
* @Description: 占位符
* @Date 2024/1/9 11:29
*/
public enum ExecutionPlaceholder {
EXECUTION_PLACEHOLDER
}
MappedStatement:MappedStatement修改,此类添加私有的变量
public class MappedStatement {
// step-17添加
private boolean flushCacheRequired;
public boolean isFlushCacheRequired() {
return flushCacheRequired;
}
}
DefaultSqlSession:此类selectList方法调用query方法时去掉Sql的参数。
public class DefaultSqlSession implements SqlSession {
@Override
public <E> List<E> selectList(String statement, Object parameter) {
logger.info("执行查询 statement:{} parameter:{}", statement, JSON.toJSONString(parameter));
MappedStatement ms = configuration.getMappedStatement(statement);
try {
// step-17修改删除参数Sql
return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
} catch (SQLException e) {
throw new RuntimeException("Error querying database. Cause: " + e);
}
}
}