本文共 12548 字,大约阅读时间需要 41 分钟。
MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。
默认情况下,MyBatis只开启一级缓存。一级缓存只是相对于同一个SqlSession而言的,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
一般只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。
前面我们提到,一级缓存只是相对于同一个SqlSession而言的,如果是相同的SQL语句,会优先命中一级缓存,那我们来验证一下,
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); UserDao dao = session1.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao.findUserById("A001").toString()); logger.debug("再次查询用户信息"); logger.info(dao.findUserById("A001").toString()); }
上述代码很简单,在同一个SqlSession内根据相同的id连续查询同一个用户信息,运行结果如下图:
那我们在未开启二级缓存的前提下,使用不同的SqlSession查询信息,代码如下,
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); UserDao dao = session1.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao.findUserById("A001").toString()); SqlSession session2 = sqlSessionFactory.openSession(true); dao = session2.getMapper(UserDao.class); logger.debug("再次查询用户信息"); logger.info(dao.findUserById("A001").toString()); }
运行结果如下图:
上述两次运行结果确实验证了一级缓存的作用范围为SqlSession,那我们如果在SqlSession内对同一份数据有修改操作,运行结果如何呢?代码如下:
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); UserDao dao = session1.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao.findUserById("A001").toString()); logger.debug("新增用户信息"); dao.addUser(new User("A002","Cathy",24)); logger.debug("再次查询用户信息"); logger.info(dao.findUserById("A001").toString()); }
上述代码在两次查询操作之间,新增了一条记录,看一下运行结果:
由此可见,在一次数据库会话中,如果对数据库发生了修改操作,一级缓存会失效。那如果在不同的数据库会话中,情况又有何不一样?代码如下:
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); UserDao dao1 = session1.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao1.findUserById("A001").toString()); logger.debug("新启回话,修改用户信息"); SqlSession session2 = sqlSessionFactory.openSession(true); UserDao dao2 = session2.getMapper(UserDao.class); dao2.updateUserById(new User("A001","Alex",30)); logger.debug("再次查询用户信息"); logger.info(dao1.findUserById("A001").toString()); }
上述代码在连续两次查询之间新启一个数据库会话修改相应数据,运行结果如下:
数据库中的信息已经有所变化,但是第二次的查询结果却没有体现,出现了脏读的现象,所以也从侧面证明了一级缓存只在数据库会话内部共享。
二级缓存是SqlSessionFactory层面的,各个SqlSession共享二级缓存。要正确的使用二级缓存,需完成如下配置的,
在MyBatis的映射XML中配置cache或者 cache-ref 。cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。
配置上述信息后,我们看一下二级缓存真的有那么神奇么?代码如下:
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); UserDao dao1 = session1.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao1.findUserById("A001").toString()); //session1.commit(); SqlSession session2 = sqlSessionFactory.openSession(true); UserDao dao2 = session2.getMapper(UserDao.class); logger.debug("再次查询用户信息"); logger.info(dao2.findUserById("A001").toString()); }
代码功能很简单,使用不同的SqlSession查询条件相同的记录,我们看一下结果:
运行结果好像并没有走到缓存上,这是为何?其实是因为事务未提交,我们加上事务提交的逻辑(上述代码中被注释的那一行),看一下运行结果:
从上图可知,sqlsession2的查询使用了缓存,并且缓存的命中率是0.5。那我们再来看一下update操作是否会刷新该namespace下的二级缓存。代码如下:
public static void main(String[] args) throws IOException{ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml")); SqlSession session1 = sqlSessionFactory.openSession(true); SqlSession session2 = sqlSessionFactory.openSession(true); SqlSession session3 = sqlSessionFactory.openSession(true); UserDao dao1 = session1.getMapper(UserDao.class); UserDao dao2 = session2.getMapper(UserDao.class); UserDao dao3 = session3.getMapper(UserDao.class); logger.debug("开始查询用户信息"); logger.info(dao1.findUserById("A001").toString()); session1.commit(); logger.debug("再次查询用户信息"); logger.info(dao2.findUserById("A001").toString()); logger.debug("修改用户信息"); dao3.updateUserById(new User("A001","Alex",35)); session3.commit(); logger.debug("第三次查询用户信息"); logger.info(dao2.findUserById("A001").toString()); }
上述代码在第三次查询之前修改了数据库信息,看一下运行结果:
可见update操作会刷新该namespace下的二级缓存。
众所周知,SqlSession对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。Executor作为SqlSession的四大对象之一,承担了数据库操作有关的职责。如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,细心的读者可以发现Executor使用了装饰者模式。
BaseExecutor是一个实现了Executor接口的抽象类,具体的操作委托给子类进行执行。看一下BaseExecutor的内部实现:
public abstract class BaseExecutor implements Executor { …… protected ConcurrentLinkedQueuedeferredLoads; protected PerpetualCache localCache; …… }
在BaseExecutor内部有一个名为localCache的成员变量,再看一下PerpetualCache的内部结构:
public class PerpetualCache implements Cache { private String id; private Map
在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,创建Executor代码如下所示:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? this.defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Object executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类 if (this.cacheEnabled) { executor = new CachingExecutor((Executor)executor); } Executor executor = (Executor)this.interceptorChain.pluginAll(executor); return executor; }
SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList,代码如下所示:
publicList selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }public List 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); }public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { …… CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); …… return cacheKey; }
上述代码会根据传入的参数生成CacheKey,将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终构成CacheKey。
由上可知只要两条SQL的下列五个值相同,即可以认为是相同的SQL。Statement Id + Offset + Limmit + Sql + Params
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
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); }
最后会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); }
此外,SqlSession的insert方法和delete方法,都会统一走update的流程,
public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }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); }
由此可见,每次执行update前都会清空localCache。至此,一级缓存的工作流程讲解以及源码分析完毕。
我们再来看二级缓存,之前提到,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类,在委托具体职责给delegate之前,实现了二级缓存的查询和写入功能。进入CahingExecutor的query方法:
publicList query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { //判断是否需要刷新缓存,在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") //MyBatis的CachingExecutor持有了TransactionalCacheManager,即此处代码中的tcm。 //在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。 //CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。 //tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。 List list = (List ) 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; } } return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
如果不调用commit方法的话,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。
public void commit(boolean force) { …… executor.commit(isCommitOrRollbackRequired(force)); dirty = false; ……}//CachingExecutor的commit方法public void commit(boolean required) throws SQLException { delegate.commit(required); tcm.commit();}//TransactionalCacheManager的commit方法public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); }}//TransactionalCache的commit方法public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset();}//TransactionalCache的flushPendingEntries方法private void flushPendingEntries() { for (Map.Entry
在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了flushCacheIfRequired方法,
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } }
在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。