一、 Mybatis 缓存
- 缓存就是内存中的数据,常常来自对数据库查询结果保存,使用缓存,我们可以避免频繁的鱼数据库进行交互,进而提高响应速度
mabatis 也提供了对缓存的支持,分为一级缓存和二级缓存,下图理解:
-
①、 一级缓存是 SqlSession 级别的缓存,在操作数据库时,需要构造SqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的SqlSession之间缓存数据区域(HashMap)是互相不影响的。
-
②、 二级缓存是Mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨 SqlSession 的
问题来了:一级缓存在哪,二级缓存在哪?上次发布这个篇章,没解决,就匆忙发布了,无耻…
见下篇幅
这两天内心不踏实,为啥不踏实,总觉得Mybatis被自己走眼观花的看了,小叶叶,你到底是学技术,还是应付面试 ,哼,两者兼顾,缓存重新温固下源码~;
1.1 一级缓存(Mybatis默认开启)
原理:
图解:
模拟流程:
-
第一次发起查询sql查询用户id为1的用户,先去找缓存中是否有id为1的用户,如果没有,再去数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
-
如果sqlsession执行了commit操作(插入,更新,删除),会清空sqlsession中的一级缓存,避免脏读
-
第二次发起查询id为1的用户,缓存中如果找到了,直接从缓存中获取用户信息
-
mybatis默认支持一级缓存。
源码探究
网上所说,以及缓存,SqlSession底层原理实现是HashMap,那么我们带三(+1)个问题去看源码
①、 缓存的底层真是HashMap?
②、 缓存啥时候创建的?如何存取的?
③、 缓存啥时候销毁的 ?
④、 一级缓存和二级缓存代码区分究竟在那? 问题放在 二级缓存 查看
问题①思路:
一级缓存的底层是HashMap?
先查看SqlSession源码,方法只有 clearCache()方法有清除缓存的意思,Ctrl + T,找实现类
都指向 :
PerpetualCache 缓存类
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
问题②思路
②、 缓存啥时候创建的?如何存取的?
缓存,缓存,不明私意肯定是对查询结果缓存,依次对Query方法跟踪 ,创建key如下截图 :
①先从缓存获取数据:
②缓存中若不存在,则从库中获取数据并存入缓存:
问题③思路
③、 缓存啥时候销毁的 ?
在 BaseExecutor 内,设计提交都会清除缓存,也就是 增删改都会清除缓存;
1.2 二级缓存(配置,手动开启)
图解:
首先要手动开启mybatis二级缓存。
PerpetualCache.java 类底层保存二级缓存数据,其中属性如下;
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap();
由此可见,底层还是基于HashMap存储;
1.1 在config.xml设置二级缓存开关 , 还要在具体的mapper.xml开启二级缓存
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
1.2 需要将映射的java pojo类实现序列化
二级缓存是多样化的,可能会存储在硬盘中,实体类序列化要求
class Student implements Serializable{}
1.3 开启本XXXMapper的二级缓存(如OrderMapper)
<!--开启本Mapper的namespace下的二级缓存-->
<cache eviction="LRU" flushInterval="10000"/>
cache属性的简介:
eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
(1) LRU(Least Recently Used),最近最少使用的,最长时间不用的对象
(2) FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们
(3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
(4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,
移除最长时间不用的对形象
flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
SQL被执行的时候才会去刷新缓存。
size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
这里配置的是1024个对象
readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有
办法修改缓存,他的默认值是false,不允许我们修改
操作过程:
1. sqlsession1查询用户id为1的信息,查询到之后,会将查询数据存储到二级缓存中。
2. 如果sqlsession3去执行相同mapper下sql,执行commit提交,会清空该mapper下的二级缓存区域的数据
3. sqlsession2查询用户id为1的信息, 去缓存找 是否存在缓存,如果存在直接从缓存中取数据
禁用二级缓存:
在statement中可以设置useCache=false,禁用当前select语句的二级缓存,默认情况为true
useCache标签:是否启用缓存
<select id="getStudentById" parameterType="java.lang.Integer" resultType="Student"
useCache="false">
在实际开发中,针对每次查询都需要最新的数据sql,要设置为useCache="false",禁用二级缓存
注解开发设置缓存参数 , Options
@Options(useCache = false)
@Select("select * from user")
public List<User> findAllUser();
flushCache标签:刷新缓存(清空缓存,默认值true ,增删改操作,都会刷新缓存)
<select id="getStudentById" parameterType="java.lang.Integer" resultType="Student"
flushCache="true">
一般下执行完commit操作都需要刷新缓存,flushCache="true 表示
刷新缓存,可以避免脏读
注解开发设置缓存参数
@Options(useCache = false , flushCache = )
@Select("select * from user")
public List<User> findAllUser();
-
二级缓存应用场景
对于访问多的查询请求并且用户对查询结果实时性要求不高的情况下,可采用mybatis二级缓存,
降低数据库访问量,提高访问速度,如电话账单查询
根据需求设置相应的flushInterval:刷新间隔时间,比如三十分钟,24小时等。。。
- 问题 ④ 思路
一级缓存和二级缓存代码在哪被区分开了? - i、 二级缓存开启需要手动配置,我们查看需要Configuration的缓存变量参数cacheEnabled
- ii、二级缓存,我们可以自己定义二级缓存实现Cache接口类,那么调用Cache接口的缓存,很可能是二级缓存
从SqlSession查询源头搞起:
SqlSession查询
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
建立缓存key对象
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
从缓存查找
- 查List缓存的属性是这个:localCache
// 如果对应的
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
// 进去查找取缓存
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 数据库查
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// handleLocallyCachedOutputParameters方法内的一段代码截取
Object cachedParameter = this.localOutputParameterCache.getObject(key);
- 这里取缓存参数属性是这个 :localOutputParameterCache ,这就疑问了,为啥定义两个缓存属性,一个查值,一个查参数,暂时确实木知,在查参后,又将Configuration的参数换成缓存参数… ~
- 查来查去,还是查完后将数据保存了缓存,这根二级缓存有关系吗?没有啊 ,说明我们源头弄错了…
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
this.localCache.putObject(key, list);
-
继续 :
-
原来 SqlSessionFactory在openSession() 的时候,已经设置好了 Executor的实现类,
Executor executor = this.configuration.newExecutor(tx, execType);
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object executor;
if (ExecutorType.BATCH == executorType) { // 批量
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction); //预处理
} else {
executor = new SimpleExecutor(this, transaction); // 简单处理(默认)
}
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor); // 缓存处理
}
// 拦截器,返回代理 executor 对象
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor的查询:
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) {
this.flushCacheIfRequired(ms); // 在设定参数为flushCache时,每次查询都要求最新数据,则每次都清理缓存
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key); // 从缓存中获取
if (list == null) {
// 调用传入的 Executor接口,进行查询,那么一级缓存就存入了
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 二级缓存也存入了
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存局限性:
mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。
1.3 Mybatis使用Redis实现二级缓存
问题产生:二级缓存,底层使用HashMap存储,这个是基于单服务器工作,无法实现分布式缓存,什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了1服务器,查询后,缓存就放在1服务器上,假设现在有个用户访问的是2服务器,那么它在2服务器上就无法获取刚那个缓存;
为此,Mybatis专门提供 Redis 依赖实现二级缓存
1.3.1 导入mybatis-redis依赖包
<!--Mybatis 对Redis 二级缓存提供依赖-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
1.3.2 Mapper接口修改二级缓存实现类
@CacheNamespace(implementation = RedisCache.class)
传统配置文件:
package com.lg.dao;
import com.lg.pojo.Order;
import com.lg.pojo.User;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@CacheNamespace(implementation = RedisCache.class) // 开启二级缓存
public interface OrderMapper {
// column = "uid" 传递的参数 ,就是该 com.lg.dao.UserDao.findById (namespace.id)的入参
// property = "user" 为 com.lg.dao.UserDao.findById 查询出的结果,封装到Order类的user属性中
@Results({
@Result(property = "id",column = "id"),
@Result(property = "orderTime",column = "orderTime"),
@Result(property = "total",column = "total"),
@Result(property = "user",column = "uid",javaType = User.class,
one = @One(select = "com.lg.dao.UserDao.findById"))
})
@Select("select * from order_yeye")
public List<Order> findOrderAndUser();
@Select("select * from order_yeye")
public List<Order> findAllOrder(Integer id);
}
1.3.3 启动本地redis
略
虚拟机Linux端启动,IP需重设;
1.3.4 添加redis.properties配置文件
备注:配置文件名不允许随意修改,可查看源码查看
redis.properties ,key值可能是redis.host、或redis.port ,我这撸源码撸了半天,不知道是不是版本问题,redisConfig实现类RedisConfigurationBuilder在解析配置文件时,赋值不上,看了下源码,配置key老是不匹配,于是将 redis.去掉,便可;
host=192.168.30.128
port=6379
connectionTimeout=5000
database=1
2.3.5 RedisCache 源码解析
Redis通过hset 哈希存储,实现缓存;
/**
*
* Mybatis二级缓存实现类
*
* @author andy
* @version 2.2
*/
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.mybatis.caches.redis;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public final class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
private String id;
private static JedisPool pool;
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
} else {
this.id = id;
// 拿到项目redis配置信息,建立redis连接池
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
}
}
private Object execute(RedisCallback callback) {
Jedis jedis = pool.getResource();
Object var3;
try {
var3 = callback.doWithRedis(jedis);
} finally {
jedis.close();
}
return var3;
}
public String getId() {
return this.id;
}
public int getSize() {
return (Integer)this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
Map<byte[], byte[]> result = jedis.hgetAll(RedisCache.this.id.toString().getBytes());
return result.size();
}
});
}
// Redis通过hset哈希存储,实现缓存
public void putObject(final Object key, final Object value) {
this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
return null;
}
});
}
public Object getObject(final Object key) {
return this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
}
});
}
public Object removeObject(final Object key) {
return this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
return jedis.hdel(RedisCache.this.id.toString(), new String[]{key.toString()});
}
});
}
public void clear() {
this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
jedis.del(RedisCache.this.id.toString());
return null;
}
});
}
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
public String toString() {
return "Redis {" + this.id + "}";
}
}
Mybatis-cache源码涉及类,较容易看懂