mybatis源码学习之缓存

mybatis源码学习之缓存

  • 什么是缓存
    • 一级缓存
    • 二级缓存
    • redis实现二级缓存
  • 学习收获

什么是缓存

可以理解为存储在内存中的数据,mybatis用在对数据库交互后产生结果的一个存储,避免与数据库的频繁交互,mybatis中的缓存分为一级缓存和二级缓存

  • 一级缓存:sqlSession级别,存储的数据结构类型为HashMap,多个SqlSession之间的缓存互不影响
  • 二级缓存:mapper级别,多个SqlSession可以共用二级缓存,二级缓存跨SqlSession

一级缓存

实例:

InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 第一次查询   首先在sqlSession的HashMap中查找是否存在 当前查询的key值
// 如果存在 返回,  我们这是第一次查看所以肯定不会存在,
// 发送sql语句 然后讲查询结果以key + value 的形式存储到当前sqlSession的缓存中
User user1 = userMapper.findUserById(1L);
System.out.println("user1 = " + user1);
// 第二次查询   同理在缓存中查找是否存在当前查询的key值, 目前我们知道sqlSession一直
// 我们的sql以及请求参数也一置,直接在缓存中读取,不需要与数据库交互
User user2 = userMapper.findUserById(1L);
System.out.println("user2 = " + user2);
// 这里我们看到 两次的查询结果同时指向了一个地址 ,那么也就说明第二次查询到的就是第一次查询结果的实例
System.out.println(user1 == user2);

控制台结果打印

// 这里表示与sql交互的过程
14:17:31,399 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
14:17:31,429 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:17:31,453 DEBUG findUserById:159 - <==      Total: 1
user1 = User{
     userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 这里并没有sql交互 而是直接在缓存中拿到了查询结果
user2 = User{
     userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 两个user的地址相同
true

产生疑问,如果我们在第一次查询之后更改了数据,那第二次查询是否还会在缓存中读取吗

两次查询中加入update方法之后

// 第一次查询的sql交互
14:32:01,779 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
14:32:01,821 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:32:01,844 DEBUG findUserById:159 - <==      Total: 1
// 第一次查询的结果
user1 = User{
     userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 调用修改方法之后commit
14:32:01,845 DEBUG updateUser:159 - ==>  Preparing: update t_user set user_name=? where user_id = ? 
14:32:01,846 DEBUG updateUser:159 - ==> Parameters: 彭于晏(String), 1(Long)
14:32:01,846 DEBUG updateUser:159 - <==    Updates: 1
14:32:01,847 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@48503868]
// 第二次查询的sql交互 
14:32:01,859 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
14:32:01,859 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:32:01,860 DEBUG findUserById:159 - <==      Total: 1
// 第二次的查询结果 
user2 = User{
     userId=1, userName='彭于晏', userPhone='null', merits=[], movies=[]}
false

由此我们可知道,修改数据的过程中一定清空了缓存数据

通过查看源码我们可知
在调用提交commit方法时清空了缓存,具体代码如下

// sqlSession.commit()方法在其实现类DefaultSqlSession中具体实现如下
public void commit(boolean force) {
     
    try {
     
        this.executor.commit(this.isCommitOrRollbackRequired(force));
        this.dirty = false;
    } catch (Exception var6) {
     
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
    } finally {
     
        ErrorContext.instance().reset();
    }
}

// 能够看出 在实现中调用了执行器executor的commit()方法 在其实现类BaseExecutor中被实现
public void commit(boolean required) throws SQLException {
     
    if (this.closed) {
     
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
     
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
     
            this.transaction.commit();
        }

    }
}

public void clearLocalCache() {
     
    if (!this.closed) {
     
        this.localCache.clear();
        this.localOutputParameterCache.clear();
    }
}

// 这里使用了PerpetualCache的clear()方法
private Map<Object, Object> cache = new HashMap();
public void clear() {
     
    this.cache.clear();
}
// 能够看到最终这个PerpetualCache的clear方法清空了HashMap的值

看到这里我们是不是就可以说mybatis的一级缓存就是一个HashMap

接下来我们查看一下这个
HashMap是怎样组成的,又是怎样被创建的

我们通过上一篇的自定义持久层框架中知道我们的sql是在executor的query中执行的
如果我们想要将查询结果存放到hashMap中那么一定要先获得查询结果,我们直接看query的源代码

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
     
    // 这里boundSql就是我们的sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    // createCacheKey这个方法字面意思就是创建一个缓存key
    CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
    return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

很明显了 createCacheKey这个方法一定创建了我们HashMap中的key,那么这个key具体是什么

// 首先先看参数
//MappedStatement mapper.xml容器对象
//parameterObject 参数
//rowBounds 分页对象
//boundSql sql容器
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
     
    if (this.closed) {
     
        throw new ExecutorException("Executor was closed.");
    } else {
     
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        Iterator var8 = parameterMappings.iterator();

        while(var8.hasNext()) {
     
            ParameterMapping parameterMapping = (ParameterMapping)var8.next();
            if (parameterMapping.getMode() != ParameterMode.OUT) {
     
                String propertyName = parameterMapping.getProperty();
                Object value;
                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 = this.configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }

                cacheKey.update(value);
            }
        }

        if (this.configuration.getEnvironment() != null) {
     
            cacheKey.update(this.configuration.getEnvironment().getId());
        }

        return cacheKey;
    }
}

通过以上代码我们可知 这个key的组成就是
statementId 、params、sql、rowBounds

回到query方法 能够看到这个生成好的key被带入了另一个query方法

CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
    return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    

我们截取这个query的部分代码

// sql在执行之前我们 这里先在内存中读取是否存在当前key
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
// 当我们的list为null时也就是说当前内存中没有我们的缓存,否则直接返回
if (list != null) {
     
    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
     
    // 这里也就是完成了我们的sql操作 并把结果封装到 key的Map中
    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

附上queryFromDatabase的部分代码

try {
     
    // 数据库交互结果返回一个List结果集
    list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
     
    // 将缓存中value为null 的当前key清除
    this.localCache.removeObject(key);
}
// 将此次查询结果 以及我们的cacheKey 放入我们的缓存中  最终返回我们的结果list
this.localCache.putObject(key, list);

一级缓存到这里就介绍结束了

二级缓存

⼆级缓存的原理和⼀级缓存原理⼀样,每次查询都去缓存中查找有就返回没有就sql交互然后存储。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也 就
是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域

白话的意思就是,多个sqlSession公用一个map

mybatis二级缓存需要我们手动打开,在核心配置文件下加入如下代码开启二级缓存

<settings> 
    <setting name="cacheEnabled" value="true"/>
</settings>

还需要在mapper.xml中开启缓存 加入
注解开发使用@CacheNamespace

测试

SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User userById1 = userMapper1.findUserById(1L);
User userById2 = userMapper2.findUserById(1L);
System.out.println(userById2 == userById1);

控制台打印

21:03:43,775 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
21:03:43,810 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:03:43,833 DEBUG findUserById:159 - <==      Total: 1
21:03:43,834 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
21:03:43,835 DEBUG JdbcTransaction:137 - Opening JDBC Connection
21:03:43,852 DEBUG PooledDataSource:406 - Created connection 1025309396.
21:03:43,852 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3d1cfad4]
21:03:43,853 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
21:03:43,853 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:03:43,855 DEBUG findUserById:159 - <==      Total: 1

这里我们看到 竟然完成了两次sql交互,人傻了,配置什么的都对,然后求助学习群里的大佬我得知

当会话提交或关闭之后才会填充二级缓存

也就是当sqlSession1查询到结果之后必须要commit一下或者close一下才会将结果填充到二级缓存,

测试2

User userById1 = userMapper1.findUserById(1L);
sqlSession1.commit();
User userById2 = userMapper2.findUserById(1L);
System.out.println(userById2 == userById1);

控制台打印

21:14:14,871 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
21:14:14,877 DEBUG JdbcTransaction:137 - Opening JDBC Connection
21:14:15,105 DEBUG PooledDataSource:406 - Created connection 32863545.
21:14:15,105 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f57539]
21:14:15,110 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
21:14:15,148 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:14:15,166 DEBUG findUserById:159 - <==      Total: 1
21:14:15,234 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.5
false

OK这次就对了
我们还看到这两个user的地址并不相等
这是因为第二次查询拿到的并不是第一次查询的实例
而是通过反序列化拿到的第一次查询的数据

redis实现二级缓存

那么我的mybatis的二级缓存有没有弊端呢,下面考虑分布式的情况

两台服务器,我们这里起名服务器1和服务器2
假设用户第一次请求访问的是服务器1那么数据就被缓存到了服务器1的主机上
第二次请求访问的是服务器2,那么就拿不到缓存数据,还得完成一次sql交互

解决办法,使用redis实现二级缓存

让我们的第一次请求的数据储存到redis中,第二次请求去redis中获取

我们也不需要手动去写这个实现步骤,mybais已经为我们准备好了一个包

<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>

导入之后再根目录下加入redis的配置文件 名称必须为redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

那么怎么使用呢

我们这边用的注解开发 所以需要在UserMapper顶部加入注解

@CacheNamespace(blocking = true,implementation = RedisCache.class)

由此我们知道redis实现二级缓存是基于RedisCache这个类去实现的

控制台结果

22:19:06,591 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
22:19:06,596 DEBUG JdbcTransaction:137 - Opening JDBC Connection
22:19:06,849 DEBUG PooledDataSource:406 - Created connection 520232556.
22:19:06,849 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f021e6c]
22:19:06,853 DEBUG findUserById:159 - ==>  Preparing: select user_id userId,user_name userName from t_user where user_id = ? 
22:19:06,891 DEBUG findUserById:159 - ==> Parameters: 1(Long)
22:19:06,913 DEBUG findUserById:159 - <==      Total: 1
22:19:06,978 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.5
false

redis库内容

127.0.0.1:6379> keys *
1) "com.hen84.mapper.UserMapper"

ok到此mybatis的缓存学习就先告一段落了

学习收获

1、mybatis缓存储存机制以及刷新机制
2、redis+二级缓存可以解决分布式数据缓存

你可能感兴趣的:(mybatis源码学习,缓存,mybatis,源码)