Mybatis緩存(一級緩存、二級緩存和自定義緩存)

簡要說明:

1、Mybatis緩存分爲一級緩存和二級緩存。在沒有配置的情況下,默認開啓一級緩存,不開啓二級緩存。

2、如果配置開啓二級緩存,會先查詢二級緩存,沒有的話再查詢一級緩存。(原理

(如果是springboot項目,默認mybatis.configuration.cache-enabled的值是true,也就是默認開啓Mybatis的二級緩存的,但也需要相應的配置才能使用二級緩存)

 

一級緩存(同一個SqlSession)

一級緩存具有和sqlsession一樣的生命週期,SqlSession相當於一個JDBC的Connection對象,在一次請求事務會話後,我們將其關閉。

import com.boot.mybatis.dao.UserMapper;
import com.boot.mybatis.po.User;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;


/**
 * @author river
 * 2020/2/11
 */
@SpringBootTest
class MybatisCacheTest {

    @Autowired
    SqlSessionFactory sqlSessionFactory;

    @Test
    void theSameSqlSession() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {//在try()中的資源會自動釋放
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //查詢結果會緩存起來,如果不希望緩存這個查詢結果,可以在<select>中加上flushCache="true",或者任何的update, insert, delete語句都會清空此緩存。
            User user1 = userMapper.selectByPrimaryKey(1);
            //下面不會查詢數據庫,第一次查詢的結果緩存在sqlSession一級緩存中
            User user2 = userMapper.selectByPrimaryKey(1);
            assertEquals(user1, user2);//同一個User對象,地址相同
        }
    }

    @Test
    void notTheSameSqlSession(){
        SqlSession sqlSession2 = null;
        try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
            UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
            //查詢結果會緩存到sqlSession1中
            User user1 = userMapper1.selectByPrimaryKey(1);
            sqlSession1.commit();//一定要提交,不然不會進入二級緩存
            sqlSession2 = sqlSessionFactory.openSession();
            UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
            //sqlSession2中沒有緩存此查詢,所以會查詢數據庫(沒有配置二級緩存的時候)
            User user2 = userMapper2.selectByPrimaryKey(1);
            assertNotEquals(user1, user2);
//        assertEquals(user1, user2);//如果配置了readOnly="true",則獲取到的是同一個User對象,地址相同
        }finally {
            if (null != sqlSession2) {
                sqlSession2.close();
            }
        }
    }


}

爲什麼需要二級緩存?

一級緩存的問題在於:

1、相同的Mapper類,相同的sql語句,不同的SqlSession卻會查詢數據庫兩次(如notTheSameSqlSession測試)。爲了解決這個問題,所以就需要二級緩存,它的作用於範圍就是Mapper級別的。這樣,使用緩存可以在SqlSessionFactory層面上能提供給各個SqlSession共享。

2、Spring與Mybatis整合時,在未開啓事務的情況之下,每次查詢,spring都會關閉舊的sqlSession而創建新的sqlSession,因此此時的一級緩存是沒有啓作用的。

什麼時候二級緩存會被清除?

如果調用相同namespace下的mapper映射文件中的增刪改SQL(insert、update、delete),並執行了commit操作。此時會清空該namespace下的二級緩存。

開啓二級緩存的條件是什麼?

緩存對象必須是可序列化的,也就是實現Serializable接口,因爲二級緩存不一定是緩存到內存中的,它的存儲介質多種多樣。

二級緩存的生命週期

二級緩存的生命週期同SqlSessionFactory。 SqlSessionFactory一般是一個全局單例,對應一個數據庫連接池,應用啓動後就一直存在。SqlSessionFactory是由SqlSessionFactoryBuilder通過XML或者Java編碼來構建的。

配置二級緩存

很簡單,一個標籤<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="com.boot.mybatis.dao.UserMapper">
  <cache />
  <resultMap ...>
  ...
</mapper>

這個標籤<cache />默認設置瞭如下特性:(源自:《深入淺出Mybatis技術原理與實戰》)

1、包含<cache />標籤的mapper文件中所有select語句都將會被緩存;

2、包含<cache />標籤的mapper文件中所有insert、update和delete都會刷新緩存;

3、緩存會使用默認的Least Recently Used(LRU,最近最少使用算法)來進行回收;

4、緩存不會自動刷新。(可以設置每隔多少時間自動刷新)

5、緩存一共可以緩存1024個對象的引用(列表、集合或者對象)

6、該緩存是可讀/可寫緩存(read/write),對象引用不是共享的,對象引用可以安全的被調用者修改。也就是每次查詢出來的對象內存地址不同,內容相同(如果沒有重載equals方法,則調用這個方法會返回false)。如果查詢出來的對象,修改了相關屬性,後續查詢出來的對象的屬性也是修改後的值。

 

設置二級緩存的屬性

  <cache
    eviction="LRU"
    flushInterval="100000"
    size="512"
    readOnly="true"
  />

eviction:表示緩存回收策略,有如下幾種:

1、LRC:最近最少使用策略,移除最長時間不使用的對象。

2、FIFO:先進先出策略,顧名思義。

3、SOFT:軟引用,移除基於垃圾回收器的狀態和軟引用規則的對象。

4、WEAK:弱引用,更積極地移除基於垃圾回收期的狀態和弱引用規則的對象。

flushInterval:刷新間隔時間,單位爲毫秒。此處100秒回自動刷新緩存。

size:引用數目,代表緩存最多可以儲存多少個對象。

readOnly:只讀,意味着緩存數據只能讀取,不能修改,這樣設置的好處是我們可以快速讀取緩存,缺點是我們沒有辦法修改緩存,它的默認值是false(可讀/可寫)。設置爲true以後,查詢出來的對象地址相同。

 

自定義緩存(原博客

1、添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、實現org.apache.ibatis.cache.Cache接口

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MybatisRedisCache implements Cache {
    private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate redisTemplate;
    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis過期時間
    public MybatisRedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }
    @Override
    public String getId() {
        return id;
    }
    /**
     * Put query result to redis
     */
    @Override
    @SuppressWarnings("unchecked")
    public void putObject(Object key, Object value) {
        RedisTemplate redisTemplate = getRedisTemplate();
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
        logger.debug("Put query result to redis");
    }
    /**
     * Get cached query result from redis
     */
    @Override
    public Object getObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        ValueOperations opsForValue = redisTemplate.opsForValue();
        logger.debug("Get cached query result from redis");
        return opsForValue.get(key);
    }
    /**
     * Remove cached query result from redis
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object removeObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.delete(key);
        logger.debug("Remove cached query result from redis");
        return null;
    }
    /**
     * Clears this cache instance
     */
    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.execute((RedisCallback) connection -> {
            connection.flushDb();
            return null;
        });
        logger.debug("Clear all the cached query result from redis");
    }
    @Override
    public int getSize() {
        return 0;
    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }
    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            //實現類對象不是由spring容器管理的,不能注入對象
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}

3、編寫獲取redisTemplate對象的工具類

@Component
public class ApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    /**
     * 實現ApplicationContextAware接口的context注入函數, 將其存入靜態變量.
     */
    public void setApplicationContext(ApplicationContext applicationContext) {
        ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR
    }
    
    /**
     * 從靜態變量ApplicationContext中取得Bean, 自動轉型爲所賦值對象的類型.
     */
    @SuppressWarnings("unchecked")
    static <T> T getBean(String name) {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext未注入,請在applicationContext.xml中定義SpringContextHolder");
        }
        return (T) applicationContext.getBean(name);
    }
}

4、配置mapper.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.boot.mybatis.dao.UserMapper">
<!--  <cache-->
<!--    eviction="LRU"-->
<!--    flushInterval="100000"-->
<!--    size="512"-->
<!--    readOnly="true"-->
<!--  />-->
  <cache type="com.boot.mybatis.cache.MybatisRedisCache"/>
  <resultMap ...>
  ...
</mapper>

5、配置redis端口和地址(Redis服務的安裝和管理

spring:
  redis:
    host: 127.0.0.1
    port: 6379

6、測試了notTheSameSqlSession(),打印結果如下:(如果再次測試,則兩次都不會訪問數據庫)

一次查詢數據庫,一次查詢緩存。

2020-02-12 15:54:09.709 DEBUG 4192 --- [           main] c.b.m.dao.UserMapper.selectByPrimaryKey  : ==>  Preparing: select id, user_name, password, create_time, update_time from user where id = ? 
2020-02-12 15:54:09.731 DEBUG 4192 --- [           main] c.b.m.dao.UserMapper.selectByPrimaryKey  : ==> Parameters: 1(Integer)
2020-02-12 15:54:09.746 DEBUG 4192 --- [           main] c.b.m.dao.UserMapper.selectByPrimaryKey  : <==      Total: 1
2020-02-12 15:54:09.755 DEBUG 4192 --- [           main] com.boot.mybatis.dao.UserMapper          : Cache Hit Ratio [com.boot.mybatis.dao.UserMapper]: 0.5

7、查看Redis Desktop Manager,如下圖:

 

 

發佈了86 篇原創文章 · 獲贊 144 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章