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.获取sqlSessionSqlSession 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.获取sqlSessionSqlSession 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;}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object key, Object value) {System.out.printf("【存入缓存数据】key:%s,value:%s%n", key, value);redisClient.set(SerializeUtil.serialize(key), SerializeUtil.serialize(value));}@Overridepublic 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;}@Overridepublic Object removeObject(Object key) {return redisClient.expire(String.valueOf(key), 0);}@Overridepublic void clear() {redisClient.flushDB();}@Overridepublic int getSize() {return Integer.parseInt(redisClient.dbSize().toString());}@Overridepublic 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.获取sqlSessionSqlSession 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: 8080spring:datasource:username: rootpassword: adminurl: jdbc:mysql://localhost:3306/db_mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverhikari:connection-timeout: 10000
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//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对象。