一级缓存
一级缓存存在于SqlSession的生命周期中。在同一个SqlSession中查询时,执行的方法、参数、结果会封装成一个CacheKey然后放到一个Map中。如果再次执行相同的方法、参数,判断是否同一个CacheKey,如果是,直接返回Map缓存对象中的结果,无需入库。
默认启用。如果要关闭需要配置flushCache。
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
select login_id, customer_id, login_time, login_ip, login_type
from customer_login_log
where login_id = #{loginId,jdbcType=INTEGER}
</select>
CacheKey的真容:
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new CacheKey(){
@Override
public void update(Object object) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
@Override
public void updateAll(Object[] objects) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
};
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return hashcode;
}
@Override
public String toString() {
StringJoiner returnValue = new StringJoiner(":");
returnValue.add(String.valueOf(hashcode));
returnValue.add(String.valueOf(checksum));
updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
return returnValue.toString();
}
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<>(updateList);
return clonedCacheKey;
}
}
一级缓存的测试
严格来说,MyBatis根据以下几个因素判断放入一级缓存的CacheKey是否同一个:
- MappedStatement的ID
- 查询sql的offset
- 查询sql的limit
- sql语句
- 入参(如果查询sql有入参的话)
<environment id="development">
只要有一个变了,就会生成不同的CacheKey对象。
对上面这句话的测试:
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select login_id, customer_id, login_time, login_ip, login_type
from customer_login_log
where login_id = #{loginId,jdbcType=INTEGER}
</select>
<select id="selectByPrimaryKey2" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select login_id, customer_id, login_time, login_ip, login_type
from customer_login_log
where login_id = #{loginId,jdbcType=INTEGER}
</select>
public interface CustomerLoginLogMapper {
CustomerLoginLog selectByPrimaryKey(Integer loginId);
CustomerLoginLog selectByPrimaryKey2(Integer loginId);
}
@Test
public void test1(){
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println(sqlSession.getClass());
try {
CustomerLoginLogMapper customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
CustomerLoginLog result = customerLoginLogMapper.selectByPrimaryKey(1);
log.info("{}",result);
CustomerLoginLog result2 = customerLoginLogMapper.selectByPrimaryKey2(1);
log.info("{}",result2);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
第一次查询和第二次查询调用的Statement不是同一个,其他的例如sql、入参、结果都相同。还是生成了2个CacheKey缓存对象。
任何的Insert、Update、Delete都会清空一级缓存的证据
首先MyBatis真正执行sql都是通过Executor执行的,结构如图:
Executor接口里定义了一些公用方法,
BaseExecutor抽象类使用模板方法模式,定义了通用的方法实现。供ReuseExecutor、SimpleExecutor、BatchExecutor调用。
SimpleExecutor是最简单的Executor接口实现,ReuseExecutor和SimpleExecutor的区别是:ReuseExecutor能重用Statement。而BatchExecutor能够批量执行sql。
CachingExecutor使用了装饰模式,给Executor接口实现类封装了二级缓存的功能,稍后专写一篇关于它的介绍。
BaseExecutor的真容如下:
public abstract class BaseExecutor implements Executor {
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
查询方法略
}
可以看到commit、rollback、update方法都会出发clearLocalCache()方法,而insert、delete、update的sql都会走BaseExecutor的update方法。
二级缓存
二级缓存存在于SqlSessionFactory生命周期中,跨SqlSession共享。
二级缓存存在于 SqlSessionFactory 的生命周期中,可以理解为跨sqlSession;缓存是以namespace为单位的,不同namespace下的操作互不影响。
setting参数 cacheEnabled,这个参数是二级缓存的全局开关,默认值是 true,如果把这个参数设置为false, 即使有后面的二级缓存配置,也不会生效;
要开启二级缓存,你需要在你的 SQL 映射文件中添加配置:<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
字面上看就是这样。这个简单语句的效果如下:
1、映射语句文件中的所有 select 语句将会被缓存。
2、映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
3、缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
4、根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
5、缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
6、缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
Tips: 使用二级缓存容易出现脏读, 建议避免使用二级缓存,在业务层使用可控制的缓存代替更好
如何开启二级缓存?
<setting name="cacheEnabled" value="true"/>
mapper文件中:
<cache eviction="FIFO" flushInterval="100000" size="100"
readOnly="true"></cache>
开启了的mapper文件中的:
1、所有insert、update、delete都会刷新缓存。
2、所有select都会被缓存。
缓存回收默认使用LRU算法。
使用二级缓存容易出现脏读,不建议使用,建议使用其他的缓存组件代替。
二级缓存测试
@Test
public void test1(){
try {
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println(sqlSession.getClass());
CustomerLoginLogMapper customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
CustomerLoginLog result = customerLoginLogMapper.selectByPrimaryKey(1);
log.info("{}",result);
CustomerLoginLog result2 = customerLoginLogMapper.selectByPrimaryKey(1);
log.info("{}",result2);
sqlSession.commit();
sqlSession.close();
sqlSession = sqlSessionFactory.openSession();
System.out.println(sqlSession.getClass());
customerLoginLogMapper = sqlSession.getMapper(CustomerLoginLogMapper.class);
result = customerLoginLogMapper.selectByPrimaryKey(1);
log.info("{}",result);
result2 = customerLoginLogMapper.selectByPrimaryKey(1);
log.info("{}",result2);
sqlSession.commit();
sqlSession.close();
} finally {
}
}
开启二级缓存后,查询的时候,先去二级缓存查。
如果二级缓存没查到,再去一级缓存查,一级缓存未查到再查数据库。
如果二级缓存查到了,返回。
第1次查询,命中率0
第2次查询,命中率0/2= 0
第3次查询,命令率1/3 = 0.33
第4次查询,命中率2/4= 0.5