症状:使用自定义MyBatis分页插件,只有分页参数不同的方法在短时间内使用不同分页参数查询出来的结果相同。
病因:自定义MyBatis插件拦截目标为StatementHandler,而在同一个SqlSession中,在StatementHandler.prepare之前,MyBatis的已经命中了一级缓存,所以直接返回了缓存中的内容。
治疗方案:重写自定义MyBatis分页插件使之拦截Executor,或增加新的插件,使之拦截Executor清除一级缓存。
这是我最近在一个项目中排查的一个问题,在这里记录一下以备后查。
首先这个项目并没有使用比较流行的PageHelper插件,而是自己实现了一个,由于不是本人的代码,就不贴出来了。网上搜一下的话也有很多类似的,主要的实现原理是使用MyBatis提供的@Intercepts拦截StatementHandler类的prepare方法,通过反射获取到MappedStatement和BoundSql。如果执行的是约定的分页方法(MappedStatement的id带有Page后缀),那么就把BoundSql中的sql字段更改为带有分页功能的sql。如果要使用分页查询,那么分页方法的参数需要带有分页参数,同时分页方法名需要带有约定的Page后缀。乍一看没有什么问题,但是在真正使用的时候,由于MyBatis一级缓存的存在,同一个SqlSession中后续的分页方法生成了相同的CacheKey,导致直接返回了缓存中的内容。这里主要的问题是拦截的时机,拦截发生在MyBatis决定是否要使用缓存之后!!
关于MyBatis的缓存机制,网上有很多资料讲的很详细,不清楚的可以先了解了解。总的来说,在同一个SqlSession中,执行同一条sql MyBatis会直接返回缓存。不过对于我们碰到的问题,一开始我是带着疑虑的,两次执行的方法明明分页参数是不同的,怎么会命中同一个缓存呢?带着这个疑问我们debug MyBatis的源码,查看其生成CacheKey的逻辑。首先当执行一条sql的时候,会进到4参数CachingExecutor.query,(网上一查,这个类是用来处理二级缓存的,明明没用二级缓存,为何会用到这个类?这里先留个悬念,后面再说):
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看这里缓存key的生成方法,实际上是调用了delegate的createCacheKey方法。(那这个delegate又是个啥?我们待会一起说):
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
查看createCacheKey方法的实现类,就只有CachingExecutor和BaseExecutor,所以这里是使用了BaseExecutor.createCacheKey生成缓存key:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for