大家都知道,数据库的本质是以文件的形式保存在硬盘上,所以每一次查询数据库的操作都是在进行IO操作,IO操作是很耗时的。所以为了提高查询效率,可以先从缓存(内存里面)取,如果缓存里面没有再访问数据库。
哪些数据适合使用缓存
经常查找又不经常改变的数据、数据的正确性对业务影响不大。有些数据尤其牵扯到钱的时候就不能使用缓存,比如股票价格、汇率、库存等
在MyBatis中缓存指的是什么
在MyBatis缓存指的是对select标签语句的缓存
MyBatis的缓存有一级、二级缓存的概念
一级缓存(默认)
使用同一个SqlSession查询相同的select标签时,缓存共享。
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
Person person1 = session.selectOne("person.getOnePerson",1);
System.out.println(person == person1); //true
}
上面的person和person1内存地址相同,说明缓存共享。
当关闭SqlSession,缓存失效。对应上面try结束后关闭SqlSession,缓存失效。
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println(person);
}
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println(person);
}
上面打印的person内存地址不同,说明缓存失效。
当执行insert、update、delete、commit等方法时,会自动清理一级缓存
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println(person);
//当执行delete\update\insert\commit时会,清楚缓存
// session.delete("namespace.delete");
// session.update("namespace.update");
// session.insert("namespace.insert");
session.commit();
Person person1 = session.selectOne("person.getOnePerson",1);
System.out.println(person1);
}
上面的person和person1内存地址不同,说明缓存失效。
其实每次我们处理客户端的请求查询数据库的时候都是开启不同的SqlSession,而不同的SqlSession缓存是不共享的,所以一级缓存命中率不高。
二级缓存(需要手动开启)
为了提高缓存命中率,可以开启二级缓存。二级缓存也就是namespace级别的缓存,即mapper.xml级别。同一个mapper.xml下的所有select标签进行缓存。当不同的SqlSession查询相同的select标签时,达到共享缓存。
二级缓存放在全局唯一的SqlSessionFactory里面。SqlSessionFactory里面有个map结构,key 为namespace ,value为对应的缓存数据。
二级缓存的时刻
当SqlSession关闭时,才会将查询的结果进行缓存。而且默认情况下,当执行mapper.xml下的insert、update、delete时,会自动清理其二级缓存。
开启二级缓存
在映射文件mapper中添加cache标签即可
<cache />
默认会缓存映射文件中的所有select结果
外界测试
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println(person);
}
try(SqlSession session = MyBatisUtils.openSession()){
Person person1 = session.selectOne("person.getOnePerson",1);
System.out.println(person1);
}
报错如下
org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: com.lff.beans.Person
JavaBean类要实现Serializable接口,实现序列化接口后,我们再次测试,遗憾的是当我们再次测试后,person和person1的对象内存地址并不相同,好奇怪?查看日志SQL语句确实只执行了一次,而且有缓存命中率,说明第二次查询的确实执行了缓存
:43:04.636 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
20:43:04.645 [main] DEBUG person.getOnePerson - ==> Preparing: SELECT person.* FROM person where id = ?
20:43:04.670 [main] DEBUG person.getOnePerson - ==> Parameters: 1(Integer)
20:43:04.685 [main] DEBUG person.getOnePerson - <== Total: 1
com.lff.beans.Person@7ac2e39b
20:43:04.690 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
20:43:04.691 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
20:43:04.693 [main] DEBUG person - Cache Hit Ratio [person]: 0.5
com.lff.beans.Person@19553973
我们尝试把映射文件中的
<cache readOnly="true"/>
执行上面相同的代码,日志信息
:45:47.140 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@28e8dde3]
20:45:47.149 [main] DEBUG person.getOnePerson - ==> Preparing: SELECT person.* FROM person where id = ?
20:45:47.189 [main] DEBUG person.getOnePerson - ==> Parameters: 1(Integer)
20:45:47.208 [main] DEBUG person.getOnePerson - <== Total: 1
com.lff.beans.Person@68fa0ba8
20:45:47.209 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@28e8dde3]
20:45:47.210 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@28e8dde3]
20:45:47.210 [main] DEBUG person - Cache Hit Ratio [person]: 0.5
com.lff.beans.Person@68fa0ba8
这次person和person1是同一个,也执行了缓存操作。
关于readOnly属性官方是这样说的
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
也就是说,当readOnly为false时
当第一次执行如下代码后
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println(person);
}
会从数据库里面查询,查询后的结果会放到二级缓存中,放到二级缓存的数据是经过序列化后的数据。
当再次查询时
try(SqlSession session = MyBatisUtils.openSession()){
Person person1 = session.selectOne("person.getOnePerson",1);
System.out.println(person1);
}
会从二级缓存反序列化一个新的person1对象。所以每次从缓存取的都是一个新的person对象,这样当修改person的属性后,也不会影响二级缓存中的数据,也就更加安全。
当然有的时候,你可能对某个select标签不想使用缓存,比如在做多条件查询的时候,每次的查询条件可能都不一样,这时使用缓存的意义不大。可以使用useCache选项进行关闭
<select id="getOnePerson" resultMap="personMap" useCache="false">
SELECT
person.*
FROM person where id = #{id}
</select>
默认情况下当执行insert、update、delete后缓存就会清空。
如果不想清空可以设置flushCache选项
<delete id="xxx" flushCache="false">
</delete>
注意点
由于二级缓存是在SqlSession关闭时,才会将查询的结果进行缓存。所以当关闭SqlSession之前如果对查询结果进行修改,会将修改后的结果进行缓存,其它SqlSession再次查询时就会得到修改后的数据。
try(SqlSession session = MyBatisUtils.openSession()){
Person person = session.selectOne("person.getOnePerson",1);
System.out.println("修改之前的age是 = "+person.getAge());
person.setAge(100);
System.out.println("修改之后的age是 = "+person.getAge());
}
try(SqlSession session = MyBatisUtils.openSession()){
Person person1 = session.selectOne("person.getOnePerson",1);
System.out.println("其他SqlSession查询的age是 = "+person1.getAge());
System.out.println(person1.getAge());
}
打印日志
09:34:53.395 [main] DEBUG person.getOnePerson - ==> Preparing: SELECT person.* FROM person where id = ?
09:34:53.419 [main] DEBUG person.getOnePerson - ==> Parameters: 1(Integer)
09:34:53.433 [main] DEBUG person.getOnePerson - <== Total: 1
修改之前的age是 = 18
修改之后的age是 = 100
09:34:53.437 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
09:34:53.438 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
09:34:53.441 [main] DEBUG person - Cache Hit Ratio [person]: 0.5
其他SqlSession查询的age是 = 100
100
可以根据情况是否将修改的数据进行缓存,如果不想将修改的数据进行缓存,可以先关闭SqlSession,再修改数据
SqlSession session = MyBatisUtils.openSession();
Person person = session.selectOne("person.getOnePerson",1);
System.out.println("修改之前的age是 = "+person.getAge());
session.close();//修改数据之前关闭SqlSession
person.setAge(100);
System.out.println("修改之后的age是 = "+person.getAge());
try(SqlSession session1 = MyBatisUtils.openSession()){
Person person1 = session1.selectOne("person.getOnePerson",1);
System.out.println("其他SqlSession查询的age是 = "+person1.getAge());
System.out.println(person1.getAge());
}
打印日志
09:37:34.143 [main] DEBUG person.getOnePerson - ==> Preparing: SELECT person.* FROM person where id = ?
09:37:34.180 [main] DEBUG person.getOnePerson - ==> Parameters: 1(Integer)
09:37:34.202 [main] DEBUG person.getOnePerson - <== Total: 1
修改之前的age是 = 18
09:37:34.207 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
09:37:34.209 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a3e78f]
修改之后的age是 = 100
09:37:34.213 [main] DEBUG person - Cache Hit Ratio [person]: 0.5
其他SqlSession查询的age是 = 18
18