当前位置: 代码迷 >> 综合 >> Mybatis系列之一级/二级缓存
  详细解决方案

Mybatis系列之一级/二级缓存

热度:37   发布时间:2023-12-17 09:02:31.0

缓存的概念大家应该都知道,所以,这里我们基于ORM框架Mybatis,来讲解一下他自带的缓存


一级缓存介绍

一级缓存是Mybatis默认开启的一个缓存机制,它跟二级缓存的区别就在于作用域大小不同,一级缓存的作用域相对比二级缓存要小,它的作用域只是基于SqlSession的(SqlSession主要是啥,后面再补充),缓存的存在主要是为了便利我们的数据查询,废话不多说,接下来我们来体验一下


一级缓存代码体验

源码

 //根据 sqlSessionFactory 产? sessionSqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);//第?次查询,发出sql语句,并将查询出来的结果放进缓存中User u1 = userMapper.selectUserByUserId(1);System.out.println(u1);//第?次查询,由于是同?个sqlSession对象,所以会在缓存中查询结果//有两种处理逻辑,缓存如果有,则直接从缓存中取出来,不会走数据库,反之直接走数据库User u2 = userMapper.selectUserByUserId(1);System.out.println(u2);sqlSession.close();

观察日志

然后我们对user表在进行两次查询,和上面代码的区别就在于,在两次查询中间进行以此修改,再观察一下日志打印情况,先上源码:

 //根据 sqlSessionFactory 产? sessionSqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);//第?次查询,发出sql语句,并将查询的结果放?缓存中User u1 = userMapper.selectUserByUserId( 1 );System.out.println(u1);//第?步进?了?次更新操作,sqlSession.commit()u1.setSex("?");userMapper.updateUserByUserId(u1);sqlSession.commit();//第?次查询,由于是同?个sqlSession,且上面的修改操作触发了sqlSession.commit(),//所以在commit之后会清空缓存信息//则此次查询也会发出sql语句User u2 = userMapper.selectUserByUserId(1);System.out.println(u2);sqlSession.close();

日志打印:

这两次源码进行查询的代码和日志对比就发现了:如果执行了新增、更新或者删除,sqlSession就会commit,默认会清空SqlSession中的一级缓存,这样做的目的,大家了解缓存的都清楚,这么做就是为了防止读取的数据不是最新的,避免了脏读


源码剖析 

1、查看SqlSession类中的方法

public interface SqlSession extends Closeable {<T> T selectOne(String statement);<T> T selectOne(String statement, Object parameter);<E> List<E> selectList(String statement);<E> List<E> selectList(String statement, Object parameter);<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);<K, V> Map<K, V> selectMap(String statement, String mapKey);<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);<T> Cursor<T> selectCursor(String statement);<T> Cursor<T> selectCursor(String statement, Object parameter);<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);void select(String statement, Object parameter, ResultHandler handler);void select(String statement, ResultHandler handler);void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);int insert(String statement);int insert(String statement, Object parameter);int update(String statement);int update(String statement, Object parameter);int delete(String statement);int delete(String statement, Object parameter);void commit();void commit(boolean force);void rollback();void rollback(boolean force);List<BatchResult> flushStatements();@Overridevoid close();void clearCache();Configuration getConfiguration();<T> T getMapper(Class<T> type);Connection getConnection();
}

乍看一下这些方法,能找到的唯一跟缓存有关系的也就是倒数第四个方法clearCache,看方法名就是清理缓存,既然只有这个方法跟缓存有关系,那我们就从它开始分析,它的父类的调用流程这里就不展示了

只需要知道有一个类PerpetualCache,看一下源码

public class PerpetualCache implements Cache {private String id;private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic ReadWriteLock getReadWriteLock() {return null;}@Overridepublic 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());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}return getId().hashCode();}}

最开始会声明一个HashMap的全局变量,然后还有一个clear方法,看进去其实它调用的是上面HashMap对象的clear,清空一级缓存其实就是清空Map数据,一级缓存其实就是本地存放的一个Map对象

那清空缓存ok了,创建缓存是谁负责在什么时候创建的呢?其他拐弯抹角的话不说了,我们直接看执行器Executor

public interface Executor {ResultHandler NO_RESULT_HANDLER = null;int update(MappedStatement ms, Object parameter) throws SQLException;<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;List<BatchResult> flushStatements() throws SQLException;void commit(boolean required) throws SQLException;void rollback(boolean required) throws SQLException;CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);boolean isCached(MappedStatement ms, CacheKey key);void clearLocalCache();void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);Transaction getTransaction();void close(boolean forceRollback);boolean isClosed();void setExecutorWrapper(Executor executor);}

上面的类中,有一个createCacheKey,看名字就知道是创建缓存,我们点进去看一下这个方法的实现

@Overridepublic CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();//封装mapper.xml中我们写标签中的namespace+id的值,<select id="">cacheKey.update(ms.getId());//0cacheKey.update(Integer.valueOf(rowBounds.getOffset()));cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//我们自己写的sql语句cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}//拼接sql中的参数cacheKey.update(value);}}if (configuration.getEnvironment() != null) {// issue #176cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}

这个value最终会是什么呢,主要是五个参数,分别是上面源码注释标记的那几个参数,还有数据库连接的配置,最后还有一个if,其中的update入参是configuration.getEnvironment().getId(),他就是数据库驱动配置中环境标签的id值,具体如下

<environments default="development"> <environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/></dataSource></environment>
</environments>

到目前为止,缓存是创建完了,我们看一下他是怎么用于查询的,还是Executor类,他里面有一个query方法,我们点进去看一下

  @SuppressWarnings("unchecked")@Overridepublic <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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}

我们看一下else中直接走数据库的方法实现

  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);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}

整体的逻辑就是:如果走数据库查询,会先创建缓存key,把数据库返回的查询结果缓存到对应的key中


到此,一级缓存的介绍、体验和源码剖析就结束啦 


二级缓存介绍

二级缓存和一级缓存大多数实现逻辑基本一样,都是第一次查询先把数据放缓存,第二次查询会先走缓存,缓存命中就从缓存拿数据,否则就直接走数据库拿数据,然后再缓存起来,唯一的区别在上面的一级缓存中也提到了:作用域不同,一级缓存的作用域是SqlSession,而二级缓存择时namespace,就是写sql的那个xml文件中的namespace,作用域在这的话,就说明多个SqlSession可以共享一个mapper中的二级缓存,并且如果两个mapper的namespace相同的话,不管他是几个xml文件,只要namespace相同,那么他们缓存就可以共享,创建,清空,查询都是同一个


开启二级缓存

一级缓存是默认开启的,二级缓存是默认关闭的,开启二级缓存有好几种方式

1、在配置xml中添加对应开启的配置

<!--开启?级缓存-->
<settings><setting name="cacheEnabled" value="true"/>
</settings>

2、在对应的mapper.xml中添加标签

<!--开启?级缓存-->
<cache></cache>

注意事项:开启二级缓存,需要对缓存的实体类进行序列化,实现Serializable接口,因为二级缓存数据可能存在内存中,还有可能会在硬盘里,所以我们要获取这个环境的话,就需要反序列化

参数讲解 

mybatis提供了userCache和flushCache等配置项

userCache:设置是否禁用二级缓存,默认是false,如果设置为true,每次查询不会再走缓存那一层,会直接走数据库

flushCache:默认是true,即刷新缓存,一般情况默认即可


缓存问题 

在分布式架构下,很少见到有用到二级缓存的,就哪怕是用了,两个或多个机器之间的二级缓存肯定是不共享的,但是有人会说:namespace不都是一样的嘛,那是没错,但是每个缓存所属的服务进程不一样啊,所以就造成了二级缓存不适用于分布式环境,那怎么解决呢?解决的入口就在于:怎么让Mybatis获取到唯一的缓存,且能够让所有的分布式机器共享这个缓存即可

解决方案:不适用mybatis自带的缓存机制,我们可以利用Mybatis自身拓展的机制,自己实现一个基于Redis的二级缓存即可 

  相关解决方案