前言
Mybatis 提供了一个非常强大的查询缓存特性 ,可以非常方便地配置和使用。合理利用缓存,可以极大地提升查询效率,但同样地不合理使用缓存可能出现数据不一致的问题。今天我们就来谈一谈如何使用缓存。
缓存机制
mybatis 分为两级缓存:一级缓存 , 二级缓存
| 一级缓存 | 二级缓存 | |
|---|---|---|
| 默认设置 | 开启 | 关闭 |
| 作用域 | SqlSession 级别 | 全局(跨 SqlSession共享) |
| 作用范围 | 全局 | 具体的mapper |
缓存 key
mybatis的查询缓存存储的类型是 HashMap的结构,结果集当map, 那么key是什么呢?
缓存key = statementId (比如xxx.mapper.select) + offset + limit + sql + param
缓存流程图
一级缓存
**默认开启 **
开启缓存
不在一个事务里,缓存失效
从上图,我们明显地看到,虽然两次查询条件一样,但第二次查询时依然发生了数据库查询操作,因此未使用缓存
在同一个事务里,缓存起作用
可以很明显的看到,第二次没有执行数据库操作,直接返回对象了。当然了,也可能通过断点的方式,在执行完第一次之后进入断点,此时手动修改数据库中的值,此时再执行第二次查询。这时候会发现,第二次查询出来的结果跟第一次的一样,跟数据库中的不一致,这就是我们前面说的可能存在的问题,二级缓存同样有这个问题,所以我们要根据业务场景合理使用。
关闭缓存
设置
mybatis.configuration.local-cache-scope: statement及缓存为语句级别,即可让一级缓存失效
即使在同一个事务里,缓存依然不起作用
不在同一个事务里,开启缓存时不能使用缓存,关闭的时候肯定也不能使用缓存。这里就不再贴那种情况的测试情况了。
二级缓存
二级缓存是指不同 sqlSession之间共享查询结果集,默认情况下是关闭的。
开启方式:
设置
mybatis.configuration.cache-enabled: true全局开启
在对应的mapper文件中,添加
<cache/>标签
在实测中,只要在 mapper文件中添加了 <cache/> 标签即可直接开启对应的二级缓存。
二级缓存需要两个sqlSession的来同时查询,来观察两个 sqlSession 是否可能用到缓存。
@Test
void testTwoLevelCache(){
countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
this.selectEO();
countDownLatch.countDown();
});
Thread t1 = new Thread(() -> {
this.selectEO2();
countDownLatch.countDown();
});
t.start();
t1.start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("全部执行完毕");
}
@Test
@Transactional
void selectEO() {
// Page<Object> page = PageMethod.startPage(1, 10);
StudentEO studentEO = studentMapper.selectEO(2L);
// System.out.println("总记录数 " + page.getTotal());
System.out.println(JSON.toJSONString(studentEO));
StudentEO studentEO2 = studentMapper.selectEO(2L);
System.out.println(JSON.toJSONString(studentEO2));
// StudentDO studentDO = new StudentDO();
// studentDO.setId(2L);
// studentDO.setAddress("地址999");
// studentMapper.updateStudentById(studentDO);
// StudentEO studentEO3 = studentMapper.selectEO(2L);
// System.out.println(JSON.toJSONString(studentEO3));
log.info("完成");
}
@Test
@Transactional
void selectEO2() {
// Page<Object> page = PageMethod.startPage(1, 10);
StudentEO studentEO = studentMapper.selectEO(2L);
// System.out.println("总记录数 " + page.getTotal());
System.out.println(JSON.toJSONString(studentEO));
studentEO.setAge(13);
StudentEO studentEO2 = studentMapper.selectEO(2L);
System.out.println(JSON.toJSONString(studentEO2));
// StudentDO studentDO = new StudentDO();
// studentDO.setId(2L);
// studentDO.setAddress("上海市浦东新区999");
// studentMapper.updateStudentById(studentDO);
// StudentEO studentEO3 = studentMapper.selectEO(2L);
// System.out.println(JSON.toJSONString(studentEO3));
log.info("完成");
}
我们在 t 线程查询到数据,完成缓存后,让 t卡在当前线程,然后去t1线程再执行查询,看此时t1是去查数据库 还是去查缓存。
- t 完成查询 ,进入断点等待
- 切换到 t1 , 执行查询 ,命中缓存
从日志中,我们可以明显发现,没有再往数据库中去查询,直接命中缓存,且命中几率为:0.5
到此,我们已经验证了,在开启二级缓存的情况下,不同事务的sqlsession 会用到缓存
推荐配置
官方推荐的配置
<cache eviction="FIFO"
flushInterval = "60000"
size="512"
readOnly="false"
/>
eviction=“FIFO”:缓存回收策略
- LRU – 最近最少使用的:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
- 默认的是 LRU。
flushInterval:刷新间隔,单位毫秒
默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:引用数目,正整数
代表缓存最多可以存储多少个对象,太大容易导致内存溢出。
readOnly:只读,true/false
- true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。速度快,性能有优势,但是不安全。
- false:读写缓存;会返回缓存对象的拷贝(通过序列化)。速度慢,但是安全,因此默认是 false。
我们重点讲一下这个 readOnly 默认是 false ,这是为了安全的保证来考虑的。每次获取的结果都是一个新对象,如果是 true的话,实际上每次获取的都是同一个对象。
接下来,我们进行验证:
- readOnly="false"
studentEO 对象的地址是:@7596
studentEO 对象的地址是:@7569
可以看到,查询返回的对象虽然属性值是一样的,但对象地址不一样,也就是新对象。
- readOnly="true"
studentEO 对象的地址是:@7546
studentEO 对象的地址是:@7546
可以看到,这时候缓存返回的是同一个对象
缓存失效
在mybatis中执行 update delete 语句时会导致缓存失效