Mybatis 入门 第十一篇 之 缓存

得意犹堪夸世俗,诏黄新湿字如鸦。这篇文章主要讲述Mybatis 入门 第十一篇 之 缓存相关的知识,希望能为你提供帮助。
一、一级缓存一级缓存讲的是SqlSession的缓存,默认是开启的

Mybatis 入门 第十一篇 之 缓存

文章图片

1.1 一级缓存的生命周期
  1. Mybatis 每次会话开启一个Session 同时会创建一个缓存对象PerpetualCache,当会话结束、SqlSession被close()或者调用clearCache()方法时缓存都会失效。
    不同的是clearCache()只是清空PerpetualCache中的缓存数据,这个对象还是可以接着用的,其他两种是直接释放这个对象了,不可用了,伴随Sqlsession的消失而消失了。
  2. 当SqlSession调用更新语句(update、delete、insert)后也会清空PerpetualCache中的数据(PerpetualCache依旧可以使用)
1.2 怎么判断是否是两个完全相同的查询
  1. 同一个statementId (xml中的id)
    Mybatis 入门 第十一篇 之 缓存

    文章图片
  2. 结果集结果范围相同
    这个指的是分页信息要一直,如果第一次是1到10,第二次是2到11 那可定不能走缓存
  3. sql语句要一直
    指的是xml中的sql语句
    Mybatis 入门 第十一篇 之 缓存

    文章图片
  4. 参数要一致
    这个指的是查询的时候传递的参数
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()); } }

我们看一下,完全一致
Mybatis 入门 第十一篇 之 缓存

文章图片

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 = https://www.songbingjia.com/android/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; }

二、 二级缓存二级缓存是应用级别的缓存,为啥叫二级缓存呢,很简单,因为已经有一级缓存了,这个时候出来个缓存可不就叫二级缓存了,哈哈 真有道理。
【Mybatis 入门 第十一篇 之 缓存】那为什么要二级缓存呢?大概是因为一级缓存是在同一个SqlSession中,sqlSession不存在的话缓存就么有啦,像我们再Spring中,都是一次请求创建一个SqlSession 这样缓存其实作用会很小的,我们的二级缓存超越了SqlSesion范围,把数据存储到比SqlSession更大的范围内,这样性能会更高
Mybatis 入门 第十一篇 之 缓存

文章图片

二级缓存使用注意事项:
  1. POJO类要可序列化 实现Serializeable接口
  2. 二级缓存默认的作用域是整个namespace
  3. 同一个namespace的所有select语句都会被缓存
  4. 同一个namespace的所有insert\\update\\delete 语句都会刷新这个namespace的缓存
  5. 缓存默认使用LRU(最近最少使用,也就是最长时间不被使用)算法回收
    其他算法:
    FIFO:先进先出,按对象进入缓存的顺序剔除
    SOFT:软引用 基于垃圾回收器状态和引用规则移除对象
    WEAK:弱引用 更积极的 基于垃圾回收器状态和引用规则移除对象
  6. 默认缓存1024个对象 超出之后就会走淘汰策略
  7. 默认缓存不会定时刷新,可以设置定时刷新时间
  8. 默认缓存会被视为读/写缓存,意味着获取的对象并不是共享的,可以安全的被调用者修改。
    意思就是你获取的对象是从缓存中克隆出来的,而不是直接给了你一个引用
    可以设置只读(如果有写操作会抛异常)
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()); } } }

输出结果:
Mybatis 入门 第十一篇 之 缓存

文章图片

配置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>

再次执行测试输出结果:
Mybatis 入门 第十一篇 之 缓存

文章图片

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()); } } }

输出结果:
Mybatis 入门 第十一篇 之 缓存

文章图片

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 = https://www.songbingjia.com/android/map.get(key); System.out.println("获取对象:key="+key+", value="https://www.songbingjia.com/android/+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()); } } }

输出:
Mybatis 入门 第十一篇 之 缓存

文章图片

四、不想被缓存影响总有一些人很特殊,也总有一些方法很特殊,由于缓存的特殊性,可能缓存有时候数据不是很及时(比如我手动修改数据库数据后 缓存是不刷新的),对于某些对数据库数据非常敏感的方法不需要缓存,但是二级缓存是作用在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 入门 第十一篇 之 缓存

文章图片


    推荐阅读