缓存是互联网系统常常用到的,其特点是将数据保存在内存中。目前流行的缓存服务器有MongoDB、Redis、Ehcache等。缓存是在计算机内存上保存的数据,在读取的时候无需再从磁盘读入,因此具备快速读取和使用的特点,如果缓存命中率高,那么可以极大地提高系统的性能。如果缓存命中率很低,那么缓存就不存在使用的意义了,所以使用缓存的关键在于存储内容访问的命中率。
一、mybatis缓存案例
1.1 一级缓存
MyBatis对缓存提供支持,但是在没有配置的默认的情况下,它只开启一级缓存(一级缓存只是相对于同一个SqlSession而言)。
所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,因为使用SqlSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,执行增删改操作或者手动清空了缓存,并且缓存没超时的情况下,SqlSession都只会取出当前缓存的数据,而不会再次发送SQL到数据库
SqlSession session = sqlSessionFactory.openSession();
PersonMapper mapper = session.getMapper(PersonMapper.class);
mapper.getOne("1");//执行一次数据库查询
mapper.getOne("1");//从缓存拿数据
session.close();
执行结果如下:
DEBUG [main] - ==> Preparing: select * from person where id = ?
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <== Total: 1
但是如果你使用的是不同的SqlSesion对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper、参数和方法,它还是会再次发送SQL到数据库去执行,返回结果。
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
mapper1.getOne("1");//执行一次数据库查询
session1.close();
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper mapper2 = session2.getMapper(PersonMapper.class);
mapper2.getOne("1");//执行一次数据库查询
session2.close();
执行结果如下:
DEBUG [main] - ==> Preparing: select * from person where id = ?
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: select * from person where id = ?
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <== Total: 1
从上面的执行结果我们发现第一次查询和第二次查询一样,那我们可不可以使第二次查询从缓存中取出呢? 为了克服这个问题,我们往往需要配置二级缓存,使得缓存在SqlSessionFactory层面上能够提供给各个SqlSession对象共享。
1.2 二级缓存
mybatis二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的,也就是要求实现Serializable接口,配置的方法很简单,只需要在映射XML文件配置就可以开启缓存了。
- 在MyBatis的配置文件中加入
<settings>
<!--开启二级缓存 这个可以不加,因为cacheEnabled默认值就为true,但是防止MyBatis将来改变其默认值为false(我们升级了版本),还是加上好的。-->
<setting name="cacheEnabled" value="true"/>
</settings>
- 在需要开启二级缓存的mapper.xml中加入caceh标签**
<cache/>
- 让使用二级缓存的POJO类实现Serializable接口
SqlSession session1 = sqlSessionFactory.openSession();
PersonMapper mapper1 = session1.getMapper(PersonMapper.class);
mapper1.getOne("1");
session1.commit();//必须有这个,否则二级缓存无作用
session1.close();
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper mapper2 = session2.getMapper(PersonMapper.class);
mapper2.getOne("1");
session2.commit();
session2.close();
执行结果如下:
DEBUG [main] - Cache Hit Ratio [com.clyu.mapper.PersonMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from person where id = ?
DEBUG [main] - ==> Parameters: 1(String)
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [com.clyu.mapper.PersonMapper]: 0.5
1.2 二级缓存策略配置
从上面我们可以看出,二级缓存配置我们可以通过配置cache标签来实现
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
eviction属性表示可用的清除策略,其值如下:
LRU
– 最近最少使用:移除最长时间不被使用的对象。这个是默认清除策略
FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。
SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval属性表示刷新间隔
属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size表示引用数目
属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly表示缓存是否是只读
属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
type自定义缓存策略
除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。
<cache type="com.domain.something.MyCustomCache"/>
这个示例展示了如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。
定义单个sql语句的缓存策略
请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache 将其设置为 fasle 后,本条查询语句会禁用二级缓存:默认true
insert,update,delete 是没有useCache属性的
1.3 二级缓存的脏读
二级缓存脏读产生原因
Mybatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都有自己的二级缓存,不同的mapper的二级缓存互不影响。这样的设计一不注意就会引起脏读,从而导致数据一致性的问题。引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。
解决方案
使用cache-ref
,实现多个命名空间中共享相同的缓存配置和实例。使用 cache-ref 元素来引用另一个缓存。
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
脏读的避免
mapper中的操作以单表操作为主,避免在关联操作中使用mapper
在关联操作的mapper中使用参照缓存
二 缓存源码分析
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//缓存key值生成器CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从MappedStatement对象中获取其,二级缓存cache。这样说明了二级缓存是namespace级别的
Cache cache = ms.getCache();
if (cache != null) {
//判断要不要刷新缓存
flushCacheIfRequired(ms);
//是否使用二级缓存,默认true
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从事物管理器获取缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
//如果没有缓存,就从数据库查
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查到后,在放入二级缓存池,
//注意:这里还没有真正放到二级缓存池,只是放到临时的二级缓存池,只有当查询提交后,才会放到真正的二级缓存池
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//进入BaseExecutor的query方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
}
/*****************************分界线********************************************/
public abstract class BaseExecutor implements Executor {
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//${}是在这里解析的
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//MappedStatement是全局共享的
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//从一级缓存中拿
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//对于储存过程有输出资源的处理
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//,一级缓存没有。查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
}
结论:如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库
三 spring整合mybatis后,mybatis一级缓存失效的原因
spring对mybatis的sqlsession的使用是由template控制的,sqlSessionTemplate又被spring当作resource放在当前线程的上下文里
同一线程里面两次查询同一数据所使用的sqlsession是不相同的,所以,给人的印象就是结合spring后,mybatis的一级缓存失效了。
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 每次执行前都创建一个新的sqlSession
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 执行方法
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
因为每次都进行创建,所以就用不上sqlSession的缓存了.
对于开启了事务为什么可以用上呢, 跟入getSqlSession方法
如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 首先从SqlSessionHolder里取出session
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
在里面维护了个SqlSessionHolder,关联了事务与session,如果存在则直接取出,否则则新建个session,所以在有事务的里,每个session都是同一个,故能用上缓存了
四 Mybatis缓存架构
他底层是利用装饰者模式设计的,这样设计的好处是:我们这样灵活的装配缓存的功能
Mybatis缓存接口是:org.apache.ibatis.cache
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {return null;}
}
其有如下类,注意他们的子类都是平级的
BlockingCache
FifoCache
LoggingCache://打印日志
LruCache //
PerpetualCache: //他是真正的缓存类,底层有个map
ScheduledCache
SerializedCache://就是用来序列化数据的
SoftCache
SynchronizedCache ://这个类就是保证线程安全。所以他的方法基本上是加上synchronized来保证线程安全的
TransactionalCache
WeakCache
4.1 LruCache
如果数据最近被访问过,那么将来被访问的几率也更高,同理:如果数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小
public class LruCache implements Cache {
//原缓存对象
private final Cache delegate;
private Map<Object, Object> keyMap;
//最长时间未使用
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
keyMap.get(key); //touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
4.2 TransactionalCache
缓存穿透:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大
缓存击穿:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//真正的缓存
private final Cache delegate;
//fasle 事物还没有提交
private boolean clearOnCommit;
//所有待提交的缓存
private final Map<Object, Object> entriesToAddOnCommit;
//未命中的缓存集合,防止缓存穿透。防止一直访问一个为null key导致一直查询数据库
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public Object getObject(Object key) {
// 根据key从缓存中获取数据
Object object = delegate.getObject(key);
if (object == null) {
//数据为null,就把这个key放到未命中的缓存集合中
entriesMissedInCache.add(key);
}//如果提交了,返回null
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
//如果事物已经提交了
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//put到真实缓存
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//也把未命中的put进去
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
//清除
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}