在这篇文章中,我们将为您详细介绍mybatis缓存的内容,并且讨论关于了解的相关问题。此外,我们还会涉及一些关于MyBatis缓存(5)、mybatis缓存(三)、Mybatis一级缓存和二级缓存Re
在这篇文章中,我们将为您详细介绍mybatis缓存的内容,并且讨论关于了解的相关问题。此外,我们还会涉及一些关于MyBatis 缓存(5)、mybatis 缓存(三)、Mybatis一级缓存和二级缓存 Redis缓存、mybatis入门教程(九)------mybatis缓存的知识,以帮助您更全面地了解这个主题。
本文目录一览:- mybatis缓存(了解)(mybatis 缓存)
- MyBatis 缓存(5)
- mybatis 缓存(三)
- Mybatis一级缓存和二级缓存 Redis缓存
- mybatis入门教程(九)------mybatis缓存
mybatis缓存(了解)(mybatis 缓存)
声明
本文为其他博主文章总结,仅用作个人学习,特此声明
参考文章链接
动态 SQL_MyBatis中文网
(3条消息) 狂神说 | Mybatis完整版笔记_小七rrrrr的博客-CSDN博客_狂神说mybatis笔记
缓存(了解)
查询:连接数据,耗资源! 一次查询的结果,给他暂存在一个可以取到的地方! ---> 内存 : 缓存 我们再次查询相同数据的时候,直接走缓存,就不用走数据库了。
1.什么是缓存[Cache]?
- 存在内中的临时数据
- 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题
2.为什么使用缓存?
- 减少和数据库的交互次数,减少系统开销,提高系统效率
3.什么样的数据能使用缓存?
- 经常查询并且不经常改变的数据【可以使用缓存】
Mybatis包含一个非常强大的查询缓存特性,它可以非常方便的定制和配置缓存。缓存可以极大的提升查询效率。
Mybatis系统默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(sqlSession级别的缓存,也称本科缓存)
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存
- 为了提高扩展性,Mybatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
缓存原理如下图所示
1. 一级缓存
-
开启日志
-
测试一个session中查询两次相同记录。
public class MyTest { @Test public void test(){ sqlSession sqlSession = MybatisUtils.getsqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.queryUser(1); System.out.println(user); System.out.println("============="); User user1 = mapper.queryUser(1); System.out.println(user1); System.out.println(user == user1); sqlSession.close(); } }
-
查看日志输出
以下情况缓存会失效:
1.查询不同的数据内容
public class MyTest {
@Test
public void test(){
sqlSession sqlSession = MybatisUtils.getsqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
System.out.println("=============");
User user1 = mapper.queryUser(2);
System.out.println(user1);
System.out.println(user == user1);
sqlSession.close();
}
}
2.映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
public class MyTest {
@Test
public void test(){
sqlSession sqlSession = MybatisUtils.getsqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
mapper.UpdateUser(new User(2,"asas","12345678"));
System.out.println("=============");
User user1 = mapper.queryUser(1);
System.out.println(user1);
System.out.println(user == user1);
sqlSession.close();
}
}
3.查询不同的mapper.xml
4.手动清除缓存
public class MyTest {
@Test
public void test(){
sqlSession sqlSession = MybatisUtils.getsqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
sqlSession.clearCache();
System.out.println("=============");
User user1 = mapper.queryUser(1);
System.out.println(user1);
System.out.println(user == user1);
sqlSession.close();
}
}
一级缓存默认开启,只在一次sqlseesion中有效,一级缓存就是一个map
2. 二级缓存
1、什么是二级缓存
- 二级缓存也叫全局缓存,一级缓存作用于太低,所以诞生了二级缓存;
- 基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
- 工作机制
- 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
- 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
- 新的会话查询信息,就可以从二级缓存中获取内容
- 不同的mapper查出的数据会放在自己对应的缓存(map)中
2、测试二级缓存
1.开启全局缓存
<setting name="cacheEnabled" value="true"/>
cacheEnabled | 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 | true | false | true |
---|
2.在要使用二级缓存的Mapper中开启
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
3.测试
public class MyTest {
@Test
public void test(){
sqlSession sqlSession = MybatisUtils.getsqlSession();
sqlSession sqlSession1 = MybatisUtils.getsqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
sqlSession.close();
//关掉第一个sqlsession才会将一级缓存中的数据保存到二级缓存中
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.queryUser(1);
System.out.println(user1);
System.out.println(user == user1);
sqlSession1.close();
}
}
3、小结
- 只要开启了二级缓存,只在同一个Mapper下有效
- 所有的数据都会先放在一级缓存中;只有当会话提交或者关闭的时候,才会提交到二级缓存中
需要注意的是:只用cache时要加序列化
<cache/>
实体类
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private int id;
private String name;
private String pwd;
public User(int id, String name, String pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
}
3. 自定义缓存-ehcache
Ehcache是一种广泛使用的开源Java分布式缓存,主要面向通用缓存
要在程序中使用ehcache:
1.导入依赖
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.0</version>
</dependency>
2.ehcache的配置文件ehcache.xml
<?xml version="1.0" encoding="UTF8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore :为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置,参数解释如下:
user.hoeme - 用户主目录
user.dir - 用户当前工作目录
javaio.tmpdir - 默认临时文件
-->
<diskStore path="./tmpdir/Tmp_EhCache/">
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowTodisk="false"
diskPersistent="false"
timetoIdleSeconds="1800"
timetoLiveSeconds="259200"
memoryStoreevictionPolicy="LRU"/>
<cache
name="cloud_user"
eternal="false"
maxElementsInMemory="5000"
overflowTodisk="false"
diskPersistent="false"
timetoIdleSeconds="1800"
timetoLiveSeconds="1800"
memoryStoreevictionPolicy="LRU"/>
</ehcache>
<!--
defaultCache:默认缓存策略,当cache找不到定义的缓存时,则使用这个缓存策略,只能定义一个
-->
<!--
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timetoIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timetoLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
overflowTodisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
diskSpoolBufferSizeMB:这个参数设置diskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOndisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreevictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
-->
3.使用ehchache
<!--自定义缓存,用第三方缓存覆盖-->
<cache type = "org.mybatis.caches.ehcache.EhcacheCache"/>
MyBatis 缓存(5)
MyBatis有必要使用缓存吗?为什么?
一般的ORM框架都会提供缓存功能来提升查询效率、减少数据库的压力。跟Hibernate一样,Mybatis也有一级缓存、二级缓存,并预留了集成第三方的缓存接口。
在Mybatis中,与缓存相关的类都在cache包中,其中有一个Cache接口,只有一个默认的实现类PerpetualCache,它是用HashMap实现的。
PerpetualCache这个对象是一定会创建的,所以是基础缓存。但是缓存又可以有很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给基础缓存加上这些功能。
除了基础缓存之外,MyBatis也定义了很多装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多功能。
所有缓存可以分为三大类:基本缓存、淘汰算法缓存、装饰器缓存。
类型 | 缓存实现类 | 描述 | 作用 | 装饰条件 |
---|---|---|---|---|
基本缓存 | PerpetualCache | 缓存基本实现类 | 默认是PeretualCache也可以自定义比如RedisCache、EhCache等,具备基本能功能的缓存 | 无 |
淘汰算法缓存 | LruCache | LRU淘汰策略的缓存 | 当缓存达到上限时,删除最少使用的缓存(Laste Recently Use) | eviction="LRU"(默认) |
FifoCache | FIFO策略缓存 | 当缓存达到上限时,删除最先入队的缓存 | evication="FIFO" | |
SoftCache \ WeakCache | 带清理策略的缓存 | 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理这些缓存。 | evication="SOFT" \ evication="WEAK" | |
装饰器缓存 | LoggingCache | 带日志功能的缓存 | 如可以输出缓存命中率 | 基本 |
SynchronnizedCache | 同步缓存 | 基于synchronized关键字实现,解决并发问题 | 基本 | |
BlockingCache | 阻塞缓存 | 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于java重入锁实现 | bloking=true | |
SerializedCache | 支持序列化的缓存 | 将对象序列化以后存到缓存中,取出时反序列化 | readOnly=false(默认) | |
ScheduledCache | 定时调度的缓存 | 在进行get 、put、remove、getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认一小时),如果是则清空缓存——每隔一段时间清空一次缓存 | flushInterval不为空 | |
TransactionnalCache | 事务缓存 | 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 | 在TransactionalCacheManager中用Map维护对应关系 |
一级缓存
一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在绘画(SqlSession)层进行缓存的。MyBatis一缓存默认是开启的,不需要任何配置(localCacheScope=STATEMENT相当于关闭一级缓存)。
可以在BaseExecutor的query()方法中找到localCacheScope清除逻辑:
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
在MyBatis执行流程里面,涉及到这么多对象,那么缓存PerpetualCache应该放在哪个对象里面去呢?
如果要在同一个会话里共享一级缓存,最好的办法是在SqlSession里创建的,作为SqlSession的一个属性,跟SqlSession生命周期相同,这样就不需要为SqlSession编号、再根据编号查找对应缓存了。
DefaultSqlSession里只有两个对象属性:Configuration和Executor。
Configuration是全局的,不属于SqlSession,所以缓存只可能放在Executor里,因为Executor是一个接口,实际上他是在基本执行器中(SimpleExecutor\ReuseExecutor\BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache)。
在同一个会话里,多次执行相同SQL语句,会直接从内存取到缓存的结果,不要再发送SQL到数据库。但在不同的会话里,即使执行的SQL一样,也不能使用一级缓存(因为跨了Session)。
下边来通过实际例子来验证一级缓存,并通过分析源码了解实现原理。
/**
* 测试一级缓存作用域,是在SqlSession -> Executor -> localCache 中存储
*/
@Test
public void testFirestCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
//使用相同sqlsession执行查询方法,只打印了1次查询语句
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.byId(1L);
User user2 = userMapper2.byId(1L);
LoggerUtil.printThread("相同Session第1个结果:" + user1.toString());
LoggerUtil.printThread("相同Session第2个结果:" + user2.toString());
LoggerUtil.split();
//使用不同session对象执行查询,会打印出2次查询语句
UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);
User user3 = userMapper3.byId(1L);
LoggerUtil.printThread("不同Session结果:" + user3.toString());
}finally {
sqlSession1.close();
sqlSession2.close();
}
}
执行结果:
通过上边的测试代码可以看出,在使用sqlSession1的分别执行的两次查询,只输出一条执行sql语句,表示第二次查询时使用了缓存,未发送sql到数据库。
在sqlSession2执行的相同SQL时新输出了一条SQL表示没有使用缓存。
一级缓存的不足
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个回话或者分布式环境下,会存在查到过时数据的问题(缓存脏读)。如下别的例子:
/**
* 缓存脏读,当有两个sqlsession同时操作一条数据时,会导致其中一个sqlsession不触发缓存清空,导致其中一个使用旧的缓存数据(已是脏数据)
*/
@Test
public void testCacheDirtyRead(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = userMapper1.byId(1001L);
LoggerUtil.printThread("session1 第1个结果:" + user1.toString());
LoggerUtil.split();
LoggerUtil.printThread("在session2 中更新数据。");
User newUser = new User(1001L,"测试" + DateUtil.now(),20);
userMapper2.update(newUser);
sqlSession2.commit();
LoggerUtil.printThread("session2 第1个结果(更新数据):" + userMapper2.byId(1001L).toString());
LoggerUtil.split();
//使用session1再次查询,使用缓存,导致使用旧数据
User user3 = userMapper1.byId(1001L);
LoggerUtil.printThread("session1 第2个结果(旧数据):" + user3.toString());
}finally {
sqlSession1.close();
sqlSession2.close();
}
}
执行结果:
可以看到,在session2时已把用户名称修改为最新时间,但因为session1中使用一级缓存,不知道其他session的变更,所以导致缓存脏读。
如果要结果这个问题,就需要用到工作范围更广的二级缓存。
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里的相同方法,都可以共享),生命周期和应用同步。
如果开启了二级缓存,二级缓存应用是工作在一级缓存之前,还是一级缓存之后呢?
作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。
而一级缓存是在SqlSession内部的,所以肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
二级缓存是在哪里维护的呢?
要夸会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。
但是二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象,如果没有,就不用这个对象,我们应用怎么做呢?
实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor的时候会对Executor进行装饰。
CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
开启二级缓存的方法
在mybatis-config.xml中配置了(可以不配置,默认是true)
<setting name="cacheEnabled" value="true" />
只要没显示的设置cacheEnabled=false,都会用CachingExecutor装饰基本的执行器(Simple、Reuse、Batch)。
二级缓存的总开关默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要用到二级缓存,还要单独打开它自己的开关。
<!-- 在mapper.xml中声明这个namespace使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!--最多缓存对象个数,默认1024-->
eviction="LRU" <!--回收策略-->
flushInterval="120000" <!--自动刷新时间ms,未配置时只有调用时刷新-->
readOnly="false"<!--默认是false(安全),改为true可读写时,对象必须支持序列化-->
/>
mapper.xml配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。
如果二级缓存拿到结果了,就直接返回(最外层判断),否则再到一级缓存,最后到数据库。
如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?
我们可以在单个Statement ID上显示关闭二级缓存(默认是true):
<select id="byId" resultMap="BaseResultMap" useCache="false" >
...
</select>
什么场景适合使用二级缓存?
-
因为所有增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单查询等(查多写少)。如果写多查少就失去了缓存的意义。
-
如果多个namespace中针对同一个表的操作,比如User表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现脏数据的情况。
所以,推荐在一个Mapper里只操作单表的情况使用。
如果让多个namespace共享一个二级缓存,应该怎么做?
<cache-ref namespace="<其他命名空间>" />
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个cache。在关联表比较少,或者按照业务可以对表进行分组的时候可以使用。
注意:这种情况下,多个Mapper操作都会引起缓存刷新,缓存的意义已经不大了。
mybatis 缓存(三)
mybatis的缓存分为一级缓存和二级缓存
一级缓存:基于SqlSession级别的缓存,也就是说,缓存了这个SqlSession执行所有的select.MapperStatement的结果集;同一个查询语句,只会请求一次;但是当前SqlSession执行增删改操作或者commit/rollback操作时,会清空SqlSession的一级缓存;
禁止一级缓存(同理也禁止了二级缓存)
xml方式:
<select id="ping" flushCache="true" resultType="string">
...
</select>
注解方式:
@Options(flushCache = FlushCachePolicy.TRUE)
一级缓存导致的问题:每个SqlSession可能会对同一个mapperStatement缓存不同的数据,如:
- sqlSession1 查询了userId=1的数据,一级缓存生效
- sqlSession2更新了userId=1的name值,然后在查询,一级缓存生效
这导致了sqlSession1和sqlSession2对于userId=1的缓存数据不一致,引入脏数据
一级缓存源代码:
public abstract class BaseExecutor implements Executor {
// 一级缓存
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
// 默认使用PerpetualCache
this.localCache = new PerpetualCache("LocalCache");
...
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
...
// 增删改删除一级缓存
clearLocalCache();
...
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
// flushCache=true时清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
...
// 判断一级缓存是否有值
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 查数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
...
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
// 一级缓存是否包含cachekey
return localCache.getObject(key) != null;
}
@Override
public void commit(boolean required) throws SQLException {
...
// commit 删除缓存
clearLocalCache();
...
}
@Override
public void rollback(boolean required) throws SQLException {
...
// rollback删除缓存
clearLocalCache();
...
}
@Override
public void clearLocalCache() {
if (!closed) {
// 清空缓存
localCache.clear();
localOutputParameterCache.clear();
}
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除原来的一级缓存
localCache.removeObject(key);
}
// 将新获取的值放入一级缓存
localCache.putObject(key, list);
。。。
}
}
二级缓存:基于SqlSessionFactory缓存,所有SqlSession查询的结果集都会共享;其实这样描述是不准确的,二级缓存是基于namespace的缓存,每个mapper对应一个全局Mapper namespace;当第一个Sqlsession查出的结果集,缓存在namespace中,第二个sqlsession再查找时会从nameSpace中获取;每个namespace是单例的;只有sqlSession调用了commit方法才会生效
禁止二级缓存:
mybatis-congif.xml:将所有的namespace都关闭二级缓存
<settings>
<setting name="cacheEnabled" value="false"/>
...
</settings>
对单个namespace是否使用二级缓存
<cache /> 当前namespace是否使用二级缓存
<cache-ref namespace="..." /> 当前namespace和其它namespace共用缓存
对一个namespace中的单个MapperStatement关闭二级缓存
<select id="selectParentBeans" resultMap="ParentBeanResultMap" useCache="false">
select * from parent
</select>
二级缓存导致的问题:当某个namespace出现多表查询时,会引起脏数据,如:
- A namespace中的mapperStatement id = "xxx"查询了表A id=1和表B id=5的数据,二级缓存生效
- 表B 更新了id=5的数据;
此时再来查A表中的mapperStatement id = "xxx"时,还是使用了A namespace中的二级缓存;这引起了B表id=5的脏数据
当然解决上述问题可以使用<cache-ref>;但这会导致缓存粒度变粗,多个namespace的操作都会影响该缓存;
二级缓存源码:
Configuration# public Executor newExecutor(Transaction transaction, ExecutorType executorType) { ... // 判断是否使用二级缓存 if (cacheEnabled) { executor = new CachingExecutor(executor); } // 对executor制定插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
public class CachingExecutor implements Executor {
// 委托设计模式
private final Executor delegate;
// 二级缓存
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 更新调用清空缓存方法(flushCache=false时不情况二级缓存)
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判断该namespace是否含有缓存
Cache cache = ms.getCache();
if (cache != null) {
// 看是否需要清空该namespace下的二级缓存
flushCacheIfRequired(ms);
// 当前MapperStatement是否需要使用二级缓存
if (ms.isUseCache() && resultHandler == null) {
...
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 存入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
...
}
}
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// flushCache=true时清空该namespace下的二级缓存,反之则不情况缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
}
public class TransactionalCacheManager { // 存储二级缓存,以namespace的cache作为key private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); // 清空namespace的缓存 public void clear(Cache cache) { getTransactionalCache(cache).clear(); } // 获取某个namespace中的cacheKey的缓存 public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } // 放入某个namespace cacheKey的缓存 public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } // 二级缓存提交 public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } // 二级缓存回滚 public void rollback() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.rollback(); } } // transactionalCaches缓存有则不存储,没有则存入 private TransactionalCache getTransactionalCache(Cache cache) { return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } }
在看源代码的时候,transactionalCaches引起深思
- 每个sqlSession中有新的CachingExecutor
- 每个CachingExecutor有新的TransactionalCacheManager
- TransactionalCacheManager中的transactionalCaches是每个sqlSession独享的,如何达到线程安全且多个sqlSession共享呢?
其实二级缓存是以namespace粒度存储在Mapper里面的;每个mapper是全局共享的;而且getTransactionalCache这个方法已经将当前的namespace存放于每个cachingExecutor中了,所以达到了线程安全且sqlSession共享
Mybatis一级缓存和二级缓存 Redis缓存
一级缓存
- Mybatis的一级缓存存放在SqlSession的生命周期,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。
- 如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在改键值时,则会返回缓存中的对象。(一个SqlSession连续两次查询 得到的是同一个java对象)
- 任何的insert update delete操作都会清空一级缓存(增删改任何记录都会清空当前SqlSession的缓存)。
Spring整合Mybatis的时候一级缓存的问题:
在未开启事物的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的
在开启事物的情况之下,spring使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的
Spring结合Mybatis一级缓存失效的问题
二级缓存
Mybatis二级缓存可以理解为存在SqlSessionFactory的生命周期
开启二级缓存:
1.在mybatis-config.xml添加如下代码
<settings>
<setting name="cacheEnable" value="true"></setting>
</settings>
2.在对应的XXXMapper.xml的namespace下添加<cache/>元素
二级缓存特点:
SqlSession1调用getMapper获取对象user1
SqlSession1调用getMapper获取对象user2
user1和user2是同一个实例(原理同一级缓存)
如果二级配置可读写的缓存 <cache readOnly="false"/>,不同SqlSession之间通过序列化和反序列化来保证通过缓存获取数据。
SqlSession2调用getMapper获取对象user1_
SqlSession2调用getMapper获取对象user2_
user1_和user2_就是反序列化得到的结果 是不同的实例
Redis缓存一致性
Mybatis默认提供的缓存是基于Map实现的内存缓存,已经可以基本满足应用。但当需要缓存大量数据的时候可以使用Redis缓存数据库来保存Mybatis的二级缓存数据。
但MySQL和Redis是两个事物,不好做强一致性。
简单点:可以延时双删+过期时间保证最终一致性。
双删的原因是防止并发情况下 update_db的过程中 其他事物发现redis缓存是空 重新赋予了Redis的值 此时如果赋值 是错误的数据
第二次延时删除的原因是要考虑MySQL数据库主从同步的耗时(如果立即删除 有别的线程从MySQL的从库查到的数据放到Redis中 此时的从库可能是没同步的错误数据)
rm_redis
update_db
sleep xxx ms
rm_redis
mybatis入门教程(九)------mybatis缓存
9. Mybatis 缓存
正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持
1. 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
2. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。
3. 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
9.1 Mybatis的一级缓存
9.1.1 数据表准备
/*用户表*/
drop table if exists user;
create table user(
id int primary key auto_increment,
username varchar(50) unique,
password varchar(100),
nickname varchar(50),
salt varchar(100),
locked boolean
)engine=InnqDB default charset=utf8;
9.1.2 User实体类准备
package com.mscncn.batis.model;
public class User {
private int id;
private int age;
private String userName;
private String userAddress;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserAddress() {
return userAddress;
}
public void setUserAddress(String userAddress) {
this.userAddress = userAddress;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
9.1.3 UserMapper.java
public interface UserMapper {
public User getUserById(int id);
}
9.1.4 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">
<!-- 这里namespace必须是PostsMapper接口的路径,不然要运行的时候要报错 “is not known to the MapperRegistry”-->
<mapper namespace="com.mscncn.batis.mapper.UserMapper">
<!-- 这儿的resultType是配置在mybatis-config.xml中得别名 -->
<select id="getUserById" parameterType="int" resultType="User">
select * from user where id=#{id}
</select>
</mapper>
9.1.5 测试
@Test
public void testgetUserById() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//注意,重写User的toString方法
User u1=mapper.getUserById(1);
System.out.println(u1);
User u2=mapper.getUserById(1);
System.out.println(u2);
sqlSession.commit();//这里一定要提交,不然数据进不去数据库中
} finally {
sqlSession.close();
}
}
9.1.6 测试结果
从以上结果中可以看出,两次调用getUserById方法,但是只有一次查询数据库的过程,这种现象产生的原因就是mybatis的一级缓存,并且一级缓存是默认开启的。
9.2 Mybatis的二级缓存
9.2.1 没有开启Mybatis二级缓存之前,测试
@Test
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
//注意,重写User的toString方法
User u1=mapper.getUserById(1);
System.out.println(u1);
User u2=mapper2.getUserById(1);
System.out.println(u2);
sqlSession.commit();//这里一定要提交,不然数据进不去数据库中
} finally {
sqlSession.close();
}
}
测试结果:
两个session,分别查询id为1 的 User ,那么mybatis与数据库交互了两次,这样说明mybatis现在没有开启二级缓存,需要我们手动的开启。
9.2.2 User.java
实体类实现可序列化接口:
public class User implements Serializable {...}
如果实体类不实现可序列化接口,使用二级缓存,那么会报下列异常:
org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException:
9.2.3 Mybatis 配置文件
默认配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
必须开启缓存配置,才能使用mybatis的二级缓存,不然不能使用
9.2.4 Mybatis二级缓存测试
@Test
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
//注意,重写User的toString方法
User u1=mapper.getUserById(1);
sqlSession.commit();
System.out.println(u1);
User u2=mapper2.getUserById(1);
System.out.println(u2);
sqlSession.commit();//这里一定要提交,不然数据进不去数据库中
} finally {
sqlSession.close();
}
}
这儿需要注意,必须使用的是两个不同的session,并且第一个session必须提交才能使用二级缓存(二级缓存必须提交前面的session,现在还没有找到原因)
9.2.5 测试结果
9.3 总结
mybatis 一级缓存:默认开启
1. 必须同一个session,如果session对象已经close()过了就不能用了
2. 查询条件必须一致
3. 没有执行过session.cleanCache();清理缓存
4. 没有执行过增删改操作(这些操作都会清理缓存)
mybatis 二级缓存:
mybatis-config.xml 中默认配置
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
必须手动开启在Mapper.xml中添加
<cache/> 有默认的参数值
<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">
<!-- 这里namespace必须是PostsMapper接口的路径,不然要运行的时候要报错 “is not known to the MapperRegistry”-->
<mapper namespace="com.mscncn.batis.mapper.UserMapper">
<cache />
<!-- 这儿的resultType是配置在mybatis-config.xml中得别名 -->
<select id="getUserById" parameterType="int" resultType="User">
select * from user where id=#{id}
</select>
</mapper>
1. 映射语句文件中的所有select语句将会被缓存。
2. 映射语句文件中的所有insert,update和delete语句会刷新缓存。
3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
4. 缓存会根据指定的时间间隔来刷新。
5. 缓存会存储1024个对象
<cache
eviction="FIFO" //回收策略为先进先出
flushInterval="60000" //自动刷新时间60s
size="512" //最多缓存512个引用对象
readOnly="true"/> //只读
关于mybatis缓存和了解的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于MyBatis 缓存(5)、mybatis 缓存(三)、Mybatis一级缓存和二级缓存 Redis缓存、mybatis入门教程(九)------mybatis缓存等相关知识的信息别忘了在本站进行查找喔。
本文标签: