缓存的概念大家应该都知道,所以,这里我们基于ORM框架Mybatis,来讲解一下他自带的缓存
一级缓存是Mybatis默认开启的一个缓存机制,它跟二级缓存的区别就在于作用域大小不同,一级缓存的作用域相对比二级缓存要小,它的作用域只是基于SqlSession的(SqlSession主要是啥,后面再补充),缓存的存在主要是为了便利我们的数据查询,废话不多说,接下来我们来体验一下
//根据 sqlSessionFactory 产⽣ session
SqlSession 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 产⽣ session
SqlSession 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 selectOne(String statement); T selectOne(String statement, Object parameter); List selectList(String statement); List selectList(String statement, Object parameter); List selectList(String statement, Object parameter, RowBounds rowBounds); Map selectMap(String statement, String mapKey); Map selectMap(String statement, Object parameter, String mapKey); Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds); Cursor selectCursor(String statement); Cursor selectCursor(String statement, Object parameter); Cursor 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 flushStatements(); @Override void close(); void clearCache(); Configuration getConfiguration(); T getMapper(Class type); Connection getConnection(); } 乍看一下这些方法,能找到的唯一跟缓存有关系的也就是倒数第四个方法clearCache,看方法名就是清理缓存,既然只有这个方法跟缓存有关系,那我们就从它开始分析,它的父类的调用流程这里就不展示了
只需要知道有一个类PerpetualCache,看一下源码
public class PerpetualCache implements Cache { private String id; private Map
最开始会声明一个HashMap的全局变量,然后还有一个clear方法,看进去其实它调用的是上面HashMap对象的clear,清空一级缓存其实就是清空Map数据,一级缓存其实就是本地存放的一个Map对象
那清空缓存ok了,创建缓存是谁负责在什么时候创建的呢?其他拐弯抹角的话不说了,我们直接看执行器Executor
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement ms, Object parameter) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; List 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,看名字就知道是创建缓存,我们点进去看一下这个方法的实现
@Override public 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的值,
这个value最终会是什么呢,主要是五个参数,分别是上面源码注释标记的那几个参数,还有数据库连接的配置,最后还有一个if,其中的update入参是configuration.getEnvironment().getId(),他就是数据库驱动配置中环境标签的id值,具体如下
到目前为止,缓存是创建完了,我们看一下他是怎么用于查询的,还是Executor类,他里面有一个query方法,我们点进去看一下
@SuppressWarnings("unchecked") @Override public
List 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 list; try { queryStack++; //查询缓存 list = resultHandler == null ? (List ) 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; } 我们看一下else中直接走数据库的方法实现
private
List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List 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中添加对应开启的配置
2、在对应的mapper.xml中添加标签
注意事项:开启二级缓存,需要对缓存的实体类进行序列化,实现Serializable接口,因为二级缓存数据可能存在内存中,还有可能会在硬盘里,所以我们要获取这个环境的话,就需要反序列化
mybatis提供了userCache和flushCache等配置项
userCache:设置是否禁用二级缓存,默认是false,如果设置为true,每次查询不会再走缓存那一层,会直接走数据库
flushCache:默认是true,即刷新缓存,一般情况默认即可
在分布式架构下,很少见到有用到二级缓存的,就哪怕是用了,两个或多个机器之间的二级缓存肯定是不共享的,但是有人会说:namespace不都是一样的嘛,那是没错,但是每个缓存所属的服务进程不一样啊,所以就造成了二级缓存不适用于分布式环境,那怎么解决呢?解决的入口就在于:怎么让Mybatis获取到唯一的缓存,且能够让所有的分布式机器共享这个缓存即可
解决方案:不适用mybatis自带的缓存机制,我们可以利用Mybatis自身拓展的机制,自己实现一个基于Redis的二级缓存即可