文章目录
- 前言
- 1. 多级缓存的概念
- 1.1 CPU 多级缓存
- 1.2 MyBatis 多级缓存
- 2. MyBatis 本地缓存
- 3. MyBatis 全局缓存
- 3.1 MyBatis 全局缓存过期算法
- 3.2 CacheAside 模式
- 后记
- MyBatis 提供了缓存切口, 采用 Redis 会引入什么问题?
- 万一遇到需强一致场景,如何增强?
前言
MyBatis 官方文档 中文版本没有翻译cache的部分,网上资料比较杂。
这里使用 Spock 测试框架验证下多级缓存,并探索 Mybatis 的 CacheAside 模式。注意:
- 本文用 本地缓存 表示一级缓存,全局缓存 表示二级缓存
- 用例仓库
1. 多级缓存的概念
多级缓存可以联系CPU的结构,离核心约近的一致性越高。
1.1 CPU 多级缓存
1.2 MyBatis 多级缓存
本地缓存默认开启,全局缓存需要使用 <cache/>
开启
By default, just local session caching is enabled that is used solely to cache data for the duration of a session. To enable a global second level of caching you simply need to add one line to your SQL Mapping file:
<mapper namespace="com.james.mapper.FileCacheMapper">
<cache/> <!-- 声明该标签,全局缓存开启 -->
<select id="select" resultType="java.lang.String">
SELECT file_name FROM file
</select>
</mapper>
2. MyBatis 本地缓存
用 Spring 注入的 mapper,调用一次select方法就会产生一个 sqlSession,没有利用到本地缓存。
def "未使用事务,第二次查询,不命中本地缓存"() {
given:
def list1 = mapper.select()
def list2 = mapper.select()
expect:
list1 !== list2
}
用事务包裹后,两次 select 共用一个 sqlSession,缓存命中
def "使用事务,命中缓存"() {
given:
def list1 = []
def list2 = []
when:
transaction.execute {
list1 = mapper.select()
list2 = mapper.select()
}
then:
// 同一个事务使用同一个SqlSession,若引用相同则认为命中缓存
list1 === list2
}
note: groovy 中
list1 === list2
表示引用相同,list1 == list2
表示两个列表的内容相同
3. MyBatis 全局缓存
上文说到,没有事务保护的 select方法调用无法公用一个 sqlSession,所以利用不了本地缓存。
全局缓存的范围更大,只要是同一个mapper的调用,都会被缓存。
def "全局缓存默认关闭,需要在xml文件中使用 <cache/> 标签启用"() {
given:
def list1 = fileCacheMapper.select()
def list2 = fileCacheMapper.select()
expect:
// 由于 SerializedCache.java:64 使用的是由byte[]序列化方式存储元素,所以实例的地址必然不同
list1 !== list2
list1 == list2
}
3.1 MyBatis 全局缓存过期算法
值得关注的是 SOFT 和 WEAK 的类型,对应Java中软引用和弱引用。
软引用是在内存不足时GC可以回收,弱引用是下次GC即可回收(比软引用)积极。
3.2 CacheAside 模式
以下是 Mybatis 默认的全局缓存失效模式,也就是 Cache Aside 模式的应用。
- 查询的时候,如果没有缓存,则写入。
- 任何数据操作,使缓存失效。
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
后记
Cache Aside 并不能保证强一致性,不然也就不会有 Paxos 这种复杂的共识算法了。 —— 《凤凰架构》
MyBatis 提供了缓存切口, 采用 Redis 会引入什么问题?
- 多实例之间缓存重复的失效问题,查询时竞争写缓存的问题。
- ORM框架与中间件耦合,违反单一职责。
万一遇到需强一致场景,如何增强?
- 两个查询请求同时到来,此时缓存为空,需要将MySql数据写入缓存。此时会出现竞争写缓存的情况。用写锁来保证缓存内的数据跟数据库保持一致。
public void query() {
if (cache 命中) {
retrun cache
}
获取缓存写锁
if (获取锁失败) {
return 查数据库
}
查数据库
写缓存
释放缓存写锁
}