mybatis一级缓存和二级缓存

计算机有两大基本的功能:计算存储
存储方面,缓存的设计和实现也是一门学问。这门学问里面包含什么门道呢?不妨研究一下MyBatis缓存类PerpetualCache,一定会大有收获的。在MyBatis里面,存在一个PerpetualCache,它是一级缓存、二级缓存的最基本实现,但PerpetualCache只不过包装了一下HashMap。Perpetual是"永久、不间断"之意,以PerpetualCache为根本,在cache.decorators包里面有多种缓存的代理,实现了各种清除策略。

缓存的设计有两个重点问题:如何存储数据数据清除策略
存储的话,用哈希表即可完美解决。对于清除策略而言,往往有多种选择。MyBatis作者Clinton Begin选择Perpetual来命名缓存,暗示这是一个最底层的缓存,数据一旦存储进来,永不清除,如果实现清除策略,请把Perpetual包装一下。在cache.decorators包里面有多种缓存,它们内部串联的主线就是按照“不同的清除策略”来贯穿的。

一级缓存

Mybatis对缓存支持,默认情况下,只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以参数完全一样的情况下,我们使用同一个SqlSession对象调用相同Mapper的相同方法,只执行一次SQL,这也是一级缓存生成key的策略。因为使用SqlSession第一次查询后,MyBatis会将其放在缓存中,再次查询的时候,如果没有声明刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。

1、同一个SqlSession是什么意思?

这里说的SqlSession不是SqlSessionTemplate,而是指通过DefaultSqlSessionFactory.openSession(执行器) 得到的DefaultSqlSession,当外部请求进来,都会重新生成一个新的DefaultSqlSession,并且在生成DefaultSqlSession的过程中,也会重新创建执行器,比如

new SimpleExecutor(this, transaction);

中间会调用父类的构造方法BaseExecutor,这里就有重新创建一级缓存对象

protected BaseExecutor(Configuration configuration, Transaction transaction) {this.transaction = transaction;//重新生成缓存对象。this.localCache = new PerpetualCache("LocalCache");this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");this.closed = false;this.configuration = configuration;
}

所以每次重行进来就会新生成一个DefaultSqlSession,那么他的缓存对象也会重新生成,所以说一级缓存级别是SqlSession级别,当执行完commit后,需要关闭sqlSession,在关闭sqlSession的同时也会清空一级缓存

private class SqlSessionInterceptor implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//1、得到新的DefaultSqlSessionSqlSession sqlSession = getSqlSession(.....);try {//2、执行查询包括数据和缓存Object result = method.invoke(sqlSession, args);if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {sqlSession.commit(true);//没有开启事务,就会自动提交事务}return result;} catch (Throwable t) {...} finally {//3、关闭sqlSessionif (sqlSession != null) {closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}}
}
//关闭sqlSession,并且清空一级缓存
@Override
public void close(boolean forceRollback) {try {....} catch (SQLException e) {。。。。} finally {transaction = null;localCache = null;//清空一级缓存localOutputParameterCache = null;closed = true;}
}

2、一级缓存的生命周期有多长?

1、MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
2、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
3、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
4、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

3、Demo

下面演示下mybatis和spring整合的项目demo,触发一级缓存sql打印的变化。

@Override
public User doSomeBusinessStuff(String userId) throws Exception {userMapper.getUser("3");userMapper.getUser("3");//再次执行相同的查询操作return null;
}

为什么会查询两次,难道没有缓存有问题?上面说了,一级缓存是SqlSession级别的,2次执行getUser方法,都创建了一个新的SqlSession,其实是userMapper的这个接口被spring代理了,里面有个很关键的代码。

当执行完getUser方法后,会调用Commit和close方法(Commit会清空整个SqlSession的一级缓存),导致第一次和第二次执行getUser得到的SqlSession不是同一个,所以执行了2次sql查询。如果避免commit和close,就要开启事务(不晓得怎么开启事务请查看这边文章:开启事务)

@Transactional
@Override
public User doSomeBusinessStuff(String userId) throws Exception {userMapper.getUser("3");userMapper.getUser("3");return null;
}

打印sql

这次只创建了一次SqlSession,且只打印了一次sql。

二级缓存

先看下二级缓存的工作机制

二级缓存默认是不开启的,如果需要开启二级缓存。开启二级缓存有3步,这里介绍使用注解的方式

1@CacheNamespace(blocking = true)2<cache-ref namespace="com.winterchen.dao.UserDao"/>3DO实现Serializable接口

在你的Mapper上加上@CacheNamespace(blocking = true)注解就可以了。看下使用二级缓存后打赢出来的日志。

@Override
public User doSomeBusinessStuff(String userId) throws Exception {userMapper.getUser("3");userMapper.getUser("3");return null;
}

为了避免一级缓存影响sql打印,先把事务关了。

可以发现也是打印了一次sql,但是创建了两次SqlSession,说明第二次是从二级缓存里面取得。二级缓存的select语句将会被缓存,insuret、update、deleted语句会刷新缓存

2、注意

二级缓存有个坑:那就是当你在2个mapper里面都引用了同一张表,就比如,mapper1里面有个User表查询操作,mapper2有个user表更新操作,当再次在mapper1里面在执行查询操作,发现拿到的还更新前的数据。这就是缓存Key生成原则问题,缓存key是通过mapper进行划分的,相同的mapper里面所有方法,使用的是同一个缓存区域,所以不同的mapper里面操作同一张表就会出现上面那种问题。在现实中,线上的一个应用最少2个实例,这个时候,这个问题就暴露出来了,所以缓存最好还是使用第三方缓存插件。

一缓存源码介绍

1、这里介绍下一级缓存的源码

一级缓存的实现类PerpetualCache,里面其实就是一个HashMap没有过多的并发并发设计。key是一个Object,他的类型是CacheKey,所以在Map里面put或者get操作时,CacheKey的HashCode方法和equals就很重要了。


public class PerpetualCache implements Cache {private Map<Object, Object> cache = new HashMap<Object, Object>();
}//Key的设计:CacheKey,很多属性
public class CacheKey implements Cloneable, Serializable {private final int multiplier;private int hashcode;   //哈希码存到Map里面会用到,这个也是提前生成好的。private long checksum;  //总和校验,当出现复合key的时候,分布计算每个key的哈希码,然后求总和private int count;      //list的数量private List<Object> updateList; //当出现复合key的时候,保存每个key。@Overridepublic boolean equals(Object object) {。。。。。。for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;}@Overridepublic int hashCode() {return hashcode;}
}

下面是实际使用时CacheKey的截图

关于key的生成规则

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {CacheKey cacheKey = new CacheKey();cacheKey.update(ms.getId());cacheKey.update(rowBounds.getOffset());  //页码cacheKey.update(rowBounds.getLimit());   //查询条数cacheKey.update(boundSql.getSql());      //sql语句for (ParameterMapping parameterMapping : parameterMappings) {....cacheKey.update(value);//将参数转成value,塞进去}return cacheKey;
}

hashCode的生成
hashCode的生成是在调用CacheKey的update方法同时设置的,其实update可以把他看做是List的add操作,只不过更改的东西比较多所以叫成update。

public void update(Object object) {//生成HashCodeint baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;  checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;//更新HashCodeupdateList.add(object);
}

小结:keyd的生成需要=MapperId+Offset+Limit+SQL+所有的入参

2、一级执行流程

最终调用BaseExecutor的query方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;//查询一级缓存if (list == null) {//缓存为空,走数据库查询list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}return list;
}//通过数据库查询,在更新缓存内容
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);//查询数据库localCache.putObject(key, list); //更新一级缓存return list;
}

2、二级缓存

开启二级缓存步骤:

步骤1、在Mapper.java类里面添加如下配置

@CacheNamespace(blocking = true)
public interface UserDao{}

步骤2、在具体Mapper.xml里面添加如下配置

<mapper namespace="com.winterchen.dao.UserDao" >.....<cache-ref namespace="com.winterchen.dao.UserDao"/>
</mapper>

3、DO对象实现接口Serializable接口

public class UserDomain implements Serializable {}

下面从源码的角度分析二级缓存

1、解析步骤1和步骤2的配置

//配置的解析是在创建SqlSessionFactory的时候
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {//解析Mapper解析类XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(....);xmlMapperBuilder.parse();//开始解析。。。。。}
public void parse() {if (!configuration.isResourceLoaded(resource)) {//1、这里会解析Mapper.xml配置文件中的"cache-ref"标签configurationElement(parser.evalNode("/mapper"));//2、解析Mapper.java上的注解,创建缓存对象bindMapperForNamespace();}
} 

先看下解析cache-ref标签

private void configurationElement(XNode context) {cacheRefElement(context.evalNode("cache-ref")); //解析Mapper.xml的配置文件中的cache-ref标签。
}

解析Mapper上的注解,Mapper加了CacheNamespace注解

public <T> void addMapper(Class<T> type) {if (type.isInterface()) {MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();}
}
//最终来到parseCache方法创建缓存对象
private void parseCache() {//type就是我们业务定义的Mapper.java类型,从里拿@CacheNamespace的注解信息CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);if (cacheDomain != null) {//数量Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();//Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();Properties props = convertToProperties(cacheDomain.properties());//创建二级缓存assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);}
}
//创建缓存对象BlockingCache
public Cache useNewCache(....) {//创建Cache对象,注意每个Namespace都有一个独立的Cache对象,也就是以Namespace进行划分,Namespace指的就是我们具体的Mapper.java的类名。//执行完最终build()方法,最终生成BlockingCache缓存对象。Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval) //缓存的刷新周期.size(size)    //缓存的容量大小.readWrite(readWrite).blocking(blocking) //缓存的是否有阻塞功能.properties(props).build();configuration.addCache(cache);存到configuration里面取,查询的时候要用return cache;
}

3、关于BlockingCache我们看下是什么

BlockingCache是阻塞功能的缓存装饰器,它保证只有一个线程到数据库中查找指定key对应的数据。
假如线程A在BlockingCache中未查找到keyA对应的缓存项时,线程A会获取keyA对应的锁,这样后续线程在查找keyA是会发生阻塞

public class BlockingCache implements Cache {private final Cache delegate;//缓存实际对象。//可选项,获取锁定超时时间,如果设置了,在指定时间内没有拿到数据就当获取所失败private long timeout;//key就是我们每次查询时基于方法+sql+分页参数生成的key,value是ReentrantLock对象,当相同的方法被调用时会,会通过key拿到对应的ReentrantLock对象,//当多个线程调用同一个查询方法,那么拿到的ReentrantLock是同一个对象,这时候多个线程就要共同来竞争这个锁了,使用tryLock或者lock来获取锁,前者是带超时时间的。private final ConcurrentHashMap<Object, ReentrantLock> locks;public BlockingCache(Cache delegate) {this.delegate = delegate;this.locks = new ConcurrentHashMap<Object, ReentrantLock>();}
}

在构建BlockingCache缓存对象时,虽然是返回了BlockingCache对象,但是BlockingCache里面对象是属性关系有点绕,就像套娃一样,其实他的最终缓存对象就是PerpetualCache类型,这个类型我们在介绍一级缓存的时候说过他了,而PerpetualCache就是对HashMap的一个封装。

4、使用缓存

当缓存创建好了,触发查询最进到CachingExecutor的query方法里面

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {//二级缓存Cache cache = ms.getCache();//也就是拿到我们上面说的BlockingCache缓存对象if (cache != null) {List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {//二级缓存没有,走数据库查询和一级缓存查询,对应的也就是上面说的BaseExecutor的query方法逻辑list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list);}return list;}//这里走数据库查询和一级缓存查询,对应的也就是上面说的BaseExecutor的query方法逻辑return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

tcm.getObject(cache,key); 这个代码东西比较多,流程上,大概就是,先通过BlockingCache拿到锁对象,然后查看缓存内是否有数据,如果有就返回,释放锁,如果没有就走下面无缓存的逻辑,也就是查数据库,最后在commit,注意是commit后才把数据库的数据存到缓存里面去,并释放锁。

下面重点分析tcm.getObject(cache,key)的逻辑,不感兴趣的可以跳过这个小点。

List<E> list = (List<E>) tcm.getObject(cache, key);//先看下tcm是什么//1、在CachingExecutor里面tcm是TransactionalCacheManager对象。
public class CachingExecutor implements Executor {//tcm是TransactionalCacheManager对象,他被包装在了CachingExecutor执行器里面,CachingExecutor这个我们知道,其实就是对我们常用的执行一个封装,只不过使用CachingExecutor就可以使用二级缓存了。private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
//2、TransactionalCacheManager又是什么
public class TransactionalCacheManager {//key是Cache类型,Cache的实现类是我们前面说的BlockingCache,//value是TransactionalCache类型,TransactionalCache是什么?//我们二级缓存其实是一个大的Map,每个Namespace都有一个BlockingCacheprivate final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
}//3、TransactionalCache又是什么
public class TransactionalCache implements Cache {//保存BlockingCache对象private final Cache delegate;//保存未提交事务前的缓存数据,当执行完commit后会把这个数据塞到BlockingCache里面去。private final Map<Object, Object> entriesToAddOnCommit = new HashMap<Object, Object>();//读取二级缓存未命中数据的key,这个是做什么用的?private final Set<Object> entriesMissedInCache = new HashSet<Object>();public TransactionalCache(Cache delegate) {this.delegate = delegate;    //构建是传进来。this.clearOnCommit = false;。。。。。}
}当我们调用tcm获取数据时,通过Map里面拿到BlockingCache所对应的TransactionalCache,再调用TransactionalCache的getObject方法,getObject()其实就是调用BlockingCachegetObject()的方法
public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);
}private TransactionalCache getTransactionalCache(Cache cache) {TransactionalCache txCache = transactionalCaches.get(cache);if (txCache == null) {txCache = new TransactionalCache(cache);//通过BlockingCache来创建TransactionalCache对象transactionalCaches.put(cache, txCache);//在保存起来,前面我们说了BlockingCache是Mapper.Java的维度生成的。}return txCache;
}

当调用BlockingCache的getObject(key)时就进到下面了的逻辑中

```java
@Override
public Object getObject(Object key) {// 获取key对应的锁acquireLock(key);// 查询key,此时的delegateObject value = delegate.getObject(key);if (value != null) {// 如果从缓存(PrepetualCache是用HashMap实现的)中查找到,则释放锁,否则继续持有锁releaseLock(key);}//注意如果value是null是不会释放锁的        return value;
}//获取锁
private void acquireLock(Object key) {//通过key拿到属于这个key的ReentrantLock对象,因为多个线程调用同一个方法拿到的ReentrantLock是同一个。Lock lock = getLockForKey(key);if (timeout > 0) {boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);if (!acquired) {throw new CacheException(....);}} else {lock.lock();}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关闭mysql,关闭redis服务

1. 关闭redis服务&#xff1a; 查询redis安装目录&#xff1a; whereis redis which redis find / -name redis 关闭redis服务&#xff1a; redis-cli -h 127.0.0.1 -p 6379 auth 输入密码 shutdown 关闭redis服务 2. 关闭mysql服务&#xff1a; 查询mysql安装目录&…

python中不可变类型和可变类型

不可变类型&#xff1a;修改之后内存存储地址不会发生改变 可变类型&#xff1a;修改之后内存存储地址发生改变 set

防火墙管理工具增强网络防火墙防御

防火墙在网络安全中起着至关重要的作用。现代企业具有多个防火墙&#xff0c;如&#xff1a;电路级防火墙、应用级防火墙和高级下一代防火墙&#xff08;NGFW&#xff09;的复杂网络架构需要自动化防火墙管理和集中式防火墙监控工具来确保边界级别的安全。 网络防火墙安全和日…

中文编程开发语言工具开发的实际软件案例:称重管理系统软件

中文编程开发语言工具开发的实际软件案例&#xff1a;称重管理系统软件 中文编程开发语言工具开发的实际软件案例&#xff1a;称重管理系统软件&#xff0c;软件可以安装在电脑上&#xff0c;也可以安装在收银机上&#xff0c;支持触摸和鼠标点&#xff0c;想学编程可以关注系统…

如何降低海康、大华等网络摄像头调用的高延迟问题(一):海康威视网络摄像头的python sdk使用(opencv读取sdk流)

目录 1.python sdk使用 1.海康SDK下载 2.opencv读取sdk流 先说效果&#xff0c;我是用的AI推理的实时流&#xff0c;延迟从高达7秒降到小于1秒 如果觉得这个延迟还不能接受&#xff0c;下一章&#xff0c;给大家介绍点上不得台面的小方法 SDK&#xff08;Software Developme…

React组件渲染和更新的过程

一、回顾Vue组件渲染和更新的过程 二、回顾JSX本质和vdom 三、组件渲染和更新 1、组件渲染过程 props state (组件有了props state)render()生成vnodepatch(elem, vnode) 2、组件更新过程 setState(newState) --> dirtyComponents (可能有子组件)render()生成newVnodepa…

温湿度监测技术又进化了,这个操作太牛了!

无论是在家庭、医疗、农业、制造业&#xff0c;还是在物流和食品行业&#xff0c;精确的温湿度监控对于确保安全、质量和效率都至关重要。 客户案例 医疗行业 在医疗行业&#xff0c;温湿度监控对于存储药品、生物样本和医疗设备至关重要。山东某医院引入了泛地缘科技推出的温湿…

LeetCode【17】电话号码的字母组合

题目&#xff1a; 思路&#xff1a; 参考&#xff1a;https://blog.csdn.net/weixin_46429290/article/details/121888154 和上一个题《子集》的思路一样&#xff0c;先画出树结构&#xff0c;看树的深度&#xff08;遍历层级&#xff09;&#xff0c;树的宽度&#xff08;横向…

智慧门牌管理系统:省市区县区划数据与国家级开发区共融

文章目录 前言一、行政区划数据的重要性二、支持国家级开发区的发展三、数据基础的重要性 前言 随着科技的飞速发展&#xff0c;我们的生活正在发生日新月异的变化。其中&#xff0c;智慧城市的概念正逐渐成为我们生活中的一部分。智慧城市&#xff0c;顾名思义&#xff0c;运…

性能测试-JMeter分布式测试及其详细步骤

性能测试概要 性能测试是软件测试中的一种&#xff0c;它可以衡量系统的稳定性、扩展性、可靠性、速度和资源使用。它可以发现性能瓶颈&#xff0c;确保能满足业务需求。很多系统都需要做性能测试&#xff0c;如Web应用、数据库和操作系统等。 性能测试种类非常多&#xff0c…

wps/word 之 word中的两个表格 如何合并成为一个表格(已解决)

第一步&#xff1a;新建两个表格&#xff1a; 如何实现上面的两个表格合并呢&#xff1f; 分别选定每个表格&#xff0c;然后鼠标右键---》表格属性 在表格属性中的 表格---》选择 无文字环绕。 第二个表格按照同样的方法 设置 无文字环绕。 然后将中的文本行删去即可以了。选…

去中心遇见混币器

区块链的去中心化交易所在保护隐私和安全性上有着无可比拟的优势&#xff0c;用户甚至不需要提供注册资料&#xff0c;只要有web3钱包即可跟智能合约交易。在uniswap上可兑换绝大多数加密币&#xff0c;新推出的衍生品交易所ununx已经可以交易美股&#xff0c;期货和外汇,一个全…