레벨 1 캐시와 레벨 2 캐시
MyBatis는 데이터 캐시를 레벨 1 캐시와 레벨 2 캐시로 구분되는 2레벨 구조로 설계합니다.
레벨 1 캐시는 다음 위치에 있는 세션 레벨 캐시입니다. 세션의 SqlSession 개체를 로컬 캐시라고도 합니다. 1차 캐시는 MyBatis 내부적으로 구현된 기능으로, 사용자가 구성할 수 없으며 기본적으로 자동으로 지원되며 사용자가 이를 커스터마이즈할 수 있는 권한은 없습니다.(단, 절대적인 것은 아닙니다. 플러그를 개발하여 수정할 수 있습니다.) -ins);
두 번째 수준 캐시는 애플리케이션의 선언 주기와 마찬가지로 긴 수명 주기를 갖습니다. 즉, 해당 범위는 애플리케이션 애플리케이션 전체입니다.
MyBatis의 1단계 캐시와 2단계 캐시의 구성은 아래와 같습니다.
1단계 캐시의 작동 메커니즘:
첫 번째 수준 캐시는 세션 세션 수준입니다. 일반적으로 SqlSession 개체는 Executor 개체를 사용하여 세션 작업을 완료하고 Executor 개체는 캐시 캐시를 유지하여 쿼리 성능을 향상시킵니다.
두 번째 수준 캐시의 작동 메커니즘:
위에서 언급했듯이 SqlSession 개체는 Executor 개체를 사용하여 세션 작업을 완료합니다. MyBatis의 두 번째 수준 캐시 메커니즘의 핵심은 소란을 피우는 것입니다. 이 Executor 객체에 대해. 사용자가 "cacheEnabled=true"를 구성하면 MyBatis가 SqlSession 개체에 대한 Executor 개체를 생성할 때 Executor 개체에 데코레이터인 CachingExecutor를 추가합니다. 이때 SqlSession은 작업 요청을 완료하기 위해 CachingExecutor 개체를 사용합니다. 쿼리 요청의 경우 CachingExecutor는 쿼리 요청에 애플리케이션 수준 보조 캐시에 캐시된 결과가 있는지 먼저 확인하고, 캐시된 결과가 없으면 캐시된 결과를 직접 반환합니다. 쿼리를 완료하기 위해 실제 Executor 객체에 전달됩니다. 작업 후 CachingExecutor는 실제 Executor가 반환한 쿼리 결과를 캐시에 저장한 다음 이를 사용자에게 반환합니다.
MyBatis의 두 번째 수준 캐시는 보다 유연하게 설계되었습니다. MyBatis 자체 정의된 두 번째 수준 캐시 구현을 사용할 수도 있습니다. org.apache.ibatis.cache.Cache 인터페이스를 구현하여 캐시를 맞춤 설정할 수도 있습니다. Memcached 등과 같은 타사 메모리 캐싱 라이브러리를 사용합니다.
캐시 변환
문제:
가장 일반적인 문제는 캐시가 켜진 후 여부에 관계없이 발생합니다. 쿼리는 첫 번째 페이지의 데이터를 반환하는 페이지에서 수행됩니다. 또한 SQL 자동 생성 플러그인을 사용하여 get 메소드의 SQL을 생성하는 경우 전달된 매개변수는 아무런 영향을 미치지 않으며 전달된 매개변수 수에 관계없이 첫 번째 매개변수의 쿼리 결과가 반환됩니다.
이런 문제는 왜 발생하는가:
앞서 마이바티스의 실행 과정을 설명할 때 캐시를 켜면 마이바티스의 실행자가 먼저 캐시에서 데이터를 읽어온다고 언급한 바 있습니다. 쿼리할 데이터베이스입니다. 문제는 SQL 자동 생성 플러그인과 페이징 플러그인의 실행 시점이 문 핸들러에 있고, SQL 자동 생성 플러그인과 페이징 플러그인 모두 이후에 문 핸들러가 실행된다는 것입니다. 실행기가 생성하는 캐시 키(키는 SQL과 해당 매개변수 값으로 구성됨)를 읽을 때 원본 SQL을 사용하므로 당연히 문제가 발생합니다.
문제 해결:
문제의 원인을 찾으면 해결이 더 쉬워집니다. 인터셉터를 통해 실행기에서 키 생성 방식을 다시 작성하고, 생성이 가능할 때 자동 생성된 sql(sql 자동 생성 플러그인에 해당)을 사용하거나 페이징 정보(paging 플러그인에 해당)를 추가하면 된다.
인터셉터 서명:
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class CacheInterceptor implements Interceptor { ... }
시그니처에서 알 수 있듯이 인터셉트할 대상 유형은 Executor입니다(참고: 유형은 인터페이스 유형으로만 구성할 수 있습니다). ), 차단 메소드는 query라는 메소드입니다.
인터셉트 구현:
public Object intercept(Invocation invocation) throws Throwable { Executor executorProxy = (Executor) invocation.getTarget(); MetaObject metaExecutor = MetaObject.forObject(executorProxy, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); // 分离代理对象链 while (metaExecutor.hasGetter("h")) { Object object = metaExecutor.getValue("h"); metaExecutor = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); } // 分离最后一个代理对象的目标类 while (metaExecutor.hasGetter("target")) { Object object = metaExecutor.getValue("target"); metaExecutor = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); } Object[] args = invocation.getArgs(); return this.query(metaExecutor, args); } public <E> List<E> query(MetaObject metaExecutor, Object[] args) throws SQLException { MappedStatement ms = (MappedStatement) args[0]; Object parameterObject = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; BoundSql boundSql = ms.getBoundSql(parameterObject); // 改写key的生成 CacheKey cacheKey = createCacheKey(ms, parameterObject, rowBounds, boundSql); Executor executor = (Executor) metaExecutor.getOriginalObject(); return executor.query(ms, parameterObject, rowBounds, resultHandler, cacheKey, boundSql); } private CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { Configuration configuration = ms.getConfiguration(); pageSqlId = configuration.getVariables().getProperty("pageSqlId"); if (null == pageSqlId || "".equals(pageSqlId)) { logger.warn("Property pageSqlId is not setted,use default '.*Page$' "); pageSqlId = defaultPageSqlId; } CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 解决自动生成SQL,SQL语句为空导致key生成错误的bug if (null == boundSql.getSql() || "".equals(boundSql.getSql())) { String id = ms.getId(); id = id.substring(id.lastIndexOf(".") + 1); String newSql = null; try { if ("select".equals(id)) { newSql = SqlBuilder.buildSelectSql(parameterObject); } SqlSource sqlSource = buildSqlSource(configuration, newSql, parameterObject.getClass()); parameterMappings = sqlSource.getBoundSql(parameterObject).getParameterMappings(); cacheKey.update(newSql); } catch (Exception e) { logger.error("Update cacheKey error.", e); } } else { cacheKey.update(boundSql.getSql()); } MetaObject metaObject = MetaObject.forObject(parameterObject, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); if (parameterMappings.size() > 0 && parameterObject != null) { TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { cacheKey.update(parameterObject); } else { for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { cacheKey.update(metaObject.getValue(propertyName)); } else if (boundSql.hasAdditionalParameter(propertyName)) { cacheKey.update(boundSql.getAdditionalParameter(propertyName)); } } } } // 当需要分页查询时,将page参数里的当前页和每页数加到cachekey里 if (ms.getId().matches(pageSqlId) && metaObject.hasGetter("page")) { PageParameter page = (PageParameter) metaObject.getValue("page"); if (null != page) { cacheKey.update(page.getCurrentPage()); cacheKey.update(page.getPageSize()); } } return cacheKey; }
플러그인 구현:
public Object plugin(Object target) { // 当目标类是CachingExecutor类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的 // 次数 if (target instanceof CachingExecutor) { return Plugin.wrap(target, this); } else { return target; } }
Java MyBatis 프레임워크의 캐싱에 대한 자세한 설명 관련 기사 캐시 활용을 개선하려면 PHP 중국어 웹사이트를 주목하세요!