緩存
是一般的ORM 框架都會提供的功能抄肖,目的就是提升查詢的效率和減少數(shù)據(jù)庫的壓力斯棒。跟Hibernate 一樣没龙,MyBatis 也有一級緩存
和二級緩存
链韭,并且預留了集成第三方緩存的接口排霉。
整體架構(gòu)
一級緩存
一級緩存
是在SqlSession
層面進行緩存的窍株。即,同一個SqlSession 攻柠,多次調(diào)用同一個Mapper和同一個方法的同一個參數(shù)球订,只會進行一次數(shù)據(jù)庫查詢,然后把數(shù)據(jù)緩存到緩沖中瑰钮,以后直接先從緩存中取出數(shù)據(jù)冒滩,不會直接去查數(shù)據(jù)庫。我們來看一級緩存的觸發(fā)條件:
- 必須是相同的SQL和參數(shù)
- 必須是相同的會話(SqlSession)
- 必須是相同的namespace 即同一個mapper
- 必須是相同的statement 即同一個mapper 接口中的同一個方法
- 查詢語句中間沒有執(zhí)行session.clearCache() 方法
- 查詢語句中間沒有執(zhí)行 insert update delete 方法(無論變動記錄是否與 緩存數(shù)據(jù)有無關(guān)系)
必須同時滿足上述所有條件浪谴,一級緩存才會觸發(fā)开睡。
源碼案例
我們使用最新版本的spring boot構(gòu)建項目。
1.搭建spring boot+mybatis測試項目
spring官方網(wǎng)站速度太慢苟耻,切換到阿里云士八。
選擇如下的依賴:
2.連接數(shù)據(jù)庫
使用IDEA自帶的DataBase工具連接。
選擇Mysql作為數(shù)據(jù)源梁呈。
填入數(shù)據(jù)庫地址及用戶名婚度、密碼,如果提示需要下載驅(qū)動官卡,點擊下載即可蝗茁,默認選擇的mysql驅(qū)動版本為8.0+。
設(shè)置一下時區(qū)寻咒,否則8.0的驅(qū)動是無法訪問數(shù)據(jù)庫的哮翘。
這樣在右邊就可以看到我們的數(shù)據(jù)庫表了,右鍵自動生成實體類毛秘。
按照需要在這里勾選對應(yīng)的功能饭寺,然后點擊生成即可阻课。
這樣pojo、dao艰匙、mapper映射文件就自動生成了限煞。
3.配置mybatis
復制下面的配置模板到項目中,修改對應(yīng)的內(nèi)容以匹配數(shù)據(jù)庫员凝。
# 應(yīng)用名稱
spring.application.name=mybatis-test
# 應(yīng)用服務(wù) WEB 訪問端口
server.port=8080
# 數(shù)據(jù)庫驅(qū)動:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 數(shù)據(jù)源名稱
spring.datasource.name=defaultDataSource
# 數(shù)據(jù)庫連接地址
spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC
# 數(shù)據(jù)庫用戶名&密碼:
spring.datasource.username=root
spring.datasource.password=123456
#掃描映射文件路徑
mybatis.mapper-locations=classpath:/mapper/**.xml
#日志輸出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
引導類上添加注解@MapperScan("com.brianxia.demo")
:
package com.brianxia.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.brianxia.demo")
public class MybatisCacheTestApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisCacheTestApplication.class, args);
}
}
4.測試一級緩存
package com.brianxia.demo;
import com.brianxia.demo.dao.TbUserDao;
import com.brianxia.demo.pojo.TbUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MybatisCacheTestApplicationTests {
@Autowired
private TbUserDao tbUserDao;
//一級緩存不生效,每次查詢都會生成新的sqlsession并執(zhí)行提交署驻,不在同一個sqlsession中
@Test
void test1() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
System.out.println(tbUser1 == tbUser2);
}
}
這個時候會發(fā)現(xiàn)一級緩存并沒有生效,每次查詢都會創(chuàng)建一個新的SqlSession并發(fā)送Sql語句到mysql中健霹。
可以通過增加事務(wù)注解避免重復創(chuàng)建SqlSession會話旺上,再次進行測試:
package com.brianxia.demo;
import com.brianxia.demo.dao.TbUserDao;
import com.brianxia.demo.pojo.TbUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
class MybatisCacheTestApplicationTests {
@Autowired
private TbUserDao tbUserDao;
//一級緩存不生效,每次查詢都會生成新的sqlsession并執(zhí)行提交,不在同一個sqlsession中
@Test
@Transactional
void test1() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
System.out.println(tbUser1 == tbUser2);
}
}
這次只創(chuàng)建了一個SqlSession糖埋,并且可以看到兩個對象指向同一塊堆內(nèi)存區(qū)域宣吱,所以一級緩存已經(jīng)生效。
5.測試必要條件
- 必須是相同的SQL和參數(shù)
//一級緩存不生效,必須是相同的SQL和參數(shù)
@Test
@Transactional
void test1() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(2L);
System.out.println(tbUser1 == tbUser2);
}
- 必須是相同的statement 即同一個mapper 接口中的同一個方法
@Select("select * from tb_user where id = #{id}")
TbUser selectByPrimaryKey2(Long id);
//一級緩存不生效,必須是相同的statement 即同一個mapper 接口中的同一個方法
@Test
@Transactional
void test2() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey2(1L);
System.out.println(tbUser1 == tbUser2);
}
- 查詢語句中間沒有執(zhí)行 insert update delete 方法(無論變動記錄是否與 緩存數(shù)據(jù)有無關(guān)系)
//一級緩存不生效,查詢語句中間沒有執(zhí)行 insert update delete 方法
@Test
@Transactional
void test2() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser = tbUserDao.selectByPrimaryKey(2L);
tbUserDao.updateByPrimaryKey(tbUser);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
System.out.println(tbUser1 == tbUser2);
}
二級緩存
一級緩存無法實現(xiàn)在多個SqlSession中共享數(shù)據(jù)瞳别,所以mybatis提供了二級緩存征候,在SqlSessionFactory層面給各個SqlSession 對象共享。默認二級緩存是不開啟的洒试,需要手動進行配置。
1.注解方式開啟
如果使用純注解的方式朴上,首先需要在mapper接口上添加注解@CacheNamespace
垒棋,這樣才能開啟二級緩存功能。
package com.brianxia.demo.dao;
import com.brianxia.demo.pojo.TbUser;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.CacheNamespaceRef;
import org.apache.ibatis.annotations.Select;
@CacheNamespace
public interface TbUserDao2 {
@Select("select * from tb_user where id = #{id}")
TbUser selectByPrimaryKey(Long id);
}
然后編寫查詢方法:
//二級緩存
@Test
void test3() {
TbUser tbUser1 = tbUserDao2.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao2.selectByPrimaryKey(1L);
}
可以觀察一下日志痪宰,應(yīng)該只有一次SQL查詢叼架,第二次SqlSession創(chuàng)建之后,會通過二級緩存查詢出數(shù)據(jù)返回衣撬。
默認的二級緩存會有如下效果:
- 映射語句文件中的所有 SELECT 語句將會被緩存乖订。
- 映射語句文件中的所有INSERT、UPDATE具练、DELETE 語句會刷新緩存乍构。
- 緩存會使用 Least Recently Used ( LRU,最近最少使用的)算法來收回扛点。
- 根據(jù)時間表刷新緩存(如 no Flush Interval 哥遮,沒有刷新間隔,緩存不會以任何時間順序來刷新)陵究。
- 緩存會存儲集合或?qū)ο螅o論查詢方法返回什么類型的值)的 1024 個引用眠饮。
- 緩存會被視為 read/write (可讀/可寫)的,意味著對象檢索不是共享的铜邮,而且可以安全地被調(diào)用者修改仪召,而不干擾其他調(diào)用者或線程所做的潛在修改寨蹋。
接口關(guān)閉緩存
如果對于某些接口需要關(guān)閉緩存,可以在接口上通過@Options注解添加具體的關(guān)閉緩存配置項扔茅,如下:
@Options(useCache = false)
@Select("select * from tb_user where id = #{id}")
TbUser selectByPrimaryKey(Long id);
這樣緩存就不會生效了已旧。
2.配置文件實現(xiàn)方式
在mapper.xml映射配置文件中,需要添加標簽<cache></cache>
咖摹,這樣就可以開啟二級緩存功能评姨。
其余功能與注解方式相同。
3.配置項詳解
以注解的使用方式為例萤晴,源碼如下:
/**
* Copyright 2009-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
/**
* The annotation that specify to use cache on namespace(e.g. mapper interface).
*
* <p>
* <b>How to use:</b>
*
* <pre>
* @CacheNamespace(implementation = CustomCache.class, properties = {
* @Property(name = "host", value = "${mybatis.cache.host}"),
* @Property(name = "port", value = "${mybatis.cache.port}"),
* @Property(name = "name", value = "usersCache")
* })
* public interface UserMapper {
* // ...
* }
* </pre>
*
* @author Clinton Begin
* @author Kazuki Shimizu
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
/**
* Returns the cache implementation type to use.
*
* @return the cache implementation type
*/
Class<? extends Cache> implementation() default PerpetualCache.class;
/**
* Returns the cache evicting implementation type to use.
*
* @return the cache evicting implementation type
*/
Class<? extends Cache> eviction() default LruCache.class;
/**
* Returns the flush interval.
*
* @return the flush interval
*/
long flushInterval() default 0;
/**
* Return the cache size.
*
* @return the cache size
*/
int size() default 1024;
/**
* Returns whether use read/write cache.
*
* @return {@code true} if use read/write cache; {@code false} if otherwise
*/
boolean readWrite() default true;
/**
* Returns whether block the cache at request time or not.
*
* @return {@code true} if block the cache; {@code false} if otherwise
*/
boolean blocking() default false;
/**
* Returns property values for a implementation object.
*
* @return property values
* @since 3.4.2
*/
Property[] properties() default {};
}
配置項
eviction(收回策略)
LRU(最近最少使用的):移除最長時間不被使用的對象吐句,這是默認值。
FIFO(先進先出):按對象進入緩存的順序來移除它們店读。
SOFT(軟引用):移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象嗦枢。
WEAK(弱引用):更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。
flushinterval(刷新間隔)
可以被設(shè)置為任意的正整數(shù)屯断,而且它們代表一個合理的毫秒形式的時間段文虏。默認情況不設(shè)置,即沒有刷新間隔殖演,緩存僅僅在調(diào)用語句時刷新氧秘。
size(引用數(shù)目)
可以被設(shè)置為任意正整數(shù),要記住緩存的對象數(shù)目和運行環(huán)境的可用內(nèi)存資源數(shù)目趴久。默認值是1024 丸相。
readOnly(只讀)
屬性可以被設(shè)置為 true 或 false。只讀的緩存會給所有調(diào)用者返回緩存對象的相同實例彼棍,因此這些對象不能被修改灭忠,這提供了很重要的性能優(yōu)勢∽叮可讀寫的緩存會通過序列化返回緩存對象的拷貝弛作,這種方式會慢一些,但是安全华匾,因此默認是 false映琳。
配置方式
@CacheNamespace(
eviction = FifoCache.class,
flushinterval = 60000,
size = 512,
readWrite = true
)
加餐1:使用redis作為二級緩存
自定義二級緩存只需要實現(xiàn)Cache接口,源碼如下:
/**
* Copyright 2009-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
/**
* SPI for cache providers.
* <p>
* One instance of cache will be created for each namespace.
* <p>
* The cache implementation must have a constructor that receives the cache id as an String parameter.
* <p>
* MyBatis will pass the namespace as id to the constructor.
*
* <pre>
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
* </pre>
*
* @author Clinton Begin
*/
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key
* Can be any object but usually it is a {@link CacheKey}
* @param value
* The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key
* The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key
* The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance.
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
* <p>
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
從上面源碼可以分析出來蜘拉,存放和獲取的對象類型都統(tǒng)一為Object類型刊头,所以如果要將Object類型轉(zhuǎn)換為json存放到redis中會遇到反序列化類型無法獲取的問題,所以需要自定義序列化器诸尽,而不能用Json作為序列化方式原杂。
代碼如下:
package com.brianxia.demo.utils;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectSerializer implements RedisSerializer<Object> {
@Override
public byte[] serialize(Object o) throws SerializationException {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(o);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if(baos != null){
baos.close();
}
if (oos != null) {
oos.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return null;
}
/*
* 反序列化
* */
public Object deserialize(byte[] bytes){
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
try{
bais = new ByteArrayInputStream(bytes);
ois = new ObjectInputStream(bais);
return ois.readObject();
}catch(Exception e){
e.printStackTrace();
}finally {
try {
} catch (Exception e2) {
e2.printStackTrace();
}
}
return null;
}
}
使用JDK的byte序列化器來進行序列化,轉(zhuǎn)換成byte[]存放到redis中您机。
創(chuàng)建RedisTemplate:
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setValueSerializer(new ObjectSerializer());
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
其中key的序列化器可以使用String穿肄,因為對key沒有g(shù)et還原成原始對象的操作年局,只是作為尋址參數(shù)。value的序列化器必須使用ObjectSerializer咸产,否則無法還原出原本的類型矢否。
編寫Cache接口實現(xiàn)類:
package com.brianxia.demo.cache;
import com.alibaba.fastjson.JSON;
import com.brianxia.demo.utils.RedisTemplateUtil;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* @author brianxia
* @version 1.0
* @date 2020/12/19 9:38
*/
public class MyCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
private RedisTemplate<String,Object> stringRedisTemplate;
private String cacheKey2String(Object key){
return JSON.toJSONString(key);
}
public MyCache(String id) {
synchronized (this){
if(stringRedisTemplate == null){
stringRedisTemplate = RedisTemplateUtil.redisTemplate();
}
}
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return stringRedisTemplate.opsForHash().size("testCache").intValue();
}
@Override
public void putObject(Object key, Object value) {
System.out.println("用了我自己的cache");
stringRedisTemplate.opsForHash().put("testCache",cacheKey2String(key), value);
}
@Override
public Object getObject(Object key) {
return stringRedisTemplate.opsForHash().get("testCache",cacheKey2String(key));
}
@Override
public Object removeObject(Object key) {
return stringRedisTemplate.opsForHash().delete("testCache",cacheKey2String(key));
}
@Override
public void clear() {
stringRedisTemplate.delete("testCache");
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
- putObject存放緩存
- getObject獲取緩存
- removeObject 移除緩存
- clear清理所有緩存
這里將所有的cache存放在hash中,方便進行統(tǒng)一的管理脑溢,否則clear方法清理大量key非常損耗性能僵朗。
最后進行測試:
package com.brianxia.demo.dao;
import com.brianxia.demo.cache.MyCache;
import com.brianxia.demo.pojo.TbUser;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.CacheNamespaceRef;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
@CacheNamespace(implementation = MyCache.class)
public interface TbUserDao2 {
//@Options(useCache = false)
@Select("select * from tb_user where id = #{id}")
TbUser selectByPrimaryKey(Long id);
}