控制台打印结果:
如上,可以看到只有第一次查询访问了数据库。第二次查询则没有访问数据库,是从内存中直接读取出来的数据。
我们上面也提到了,如果进行了增、删、改的sql操作并进行了事务的commit提交操作后,SqlSession中的一级缓存就会被清空,不会导致脏数据的出现。同样的,我们可以使用测试用例来演示这一点,修改测试代码如下:
est
public void testMybatisCache() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 进行第一次查询
Student student1 = studentMapper.selectByPrimaryKey(2);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student1));
Student stuUpdate = new Student();
stuUpdate.setSid(2);
stuUpdate.setSname(“渣渣辉”);
stuUpdate.setAge(21);
int rowCount = studentMapper.updateByPrimaryKeySelective(stuUpdate);
if (rowCount > 0) {
sqlSession.commit();
System.out.println(“更新student数据成功”);
}
// 进行第二次查询
Student student2 = studentMapper.selectByPrimaryKey(2);
System.out.println(“sqlSession1 第二次查询:” + new JSONObject(student2));
sqlSession.close();
}
控制台打印结果:
如上,可以看到当数据更新成功并commit后,会清空SqlSession中的一级缓存,第二次查询就会访问数据库查询最新的数据了。
不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。所以在这种情况下,是不能实现跨表的session共享的。有一点值得注意的是,由于不同的sqlSession之间的缓存数据区域不共享,如果使用多个SqlSession对数据库进行操作时,就会出现脏数据。我们可以修改之前的测试用例来演示这个现象,修改测试代码如下:
@Test
public void testMybatisCache() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 使用sqlSession1进行第一次查询
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student));
// 使用sqlSession2进行数据的更新
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
Student student2 = new Student();
student2.setSid(1);
student2.setSname(“渣渣辉”);
student2.setAge(21);
int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
if (rowCount > 0) {
sqlSession2.commit();
System.out.println(“sqlSession2 更新student数据成功”);
}
// 使用sqlSession1进行第二次查询
student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第二次查询:” + new JSONObject(student));
sqlSession1.close();
sqlSession2.close();
}
控制台打印结果:
sqlSession1 第一次查询:{“address”:“湖南”,“sname”:“小明”,“sex”:“男”,“age”:16,“sid”:1,“cid”:1}
sqlSession2 更新student数据成功
sqlSession1 第二次查询:{“address”:“湖南”,“sname”:“小明”,“sex”:“男”,“age”:16,“sid”:1,“cid”:1}
由此可见,Mybatis的一级缓存只存在于SqlSession中,可以提高我们的查询性能,降低数据库压力,但是不能实现多sql的session共享,所以使用多个SqlSession操作数据库会产生脏数据。
二级缓存
====
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是可以横跨跨SqlSession的。
示意图:
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,如果使用mapper代理方法每个mapper的namespace都不同,此时可以理解为二级缓存区域是根据mapper划分,也就是根据命名空间来划分的,如果两个mapper文件的命名空间一样,那样,不同的SqlSession之间就可以共享一个mapper缓存。
示意图:
在默认情况下是没有开启二级缓存的,除了局部的 session 缓存。而在一级缓存中我们也介绍了,不同的SqlSession之间的一级缓存是不共享的,所以如果我们用两个SqlSession去查询同一个数据,都会往数据库发送sql。这一点,我们也可以通过测试用例进行测试,测试代码如下:
@Test
public void testMybatisCache2() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 使用sqlSession1进行第一次查询
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student));
sqlSession1.close();
// 使用sqlSession2进行第一次查询
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
Student student2 = studentMapper2.selectByPrimaryKey(1);
System.out.println(“sqlSession2 第一次查询:” + new JSONObject(student2));
sqlSession2.close();
}
控制台输出结果:
如果想要开启二级缓存,你需要在你的mybatis主配置文件里加入:
然后在需要被缓存的 SQL 映射文件中添加一行cache配置即可:
…
…
…
字面上看就是这样。这个简单语句的效果如下:
-
映射语句文件中的所有 select 语句将会被缓存。
-
映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
-
缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
-
根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
-
缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
-
缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
注:缓存只适用于缓存标记所在的映射文件中声明的语句。如果你使用的是java的API和XML映射文件一起,默认情况下不会缓存接口中声明的语句。你需要把缓存区使用@CacheNamespaceRef注解进行声明。
假如说,已开启二级缓存的Mapper中有个statement要求禁用怎么办,那也不难,只需要在statement中设置useCache="false"就可以禁用当前select语句的二级缓存,也就是每次都会生成sql去查询,ps:默认情况下默认是true,也就是默认使用二级缓存。如下示例:
select
from
student
除此之外,还有个flushCache属性,该属性用于刷新缓存,将其设置为 true时,任何时候只要语句被调用,都会导致一级缓存和二级缓存都会被清空,默认值:false。在mapper的同一个namespace中,如果有其他insert、update、delete操作后都需要执行刷新缓存操作,来避免脏读。这时我们只需要设置statement配置中的flushCache="true"属性,就会默认刷新缓存,相反如果是false就不会了。当然,不管开不开缓存刷新功能,你要是手动更改数据库表,那都肯定不能避免脏读的发生。如下示例:
…
那既然能够刷新缓存,能定时刷新吗?也就是设置时间间隔来刷新缓存,答案是肯定的。我们在mapper映射文件中添加来表示开启缓存,所以我们就可以通过元素的属性来进行配置。比如:
<cache
eviction=“FIFO”
flushInterval=“60000”
size=“512”
readOnly=“true”/>
这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。
-
flushInterval(刷新间隔) 可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
-
size(引用数目) 可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
-
readOnly(只读) 属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
可用的收回策略有:
-
LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认)
-
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
-
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
-
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
开启了二级缓存之后,我们再来进行测试,但是在运行测试用例之前,我们需要给pojo类加上实现序列化接口的代码,不然在关闭SqlSession的时候就会报错,代码如下:
package org.zero01.pojo;
import java.io.Serializable;
public class Student implements Serializable {
…
}
测试代码不变,运行后,控制台输出结果如下:
可以看到,开启二级缓存后,SqlSession之间的数据就可以通过二级缓存共享了,和一级缓存一样,当执行了insert、update、delete等操作并commit提交后就会清空二级缓存区域。当一级缓存和二级缓存同时存在时,会先访问二级缓存,再去访问各自的一级缓存,如果都没有需要的数据,才会往数据库发送sql进行查询。这一点,我们也可以通过测试用例来进行测试,测试代码如下:
@Test
public void testMybatisCache() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 使用sqlSession1进行第一次查询
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student));
// 使用sqlSession2进行数据的更新
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
Student student2 = new Student();
student2.setSid(1);
student2.setSname(“小明”);
student2.setAge(16);
int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
if (rowCount > 0) {
sqlSession2.commit();
System.out.println(“sqlSession2 更新student数据成功”);
}
// 使用sqlSession1进行第二次查询
student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第二次查询:” + new JSONObject(student));
// 使用sqlSession2进行第一次查询
student2 = studentMapper2.selectByPrimaryKey(1);
System.out.println(“sqlSession2 第一次查询:” + new JSONObject(student2));
// 关闭会话
sqlSession1.close();
sqlSession2.close();
}
运行测试代码后,控制台输出结果如下:
通过此测试用例可以看出两点:
-
1.Mybatis的二级缓存是跨Session的,每个Mapper享有同一个二级缓存域,同样,每次执行commit操作之后,会清空二级缓存区域。
-
2.如果数据存在一级缓存的话,依旧会去一级缓存中读取数据,这样会发生脏读现象,不过我们可以在相应的statement中,设置flushCache=“true”,这样每次都会清除缓存,并向数据发送sql来进行查询。
或者全局关闭本地、二级缓存:
但是在使用多个sqlSession操作数据库的时候,还有一个需要注意的问题,那就是事务隔离级别,mysql的默认事务隔离级别是REPEATABLE-READ(可重复读)。这样当多个sqlsession操作同一个数据的时候,可能会导致两个不同的事务查询出来的数据不一致,例如,sqlsession1 在同一个事务中读取了两次数据,而 sqlsession2 在 sqlsession1 第一次查询之后就更新了数据,那么由于可重复读的原因,sqlsession1 第二次查询到的依旧是之前的数据。
我们可以使用测试用例来测试一下,首先得关闭缓存或者在相应的statement中设置flushCache属性值为true(此时没有缓存),测试用例代码如下:
@Test
public void testMybatisCache() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
// 使用sqlSession1进行第一次查询
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student));
// 使用sqlSession2进行数据的更新
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
Student student2 = new Student();
student2.setSid(1);
student2.setSname(“小明”);
student2.setAge(16);
int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
if (rowCount > 0) {
sqlSession2.commit();
System.out.println(“sqlSession2 更新student数据成功”);
}
// 使用sqlSession1进行第二次查询
student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第二次查询:” + new JSONObject(student));
// 使用sqlSession2进行第一次查询
student2 = studentMapper2.selectByPrimaryKey(1);
System.out.println(“sqlSession2 第一次查询:” + new JSONObject(student2));
// 关闭会话
sqlSession1.close();
sqlSession2.close();
}
控制台输出结果:
这就是mysql默认事务隔离级别REPEATABLE-READ(可重复读)导致的现象,这种隔离级别能够保证同一个事务的生命周期内,读取的数据是一致的,但是两个不同的事务之间读取出来的数据就可能不一致。
不过,如果你希望在不同的事务的生命周期内读取的数据一致的话,就需要把事务隔离级别改成READ-COMMITTED(读已提交),该级别会导致不可重复读,也就是说在同一个事务的生命周期内读取到的数据可能是不一致的,而在两个不同的事务之间读取的数据则是一致的。同样的我们可以使用测试用例进行测试,修改测试代码如下:
@Test
public void testMybatisCache() throws IOException {
String confPath = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 设置事务隔离级别为读已提交
SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
// 使用sqlSession1进行第一次查询
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第一次查询:” + new JSONObject(student));
// 使用sqlSession2进行数据的更新
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
Student student2 = new Student();
student2.setSid(1);
student2.setSname(“阿基米德”);
student2.setAge(22);
int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
if (rowCount > 0) {
sqlSession2.commit();
System.out.println(“sqlSession2 更新student数据成功”);
}
// 使用sqlSession1进行第二次查询
student = studentMapper.selectByPrimaryKey(1);
System.out.println(“sqlSession1 第二次查询:” + new JSONObject(student));
// 使用sqlSession2进行第一次查询
student2 = studentMapper2.selectByPrimaryKey(1);
System.out.println(“sqlSession2 第一次查询:” + new JSONObject(student2));
// 关闭会话
sqlSession1.close();
sqlSession2.close();
}
控制台输出结果:
可以看到,设置成读已提交后,两个事务在数据更新后查询出来的数据是一致的了。至于是使用可重复读还是读已提交,就取决于实际的业务需求了,如果希望同一个事务的生命周期内,读取的数据是一致的,就使用可重复读级别。如果希望两个不同的事务之间查询出来的数据是一致的,那么就使用读已提交级别。
自定义缓存
=====
mybatis自身的缓存做的并不完美,不过除了使用mybatis自带的二级缓存, 你也可以使用你自己实现的缓存或者其他第三方的缓存方案创建适配器来完全覆盖缓存行为。所以它提供了使用自定义缓存的机会,我们可以选择使用我们喜欢的自定义缓存,下面将介绍一下,使用ehcache作为mybatis的自定义缓存的具体步骤。
首先,要想使用mybatis自定义缓存,就必须让自定义缓存类实现mybatis提供的Cache 接口(org.apache.ibatis.cache.Cache):
public interface Cache {
// 获取缓存编号
String getId();
// 获取缓存对象的大小
int getSize();
// 保存key值缓存对象
void putObject(Object key, Object value);
// 通过kEY获取值
Object getObject(Object key);
// 缓存中是否有某个key
boolean hasKey(Object key);
// 获取缓存的读写锁
ReadWriteLock getReadWriteLock();
// 通过key删除缓存对象
Object removeObject(Object key);
// 清空缓存
void clear();
}
我们要使用ehcache做自定义缓存,就应该完成这个自定义缓存类,但mybatis的git上提供了相对于的适配包,我们只需要下载即可,下面是适配包的maven依赖:
org.mybatis.caches
mybatis-ehcache
1.1.0
接着在相应的 mapper xml文件中配置相应的缓存实现类:
实现Cache接口的是EhcacheCache的父类AbstractEhcacheCache,我们可以看一下它的源码:
package org.mybatis.caches.ehcache;
import java.util.concurrent.locks.ReadWriteLock;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import org.apache.ibatis.cache.Cache;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习
已经将知识体系整理好(源码,笔记,PPT,学习视频)
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
在。**
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-yuUcWim5-1713528299030)]
[外链图片转存中…(img-lmsAjWHZ-1713528299031)]
[外链图片转存中…(img-Fkv011JM-1713528299031)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习
已经将知识体系整理好(源码,笔记,PPT,学习视频)
[外链图片转存中…(img-sJZ1aH3n-1713528299031)]
[外链图片转存中…(img-wzj9vVQv-1713528299032)]
[外链图片转存中…(img-jn33Q7WV-1713528299032)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!