聊聊MyBatis缓存机制(一)
前言
Mybatis是常见的Java数据库访问层框架,虽然我们在日常的开发中一般都是使用Mybatis Plus,但是从官网信息可以知道,其实Mybatis Plus只是让开发者在使用上更简单,并没有改动核心原理。在日常工作中,大多数开发者都是使用的默认缓存配置,但是Mybatis缓存机制有一些不足之处,在使用过程中容易引起脏数据,存在一些潜在的隐患。带着个人的兴趣,希望从应用及源码的角度为读者梳理MyBatis缓存机制。
一级缓存
一级缓存介绍
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示。
一级缓存配置
一级缓存的配置值有两个选项,SESSION或者STATEMENT,默认是SESSION级别,Configuration类中可以进行设置
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(“localCacheScope”, “SESSION”)));
public enum LocalCacheScope {
SESSION,STATEMENT
}
一级缓存的使用
一级缓存说的是如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
我准备了一个SkyworthUser的实体类,定义UserInfoMapper接口继承BaseMapper,这样无需编写mapper.xml文件,即可获取CRUD功能。
下面是Service实现类的代码
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public SkyworthUser getUserInfoById(String id){
return userInfoMapper.selectById(id);
}
}
根据理解我这个方法重复查询两次,那必须是会使用所谓的一级缓存的,所以进行尝试调用
AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext(StartConfig.class);
UserService bean = annotationConfigApplicationContext.getBean(UserService.class);
SkyworthUser userInfoById = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
SkyworthUser userInfoById2 = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
System.out.println(userInfoById.toString());
System.out.println(userInfoById2.toString());
为了证明我的猜想,我在userInfoById2这一行打个断点,执行完第一个之后,我手动去修改一下数据库的某个属性值,看下userInfoById2输出是不是和userInfoById一致。
userInfoById:输出内容如下
10:40:40.494 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55a609dd]
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a
10
10
10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=0)
修改数据库的isFirstLogin=1
userInfoById2:输出内容如下
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a
10
10
10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)
怎么回事?说好的一级缓存默认开启呢?明明什么配置都没用修改,直接调用就这样了?
带着问题开始源码的查看,找到具体的原因。
一级缓存源码分析
分析源码从两个地方出发,首先是@MapperScan注解,另外一个是MybatisPlusAutoConfiguration.class类。博主下载的是mybatis plus的原代码,所以会有MybatisPlusAutoConfiguration这个类。
首先分析@MapperScan注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
注解中@Import注解引入了MapperScannerRegistrar.class,MapperScannerRegistrar 实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar在运行时动态地注册 BeanDefinition,查看源码后,可以看出其实就是为了注册MapperScannerConfigurer类到容器中,为了方便查看,只截出部分原代码,如下所示:
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
............
}
此时spring需要初始化MapperScannerConfigurer这个类,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,InitializingBean等接口,InitializingBean在初始化完成后会执行afterPropertiesSet()方法,所以一般需要关注下这个接口,但是MapperScannerConfigurer好像没有做什么处理。
接下来就是BeanDefinitionRegistryPostProcessor接口,它是Spring中的一个扩展点,用于BeanDefinition加载到Spring容器前或之后对其进行修改或者添加,BeanDefinitionRegistryPostProcessor 执行的时机是在BeanDefinition加载和解析完成之后,Bean实例化和依赖注入之前,也就是在BeanDefinition的预处理阶段。
在 BeanDefinition 的预处理阶段,Spring 容器会调用所有实现了 BeanDefinitionRegistryPostProcessor 接口的类的 postProcessBeanDefinitionRegistry() 方法和 postProcessBeanFactory() 方法,用于对 BeanDefinition 进行修改或添加。其中,postProcessBeanDefinitionRegistry() 方法用于在 BeanDefinition 加载到 Spring 容器之前对其进行修改或添加,而 postProcessBeanFactory() 方法用于在 BeanDefinition 加载到 Spring 容器之后对其进行修改或添加。
postProcessBeanFactory方法是个空方法,所以不管他,只关注postProcessBeanDefinitionRegistry方法,如下所示:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
上面的代码就是创建了ClassPathMapperScanner类,执行了scan方法,从字面意思理解就是扫描,估计就是Mapper接口的扫描了。
实际上执行是ClassPathBeanDefinitionScanner类中的scan方法。
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
scan调用了本类中的doScan方法,但是这个doScan被子类ClassPathMapperScanner重写了,所以直接看这个方法,代码如下:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
其实就是调用父类的doScan方法,但是自己在后续做了一些处理,super.doScan(basePackages)获取的是一个BeanDefinitionHolder的集合,BeanDefinitionHolder包含了beanDefinition和beanName,如果集合不为空,那么processBeanDefinitions(beanDefinitions)应该就是将接口按照一定规则生成代理对象,这样就能把接口初始化,猜测大概思想和FactoryBean差不多。
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
definition.setBeanClass(this.mapperFactoryBeanClass);
processBeanDefinitions(beanDefinitions);的简化代码如上所示,很明显,我们的UserInfoMapper接口在初始化的时候,会用到MapperFactoryBean来生成代理对象,完美的预测成功,因为MapperFactoryBean实现了FactoryBean接口,在获取的时候会调用getObject方法来生成代理对象。
MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean,我们了解FactoryBean,但是继承了SqlSessionDaoSupport,这个类需要查看下具体是做什么的,查看发现SqlSessionDaoSupport 是个抽象类,继承了DaoSupport,DaoSupport肯定也是一个抽象类,但是它实现了InitializingBean。
所以我们查看下DaoSupport的afterPropertiesSet()方法,发现有个checkDaoConfig方法和initDao方法,主要看这个checkDaoConfig方法,因为initDao是个空的方法并且没有被重写过;通过类的继承关系我们可以发现MapperFactoryBean是重写了checkDaoConfig方法的,所以直接查看MapperFactoryBean的checkDaoConfig方法
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
这段代码有点抽象,因为我们不知道configuration.addMapper(this.mapperInterface);具体是干嘛的,并且Configuration这个类是什么作用,类里面什么注释都没有,吐槽下阿里的大佬们…不理解就先跳过,我们只知道把这个UserInfoMapper接口放到这个Configuration类,直接点进去查看addMapper方法做了什么事情
点进去就到了Configuration中的addMapper方法,方法内是 mapperRegistry.addMapper(type)方法,继续往下走,就到了MapperRegistry的addMapper方法,这个方法被MybatisMapperRegistry重写了,那么就查看里面的逻辑,如下图所示:
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// TODO 这里就不抛异常了
// throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
当看到第一个判断逻辑的时候,我几乎确定了我之前的流程是没问题的,所以加油往下,parser.parse()方法很特别,感觉是解析什么东西,有没有可能是UserInfoMapper命名空间的xml解析,带着猜测继续往下走,parse进入MybatisMapperAnnotationBuilder 的parse方法,代码如下:
public void parse() {
String resource = type.toString();
// 避免重复加载
if (!configuration.isResourceLoaded(resource)) {
// 如果没有加载过xml文件,就重新加载,此处一般是加载好了,具体的加载地方在sqlSessionFactoryBean,
// 感兴趣的可以先自己看看,后续如果有时间可能把详细讲下mybatis的运行流程。
loadXmlResource();
// 避免重复加载
configuration.addLoadedResource(resource);
String mapperName = type.getName();
// 设置命名空间
assistant.setCurrentNamespace(mapperName);
// 解析二级缓存
parseCache();
parseCacheRef();
InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// TODO 加入 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
// 解析方法上的注解方法
parseStatement(method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
try {
// https://github.com/baomidou/mybatis-plus/issues/3038
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
parsePendingMethods();
}
到这里我们大概知道了我们的UserInfoMapper接口会生成MapperFactoryBean类,在初始化完这个类之后会对这个类进行特定的处理,把信息放到Configuration这个类里面去,方便后续直接使用,设置了命名空间、缓存信息等。
但是我们要创建UserInfoMapper类的代理对象的时候会执行MapperFactoryBean.getObject方法。最后的逻辑会进入到MybatisMapperRegistry的getMapper方法,代码如下:
@Override
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
// fix https://github.com/baomidou/mybatis-plus/issues/4247
MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
.filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
.orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
在这个代码中newInstance就是生成代理对象,核心代码如下:
public T newInstance(SqlSession sqlSession) {
final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
由此可以判断,如果我们执行userinfoMapper的方法时,会进入到MybatisMapperProxy类的invoke方法。到此我们就可以开始分析为什么我们一级缓存没有生效的原因。
问题排查
回忆一下,我们这样调用了两次,在第二次执行前修改数据库字段值,发现没有走缓存,两次查询不一致。因为博主使用的mybatis Plus+spring的源码,没有用Springboot,所以这样调用一下,在service里调用两次也是一样的效果,测试过。
AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext(StartConfig.class);
UserService bean = annotationConfigApplicationContext.getBean(UserService.class);
SkyworthUser userInfoById = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
SkyworthUser userInfoById2 = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
System.out.println(userInfoById.toString());
System.out.println(userInfoById2.toString());
1、bean.getUserInfoById方法最终进入到MybatisMapperProxy的invoke方法中
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// cachedInvoker会组装PlainMethodInvoker或者DefaultMethodInvoker
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
2、cachedInvoker会组成PlainMethodInvoker,所以cachedInvoker(method).invoke()方法会进入到PlainMethodInvoker的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
3、执行execute方法进入MybatisMapperMethod的execute方法,我们的getUserInfoById是使用的selectOne
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
// TODO 这里下面改了
if (IPage.class.isAssignableFrom(method.getReturnType())) {
result = executeForIPage(sqlSession, args);
// TODO 这里上面改了
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
4、可以断点一路跟下来,会到DefaultSqlSession的selectList方法
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
5、从configuration中获取到ms,这个ms是之前的MapperFactoryBean初始化之后组装放入configuration中的,后续mybatis会使用Executor来执行sql,会进入到BaseExecutor的query方法,核心代码如下:
@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;
}
6、 list = resultHandler == null ? (List) localCache.getObject(key) : null; localCache就是我们的一级缓存,我们打断点,查看两次获取的是不是一样的,最后发现每次获取到的都是空,但是key都是一样的。这个时候我开始怀疑可能不是一个sqlSession,所以我在MybatisMapperProxy中的invode方法中打断点,查看这个值。
确实,两次的sqlSessionProxy是不一样的,那么意思我两次查询用的是两个sqlSession,所以导致每次从缓存中获取为空。
所以我们需要返回回去查看MapperFactoryBean的getObject方法中的getSqlSession方法获取sqlsession的逻辑。
7、查看sqlsessionTemplate的构造函数
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
8、发现sqlSessionProxy 两次都不一致,所以查看这个代理对象的invoke方法
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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);
}
}
}
}
9、第一句就是getSqlsession,继续往内部去查,进入到SqlSessionUtils的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);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
10、查看里面的逻辑,发现sessionHolder方法里面有个奇怪的东西,让我猜测是不是和事务有关。
private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
SqlSession session = null;
if (holder != null && holder.isSynchronizedWithTransaction()) {
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException(
"Cannot change the ExecutorType when there is an existing transaction");
}
holder.requested();
LOGGER.debug(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
session = holder.getSqlSession();
}
return session;
}
11、这里面确实判断了是不是属于同一个事务,如果是在事务中,直接从holder中获取,不然就是session为null,就导致了博主这边连续调用两次,但是缓存并没有生效的原因。
复测问题
我写个方法,加上事务,在return这里打断点,isFirstLogin字段一开始为1,然后执行到之后去数据库修改为0,查看两次的值
@Transactional
public SkyworthUser getUserInfoById(String id){
// 执行第一次
userInfoMapper.selectById(id);
// 执行第二次
return userInfoMapper.selectById(id);
}
第一次:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a
10
10
10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)
第二次:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a
10
10
10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)
并且在list = resultHandler == null ? (List) localCache.getObject(key) : null;中是直接从localCache获取到list直接返回的,这个问题就解决了。
总结
- Mybatis一级缓存的生命周期和Sqlsession一致,一级缓存就是perpetualCache这个类
- Mybatis一级缓存内部设计较为简单,只是一个没有容量的HashMap,在缓存功能上有所欠缺
- Mybatis一级缓存的最大范围是sqlSession内部,有多个sqlsession或者分布式环境下,数据库写操作会引起脏数据,建议设定缓存级别是statement,默认是session级别。
参考资料
聊聊Mybatis缓存机制