一次查詢如果結果返回太多(1萬或更多),往往會導致系統效能下降,有時更會記憶體不足,影響系統穩定性,故需要做限制。
1.經分析最後決定,應限制一次查詢返回的最大結果數量不應該超出1萬,對於一次返回結果大於限制的時候應該丟擲異常,而不應該擷取(limit 10000)最大結果(結果需求不匹配)。
2.利用mybatis攔截器技術,統一攔截sql,並真對大結果的查詢先做一次count查詢。
public class PreCheckBigQueryInnerInterceptor implements InnerInterceptor {}
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 解析sql
Statement stmt = CCJSqlParserUtil.parse(boundSql.getSql());
if (stmt instanceof Select) {
PlainSelect selectStmt = (PlainSelect) ((Select) stmt).getSelectBody();
if (Objects.nonNull(selectStmt.getLimit())) {
//包含limit查詢
return true;
}
for (SelectItem selectItem : selectStmt.getSelectItems()) {
//計數查詢 count();
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
if (selectExpressionItem.getExpression() instanceof Function) {
//包含function查詢
return true;
}
}
Long aLong = doQueryCount(executor, ms, parameter, rowBounds, resultHandler, boundSql);
if (aLong == 0L) {
return false;
}
if (aLong > 20) {
throw new RuntimeException("單個查詢結果大於20條!!!");
}
}
return true;
}
private Long doQueryCount(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
MappedStatement countMs = buildAutoCountMappedStatement(ms);
String countSqlStr = autoCountSql(true, boundSql.getSql());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
BoundSql countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
Object result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql).get(0);
System.out.println(result);
return (result == null ? 0L : Long.parseLong(result.toString()));
}
<mapper namespace="com.example.UserMapper">
<select id="selectAllUsers" resultType="com.example.User">
SELECT * FROM user
</select>
</mapper>
注意:必須重新構造,不能直接使用入參中的ms
String.format("SELECT COUNT(1) FROM (%s) TOTAL", originalSql);
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//分頁外掛(Mybatis-plus)
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());//防止全表更新(Mybatis-plus)
interceptor.addInnerInterceptor(new PreCheckBigQueryInnerInterceptor());//防止全表查詢(自定義外掛)
return interceptor;
}
public class MybatisPlusInterceptor implements Interceptor {
@Setter
private List<InnerInterceptor> interceptors = new ArrayList<>();
}
他是基於mybatis的Interceptor介面做的攔截器,上文中我們 註冊攔截器PreCheckBigQueryInnerInterceptor的攔截器其實新增到MybatisPlusInterceptor.interceptors集合中。
public Object intercept(Invocation invocation) throws Throwable {
......
for (InnerInterceptor query : interceptors) {
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
return Collections.emptyList();
}
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
......
return invocation.proceed();
}
2.1 willDoQuery先於beforeQuery方法,且一定會執行