Mybatis学习笔记-Mabatis缓存

Mybatis学习笔记

Mybatis缓存

​ 缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度

​ MyBatis提供了对缓存的支持,分为一级缓存和二级缓存,可以通过一下图解来理解:

在这里插入图片描述

​ 一级缓存是SqlSession级别的缓存,默认开启,在操作数据库是要构造SqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的SqlSession之间的数据缓存区域是互相不影响的。

​ 二级缓存是Mapper级别的缓存,多个SqlSession去操作通一个Mapper的SQL语句,多个SqlSession可以公用二级缓存,二级缓存是跨SqlSession的。

一级缓存
  • 我们在一个SqlSession中,对User表查询所有用户进行两次查询,查看他们发出SQL语句的情况。

    
     @Test
        public void selectTest(){
            SqlSession sqlSession = sessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = userMapper.selectList();
            users.forEach(user -> {
                System.out.println(user);
            });
    
            List<User> users1 = userMapper.selectList();
            users1.forEach(user -> {
                System.out.println(user);
            });
        }
    

在这里插入图片描述

​ 从上面的SQL语句执行结果打印可以看到,我们执行了两次的selectList方法,但是只打印了一次SQL语句的执行日志,可见第二次执行selectList时取得是缓存中的数据。当缓存中没有数据的时候,会执行SQL语句获取结果,然后将数据放到缓存中,当第二次执行同一个SQL语句时,就会取到缓存中的数据。

  • 同样时对user表进行两次查询,只不过之间进行一次update操作

     @Test
        public void selectTest(){
            SqlSession sqlSession = sessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = userMapper.selectList();
            users.forEach(user -> {
                System.out.println(user);
            });
    
            User u1 = users.get(0);
            u1.setUsername(u1.getUsername()+"-update");
            userMapper.update(u1);
    
            List<User> users1 = userMapper.selectList();
            users1.forEach(user -> {
                System.out.println(user);
            });
        }
    

    在这里插入图片描述

可以看到,当我们执行了一次update方法后,再次执行selectList时,我们又执行了一次数据库查询,由此可见,更新数据库信息,会导致缓存失效

  • 一级缓存示意图
    在这里插入图片描述
  • 一级缓存总结:
    • 第一次发起数据查询时,SqlSession会先去缓存中找是否有需要的数据,如果没有,从数据库中查询,得到数据后,将数据存储到SqlSession一级缓存中。
    • 如果中间SqlSession执行了commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的是为了让缓存中存储的是最新的信息,避免了脏读。
    • 第二次发起数据查询时,同样会先去缓存中查找是否有数据,如果有则直接返回结果,没有则和第一次查询的操作一致。
一级缓存原理探究与源码分析

​ 一级缓存是到底是什么?一级缓存何时备创建?一级缓存的工作流程是怎么样的?

​ 我们知道一级缓存是SqlSession层次的缓存,所有我们可以从SqlSession中出发,看看有没有和缓存相关的属性或方法

在这里插入图片描述
​ 从SqlSession的所有方法中看到,只有一个clearCache()方法和缓存有点关系,那我们就从这个方法开始,看下SqlSession的缓存是怎么回事。

在这里插入图片描述

​ 从SqlSession开始,流程最后走到了PrepetualCahe#clear()方法中,在这里调用cache.clear()方法,翻阅PrepetualCache类可以看到,cache实际上就是一个Map对象,调用cache.clear()方法,其实就是调用的Map的clear()方法。

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public boolean equals(Object o) {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if (this == o) {
            return true;
        } else if (!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}

​ 那么一级缓存是何时创建的呢?

​ 我们在使用SqlSession进行数据操作时,最终执行SQL请求的是Excutor接口中的query()方法,而在Excutor接口的抽象实现类BaseExector的query方法中,有如下一段代码

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler 		resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

​ 从上面的代码中,我们看到,在query方法中,在执行具体实现类的query方法之前,创建了一个CacheKey对象,而cacheKey是通过参数MappedStatement参数,SQL参数,RowBounds参数和ResultHandler参数来创建的

 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());
           	//****
            return cacheKey;
        }
    }

再query方法中,CacheKey对象被当作参数向后传递,在quer方法的重载方法中,首先会根据这个key在本地缓存中获取一次数据,如果存在数据,则返回该数据,如果不存在则调用queryFromDatabase()方法,从数据库中获取数据

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
			...
            List list;
            try {
                ++this.queryStack;
                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);
                }
            } finally {
                --this.queryStack;
            }
           ...
            return list;
        }
    }

而在queryFromDatabase方法中,将获取到的数据添加到了本地缓存中

 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }

        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }

从上面的代码中可以看到,MyBatis的一级缓存,SqlSession缓存是在执行Sql语句之后,查询到结果集并完成封装之后创建的,同时也可以看出一级缓存的工作流程。

  • 一级缓存原理探究与源码分析总结:
    • 一级缓存的本质是一个HashMap对象
    • 一级缓存是在执行SQL语句之前,根据MappedStatemtnt参数,SQL参数,ResultHandler参数和RowBounds参数,创建了CacheKey,然后在SQL语句执行完成之后,将数据添加到了localCache中,即HashMap集合中
    • 一级缓存的工作流程从SqlSession->Excutor#query()->BaseExcutor#query()->BaseExcutor#queryFromDataBase()->PerpetualCache#putObject(),最后在putObject()方法中完成缓存数据的添加
二级缓存

​ 和一级缓存不同,二级缓存是需要我们手动开启的。但是二级缓存的原理和一级缓存一样,第一次查询,将数据放入缓存中,第二次查询则会直接从缓存中取。MyBatis的二级缓存是在Mapper层次,而一级缓存是SqlSession层次的,也就是说多个SqlSession可以共享一个mapper的二级缓存。同样的,当SqlSession执行插入、修改、删除等操作时,会清空Mapper二级缓存。

  • 如何使用二级缓存:

    • 开启二级缓存

      首先我们需要在MyBatis的配置文件中开启二级缓存

      <settings>
              <!-- 1. 显示SQL语句 -->
              <setting name="logImpl" value="STDOUT_LOGGING"/>
              <!--2、开启二级缓存-->
              <setting name="cacheEnabled" value="true"/>
      </settings>
      
    • 然后在Mapper.xml中开启缓存

      <!--开启二级缓存-->
      <cache></cache>
      

      在mapper.xml中,我们只提供了一个空标签,但其实这里是可以配置的。在Mybatis中,PerpetualCache这个类是Mybatis的默认实现缓存功能的类。我们不写cache标签的type参数,就是使用默认的缓存。同时我们也可以实现Cache接口,来实现自定义的二级缓存。同时用于二级缓存的存储介质多样化,不一定是只存在内存中,也有可能存在硬盘中,所以我们从二级缓存再取数据的时候,可能需要反序列化操作,因此,mybatis中的所有数据实体类pojo需要实现Serializable接口。

  • 测试二级缓存

    • 测试二级缓存和SqlSession无关

       	@Test
          public void mapperCacheTest() {
              SqlSession sqlSession = sessionFactory.openSession(true);
              SqlSession sqlSession2 = sessionFactory.openSession(true);
              UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
      
              List<User> users = userMapper.selectList();
              users.forEach(user -> {
                  System.out.println(user);
              });
              System.out.println("=====================================");
              sqlSession.close();
      
              List<User> users1 = userMapper2.selectList();
              users1.forEach(user -> {
                  System.out.println(user);
              });
          }
      

      控制台打印结果:

      在这里插入图片描述
      从控制台打印结果中可以看到,即使我们已经关闭了sqlSession,但是sqlSession2执行查询时乃未打印SQL查询语句。

    • 执行commit操作,验证二级缓存是否清空

      	@Test
          public void mapperCacheTest() {
              SqlSession sqlSession = sessionFactory.openSession(true);
              SqlSession sqlSession2 = sessionFactory.openSession(true);
              SqlSession sqlSession3 = sessionFactory.openSession(true);
              UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
              UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
      
              List<User> users = userMapper.selectList();
              users.forEach(user -> {
                  System.out.println(user);
              });
              System.out.println("=====================================");
              sqlSession.close();
      
              User u1 = users.get(0);
              u1.setUsername(u1.getUsername().replace("-update",""));
              userMapper3.update(u1);
      
      
              List<User> users1 = userMapper2.selectList();
              users1.forEach(user -> {
                  System.out.println(user);
              });
          }
      

      控制台打印结果:

    在这里插入图片描述
    从打印结果中,我们看到,执行完update语句后,我们再次查询时,又执行了一次select语句,由此可见,当我们进行了commit()操作后,Mapper中的二级缓存会被清空

  • 二级缓存整合redis

    上面我们使用的是mybatis自带的二级缓存,但是这个缓存是单服务器工作的,无法实现分布式缓存,即不同服务器不能共享缓存。为了解决这个问题,就得找一个分布式缓存,专门来存储缓存数据,这样不同得服务器得缓存数据都存在该处,缓存也从该处取。

    • mybatis与redis整合

      之前我们提到过,mybatis的默认缓存类是PrepetualCache,它实现了Cache接口,而我们如果要实现自己的缓存逻辑,我们也只需要实现Cache接口即可。PrepetualCache类的缓存逻辑,无法实现分布式缓存,因此,mybatis提供了一个针对Cache接口的redis实现类,该类存在于mybatis-redis包中

      • 实现

        • 引入jar包的坐标

          <dependency>
          	<groupId>org.mybatis.caches</groupId>
           	<artifactId>mybatis-redis</artifactId>
           	<version>1.0.0-beta2</version>
          </dependency>
          
        • 在Mapper.xml中配置cache标签的type属性值

          <!--开启二级缓存-->
          <cache type="org.mybatis.caches.redis.RedisCache"></cache>
          
        • redis配置 redis.properties

          注意,此处redis的配置文件的名称不能随意修改,固定未redis.properties

          host=192.168.2.107
          port=6379
          connectionTimeout=5000
          password=
          database=0
          
        • 测试

           	@Test
              public void redisCacheTest() {
                  SqlSession sqlSession = sessionFactory.openSession(true);
                  SqlSession sqlSession2 = sessionFactory.openSession(true);
                  SqlSession sqlSession3 = sessionFactory.openSession(true);
                  UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
                  UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
                  UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
          
                  List<User> users = userMapper.selectList();
                  users.forEach(user -> {
                      System.out.println(user);
                  });
                  System.out.println("=====================================");
                  sqlSession.close();
          
                  User u1 = users.get(0);
                  u1.setUsername(u1.getUsername().replace("-update", ""));
                  userMapper3.update(u1);
          
          
                  List<User> users1 = userMapper2.selectList();
                  users1.forEach(user -> {
                      System.out.println(user);
                  });
              }
          

          在这里插入图片描述

        可以看到关闭了进行了commit()操作后,我们进行第二次查询未发送SQL语句,此时第二次查询从redis中进行了获取

RedisCache源码分析

​ RedisCache是mybatis-redis提供的实现了Cache接口的类,实现了redis分布式缓存的逻辑。从RedisCache的构造方法中,我们可以看到,传入了一个id参数,这个id参数就是我们Mapper.xml中对用的namespace的属性;然后调用了RedisConfigurationBuilder来创建RedisConfig配置对象,在RedisConfiguraitonBuilder中,我们可以看到一些常量信息,包括redis.properties,这也是为什么redis的配置文件不能随便修改的原因。获取了RedisConfig配置后,就是获取JedisPool,redis的操作都是通过JedisPool中的jedis来实现的

 public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            this.id = id;
            RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
            pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
        }
    }
 private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();
    private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
    private static final String REDIS_RESOURCE = "redis.properties";
    private final String redisPropertiesFilename = System.getProperty("redis.properties.filename", "redis.properties");
 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()));
            }
        });
    }

然后我们可以看下,redisCache中的putObject()和getObject()方法,这两个方法中,详细的指出了,RedisCache的数据结构,就是哈希结构。

到这里,mybatis的缓存大概就学完了,若存在不正确的地方,麻烦各位大佬指正,谢谢~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值