缓存目的
缓存的目的是为了解决速度不匹配问题。内存的访问速度远远超过磁盘的访问速度,所以为了减少磁盘的IO次数所带来的时间消耗,很多场景下都会选择在内存中开辟一块比较小的区域充当缓存。
Mybatis缓存
Mybatis缓存机制是在执行一条查询SQL语句后,会将查询到的结果集以键值对的方式存储起来,等到下一次执行相同的查询SQL时,会先尝试从缓存中读取数据,如果从缓存中读取到了对应的结果,则直接返回;否则查询数据库,并且将查询到的结果放置在缓存中。
Mybatis缓存分为一级缓存和二级缓存:
一级缓存:SQLSession级别缓存,每次MyBatis开启一次和数据库的会话,就会创建出一个SqlSession对象表示一次数据库会话。所以一个一级缓存对应一个SqlSession。
二级缓存:Mapper级别缓存。对于二级缓存的开启条件之一就是需要在XXXMapper.xml
中书写<cache/>
标签。所以一个二级缓存对应一个或多个XXXMapper.xml文件。(通过<cache-ref/>
标签设置多个XXXMapper.xml文件共用同一个二级缓存)
Mybatis一级缓存概述
Mybatis一个SqlSession对象中创建一个本地缓存localCache
,在二级缓存不命中情况下,对于每一次查询,都会尝试去本地缓存中查找当前查询Sql的结果数据,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
Mybatis查询数据顺序:二级缓存 ---> 一级缓存 ---> 数据库
一级缓存是SQLSession级别的,每一个SqlSession会创建一个一级缓存,两个不同的SqlSession执行同一个查询Sql也是查询各自的一级缓存。
一级缓存工作流程
下图是查询一级缓存的流程,重点看一下Cache接口:
-
根据SQLSession执行查询Sql,Executor会根据Sql语句、查询参数等内容创建一个key值,根据key值查询一级缓存;
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
-
根据key值就会从Cache(PerpetualCache)中获取对应的缓存结果;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
-
如果命中,直接将缓存结果返回;
-
如果不命中,则会查询数据库,并且将查询到的数据存放在缓存中,最后返回结果;
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; //占位符 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { //查询数据库 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { //清除占位符 localCache.removeObject(key); } //将从数据库中的数据存放在缓存中 localCache.putObject(key, list); //执行Sql都是采用PreparedStatement,可以忽略下面的if语句块 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } //返回结果 return list; }
Cache接口
public interface Cache {
//Cache的id值,用于唯一标识Cache
String getId();
//向缓存中添加数据,键值对结构的
void putObject(Object key, Object value);
//根据key,获取缓存对应的数据
Object getObject(Object key);
//根据key,删除对应的缓存数据
Object removeObject(Object key);
//清空缓存
void clear();
//缓存大小
int getSize();
//读写锁,不用该方法,Cache接口的实现类也没有实现该方法
ReadWriteLock getReadWriteLock();
}
PerpetualCache
对于Cache接口,Mybatis提供了很多的实现类,其中除了perpetualCache
,其他的实现类都属于对Cache的装饰器类。一级缓存也是通过PerpetualCache
实现的:
进入到PerpetualCache
中,也只是采用HashMap
充当缓存:
public class PerpetualCache implements Cache {
private final String id;
//通过Map充当缓存,key和value都是Object类型
private Map<Object, Object> cache = new HashMap<Object, Object>();
//通过构造方法唯一标识Cache的id
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
//并没有实现获取读写锁方法
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
这里采用HashMap
充当一级缓存,没有限制Map的大小,是因为SqlSession
的生存时间比较短,不会向Map中存放过多的缓存数据,当SqlSession
消亡时,对应的一级缓存也会被清空;另外执行insert|update|delete
修改操作时也会清空一级缓存。
Cache的装饰器
在Cache
接口的实现类中,除了PerpetualCache
类,其他的实现类都属于装饰器类:
-
FifoCache
:先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉 -
LruCache
:最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近最少被使用的缓存记录清除掉 -
LoggingCache
:为Cache增加日志功能,调用getObject(Object key)
方法时打印日志 -
ScheduledCache
:指定的某一个时间间隔将Cache缓存中的数据清空 -
SerializedCache
:添加序列化功能,在调用getObject(Object key)和putObject(Object key, Object value)
方法时对key和value进行序列化和反序列化功能。Mybatis与Redis整合时,Redis重写了Cache接口中的方法,其中putObject和getObject就对key和value进行了序列化和反序列化操作。虽然Redis中的key一般都是String类型,但是Mybatis缓存中的key是一个CacheKey对象,并不是一个字符串。而且对key和value进行序列化,也可以节省缓存空间。
-
SynchronizedCache
:为Cache中的方法加上了Synchronized
关键字,实现了不同线程对Cache的同步 -
TransactionalCache
:针对二级缓存,只有在事务提交后,才会将结果存放在二级缓存中。
这些装饰器可以对指定的Cache进行增强,例如:
//核心Cache
PerpetualCache perpetualCache = new PerpetualCache("123");
//通过装饰器模式,为Cache增加功能
//增加日志功能
LoggingCache loggingCache = new LoggingCache(perpetualCache);
//增加LRU算法
LruCache lruCache = new LruCache(loggingCache);