本文要解決的問題:細粒度控制mybatis的二級緩存。mybatis的二級緩存的問題:當更新SQL執行時只清除當前SQL所在命名空間(namespace)的緩存。若是存在2個命名空間namespaceA和namespaceB,當namespaceA下執行更新操做時,namespaceB的緩存並不會清除。正常的狀況下,這種策略是不會有問題。可是,若是存在關聯查詢時就有可能出現問題了,例如namespaceA關聯namespaceB,當namespaceB執行更新時清除namespaceB的緩存,這是查詢namespaceA時,會致使查詢出來的結果是緩存的,即數據不是最新的。步驟以下:java
1. 開啓mybatis二級緩存git
<settings> <setting name="cacheEnabled" value="true" /> </settings>
2. 加入SQL分析的jar依賴github
<dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>0.9.4</version> </dependency>
3. 複製以下攔截器代碼sql
package com.xyz.core.inctercepter; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.util.TablesNamesFinder; /** * 緩存控制攔截器 * * @author lee * @since 2016年3月17日 */ @Intercepts({ @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(method = "update", type = Executor.class, args = { MappedStatement.class, Object.class }) }) public class CachingInterceptor implements Interceptor { private final static Logger logger = LoggerFactory.getLogger(CachingInterceptor.class); /** * 表名關聯的命名空間<br/> * 一個table下關聯的namespace */ private final static Map<String, Map<String, String>> tableLinks = new HashMap<String, Map<String, String>>(); /** * 記錄已經解析過的SQL,確保一條SQL只被解析一次,提升效率 */ private final static Map<String, String> dealedMap = new HashMap<String, String>(); public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; BoundSql boundSql = ms.getBoundSql(args[1]); if (!ms.getConfiguration().isCacheEnabled() || ms.getCache() == null) return invocation.proceed(); if (dealedMap.containsKey(ms.getId()) && dealedMap.get(ms.getId()).equals(boundSql.getSql())) { return invocation.proceed(); } dealedMap.put(ms.getId(), boundSql.getSql()); final String operate = invocation.getMethod().getName(); final List<String> tableNames = getTableList(boundSql.getSql()); final String namespace = ms.getCache().getId(); logger.debug("當前操做SQL中包含的namespace:" + namespace); if ("query".equals(operate)) { deal(namespace, tableNames); } else { Configuration configuration = ms.getConfiguration(); clearCache(tableNames, configuration.getMappedStatements()); } return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { } /** * 一個namespace包含多個SQL<br/> * 一條SQL語句包含多個table * * @param namespace * @param tableNames */ private void deal(final String namespace, final List<String> tableNames) { if (tableNames == null || tableNames.size() == 0) return; for (String tableName : tableNames) { Map<String, String> namespaces = tableLinks.get(tableName); if (namespaces == null) { namespaces = new HashMap<String, String>(); namespaces.put(namespace, namespace); tableLinks.put(tableName, namespaces); } else if (!namespaces.containsKey(namespace)) { namespaces.put(namespace, namespace); } } } /** * 清除緩存 * * @param mappedStatments */ @SuppressWarnings("rawtypes") private void clearCache(List<String> tableNames, Collection mappedStatments) { if (tableNames == null || tableNames.isEmpty()) return; for (String tableName : tableNames) { Map<String, String> namespaces = tableLinks.get(tableName); if (namespaces == null) continue; for (String namespaceNeedClearKey : namespaces.keySet()) { for (Object o : mappedStatments) { if (o instanceof MappedStatement) { MappedStatement sta = ((MappedStatement) o); final String namespace = sta.getCache().getId(); if (namespaceNeedClearKey.equals(namespace)) { logger.debug("命名空間[{}]的緩存被清除", namespace); sta.getCache().clear(); break; } } } } } } /** * 解析SQL中包含的表名 * * @param sql * @return */ public List<String> getTableList(final String sql) { try { Statement statement = CCJSqlParserUtil.parse(sql); TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); List<String> tableList = null; if (statement instanceof Select) { Select selectStatement = (Select) statement; tableList = tablesNamesFinder.getTableList(selectStatement); } else if (statement instanceof Update) { Update updateStatement = (Update) statement; tableList = tablesNamesFinder.getTableList(updateStatement); } else if (statement instanceof Insert) { Insert insertStatement = (Insert) statement; tableList = tablesNamesFinder.getTableList(insertStatement); } else if (statement instanceof Delete) { Delete deleteStatement = (Delete) statement; tableList = tablesNamesFinder.getTableList(deleteStatement); } logger.debug("SQL:{}中包含的表名:" + tableList, sql); return tableList; } catch (JSQLParserException e) { logger.error("解析sql異常:" + sql, e); } return null; } }
4. 配置mybatis攔截器apache
<plugins> <plugin interceptor="com.xyz.core.inctercepter.CachingInterceptor" /> </plugins>