mybatis缓存
mybatis缓存有一级缓存和二级缓存,mybatis为什么会采用两种缓存机制呢?让我们带着问题进入今天的主题吧。
mybatis 一级缓存
mybatis缓存在执行相同的SQL和相同的参数时,不会去数据库中重复调用,只需要从缓存中取出来即可,只要保持在同一会话和其它相关的条件下,我们的多求请求实际只调用了一次,可以大大节省查询数据的开销。但在我们实际开发中,一级缓存使用的频率较低, 为什么呢?
一级缓存只存在于当前会话中, 如果脱离了当前会话将不可以使用,但我们常搭配spring来使用mybaits,但spring中每 条sq|语句处于单独的事务中,执行成功后默认就提交了事务并且关闭了会话。
小知识:我们之前有提到一个会话中有且只存在一 个事务, 所以spring在使用mybatis时默认每次都会重新开启一个新的会话,但如果我们给语言加上一 个事务@Transactional, 就可以使用事务了。
解读一级缓存
一级缓存的逻辑在BaseExecutor里面完成的,主要逻辑如下:
//源码位为BaseExecutor 140行
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//flushCache 设置判断是不是需要清除缓存,这个参数可以设置
//queryStack =0 时没有递归调用
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) {
//若在缓存列表中找到有对应的缓存,处理localOutputParameterCache
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//未命中缓存时从数据库中寻找
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
//延迟加载队列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
//清空延迟加载队列
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
//缓存作用域如果是STATEMENT,由清除一级缓存和参数缓存
clearLocalCache();
}
}
return list;
}
一级缓存的命中场景
运行时必要条件:
1.必须必在同一个会话
2.SQL语句、参数必须相同
3.相同的statementId
4.RowBounds 相同(即添加的分页属性)
// 1.sql 和参数必须相同
// 2.必须是相同的statementID
// 3.sqlSession必须一样 (会话级缓存)
// 4.RowBounds 返回行范围必须相同
@Test
public void test1(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// com.toto.UserMapper.selectByid
User user = mapper.selectByid(1);
RowBounds rowbound=RowBounds.DEFAULT;
//selectOne最终的底层调用的也是selectList,因为statementID、sqlSession、rowbound、sql和参数一样,所以一级缓存可用
List user1 = sqlSession.selectList(
"com.toto.UserMapper.selectByid", 1,rowbound);
System.out.println(user == user1.get(0));
}
输出结果:
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, 管理员大哥, 2016-08-07 14:07:10
Total: 1
Cache Hit Ratio [com.toto.UserMapper]: 0.0
true
操作与配置相关条件:
1.未提交、回滚当前事务,这相当于手动清空缓存
2.未配置flushCache = true,默认default,如果配置true上面代码就会删除缓存,下面有图例
3.未执行update
4.缓存作用域不是STATEMENT
// 1.未手清空
// 2.未调用 flushCache=true的查询
// 3.未执行Update
// 4.缓存作用域不是 STATEMENT-->
@Test
public void test2(){
// 会话生命周期是很短暂的
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid3(1);
sqlSession.commit();// 这会调用clearCache()
sqlSession.rollback();// 这会调用clearCache()
// mapper.setNickName(1,"管理员小弟");// 修改时也会 clearCache()
User user1 = mapper.selectByid3(1);// 数据一致性问题
System.out.println(user == user1);
}
一级缓存源码解析
我先发我整理的图运行流程图(太丑勿喷,能看就行):
我跟着流程把关键代码走读一下(多图警告):
Spring与Mybatis一级缓存不得不说的事
前面我有提到增加事务就能使用Mybatis的事务,这是为什么呢?
让我们带速读一下流程吧
@Test
public void testBySpring(){
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
UserMapper mapper = context.getBean(UserMapper.class);
//动态代理 动态代理 MyBatis
// mapper ->SqlSessionTemplate --> SqlSessionInterceptor-->SqlSessionFactory
DataSourceTransactionManager transactionManager =
(DataSourceTransactionManager) context.getBean("txManager");
// 手动开启事物
TransactionStatus status = transactionManager
.getTransaction(new DefaultTransactionDefinition());
User user = mapper.selectByid(1); // 每次都会构造一个新会话 发起调用
// 每次都会构造一个新会话 发起调用,但开启事物后原会话没被关闭,仍使用同一个事务,即可使用一级缓存
User user1 =mapper.selectByid(1);
System.out.println(user == user1);
}
总结
1.与会话密切相关,相同会话才能触发
2.参数条件相关,包括分页属性也是参数的条件,还有参数及其相关配置项
3.提交回滚,修改都会清空
MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码。所以参数条件很重要
Mybatis 二级缓存
应用级缓存,可以跨线程使用,一般应用于修改情况比较少的表做查询缓存,并且查询时优先于一级缓存,搭配使用时攻防兼备。
完整的解决方案
1.内存存储: 实现简单,速度比较快,但不能持久化,并且容量比较有限
2.存到硬盘:可以持久化,容量较大,但访问速度没内存快
3.分布式存储:分布式场景存到单硬盘/内存服务器存储缓存会造成一些通病问题,可以使用redis来进行缓存,这也是分布式场景的解决方法之一。
缓存溢出淘汰机制
当容量满的了时候我们就会对缓存进行清除,清除的算法的就是溢出淘汰机制。
常见的算法有:
1.FIFO:先进先出算法
2.LRU:最近最少使用
3.WeakReference:弱引用
将缓存对象进行弱引用包装,当Java进行gc的时候, 不论当前的内存空间是否足够,这个对象都会被回收
4.SoftReference:软引用
与弱引用类似,不同在于只有当空间不足时GC才回收软引用对象
缓存线程的安全问题
我们要保证二级缓存被其它线程使用,为了保证线程写和读的安全,当拿到缓存数据后,可以对它进行修改而不能影响原缓存数据, 通常采取的是序列化缓存对象然后进行深拷贝操作。所以这要求我们要缓存的实体类必须序列化(实现Serializable) ,不然会出现不能序列化的BUG.
小知识:我们常使用的存储类型基本都实现了Srializable方法,例如: hashmap,arraylist,String …
Mybatis二级缓存解决方案
mybaits是怎么实现它的二级缓存的解决方案呢?
mybaits使用的是责任链的解决方案,首先它定义了缓存中的一些最基本的公用方法,全部定义到Cache的类里面, 这些功能包括设置缓存,获取缓存,清除缓存,获取缓存数量,然后我们上述的所有功能都会对应一个组件类,并基于装饰者模式加责任链的模式,将各个组件进行串连。在执行缓存的基本功能时,其它的缓存通过责任链来依次往下传递。
那它的设计模式有哪些优点呢?
职责单一,各个节点只负责自己的逻辑, 不关心其它逻辑
扩展性强,可根据需要扩展节点,删除节点还可以调换顺序,保证了灵活性
有松耦合,各节点之间不仅没有强制依赖其它节点,而是通过顶层的Cache接口进行间接依赖
详细介绍每个缓存对应的功能篇幅较长,后面挖坑再更,mybatis提供的一些cache在org.apache.ibatis.cache包下,其中一级缓存使用的impl下的,二级缓存使用的是decorators包下的。
二级缓存开启条件
二级缓存不像一级缓存一样会默认开启,我们可以通过@CacheNamespace或为指定Mappedstatement做声明。声明后该缓存为该Mapper所独有, 其它未设置的不能便用。但如果需要多个mapper用同一个缓存空间,我们可以通过@CacheNamespaceRef来进行引用到同一个缓存空间。
如果是xml,可以通过标签来添加对此mapper进行级缓存,同理使用可以引用到同一个缓存会话空间。
下面介绍一下我们使用缓存时可以配置的相关参数:
配置名 | 配置描述 |
---|---|
implementation | 指定缓存的存储实现类默认是用PerpetualCache的HashMap存储在内存当中 |
eviction | 指定缓存溢出淘汰实现类,默认LRU,清除最近最少使用 |
flushInterval | 设置缓存定时全部清空时间,默认不清空。清空策略是到时间整体清空而不是为某个值清空 |
size | 指定缓存容量,超出后会按eviction所指定的算法进行淘汰 |
readWith | tue通过序列化复制来保证缓存对象是可读与的,默认true, false则多线程操作同个对象 |
blocking | 为每个key的访问添加了阻塞锁,可以有效防止缓存击穿 |
properties | 为上述组件,配置额外参数,key对应组件中的字段名 |
如:
@CacheNamespace(implementation = MybatisRedisCache.class,eviction = FifoCache.class)
<cache type="com.toto.MybatisRedisCache">
<property name="eviction" value="LRU" />
<property name="size" value="1024" />
<property name="readOnly" value="false" />
</cache>
二级缓存命中条件
二级缓存和一级缓存命中差不多,但必须要提交会话,并且可以不同会话,下面列举了命中条件
1.会话提交
2.SQL语句和参数相同
3.相同的StatementId
4.RowBounds相同
为什么二级缓存一定需要提交事务呢,我画一张图来表示吧:
如果会话不提交,则会出现脏读,为了避免这种事情发生,所以必须先提交才能获取二级缓存,包括对缓存的清空,也必须是会话正常提交之后才生效。 那如果没提交,是不是存在一个临时存储区域呢?
二级缓存结构
为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都一个唯一的事物缓存管理器,所以这里暂存区也可叫做事物缓存。
如图:
我画了一张两个会话情景下事物缓存和暂存区的图:
二级缓存执行流程
原本会话是通过执行器来实现SQL调用,这里是基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截,然后嵌入了二级缓存相关逻辑。
查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
源代码:
//CachingExecutor 93 行
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms); //是否有设置了清除缓存
if (ms.isUseCache() && resultHandler == null) { //如果设置了返回值处理Handler则不会触发缓存
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);//从缓存的tcm中获取缓存
if (list == null) {
// 向下通往BaseExecutor,先走一级缓存,最后取真实的执行器执行
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 查询成功后添加进二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
更新操作update
当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。但如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms); //是否有设置了清除缓存,update
return delegate.update(ms, parameterObject);
}
提交操作commit
当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}