Mybatis 入门 第十一篇 之 缓存
Posted 木子的昼夜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis 入门 第十一篇 之 缓存相关的知识,希望对你有一定的参考价值。
一、一级缓存
一级缓存讲的是SqlSession的缓存,默认是开启的
1.1 一级缓存的生命周期
- Mybatis 每次会话开启一个Session 同时会创建一个缓存对象PerpetualCache,当会话结束、SqlSession被close()或者调用clearCache()方法时缓存都会失效。
不同的是clearCache()只是清空PerpetualCache中的缓存数据,这个对象还是可以接着用的,其他两种是直接释放这个对象了,不可用了,伴随Sqlsession的消失而消失了。 - 当SqlSession调用更新语句(update、delete、insert)后也会清空PerpetualCache中的数据(PerpetualCache依旧可以使用)
1.2 怎么判断是否是两个完全相同的查询
- 同一个statementId (xml中的id)
- 结果集结果范围相同
这个指的是分页信息要一直,如果第一次是1到10,第二次是2到11 那可定不能走缓存 - sql语句要一直
指的是xml中的sql语句 - 参数要一致
这个指的是查询的时候传递的参数
1.3 证明一下走缓存了
想要证明是否走缓存,看一下两次查询出来的hashCode 如果一致那肯定是用了同一个对象(从缓存取出来的上次的查询结果)
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list01 = mapper.list();
List<Person> list02 = mapper.list();
System.out.println("第一次查询hashCode:"+list01.hashCode());
System.out.println("第二次查询hashCode:"+list02.hashCode());
}
}
我们看一下,完全一致
1.4 不想用SqlSession一级缓存 ?
用flushCache
<!--查询-->
<select id="list" resultType="person" flushCache="true">
select a.* from person a
</select>
1.5 我怎么确认我说的这些对呢
我们看一下SqlSession的close()、clearCache()、update()方法
close:
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There\'s nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
// 直接把缓存对象置空了
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
clearLocalCache:
@Override
public void clearLocalCache() {
if (!closed) {
// 这个是清空缓存数据 没有把缓存对象置空
localCache.clear();
localOutputParameterCache.clear();
}
}
update:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 先调用清空缓存数据的方法
clearLocalCache();
return doUpdate(ms, parameter);
}
我们再看一下获取cacheKey的方法:
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 不用全部读懂 最起码能看出大概逻辑用到了那些值
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// statmentId
cacheKey.update(ms.getId());
// 分页信息
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// sql
cacheKey.update(boundSql.getSql());
// 参数 参数是最复杂的 因为这个不确定性高
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
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) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
cacheKey.update 就是更新缓存key的内容,每次update都会修改他的hashcode的值,具体用了什么算法怎么计有兴趣的可以去看看源码
看一下flushCache="true" 为什么能不用缓存:
// 这就是查询方法
@Override
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());
// 如果执行过程中sqlSession被关闭了直接抛异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 这就是用到了flushCache
// queryStack == 0 先不考虑 这个应该是防止了一个并发查询情况下不能随意清空缓存
// isFlushCacheRequired 这个就用到了 flushCache
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 这里看到了 如果有resultHandler也不走缓存 什么是resultHandler 后边写文章说明
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
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
clearLocalCache();
}
}
return list;
}
什么时候放入缓存的呢 ? queryFromDatabase:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 这里会占位 我的理解是如果一个线程在查询
// 另外一个线程可以直接从缓存拿数据(是个占位符数据 不是真实数据)
// 然后转换的时候就会报错 转换异常
// 这也是为什么sqlSession线程不安全的原因之一吧
// 官方不会让数据出错 会让你直接抛类型转换异常 高明呀!
// 自己项目中如果有类似场景 也可以考虑这样做
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除占位
localCache.removeObject(key);
}
// 设置缓存数据
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
其实SqlSession一级缓存的缓存结构很简单:
说白了就是用了一个Map来存储数据,用Map做缓存在很多框架中都用到了包括Spring,eruka
我之前项目也用到了,是临时存储了一个key-value对应关系的信息
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
二、 二级缓存
二级缓存是应用级别的缓存,为啥叫二级缓存呢,很简单,因为已经有一级缓存了,这个时候出来个缓存可不就叫二级缓存了,哈哈 真有道理。
那为什么要二级缓存呢?大概是因为一级缓存是在同一个SqlSession中,sqlSession不存在的话缓存就么有啦,像我们再Spring中,都是一次请求创建一个SqlSession 这样缓存其实作用会很小的,我们的二级缓存超越了SqlSesion范围,把数据存储到比SqlSession更大的范围内,这样性能会更高
二级缓存使用注意事项:
-
POJO类要可序列化 实现Serializeable接口
-
二级缓存默认的作用域是整个namespace
-
同一个namespace的所有select语句都会被缓存
-
同一个namespace的所有insert\\update\\delete 语句都会刷新这个namespace的缓存
-
缓存默认使用LRU(最近最少使用,也就是最长时间不被使用)算法回收
其他算法:
FIFO:先进先出,按对象进入缓存的顺序剔除
SOFT:软引用 基于垃圾回收器状态和引用规则移除对象
WEAK:弱引用 更积极的 基于垃圾回收器状态和引用规则移除对象 -
默认缓存1024个对象 超出之后就会走淘汰策略
-
默认缓存不会定时刷新,可以设置定时刷新时间
-
默认缓存会被视为读/写缓存,意味着获取的对象并不是共享的,可以安全的被调用者修改。
意思就是你获取的对象是从缓存中克隆出来的,而不是直接给了你一个引用
可以设置只读(如果有写操作会抛异常)
2.1 使用一波
Person可序列化
public class Person implements Serializable {
private Long id;
private String name;
private String jobName;
private BigDecimal salary;
private Integer age;
private String gender;
private String address;
private String hobby;
}
分三个SqlSession测试:
public class TestMain {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 第一次
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("第一次查询hashCode:"+list.hashCode());
}
// 第二次
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("第二次查询hashCode:"+list.hashCode());
}
// 第三次
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("第三次查询hashCode:"+list.hashCode());
}
}
}
输出结果:
配置PersonMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.PersonMapper">
<!--
eviction:FIFO 淘汰策略先进先出 flushInterval:60000 缓存60秒刷新一次
size:512 最多存储512个缓存对象 readOnly:true 设置为只读 返回同一个引用-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<!--查询-->
<select id="list" resultType="person" >
select a.* from person a
</select>
</mapper>
再次执行测试输出结果:
2.2 一定要注意在同一个namespace缓存才会有用,我们测试一下不同namespace的效果
mybatis-config.xml
<!--扫描-->
<mappers>
<mapper resource="PersonMapper.xml"/>
<mapper resource="PersonMapper02.xml"/>
</mappers>
PersonMapper
public interface PersonMapper {
List<Person> list();
}
PersonMapper.xml
<mapper namespace="dao.PersonMapper">
<!--
eviction:FIFO 淘汰策略先进先出 flushInterval:60000 缓存60秒刷新一次
size:512 最多存储512个缓存对象 readOnly:true 设置为只读 返回结果如果进行修改就会报错-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<!--查询-->
<select id="list" resultType="person" >
select a.* from person a
</select>
</mapper>
PersonMapper02
public interface PersonMapper {
List<Person> list();
}
PersonMapper02.xml
<mapper namespace="dao.PersonMapper02">
<!--
eviction:FIFO 淘汰策略先进先出 flushInterval:60000 缓存60秒刷新一次
size:512 最多存储512个缓存对象 readOnly:true 设置为只读 返回结果如果进行修改就会报错-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<!--查询-->
<select id="list" resultType="person" >
select a.* from person a
</select>
</mapper>
测试TestMain:
public class TestMain {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("PersonMapper查询,hashCode:"+list.hashCode());
}
//
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper02 mapper = session.getMapper(PersonMapper02.class);
List<Person> list = mapper.list();
System.out.println("PersonMapper02查询,hashCode:"+list.hashCode());
}
}
}
输出结果:
2.3 注意了
如果readOnly 设置为false 那么返回的对象就不是同一个引用,那么就不能用hashCode看是否使用了缓存,这个时候有一个很好的方式,就是源码打断点,或者输出查询日志或者第一次查询执行完后Thread.sleep一分钟,然后手动去数据库修改一下数据,手动修改数据Mybatis是无感知的,这时候他查出来的数据还是从缓存中获取的
三、 自定义缓存
定义Cache类:
package cache;
import org.apache.ibatis.cache.Cache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
* @create 2021-09-05 15:55
*/
public class MyCache implements Cache {
// 读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
// 这里我们可以用ehcache redis MongoDB 等技术
Map<Object,Object> map = new ConcurrentHashMap<>();
// cache的ID
private String id ;
public MyCache(){
System.out.println("无参构造");
}
public MyCache(String id){
this.id = id;
System.out.println("构造函数id(namespace):"+id);
}
@Override
public String getId() {
System.out.println("获取id:" + id);
return id;
}
@Override
public void putObject(Object key, Object value) {
map.put(key,value);
}
@Override
public Object getObject(Object key) {
Object value = map.get(key);
System.out.println("获取对象:key="+key+", value="+value);
return value;
}
@Override
public Object removeObject(Object key) {
return map.remove(key);
}
@Override
public void clear() {
map.clear();
}
@Override
public int getSize() {
return map.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return lock;
}
}
指定cache使用自定义类:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.PersonMapper">
<cache type="cache.MyCache"></cache>
<!--查询-->
<select id="list" resultType="person" >
select a.* from person a
</select>
</mapper>
测试:
public class TestMain {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 第一次
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("第一次查询,hashCode:"+list.hashCode());
}
// 第二次
try (SqlSession session = sqlSessionFactory.openSession()) {
// 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> list = mapper.list();
System.out.println("第二次查询,hashCode:"+list.hashCode());
}
}
}
输出:
四、不想被缓存影响
总有一些人很特殊,也总有一些方法很特殊,由于缓存的特殊性,可能缓存有时候数据不是很及时(比如我手动修改数据库数据后 缓存是不刷新的),对于某些对数据库数据非常敏感的方法不需要缓存,但是二级缓存是作用在namespace上的,我们需要不让它受影响
这时候就用到了flushCache 、useCache
flushCache : 执行操作前是否清空缓存
useCache : 是否使用缓存
例如:
<!--不使用缓存-->
<select id="list" resultType="person" useCache="false">
select a.* from person a
</select>
<!--执行前会清空缓存-->
<update id="updt" parameterType="entity.Person" flushCache="true">
update person set salary = #{salary} where id =#{id}
</update>
有事儿没事儿关注公众号: 木子的昼夜编程
以上是关于Mybatis 入门 第十一篇 之 缓存的主要内容,如果未能解决你的问题,请参考以下文章