缓存的重要性是不言而喻的。 使用缓存, 我们可以避免频繁的与数据库进行交互, 尤其是在查询越多、缓存命中率越高的情况下, 使用缓存对性能的提高更明显。
mybatis 也提供了对缓存的支持, 分为一级缓存和二级缓存。 但是在默认的情况下, 只开启一级缓存(一级缓存是对同一个 SqlSession 而言的)。
以下的项目是在mybatis 初步使用(IDEA的Maven项目, 超详细)的基础上进行。
对以下的代码, 你也可以从我的GitHub中获取相应的项目。
同一个
SqlSession
对象, 在参数和 SQL 完全一样的情况先, 只执行一次 SQL 语句(如果缓存没有过期)
也就是只有在参数和 SQL 完全一样的情况下, 才会有这种情况。
@Test
public void oneSqlSession() {
SqlSession sqlSession = null;
try {
sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 执行第一次查询
List students = studentMapper.selectAll();
for (int i = 0; i < students.size(); i++) {
System.out.println(students.get(i));
}
System.out.println("=============开始同一个 Sqlsession 的第二次查询============");
// 同一个 sqlSession 进行第二次查询
List stus = studentMapper.selectAll();
Assert.assertEquals(students, stus);
for (int i = 0; i < stus.size(); i++) {
System.out.println("stus:" + stus.get(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
在以上的代码中, 进行了两次查询, 使用相同的 SqlSession
, 结果如下
在日志和输出中:
第一次查询发送了 SQL 语句, 后返回了结果;
第二次查询没有发送 SQL 语句, 直接从内存中获取了结果。
而且两次结果输入一致, 同时断言两个对象相同也通过。
@Test
public void differSqlSession() {
SqlSession sqlSession = null;
SqlSession sqlSession2 = null;
try {
sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 执行第一次查询
List students = studentMapper.selectAll();
for (int i = 0; i < students.size(); i++) {
System.out.println(students.get(i));
}
System.out.println("=============开始不同 Sqlsession 的第二次查询============");
// 从新创建一个 sqlSession2 进行第二次查询
sqlSession2 = sqlSessionFactory.openSession();
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
List stus = studentMapper2.selectAll();
// 不相等
Assert.assertNotEquals(students, stus);
for (int i = 0; i < stus.size(); i++) {
System.out.println("stus:" + stus.get(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
if (sqlSession2 != null) {
sqlSession2.close();
}
}
}
在代码中, 分别使用 sqlSession 和 sqlSession2 进行了相同的查询。
其结果如下
从日志中可以看到两次查询都分别从数据库中取出了数据。 虽然结果相同, 但两个是不同的对象。
刷新缓存是清空这个 SqlSession 的所有缓存, 不单单是某个键。
@Test
public void sameSqlSessionNoCache() {
SqlSession sqlSession = null;
try {
sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 执行第一次查询
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println("=============开始同一个 Sqlsession 的第二次查询============");
// 同一个 sqlSession 进行第二次查询
Student stu = studentMapper.selectByPrimaryKey(1);
Assert.assertEquals(student, stu);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
如果是以上, 没什么不同, 结果还是第二个不发 SQL 语句。
在此, 做一些修改, 在 StudentMapper.xml 中, 添加
flushCache=“true”
修改后的配置文件如下:
第一次, 第二次都发送了 SQL 语句, 同时, 断言两个对象相同出错。
在同一个 SqlSession
中, Mybatis 会把执行的方法和参数通过算法生成缓存的键值, 将键值和结果存放在一个 Map 中, 如果后续的键值一样, 则直接从 Map 中获取数据;
不同的 SqlSession
之间的缓存是相互隔离的;
用一个 SqlSession
, 可以通过配置使得在查询前清空缓存;
任何的 UPDATE, INSERT, DELETE 语句都会清空缓存。
二级缓存存在于 SqlSessionFactory 生命周期中。
在 mybatis 中, 二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml 中如下配置:
默认是为 true, 即默认开启总开关。
分开关就是说在 *Mapper.xml 中开启或关闭二级缓存, 默认是不开启的。
public class Student implements Serializable {
private static final long serialVersionUID = -4852658907724408209L;
...
}
@Test
public void secendLevelCacheTest() {
// 获取 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取 Mapper 对象
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 使用 Mapper 接口的对应方法,查询 id=2 的对象
Student student = studentMapper.selectByPrimaryKey(2);
// 更新对象的名称
student.setName("奶茶");
// 再次使用相同的 SqlSession 查询id=2 的对象
Student student1 = studentMapper.selectByPrimaryKey(2);
Assert.assertEquals("奶茶", student1.getName());
// 同一个 SqlSession , 此时是一级缓存在作用, 两个对象相同
Assert.assertEquals(student, student1);
sqlSession.close();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
Student student2 = studentMapper1.selectByPrimaryKey(2);
Student student3 = studentMapper1.selectByPrimaryKey(2);
// 由于我们配置的 readOnly="true", 因此后续同一个 SqlSession 的对象都不一样
Assert.assertEquals("奶茶", student2.getName());
Assert.assertNotEquals(student3, student2);
sqlSession1.close();
}
结果如下:
2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 242282810.
2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:26,897 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==> Preparing: select student_id, name, phone, email, sex, locked, gmt_created, gmt_modified from student where student_id=?
2018-09-29 23:14:26,999 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 2(Integer)
2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <== Columns: student_id, name, phone, email, sex, locked, gmt_created, gmt_modified
2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <== Row: 2, 小丽, 13821378271, [email protected], 0, 0, 2018-09-04 18:27:42.0, 2018-09-04 18:27:42.0
2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <== Total: 1
2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.0
2018-09-29 23:14:27,108 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 242282810 to pool.
2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.3333333333333333
2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.5
以上结果, 分几个过程解释:
第一阶段:
SqlSession
中, 查询出 student
对象, 此时发送了 SQL 语句;student
更改了name
属性;SqlSession
再次查询出 student1
对象, 此时不发送 SQL 语句, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是没有命中。 因为一级缓存先作用了。sqlSession.close()
, 此时将数据序列化并保持到二级缓存中。第二阶段:
sqlSession.close()
对象;student2
对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时查了 3 个对象,但只有一个命中, 因此 命中率 1/3=0.333333;student3
对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时查了 4 个对象,但只有一个命中, 因此 命中率 2/4=0.5;readOnly=“true”
, 因此 student2
和 student3
都是反序列化得到的, 为不同的实例。查看 dtd 文件, 可以看到如下约束:
从中可以看出:
cache
中可以出现任意多个 property
子元素;cache
有一些可选的属性 type
, eviction
, flushInterval
, size
, readOnly
, blocking
.type
用于指定缓存的实现类型, 默认是PERPETUAL
, 对应的是 mybatis 本身的缓存实现类 org.apache.ibatis.cache.impl.PerpetualCache
。
后续如果我们要实现自己的缓存或者使用第三方的缓存, 都需要更改此处。
eviction
对应的是回收策略, 默认为 LRU。
LRU: 最近最少使用, 移除最长时间不被使用的对象。
FIFO: 先进先出, 按对象进入缓存的顺序来移除对象。
SOFT: 软引用, 移除基于垃圾回收器状态和软引用规则的对象。
WEAK: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象。
flushInterval
对应刷新间隔, 单位毫秒, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。
如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据。
size
对应为引用的数量,即最多的缓存对象数据, 默认为 1024。
readOnly
为只读属性, 默认为 false
false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。
true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。
blocking
为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache
进行封装。
使用 BlockingCache
会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。
由于在更新时会刷新缓存, 因此需要注意使用场合:查询频率很高, 更新频率很低时使用, 即经常使用 select, 相对较少使用delete, insert, update。
缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响。但刷新缓存是刷新整个 namespace 的缓存, 也就是你 update 了一个, 则整个缓存都刷新了。
最好在 「只有单表操作」 的表的 namespace 使用缓存, 而且对该表的操作都在这个 namespace 中。 否则可能会出现数据不一致的情况。