簡要說明:
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,如下圖: