缓存
是一般的ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate 一样,MyBatis 也有一级缓存
和二级缓存
,并且预留了集成第三方缓存的接口。
整体架构
一级缓存
一级缓存
是在SqlSession
层面进行缓存的。即,同一个SqlSession ,多次调用同一个Mapper和同一个方法的同一个参数,只会进行一次数据库查询,然后把数据缓存到缓冲中,以后直接先从缓存中取出数据,不会直接去查数据库。
我们来看一级缓存的触发条件:
- 必须是相同的SQL和参数
- 必须是相同的会话(SqlSession)
- 必须是相同的namespace 即同一个mapper
- 必须是相同的statement 即同一个mapper 接口中的同一个方法
- 查询语句中间没有执行session.clearCache() 方法
- 查询语句中间没有执行 insert update delete 方法(无论变动记录是否与 缓存数据有无关系)
必须同时满足上述所有条件,一级缓存才会触发。
源码案例
我们使用最新版本的spring boot构建项目。
1.搭建spring boot+mybatis测试项目
spring官方网站速度太慢,切换到阿里云。
选择如下的依赖:
2.连接数据库
使用IDEA自带的DataBase工具连接。
选择Mysql作为数据源。
填入数据库地址及用户名、密码,如果提示需要下载驱动,点击下载即可,默认选择的mysql驱动版本为8.0+。
设置一下时区,否则8.0的驱动是无法访问数据库的。
这样在右边就可以看到我们的数据库表了,右键自动生成实体类。
按照需要在这里勾选对应的功能,然后点击生成即可。
这样pojo、dao、mapper映射文件就自动生成了。
3.配置mybatis
复制下面的配置模板到项目中,修改对应的内容以匹配数据库。
# 应用名称
spring.application.name=mybatis-test
# 应用服务 WEB 访问端口
server.port=8080
# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC
# 数据库用户名&密码:
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并执行提交,不在同一个sqlsession中
@Test
void test1() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
System.out.println(tbUser1 == tbUser2);
}
}
这个时候会发现一级缓存并没有生效,每次查询都会创建一个新的SqlSession并发送Sql语句到mysql中。
可以通过增加事务注解避免重复创建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并执行提交,不在同一个sqlsession中
@Test
@Transactional
void test1() {
TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
System.out.println(tbUser1 == tbUser2);
}
}
这次只创建了一个SqlSession,并且可以看到两个对象指向同一块堆内存区域,所以一级缓存已经生效。
5.测试必要条件
- 必须是相同的SQL和参数
//一级缓存不生效,必须是相同的SQL和参数
@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);
}
- 查询语句中间没有执行 insert update delete 方法(无论变动记录是否与 缓存数据有无关系)
//一级缓存不生效,查询语句中间没有执行 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);
}
二级缓存
一级缓存无法实现在多个SqlSession中共享数据,所以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);
}
可以观察一下日志,应该只有一次SQL查询,第二次SqlSession创建之后,会通过二级缓存查询出数据返回。
默认的二级缓存会有如下效果:
- 映射语句文件中的所有 SELECT 语句将会被缓存。
- 映射语句文件中的所有INSERT、UPDATE、DELETE 语句会刷新缓存。
- 缓存会使用 Least Recently Used ( LRU,最近最少使用的)算法来收回。
- 根据时间表刷新缓存(如 no Flush Interval ,没有刷新间隔,缓存不会以任何时间顺序来刷新)。
- 缓存会存储集合或对象(无论查询方法返回什么类型的值)的 1024 个引用。
- 缓存会被视为 read/write (可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
接口关闭缓存
如果对于某些接口需要关闭缓存,可以在接口上通过@Options注解添加具体的关闭缓存配置项,如下:
@Options(useCache = false)
@Select("select * from tb_user where id = #{id}")
TbUser selectByPrimaryKey(Long id);
这样缓存就不会生效了。
2.配置文件实现方式
在mapper.xml映射配置文件中,需要添加标签
,这样就可以开启二级缓存功能。
其余功能与注解方式相同。
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).
*
*
* How to use:
*
*
* @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 {
* // ...
* }
*
*
* @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(软引用):移除基于垃圾回收器状态和软引用规则的对象。
WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushinterval(刷新间隔)
可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
size(引用数目)
可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024 。
readOnly(只读)
属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。
配置方式
@CacheNamespace(
eviction = FifoCache.class,
flushinterval = 60000,
size = 512,
readWrite = true
)
加餐1:使用redis作为二级缓存
自定义二级缓存只需要实现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.
*
* One instance of cache will be created for each namespace.
*
* The cache implementation must have a constructor that receives the cache id as an String parameter.
*
* MyBatis will pass the namespace as id to the constructor.
*
*
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
*
*
* @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.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
从上面源码可以分析出来,存放和获取的对象类型都统一为Object类型,所以如果要将Object类型转换为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
使用JDK的byte序列化器来进行序列化,转换成byte[]存放到redis中。
创建RedisTemplate:
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setValueSerializer(new ObjectSerializer());
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
其中key的序列化器可以使用String,因为对key没有get还原成原始对象的操作,只是作为寻址参数。value的序列化器必须使用ObjectSerializer,否则无法还原出原本的类型。
编写Cache接口实现类:
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
- putObject存放缓存
- getObject获取缓存
- removeObject 移除缓存
- clear清理所有缓存
这里将所有的cache存放在hash中,方便进行统一的管理,否则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);
}