Mybatis一级缓存&二级缓存
- 概述
- 一级缓存
- 特点
- 演示前准备
- 效果演示
- 在同一个SqlSession中
- 在不同的SqlSession中
- 源代码
- 怎么禁止使用一级缓存
- 一级缓存在什么情况下会被清除
- 二级缓存
- 特点
- 演示前准备
- 效果演示
- 在不同的SqlSession中
- 源代码
- 怎么关闭二级缓存
- 一级缓存(Spring整合Mybatis)
- 演示前准备
- 效果演示
- 不开启事务,调用多次接口
- 开启事务,调用多次接口
- 不开启事务,接口中多次调用查询方法
- 开启事务,接口中多次调用查询方法
- 总结
- 源代码
概述
缓存越小,查询速度越快,缓存数据越少
缓存越大,查询速度越慢,缓存数据越多
在多级缓存中,一般常见的是先查询一级缓存,再查询二级缓存,但在Mybatis中是先查询二级缓存,再查询一级缓存。
在Mybatis中,BaseExecutor属于一级缓存执行器,CachingExecutor属于二级缓存执行器,二者采用了装饰器设计模式。
一级缓存:默认情况下一级缓存是开启的,而且是不能关闭的,一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中使用相同的SQL语句进行查询时,第二次以及之后的查询都不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条SQL。
二级缓存:二级缓存是指可以跨SqlSession的缓存。是mapper级别的缓存,对于mapper级别的缓存不同的SqlSession是可以共享的,需要额外整合第三方缓存,例如Redis、MongoDB、oscache、ehcache等。
注:本文代码演示基于《Mybatis环境搭建与使用》中的“基于XML方式-mapper代理开发”的代码进行调整。
一级缓存
特点
一级缓存也叫本地缓存,在Mybatis中,一级缓存是在会话层面(SqlSession)实现的,这就说明一级缓存的作用范围只能在同一个SqlSession中,在多个不同的SqlSession中是无效的。
在Mybatis中,一级缓存是默认开启的,不需要任何额外的配置。
演示前准备
为了能够看到演示的效果,需要在mybatis-config.xml文件中加上以下配置
<settings>
<!-- 打印sql日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
效果演示
在同一个SqlSession中
MybatisTest03.java
package com.mybatis.test;
import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author honey
* @date 2023-08-01 16:23:53
*/
public class MybatisTest03 {
public static void main(String[] args) throws IOException {
// 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
System.out.println("【一级缓存-在同一个SqlSession中】第一次查询");
List<UserEntity> list1 = mapper1.listUser();
System.out.println("list1:" + list1);
UserMapper mapper2 = sqlSession.getMapper(UserMapper.class);
System.out.println("【一级缓存-在同一个SqlSession中】第二次查询");
List<UserEntity> list2 = mapper2.listUser();
System.out.println("list2:" + list2);
sqlSession.close();
}
}
运行上面的代码可以看到,在同一个SqlSession中,第二次查询是没有去查询数据库的,而是直接读取的缓存数据。
源码Debug分析
BaseExecutor.java
在不同的SqlSession中
MybatisTest04.java
package com.mybatis.test;
import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author honey
* @date 2023-08-01 16:23:53
*/
public class MybatisTest04 {
public static void main(String[] args) throws IOException {
// 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.获取sqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println("【一级缓存-在不同的SqlSession中】第一次查询");
List<UserEntity> list1 = mapper1.listUser();
System.out.println("list1:" + list1);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println("【一级缓存-在不同的SqlSession中】第二次查询");
List<UserEntity> list2 = mapper2.listUser();
System.out.println("list2:" + list2);
sqlSession1.close();
sqlSession2.close();
}
}
运行上面的代码可以看到,在不同的SqlSession中,两次查询都是查询的数据库,也就是说一级缓存并没有生效。
源代码
怎么禁止使用一级缓存
- 在SQL语句上加上随机生成的参数;(不推荐)
- 开启二级缓存;
- 使用SqlSession强制清除缓存;
- 每次查询都使用新的SqlSession;
- 通过配置清除缓存;
一级缓存在什么情况下会被清除
- 提交事务/回滚事务/强制清除缓存
sqlSession.commit();
sqlSession.rollback();
sqlSession.clearCache()
以提交事务为例,回滚事务/强制清除缓存同理
MybatisTest03.java
DefaultSqlSession.java
BaseExecutor.java
- 在执行insert、update、delete语句时
BaseExecutor.java
- 使用配置清除一级缓存
<!-- 设置一级缓存作用域 -->
<setting name="localCacheScope" value="STATEMENT"/>
mybatis-config.xml
BaseExecutor.java
二级缓存
特点
二级缓存是mapper级别的缓存,通过整合第三方缓存实现,二级缓存的作用范围可以在不同的SqlSession中。
在Mybatis中,二级缓存默认是开启的,但还需要做一些额外的配置才能生效。
演示前准备
- 启动Redis
- 添加pom依赖
pom.xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
- 实现Cache类
RedisCache.java
package com.mybatis.cache;
import com.mybatis.utils.SerializeUtil;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author honey
* @date 2023-08-01 23:44:10
*/
public class RedisCache implements Cache {
private final Jedis redisClient = createRedis();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id;
public RedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
System.out.printf("【存入缓存数据】key:%s,value:%s%n", key, value);
redisClient.set(SerializeUtil.serialize(key), SerializeUtil.serialize(value));
}
@Override
public Object getObject(Object key) {
byte[] bytes = redisClient.get(SerializeUtil.serialize(key));
if (bytes == null) {
return null;
}
Object value = SerializeUtil.deserialize(bytes);
System.out.printf("【读取缓存数据】key:%s,value:%s%n", key, value);
return value;
}
@Override
public Object removeObject(Object key) {
return redisClient.expire(String.valueOf(key), 0);
}
@Override
public void clear() {
redisClient.flushDB();
}
@Override
public int getSize() {
return Integer.parseInt(redisClient.dbSize().toString());
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
protected static Jedis createRedis() {
JedisPool pool = new JedisPool("127.0.0.1", 6379);
return pool.getResource();
}
}
SerializeUtil.java
package com.mybatis.utils;
import java.io.*;
/**
* @author honey
* @date 2023-08-02 00:50:37
*/
public class SerializeUtil {
public static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
// 序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
close(oos);
close(baos);
}
return null;
}
public static 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 {
close(bais);
close(ois);
}
return null;
}
/**
* 关闭io流对象
*
* @param closeable closeable
*/
public static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
注意:UserEntity需要实现序列化接口
UserEntity.java
- 添加配置(userMapper.xml)
userMapper.xml
<cache eviction="LRU" type="com.mybatis.cache.RedisCache"/>
效果演示
在不同的SqlSession中
MybatisTest05.java
package com.mybatis.test;
import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author honey
* @date 2023-08-01 16:23:53
*/
public class MybatisTest05 {
public static void main(String[] args) throws IOException {
// 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.获取sqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println("【二级缓存-在不同的SqlSession中】第一次查询");
List<UserEntity> list1 = mapper1.listUser();
System.out.println("list1:" + list1);
sqlSession1.close();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println("【二级缓存-在不同的SqlSession中】第二次查询");
List<UserEntity> list2 = mapper2.listUser();
System.out.println("list2:" + list2);
sqlSession2.close();
}
}
运行上面的代码可以看到,在不同的SqlSession中,第一次查询读取的是数据库中的数据,而第二次查询读取的是缓存中的数据。
注意:查询到的数据并不是在第一时间就存入缓存,而是在提交事务(sqlSession1.close())的时候才存入缓存。
源代码
CachingExecutor.java
TransactionalCacheManager.java
根据Cache(id=“mapper全限定名”)获取对应的TransactionalCache对象,并将数据临时存放在该对象中。
TransactionalCache.java
在执行sqlSession1.close()这行代码时,会将临时存放的数据存入缓存。
DefaultSqlSession.java
CachingExecutor.java
- 如果是提交事务,则会先将临时存放的数据存入缓存,再将临时存放的数据清空
TransactionalCacheManager.java
TransactionalCache.java
- 如果是回滚事务,则只会将临时存放的数据清空
TransactionalCacheManager.java
TransactionalCache.java
怎么关闭二级缓存
修改配置文件(mybatis-config.xml)
<setting name="cacheEnabled" value="false"/>
一级缓存(Spring整合Mybatis)
在未开启事务的情况下,每次查询Spring都会关闭旧的SqlSession而创建新的SqlSession,因此此时的一级缓存是没有生效的;
在开启事务的情况下,Spring模板使用threadLocal获取当前资源绑定的同一个SqlSession,因此此时一级缓存是有效的;
演示前准备
项目结构
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com</groupId>
<artifactId>springboot-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8080
spring:
datasource:
username: root
password: admin
url: jdbc:mysql://localhost:3306/db_mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 10000
UserMapper.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.mybatis.mapper.UserMapper">
<select id="listUser" resultType="com.mybatis.entity.UserEntity">
select * from tb_user
</select>
</mapper>
AppMybatis.java
package com.mybatis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author honey
* @date 2023-08-02 02:58:16
*/
@SpringBootApplication
public class AppMybatis {
public static void main(String[] args) {
SpringApplication.run(AppMybatis.class);
}
}
UserEntity.java
package com.mybatis.entity;
import lombok.Data;
/**
* @author honey
* @date 2023-08-02 03:03:19
*/
@Data
public class UserEntity {
private Long id;
private String name;
}
UserMapper.java
package com.mybatis.mapper;
import com.mybatis.entity.UserEntity;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author honey
* @date 2023-07-26 21:04:23
*/
@Mapper
public interface UserMapper {
/**
* 查询用户列表
*
* @return List<UserEntity>
*/
List<UserEntity> listUser();
}
UserController.java
package com.mybatis.controller;
import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author honey
* @date 2023-08-02 03:09:13
*/
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserMapper userMapper;
@RequestMapping("listUser")
public void listUser(){
List<UserEntity> list = userMapper.listUser();
System.out.println(list);
}
}
效果演示
不开启事务,调用多次接口
第一次调用
第二次调用
两次调用获取到的是不同的SqlSession,一级缓存不生效
开启事务,调用多次接口
第一次调用
第二次调用
两次调用获取到的也是不同的SqlSession,一级缓存不生效
不开启事务,接口中多次调用查询方法
第一次调用
第二次调用
两次调用获取到的依然是不同的SqlSession,一级缓存不生效
开启事务,接口中多次调用查询方法
第一次调用
第二次调用
两次调用获取到的是相同的SqlSession,一级缓存生效
总结
只有在同一个事务内执行查询,一级缓存才会生效。
源代码
MapperMethod.java
在Spring整合Mybatis的代码中,新增了SqlSessionTemplate类对DefaultSqlSession类的功能进行增强。
SqlSessionTemplate.java
SqlSessionUtils.java
能获取到SqlSessionHolder对象的前提是开启了事务。如果当前线程开启了事务,则不会直接关闭SqlSession对象,而是在下一次调用时复用SqlSession对象。