Mybatis中支持缓存的query与不支持缓存的query

mybatis拦截器中,通常添加两个query的签名方法,如下:

java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})

第一个,表示不支持缓存的query。

第二个,表示支持缓存的query。

a.某些数据变化不频繁,但查询非常频繁。缓存可以减少数据库查询次数,提高响应速度。

b.在分页查询中,缓存可以显著提高性能,尤其是当用户频繁浏览不同页面时。

c.对于复杂查询,生成的 SQL 可能涉及多个表的联接,执行时间较长。缓存可以显著减少这种查询的执行次数。

具体区别

  1. 访问频率和实时性

    • 不需要缓存:每次查询都直接访问数据库,适用于数据变化频繁或需要最新数据的场景。
    • 需要缓存:查询结果可以被缓存,适用于数据变化不频繁但查询频繁的场景。
  2. 性能和资源使用

    • 不需要缓存:每次都访问数据库,可能会增加数据库负载。
    • 需要缓存:利用缓存减少数据库访问次数,显著提高性能和响应速度。

有哪些方法,可以判断是否应用缓存:

1.通过sql语句标识:

java 复制代码
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class CacheInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String sqlId = mappedStatement.getId();

        // 假设我们有一个特定的SQL ID需要使用缓存
        if ("com.example.mapper.UserMapper.selectUsers".equals(sqlId)) {
            // 使用缓存逻辑
            CacheKey cacheKey = ...; // 创建 CacheKey
            BoundSql boundSql = ...; // 获取 BoundSql
            // 执行带缓存的查询
            return executor.query(mappedStatement, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else {
            // 直接访问数据库
            return invocation.proceed();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可选:设置一些属性
    }
}

2.通过注解或配置

java 复制代码
<select id="selectUsers" resultType="User" useCache="true">
    SELECT * FROM users
</select>



@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class CacheInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
        ResultHandler resultHandler = (ResultHandler) invocation.getArgs()[3];
        Executor executor = (Executor) invocation.getTarget();

        // 读取自定义属性
        Boolean useCache = (Boolean) mappedStatement.getConfiguration().getVariables().get("useCache");

        if (useCache != null && useCache) {
            // 使用缓存逻辑
            CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, mappedStatement.getBoundSql(parameter));
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            return executor.query(mappedStatement, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else {
            // 直接访问数据库
            return invocation.proceed();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可选:设置一些属性
    }
}

3.通过业务逻辑判断

java 复制代码
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class CacheInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
        ResultHandler resultHandler = (ResultHandler) invocation.getArgs()[3];
        Executor executor = (Executor) invocation.getTarget();

        // 根据业务逻辑判断
        if (shouldUseCache(mappedStatement, parameter)) {
            // 使用缓存逻辑
            CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, mappedStatement.getBoundSql(parameter));
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            return executor.query(mappedStatement, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else {
            // 直接访问数据库
            return invocation.proceed();
        }
    }

    private boolean shouldUseCache(MappedStatement mappedStatement, Object parameter) {
        // 根据业务逻辑判断是否使用缓存
        // 示例:如果参数包含某个特定值,则使用缓存
        if (parameter instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) parameter;
            return "useCache".equals(paramMap.get("cacheFlag"));
        }
        return false;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可选:设置一些属性
    }
}

两个query方法的区别:

其实,mapper文件中useCache参数会用来构建MappedStatement对象。即ms.isUseCache()被用来判断是否走缓存逻辑。

或者 通过@Options注解方式配置useCache参数:

java 复制代码
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
    
    @Select("SELECT * FROM your_table WHERE your_conditions")
    @Options(useCache = false)
    List<YourResultType> selectRealTimeData();
}
java 复制代码
public abstract class BaseExecutor implements Executor {

    @Override
    public <E> List<E> 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);
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // 检查二级缓存
        if (ms.isUseCache() && resultHandler == null) {
            Cache cache = ms.getCache();
            if (cache != null) {
                // 从二级缓存中获取结果
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) cache.getObject(key);
                if (list != null) {
                    return list;
                }
            }
        }
        // 如果缓存没有命中,执行数据库查询
        List<E> result = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        // 将结果存入二级缓存
        if (ms.isUseCache() && resultHandler == null && cache != null) {
            cache.putObject(key, result);
        }
        return result;
    }

    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
}

其中,mybatis中,启用二级缓存的配置方式:

1.全局配置

java 复制代码
<configuration>
    <!-- 其他配置 -->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
</configuration>

2.映射文件配置

java 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 启用二级缓存 -->
    <cache/>

    <!-- 其他映射配置 -->
    <select id="selectUsers" resultType="User">
        SELECT * FROM users
    </select>
</mapper>

3.自定义缓存配置

java 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 启用二级缓存,并设置自定义属性 -->
    <cache
        eviction="LRU"       <!-- 缓存回收策略:LRU(默认) -->
        flushInterval="60000" <!-- 刷新间隔,单位:毫秒 -->
        size="512"           <!-- 缓存大小 -->
        readOnly="true"/>    <!-- 只读缓存 -->

    <!-- 其他映射配置 -->
    <select id="selectUsers" resultType="User">
        SELECT * FROM users
    </select>
</mapper>

4.使用注解配置

java 复制代码
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.cache.decorators.LruCache;

@CacheNamespace(
    eviction = LruCache.class,   // 缓存回收策略
    flushInterval = 60000,       // 刷新间隔,单位:毫秒
    size = 512,                  // 缓存大小
    readWrite = false            // 是否可读写
)
public interface UserMapper {
    
    @Select("SELECT * FROM users")
    List<User> selectUsers();
}

提取有效信息:

java 复制代码
private Object extractRouteParameterValue(Invocation invocation, Set<String> routerPropertyNames) {

        Object routeValue = null;
        try {
            Object[] args = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) args[0];
            Object parameterObject = args[1];
            BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
            Configuration configuration = mappedStatement.getConfiguration();

            for (ParameterMapping parameterMapping : parameterMappings) {
                String rawPropertyName = parameterMapping.getProperty();
                String actualPropertyName = resolvePropertyName(rawPropertyName);

                if (!routerPropertyNames.contains(actualPropertyName.toLowerCase())) {
                    continue;
                }
                // copy from DefaultParameterHandler.setParameter方法
                //  ParameterMode.IN 输入, OUT、INOUT 是在存储过程中使用,暂时无视
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        routeValue = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        routeValue = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        routeValue = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        routeValue = metaObject.getValue(propertyName);
                    }
                }
                if (routeValue != null && hasText(routeValue.toString())) {
                    return routeValue;
                }
                throw new DataSourceRoutingException(String.format("未检测到有效的数据库路由,请检测是否传入:(%s)", boundSql.getSql()));
            }
        } catch (DataSourceRoutingException dataSourceRoutingException) {
            throw dataSourceRoutingException;
        } catch (RuntimeException e) {
            throw new DataSourceRoutingException(String.format("数据库路由解析异常, invocation method:{%s}, args:{%s}, routerPropertyNames:{%s}",
                    invocation.getMethod().toGenericString(), Arrays.toString(invocation.getArgs()), routerPropertyNames), e);
        }
        return null;
    }
相关推荐
浅念同学28 分钟前
算法-常见数据结构设计
java·数据结构·算法
杰哥在此3 小时前
Java面试题:讨论持续集成/持续部署的重要性,并描述如何在项目中实施CI/CD流程
java·开发语言·python·面试·编程
咖啡煮码3 小时前
深入剖析Tomcat(十五、十六) 关闭钩子,保证Tomcat的正常关闭
java·tomcat
C.C3 小时前
java IO流(1)
java·开发语言
黑头!5 小时前
Tomcat注册为服务之后 运行时提示JVM异常
java·jvm·tomcat
袁震5 小时前
Java---Mybatis详解二
java·开发语言·mybatis
《黑巧克力》5 小时前
【JavaEE】多线程进阶
java·spring·java-ee·maven·dubbo·idea
idealzouhu5 小时前
函数式接口的定义及常见类型
java
多多*5 小时前
每天一道面试题之浅浅讲一下java5中的自动装箱和自动拆箱
java·开发语言·spring boot·后端·spring
Gratitute_林腾6 小时前
MyBatisPlus-分页插件的基本使用
java·java-ee