Mybatis的二級快取、使用Redis做二級快取

鄧曉暉發表於2020-11-12

什麼是二級快取?

二級快取和一級快取的原理是一樣的,第一次查詢,會將資料放入快取中,然後第二次查詢則會直接去快取中取。但是一級快取是基於的sqlSession,而二級快取是基於mapper檔案的namespace的,也就是說多個sqlSession可以共享一個mapper中的二級快取區域,並且如何兩個mapper的namespace相同,即使兩個mapper,那這兩個mapper中執行sql查詢到的資料也將存在相同的二級快取區域中

image-20201111211828145

  • 如上圖sqlSession1在查詢時會從UserMapper的二級快取中取,如果沒有則執行資料庫查詢操作。
  • 然後寫入到二級快取中
  • sqlSession2則執行同樣的UserMapper查詢時,會從UserMapper的二級快取中取,此時的二級快取中已經有內容了,所以就可以直接取到,不再與資料庫互動。
  • sqlSession3在執行事務操作(插入、更新、刪除)時,會清空UserMapper的二級快取

1. 開啟二級快取

如何使用二級快取:

mybatis中,一級快取是預設開啟的,但是二級快取需要配置才可以使用

  1. 在全域性配置檔案sqlMapConfig.xml中加入如下程式碼:

    <!--開啟二級快取-->
    <settings>
    	<setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 其次在哪個namespace中開啟二級就在哪裡配置,因為mybatis有註解和xml兩種方式所以:

  • 註解
    image-20201111221707937
    註解擴充套件:

    //我們預設使用的是mybatis自帶的二級快取,它的實現在PerpetualCache類中,所以可以寫成
    @CacheNamespace(implementation = PerpetualCache.class)
    //如果是使用redis作為二級快取的話,下面第二部分會講到
    
  • xml
    image-20201111221501041
    這樣就開啟了UserMapper的二級快取

  1. 測試一:

    我們要根據使用者id查詢使用者資訊:

    image-20201111222635859

    注意:將快取的pojo實現Serializable介面,為了將快取資料取出執行反序列化操作,因為二級快取的儲存介質多種多樣,不一定只在記憶體中,也可能在硬碟中,如果我們要再取出這個快取的話,就需要反序列化了。所以mybatis的pojo都去實現Serializable介面
    image-20201111222950505
    最後執行看到列印日誌:
    image-20201111223421674
    為什麼System.out.println(user1==user2)為false ?

    二級快取和一級快取不同,二級快取快取的不是物件,而是資料,在第二次查詢時底層重新建立了一個User物件,並且把二級快取中的資料重新封裝成了物件並返回。所以user1和user2不是一個物件。

  2. 測試二:
    我們在測試二中進行一下事務操作,看看是否能清空二級快取:
    image-20201111224352601

    image-20201111224330557

​ 增加了一個修改操作,發現執行了兩個select,說明提交事務會重新整理二級快取

userCache和flushCache

還可以配置userCacheflushCache

  • userCache : 是用來設定是否禁用二級快取的,在statement中設定可以禁用當前select語句的二級快取,即每次查詢都會發出sql。預設情況為true.

    image-20201111225137076
    image-20201111225544937

  • flushCache : 在mapper的同一個namespace中,如果有其它的增刪改操作後需要重新整理快取,如果部執行重新整理快取會出現髒讀。
    設定statement配置中的flushCache="true",即重新整理快取,如果改成false則不會重新整理,有可能出現髒讀。所以一般情況下沒必要改
    image-20201111225901338

Mybatis二級快取和一級快取一樣也是使用到了org.apache.ibatis.cache.impl.PerpetualCache

這個類是mybatis的預設快取類,同時,想要自定義快取必須實現cache介面

image-20201112005725676

2. 使用Redis實現二級快取

Mybatis自帶的二級快取是有缺點的,就是這個快取是單伺服器進行工作的,無法實現分散式快取。
image-20201112010039059

所以為了解決這個問題,必須找一個分散式快取專門存放快取資料。
image-20201112010027075

如何使用

mybatis提供了一個針對cache介面的redis實現類,在mybatis-redis包中

  1. 首先我們引入jar包

    <dependency> 
        <groupId>org.mybatis.caches</groupId> 
        <artifactId>mybatis-redis</artifactId> 
        <version>1.0.0-beta2</version> 
    </dependency>
    
  2. 修改Mapper.xml檔案

    //**********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="com.lagou.mapper.IUserMapper"> 
        //表示針對於當前的namespace開啟二級快取
        <cache type="org.mybatis.caches.redis.RedisCache" /> 
        
        <select id="findAll" resultType="com.lagou.pojo.User" useCache="true"> 
        	select * from user 
        </select>
    
    //*******註解方式**********
    @CacheNamespace(implementation = RedisCache .class)
    public interface UserMapper {
        //根據id查詢使用者 註解使用
        @Select("select * from user where id=#{id}")
        public User findById(Integer id);
    
    

    這個類同樣實現了Cache介面
    image-20201112011613642

  3. 配置redis的配置檔案

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

    測試方法同自帶的二級快取一樣。

3. Redis二級快取原始碼分析

RedisCache和Mybatis二級快取的方案都差不多,無非是實現Cache介面,並使用jedis操作快取,不過在設計細節上有點區別。
我們帶著問題分析原始碼:

  • 在RedisCache類中如何向redis中進行快取值的存取 ?
  • 使用了哪種資料結構 ?
package org.mybatis.caches.redis;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

//首先其實現了Cache介面,被mybatis初始化的時候的CacheBuilder建立
//建立方式就是呼叫了下面的有參構造
public final class RedisCache implements Cache {
  private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
  private String id;
  private static JedisPool pool;
  //有參構造  
  public RedisCache(final String id) {
    if (id == null) {
      throw new IllegalArgumentException("Cache instances require an ID");
    }
    this.id = id;
    //RedisConfigurationBuilder呼叫parseConfiguration()方法建立RedisConfig物件
    RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
    //構建Jedis池
	pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(),
			redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(),
			redisConfig.getDatabase(), redisConfig.getClientName());
  }
  
  //模板方法,下面的putObject和getObject、removeObject都會用到這個方法
  private Object execute(RedisCallback callback) {
    Jedis jedis = pool.getResource();
    try {
      return callback.doWithRedis(jedis);
    } finally {
      jedis.close();
    }
  }
    
  //。。。。。。。。省略部分程式碼  
    
    
  @Override
  public void putObject(final Object key, final Object value) {
    execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
        return null;
      }
    });
  }

  @Override
  public Object getObject(final Object key) {
    return execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
      }
    });
  }

  @Override
  public Object removeObject(final Object key) {
    return execute(new RedisCallback() {
      @Override
      public Object doWithRedis(Jedis jedis) {
        return jedis.hdel(id.toString(), key.toString());
      }
    });
  }


}

  1. RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
    image-20201112014625885
    RedisConfig中封裝了預設的Redis配置資訊

    image-20201112014920980
    這個方法讀取了我們配置在/resource/redis.properties這個檔案
    RedisConfig後構建了Jedis池

  2. put方法

    private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();
        try {
          return callback.doWithRedis(jedis);
        } finally {
          jedis.close();
        }
      }
    
    public void putObject(final Object key, final Object value) {
        execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
            return null;
          }
        });
      }
    

    我們可以看到,put方法呼叫了模板方法得到 一個jedis連結,然後呼叫doWithRedis()方法

    jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
    

    可以很清楚的看到,mybatis-redis在儲存資料的時候,是使用的hash結構,把cache的id作為這個hash的key (cache的id在mybatis中就是mapper的namespace);這個mapper中的查詢快取資料作為 hash的field,需要快取的內容直接使用SerializeUtil儲存,SerializeUtil和其他的序列化類差不多,負責物件的序列化和反序列化;

相關文章