一、一级缓存

一级缓存讲的是SqlSession的缓存,默认是开启的

Mybatis 入门 第十一篇 之 缓存_java

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 入门 第十一篇 之 缓存_java_02

  2. 结果集结果范围相同
    这个指的是分页信息要一直,如果第一次是1到10,第二次是2到11 那可定不能走缓存

  3. sql语句要一直
    指的是xml中的sql语句
    Mybatis 入门 第十一篇 之 缓存_xml_03

  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 入门 第十一篇 之 缓存_缓存_04

我们看一下,完全一致

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更大的范围内,这样性能会更高

Mybatis 入门 第十一篇 之 缓存_java_05

二级缓存使用注意事项:

  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 入门 第十一篇 之 缓存_java_06

配置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 入门 第十一篇 之 缓存_java_07

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 入门 第十一篇 之 缓存_java_08

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

输出:

Mybatis 入门 第十一篇 之 缓存_sql_09

四、不想被缓存影响

总有一些人很特殊,也总有一些方法很特殊,由于缓存的特殊性,可能缓存有时候数据不是很及时(比如我手动修改数据库数据后 缓存是不刷新的),对于某些对数据库数据非常敏感的方法不需要缓存,但是二级缓存是作用在namespace上的,我们需要不让它受影响

这时候就用到了flushCacheuseCache

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 入门 第十一篇 之 缓存_java_10