Spring Boot Mybatis實現分表

      開發過程中可能會碰到分表的場景,數據庫的數據量相當大的時候可能需要按天分表或者按月分表啥的(分表策略)。接下來就教大家用最簡單的方式實現這一需求。

      咱們接下來主要實現以下兩個大功能:

  • 自動建表,當表不存在的時候自動把表創建出來。
  • 自動分表,根據操作數據庫的某個參數進行分表。

      自動建表,自動分表核心思想在Mybatis攔截器的使用。強烈建議大家先去了解下Mybatis攔截器的使用(之前也寫過一遍關於Mybatis攔截器的使用的文章,有興趣的可以看下 https://blog.csdn.net/wuyuxing24/article/details/89343951 )。

      根據實際情況我們做如下規定:

  • 每個需要分表的表都有一個基礎表名。比如按月分表之後的表名爲“XXX-201909”,那麼我們認爲"XXX"就是基礎表名。所有的sql語句裏面還是用基礎表名,我們會在自定義Mybatis攔截器裏面找到基礎表名替換成分表表名。
  • 分表的依據來源於操作數據庫的參數當中的一個。我們會通過參數註解(TableShardParam)來標識哪個操作作爲分表依據。
  • 每個分表需要自己指定分表策略(ITableNameStrategy),針對每個分表我們需要自己去實現自己的分表策略,自己實現ITableNameStrategy接口。

一 自動建表準備

      我們考慮到大部分分表的情況下,都希望在代碼裏面能夠自動建表。操作表之前判斷表是否存在,如果表不存在則自動幫我們把表建出來。

      關於自動建表,結合實際情況,我們認爲建表是和每個表對應的實體類綁定在一起的。所以我們會有一個建表相關的TableCreate註解,TableCreate註解是添加在每個表對應的實體類上的。TableCreate註解的元數據會告訴我們當前實體類對應表的基礎表名,已經去哪裏找到相關的建表語句。

TableCreate註解需要添加在表對應的實體類上

/**
 * @name: TableCreate
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description: TableCreate註解用於告訴我們怎麼找到建表語句(如果表不存在的情況下, 我們程序裏面自己去建表)
 * <p>
 * tableName -- 基礎表名
 * autoCreateTableMapperClass -- mapper class對應的名字
 * autoCreateTableMapperMethodName -- mapper class 裏面對應的方法
 * <p>
 * 最終我們會去mapper class裏面找到對應的對應的方法,最終拿到建表語句
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TableCreate {

    /**
     * table的基礎表名
     */
    String tableName();

    /**
     * Mapper類,不能爲空
     */
    Class<?> autoCreateTableMapperClass();

    /**
     * Mapper文件裏面的函數名字(創建表對應的函數)
     */
    String autoCreateTableMapperMethodName();
}

      爲了方便Mybatis攔截器裏面自動建表的使用,每個表對應的建表信息我們用TableCreateConfig類做一個簡單的分裝。TableCreateConfig會告訴我們基礎表名,以及我們需要的建表語句在哪個Mapper類哪個方法裏面。


/**
 * @name: TableCreateConfig
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description: 自動建表相關的一些配置信息
 * 在攔截器裏面我們會根據autoCreateTableMapperClass類的autoCreateTableMapperMethodName方法找到建表語句
 */
@Data
@Accessors(chain = true)
public class TableCreateConfig {

    /**
     * 表名
     */
    private String tableName;

    /**
     * 自動建表Mapper類
     */
    private Class<?> autoCreateTableMapperClass;

    /**
     * 自動建表Mapper中的方法
     */
    private String autoCreateTableMapperMethodName;

}

      在Spring Boot啓動的時候,我們會表實體類對應的包下面讀取所有添加了TableCreate註解的相關信息,把讀取的信息封裝到TableCreateConfig類裏面,並且保存在單例類TableCreateManager裏面。這一部分內容大家可以看下我給出的源碼裏面TableCreateScan,TableCreateScanRegister類裏面邏輯。

      簡單總結下關於自動建表我們做了那些準備工作。我們會在Spring Boot啓動的過程中去讀取所有添加了TableCreate註解的實體類。把讀取到的信息保存在單例類TableCreateManager裏面。單例TableCreateManager裏面會維護一個Map:key就是每個需要建表的基礎表名,value則是建表相關的信息。建表相關的信息會和Mapper裏面的某個方法關聯起來。具體可以看下下面Mybatis攔截器的具體實現。

二 自動分表準備

      分表,我們需要兩個東西:分表策略、分表依據。

2.1 分表策略

      分表策略,我們定義一個分表接口,讓每個分表去實現直接的分表策略。分表策略我們給兩個參數,一個是基礎表名,一個是分表依據。

/**
 * @name: ITableNameStrategy
 * @author: tuacy.
 * @date: 2019/8/13.
 * @version: 1.0
 * @Description: 分表對應的策略
 */
public interface ITableNameStrategy {

    /**
     * 表名字
     *
     * @param oldTableName     表基本名字
     * @param dependFieldValue 根據該字段確定表名(Mapper方法的某個參數對應的值)
     * @return 表名
     */
    String tableName(String oldTableName, String dependFieldValue);

}

      分表策略的配置,我們把他們放在操作數據庫的方法上。在TablePrepare註解裏面指定。TablePrepare註解也用於標識是否進入我們自定義的Mybatis攔截器裏面去。

/**
 * @name: TablePrepare
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description:
 */
@Documented
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TablePrepare {

    /**
     * 啓用自動建表,當表不存在的時候,是否創建表
     */
    boolean enableAutoCreateTable() default true;

    /**
     * 啓用分表
     */
    boolean enableTableShard() default false;

    /**
     * 指定表,如果設置該值,則只會處理指定的表,沒有則會處理sql中的所有表
     * 如果自己設置了基礎表的名字,那麼我們處理建表和分表的時候只會處理這些指定的表.
     * 如果沒有設置基礎表的時候,我們會自動去sql語句裏面解析出所有的表名.做相應的建表和分表的邏輯
     */
    String[] appointTable() default {};

    /**
     * 表名策略,通過某種規則得到表名
     */
    Class<? extends ITableNameStrategy> strategy() default TableNameStrategyVoid.class;
}

2.2 分表依據

      結合實際情況,我們認爲分表的依據都是來源於操作數據的某個參數(也可能是某個參數的某個字段)。那這裏就有問題了,操作數據庫有的時候有多個參數,哪個參數作爲分表依據呢。我們定義一個參數註解TableShardParam。哪個參數添加了該註解,我們就認爲這個參數是分表依據(目前只支持一個參數作爲依據)。我們會在我們自定義的Mybatis攔截器裏面找到添加了TableShardParam註解的參數對應的值。

      爲了應對多種情況。TableShardParam支持以下幾種情況(這部分具體的實現,需要仔細看下下面自定義Mybatis攔截器裏面這部分的具體實現)。大家可以根據自己的實際情況做相應的修改。

  • TableShardParam添加在java基礎類型上,比如int,long等,我們會把基礎類型轉換爲String,最終傳遞給分表策略。
  • TableShardParam添加在對象類型上,我們可以找到對象的某個屬性(反射)對應的值,最終傳遞給分表策略。
  • TableShardParam添加在List上,我們會找到List對象的一個元素,如果List裏面的元素是java基礎類型,直接獲取到第一個元素對應的值,如果List裏面的元素是對象,則獲取到對象某個屬性對應的值。在最終把他們傳遞給分表策略。
/**
 * @name: TableShardParam
 * @author: tuacy.
 * @date: 2019/8/30.
 * @version: 1.0
 * @Description: 添加在參數上的註解, 一定要配置mybatis 的Param註解使用
 * <p>
 * 我們是這樣考慮的,分表核心在於確定表的名字,表的名字怎麼來,肯定是通過某個參數來獲取到.
 * 所以,這裏我們設計TableShardParam註解,用於添加在參數上,讓我們方便的獲取到通過那個參數來獲取表名
 * 1. int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);
 * -- 分表依據對應AccHour對象recTime屬性對應的值
 * 2. int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);
 * -- 分表依據對應list的第一個對象recTime屬性對應的值
 * 3. List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);
 * -- 分表依據對應endTime對應的值
 */
@Documented
@Inherited
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShardParam {

    @AliasFor("dependFieldName")
    String value() default "";

    /**
     * dependFieldName取到我們需要的獲取表名的依據
     */
    @AliasFor("value")
    String dependFieldName() default "";

}

三 自定義Mybatis攔截器

       關於自定義Mybatis攔截器的具體實現,我這裏就直接貼代碼了。肯定有些地方是還沒有考慮到的,大家需要根據自己的需求做修改,重點關注以下幾個部分:

  • 攔截器裏面我們是怎麼拿到相應的建表語句的。
  • 攔截器裏面我們是怎麼去執行建表語句的。
  • 攔截器裏面我們是怎麼拿到分表依據的,裏面考慮了多種情況。
  • 每個sql語句,我們是怎麼解析出表名的。怎麼把我們把我們分表表名替換進去的。
/**
 * @name: TableShardInterceptor
 * @author: tuacy.
 * @date: 2019/8/13.
 * @version: 1.0
 * @Description: 自動建表 + 分表 攔截器的實現
 */
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class TableShardInterceptor implements Interceptor {

    /**
     * sql語句裏面去獲取表名的依據(主要,全部是小寫的)
     * 說白了就是哪些字符串後面會跟上表名
     */
    private final static String[] SQL_TABLE_NAME_FLAG_PREFIX = {"from", "join", "update", "insert into"};

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
            return invocation.proceed();
        }

        try {
            RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
            // MetaObject是mybatis裏面提供的一個工具類,類似反射的效果
            MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");//獲取sql語句
            String originSql = boundSql.getSql();

            if (StringUtils.isEmpty(originSql)) {
                return invocation.proceed();
            }

            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            // 判斷方法上是否添加了 TableShardAnnotation 註解,因爲只有添加了TableShard註解的方法我們纔會去做分表處理
            TablePrepare tablePrepare = getTableShardAnnotation(mappedStatement);

            // 沒有加@TablePrepare註解則不填家我們自定義的邏輯
            if (tablePrepare == null) {
                return invocation.proceed();
            }

            boolean enableAutoCreateTable = tablePrepare.enableAutoCreateTable(); // 表不存在的是哈,事發創建
            boolean enableTableShard = tablePrepare.enableTableShard(); // 事發進行分表邏輯處理
            // 自動建表和分表是否開啓,都沒有則退出往下走
            if (!enableAutoCreateTable && !enableTableShard) {
                invocation.proceed();
            }

            // 獲取到需要處理的表名
            String[] appointTable = tablePrepare.appointTable();
            if (appointTable.length == 0) {
                List<String> tableNameList = getTableNamesFromSql(originSql);
                if (tableNameList == null || tableNameList.isEmpty()) {
                    return invocation.proceed();
                } else {
                    // 去掉前後空格和/n
                    tableNameList = tableNameList.stream().map(item -> {
                        if (item == null) {
                            return null;
                        }
                        return item.trim().replaceAll("[\r\n]", "");
                    }).collect(Collectors.toList());
                    appointTable = new String[tableNameList.size()];
                    tableNameList.toArray(appointTable);
                }
            }


            // 獲取分表表名處理策略
            Class<? extends ITableNameStrategy> strategyClass = tablePrepare.strategy();
            ITableNameStrategy tableStrategy = null;
            if (!strategyClass.equals(TableNameStrategyVoid.class)) {
                tableStrategy = strategyClass.newInstance();
            }

            // 分表處理的時候,我們一般是依賴參數裏面的某個值來進行的.這裏用於獲取到參數對應的值.
            String dependValue = getDependFieldValue(tablePrepare, metaStatementHandler, mappedStatement);

            // 自動建表處理邏輯(表不存在的時候,我們會建表)
            if (tablePrepare.enableAutoCreateTable()) {
                SqlSessionTemplate template = SpringContextHolder.getBean(SqlSessionTemplate.class);
                for (String tableName : appointTable) {
                    TableCreateConfig classConfig = TableCreateManager.INSTANCE.getClassConfig(tableName);
                    if (classConfig == null) {
                        // 沒有找到建表語句則跳過
                        continue;
                    }

                    String createSqlMethodPath = classConfig.getAutoCreateTableMapperClass().getName() + "." + classConfig.getAutoCreateTableMapperMethodName();
                    String sql = template.getConfiguration().getMappedStatement(createSqlMethodPath).getBoundSql("delegate.boundSql").getSql();
                    if (StringUtils.isEmpty(sql)) {
                        // 建表sql爲空時不理,直接跳過
                        continue;
                    }

                    if (!StringUtils.isEmpty(dependValue) && strategyClass != TableNameStrategyVoid.class) {
                        sql = sql.replace(tableName, tableStrategy.tableName(tableName, dependValue));
                    }

                    Connection conn = (Connection) invocation.getArgs()[0];
                    boolean preAutoCommitState = conn.getAutoCommit();
                    conn.setAutoCommit(false);//將自動提交關閉
                    try (PreparedStatement countStmt = conn.prepareStatement(sql)) {
                        // 把新語句設置回去
                        metaStatementHandler.setValue("delegate.boundSql.sql", sql);
                        countStmt.execute();
                        conn.commit();//執行完後,手動提交事務
//                        System.out.println(isSuccess);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        conn.setAutoCommit(preAutoCommitState);//在把自動提交打開
                    }
                }
            }

            // 分表處理邏輯
            if (strategyClass != TableNameStrategyVoid.class) {
                if (tablePrepare.enableTableShard()) {
                    String updateSql = originSql;
                    for (String tableName : appointTable) {
                        // 策略處理表名
                        String newTableName = tableStrategy.tableName(tableName, dependValue);
                        updateSql = updateSql.replaceAll(tableName, newTableName);
                    }

                    // 把新語句設置回去,替換表名
                    metaStatementHandler.setValue("delegate.boundSql.sql", updateSql);
                }
            } else {
                // fix 啓用了自動建表,但是沒有啓用分表的時候,sql被替換成建表的sql。沒有設置回來的問題
                metaStatementHandler.setValue("delegate.boundSql.sql", originSql);
            }
        } catch (Exception ignored) {
            // ignore 任何一個地方有異常都去執行原始操作 -- invocation.proceed()
        }
        return invocation.proceed();
    }

    /**
     * 從參數裏面找到指定對象指定字段對應的值
     */
    private String getDependFieldValue(TablePrepare tablePrepare, MetaObject metaStatementHandler, MappedStatement mappedStatement) throws Exception {

        // 以上情況下不滿足則走@TableShardParam機制
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        Method[] methods = Class.forName(className).getMethods();
        Method method = null;
        for (Method me : methods) {
            if (me.getName().equals(methodName) && me.isAnnotationPresent(tablePrepare.annotationType())) {
                method = me;
            }
        }

        if (method == null) {
            return null;
        }

        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return null;
        }

        int flag = 0;
        Parameter parameter = null;
        for (Parameter p : parameters) {
            // TableShardParam和Param需要同時添加
            if (p.getAnnotation(TableShardParam.class) != null && p.getAnnotation(Param.class) != null) {
                parameter = p;
                flag++;

            }
        }

        // 參數沒有註解則退出
        if (flag == 0) {
            return null;
        }

        // 多個則拋異常
        if (flag > 1) {
            throw new RuntimeException("存在多個指定@TableShardParam的參數,無法處理");
        }

        String tableSharedFieldParamKey = parameter.getAnnotation(Param.class).value();
        TableShardParam annotation = parameter.getAnnotation(TableShardParam.class);
        Class<?> parameterType = parameter.getType(); // 參數的類型
        String dependFieldName = StringUtils.isEmpty(annotation.value()) ? annotation.dependFieldName() : annotation.value();
        if (isPrimitive(parameterType) || StringUtils.isEmpty(dependFieldName)) {
            return getPrimitiveParamFieldValue(metaStatementHandler, tableSharedFieldParamKey);
        } else {
            return getParamObjectFiledValue(metaStatementHandler, tableSharedFieldParamKey, dependFieldName);
        }
    }

    /**
     * 判斷是否是基礎類型 9大基礎類型及其包裝類
     *
     * @return 是否是基礎類型, long, int, Long 等等
     */
    private boolean isPrimitive(Class<?> clazz) {
        if (clazz.isPrimitive()) {
            return true;
        }

        try {
            if (((Class) clazz.getField("TYPE").get(null)).isPrimitive()) {
                return true;
            }
        } catch (Exception e) {
            return false;
        }

        return clazz.equals(String.class);
    }

    /**
     * 解析sql獲取到sql裏面所有的表名
     *
     * @param sql sql
     * @return 表名列表
     */
    private List<String> getTableNamesFromSql(String sql) {
        // 對sql語句進行拆分 -- 以','、'\n'、'\t'作爲分隔符
        List<String> splitterList = Lists.newArrayList(Splitter.on(new CharMatcher() {
            @Override
            public boolean matches(char c) {
                return Character.isWhitespace(c) || c == '\n' || c == '\t';
            }
        }).omitEmptyStrings().trimResults().split(sql))
                .stream()
                .filter(s -> !s.equals(","))
                .filter(s -> !s.equals("?"))
                .filter(s -> !s.equals("?,"))
                .filter(s -> !s.equals("("))
                .filter(s -> !s.equals(")"))
                .filter(s -> !s.equals("="))
                .collect(Collectors.toList());
        List<String> tableNameList = Lists.newArrayList();
        for (String item : SQL_TABLE_NAME_FLAG_PREFIX) {
            tableNameList.addAll(getTableName(splitterList, Lists.newArrayList(Splitter.on(' ').split(item))));
        }
        return tableNameList;
    }

    /**
     * 獲取表名
     */
    private List<String> getTableName(List<String> splitterList, List<String> list) {
        List<String> retList = Lists.newArrayList();
        if (list == null || list.isEmpty() || splitterList == null || splitterList.isEmpty() || splitterList.size() <= list.size()) {
            return retList;
        }
        for (int index = 0; index < splitterList.size(); index = index + list.size()) {

            if (index < splitterList.size() - list.size()) {
                boolean match = true;
                for (int innerIndex = 0; innerIndex < list.size(); innerIndex++) {
                    if (!splitterList.get(index + innerIndex).toLowerCase().equals(list.get(innerIndex).toLowerCase())) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    if ("update".toLowerCase().equals(list.get(0).toLowerCase())) {
                        // ON DUPLICATE KEY UPDATE 需要過濾出來
                        if (index < 3 || !(splitterList.get(index - 1).toLowerCase().equals("key".toLowerCase()) &&
                                splitterList.get(index - 2).toLowerCase().equals("DUPLICATE".toLowerCase()) &&
                                splitterList.get(index - 3).toLowerCase().equals("ON".toLowerCase()))) {
                            retList.add(splitterList.get(index + list.size()));
                        }
                    } else {
                        retList.add(splitterList.get(index + list.size()));
                    }
                }
            }

        }
        return retList;
    }


    /**
     * 獲取方法上的TableShard註解
     *
     * @param mappedStatement MappedStatement
     * @return TableShard註解
     */
    private TablePrepare getTableShardAnnotation(MappedStatement mappedStatement) {
        TablePrepare tablePrepare = null;
        try {
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.substring(id.lastIndexOf(".") + 1);
            final Method[] method = Class.forName(className).getMethods();
            for (Method me : method) {
                if (me.getName().equals(methodName) && me.isAnnotationPresent(TablePrepare.class)) {
                    tablePrepare = me.getAnnotation(TablePrepare.class);
                    break;
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return tablePrepare;
    }

    /**
     * 從參數裏面找到指定對象指定字段對應的值--基礎類型
     */
    private String getPrimitiveParamFieldValue(MetaObject metaStatementHandler, String fieldParamKey) {
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object parameterObject = boundSql.getParameterObject();
        if (parameterObject == null) {
            return null;
        }
        Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);
        if (filterFiledObject == null) {
            return null;
        }
        Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
        return dependObject == null ? null : dependObject.toString();
    }

    /**
     * 獲取參數裏面的對象
     */
    private Object recursiveGetEffectiveObject(Object srcObject) {

        if (!(srcObject instanceof List)) {
            return srcObject;
        }
        Object listItemObject = ((List) srcObject).get(0);
        while (listItemObject instanceof List) {
            listItemObject = ((List) listItemObject).get(0);
        }
        return listItemObject;
    }


    /**
     * 從參數裏面找到指定對象指定字段對應的值--對象
     * 如該參數是List.指定對象爲第一個元素
     */
    private String getParamObjectFiledValue(MetaObject metaStatementHandler, String fieldParamKey, String dependFieldName) {

        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object parameterObject = boundSql.getParameterObject();
        Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);

        if (filterFiledObject == null) {
            return null;
        }
        Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
        try {
            return ReflectUtil.getFieldValue(dependObject, dependFieldName);
        } catch (Exception ignored) {
        }

        return null;
    }

    @Override
    public Object plugin(Object target) {
        // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的次數
        return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

四 怎麼使用

      我們用一個簡單實例來教大家怎麼使用我們實現的分表功能。基礎表名StatisAccHour,

4.1 建表語句

      和我們平常使用Mybatis一樣的,一個Mapper接口和一個Mapper xml。

public interface CreateTableMapper {

    int createAccHour();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tuacy.tableshard.mapper.CreateTableMapper">

    <!-- acc 小時表, 一個小時一張表 -->
    <update id="createAccHour">
        CREATE TABLE IF NOT EXISTS `StatisAccHour` (
            `recTime` bigint(20) NOT NULL,
            `ptId` int(11) NOT NULL,
            `value` double DEFAULT NULL,
            PRIMARY KEY (`RecTime`,`PtId`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    </update>

</mapper>

4.2 表對應實體類

      "StatisAccHour"基礎表對應的實體類,三個字段和表裏面的字段一一對應。同時我們添加了TableCreate註解在實體了上,指定了基礎表名“StatisAccHour”,建表語句在CreateTableMapper類的createAccHour方法裏面。

@TableCreate(
        tableName = "StatisAccHour",
        autoCreateTableMapperClass = CreateTableMapper.class,
        autoCreateTableMapperMethodName = "createAccHour"
)
@Getter // lombok 註解,不用手動去寫get set方法
@Setter
public class AccHour {

    /**
     * 針對recTime做一個簡單說明,
     * 比如當前時間是 2019年08月31日00時31分46秒141微妙
     * 則我們在數據庫裏面存20190831003146141
     */
    private Long recTime;
    private Long ptId;
    private Double value;

}

4.3 分表策略

      基礎表名和分表依據字段的前十個字符組成分表對應的表名。


/**
 * 分表方案 按照年月日時分表
 */
public class SuffixYYYYMMDDHHNameStrategy implements ITableNameStrategy {

    private static final int SUFFIX_LENGTH = 10; // yyyymmddhh

    @Override
    public String tableName(String baseTableName, String dependFieldValue) {
        return baseTableName + dependFieldValue.substring(0, SUFFIX_LENGTH);
    }
}

4.4 數據庫操作

      注意TablePrepare註解的添加,每個sql裏面的表名還是用的基礎表名。最終會在自定義攔截器裏面替換。

/**
 * AccHour 每個小時一張表(多數據源,我們有三個數據源,我們假設該表放在statis數據源下面)
 */
public interface AccHourMapper {

    /**
     * 往數據庫裏面插入一條記錄
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);

    /**
     * 往數據庫裏面插入多條
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);

    /**
     * 往數據庫裏面插入多條
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    AccHour selectItem(@TableShardParam(dependFieldName = "recTime") @Param("recvTime") Long recvTime, @Param("pkId") Long pkId);

    /**
     * 查詢指定時間範圍內的列表
     *
     * @param startTIme 開始時間
     * @param endTime   解釋時間
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);


}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tuacy.tableshard.mapper.AccHourMapper">

    <!-- 基礎表名 StatisAccHour -->

    <!-- 往數據庫裏面插入一條記錄 -->
    <insert id="insertItem">

        insert into StatisAccHour (
            `recTime`,
            `ptId`,
            `value`
        ) value (
            #{item.recTime},
            #{item.ptId},
            #{item.value}
        )

    </insert>

    <!-- 批量插入多條記錄 -->
    <insert id="insertList">

        insert into StatisAccHour (
            `recTime`,
            `ptId`,
            `value`
        ) values
         <foreach collection="list" item="item" separator=",">
             (
                #{item.recTime},
                #{item.ptId},
                #{item.value}
             )
         </foreach>

    </insert>

    <!-- 查詢一條記錄 -->
    <select id="selectItem" resultType="com.tuacy.tableshard.entity.model.AccHour">

        select
        `recTime` as recTime,
        `ptId` as ptId,
        `value` as value
        from StatisAccHour
        where recTime = #{recvTime} and ptId = #{pkId}

    </select>

    <!-- 查詢一條記錄 -->
    <select id="selectLIst" resultType="com.tuacy.tableshard.entity.model.AccHour">

        select
        `recTime` as recTime,
        `ptId` as ptId,
        `value` as value
        from StatisAccHour
        where recTime >= ${startTime} and recTime <![CDATA[<=]]> ${endTime}

    </select>

</mapper>

4.5 DAO使用

      特別要注意,在Dao層我們需要自己保證每一次操作的數據庫都是屬於同一個分表的。比如插入一批數據的時候,我們需要自己對不同分表的數據做分批次處理。保存每個調用mapper插入的時候都是屬於同一個分表的數據。具體可以看看下面insertList()方法的具體實現。

@Repository
public class AccHourDao extends BaseDao implements IAccHourDao {

    /**
     * 基礎表名
     */
    private static final String BASE_TABLE_NAME = "StatisAccHour";
    /**
     * 分表策略
     */
    private static final ITableNameStrategy STRATEGY = new SuffixYYYYMMDDHHNameStrategy();

    private AccHourMapper accHourMapper;

    @Autowired
    public void setAccHourMapper(AccHourMapper accHourMapper) {
        this.accHourMapper = accHourMapper;
    }

    /**
     * DataSourceAnnotation 用於指定數據源,放到統計數據庫裏面
     */
    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public int insertItem(AccHour item) {
        return accHourMapper.insertItem(item);
    }

    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public int insertList(List<AccHour> list) {
        if (list == null || list.isEmpty()) {
            return 0;
        }
        // 首先,我們不能保證list所有的數據都是一張表的,所以我們先得對數據分類,按表來分類
        Map<String, List<AccHour>> groupingByTable = list.stream().collect(Collectors.groupingBy(
                item -> STRATEGY.tableName(BASE_TABLE_NAME, item.getRecTime().toString()),
                (Supplier<Map<String, List<AccHour>>>) HashMap::new,
                Collectors.toList()));
        // 遍歷存儲(上面的代碼我們已經保存了每個Map.)
        int sucessCount = 0;
        for (List<AccHour> mapValueItem : groupingByTable.values()) {
            sucessCount += accHourMapper.insertList(mapValueItem);
        }
        return sucessCount;
    }

    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public AccHour selectItem(Long recvTime, Long ptId) {
        return accHourMapper.selectItem(recvTime, ptId);
    }

    /**
     * 查詢指定時間範圍的數據
     * 針對time做一個簡單說明,
     * 比如當前時間是 2019年08月31日00時31分46秒141微妙
     * 則我們在數據庫裏面存20190831003146141
     *
     * @param startTime 開始時間
     * @param endTime   結束時間
     * @return 所有查詢到的記錄
     */
    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public List<AccHour> selectList(Long startTime, Long endTime) {
        // long類型是20190831003146141的形式轉換爲2019年08月31日00時31分46秒141微妙對應的LocalDateTime
        LocalDateTime startTimeDate = DbDataTimeUtils.long2DateTime(startTime);
        LocalDateTime endTimeDate = DbDataTimeUtils.long2DateTime(endTime);
        if (startTimeDate.isAfter(endTimeDate)) {
            return null;
        }
        // 數據庫裏面所有的表
        List<String> allTableName = allTableName();
        if (allTableName == null || allTableName.isEmpty()) {
            return null;
        }
        // 全部轉換成小寫
        allTableName = allTableName.stream().map(String::toLowerCase).collect(Collectors.toList());
        List<TwoTuple<Long, Long>> singleTableConditionList = Lists.newArrayList();
        // 我們已經確定了當前是按小時分表的,表名類似於 StatisAccHour2019083122 的形式,先確定指定的時間範圍裏面有多少張表
        while (startTimeDate.isBefore(endTimeDate) || startTimeDate.equals(endTimeDate)) {
            String tableName = STRATEGY.tableName(BASE_TABLE_NAME, String.valueOf(DbDataTimeUtils.dateTime2Long(startTimeDate)));
            if (allTableName.contains(tableName.toLowerCase())) {
                // 有這個表存在
                Long singleTableStartTime = DbDataTimeUtils.dateTime2Long(startTimeDate);
                if (singleTableStartTime < startTime) {
                    singleTableStartTime = startTime;
                }
                singleTableConditionList.add(new TwoTuple<>(singleTableStartTime, endTime));
            }
            startTimeDate = startTimeDate.plusHours(1);
        }
        if (singleTableConditionList.isEmpty()) {
            return null;
        }
        List<AccHour> retList = Lists.newArrayList();
        for (TwoTuple<Long, Long> item : singleTableConditionList) {
            retList.addAll(accHourMapper.selectLIst(item.getFirst(), item.getSecond()));
        }

        return retList;
    }
}

      關於Spring Boot Mybatis實現分表功能,整個的實現邏輯就這麼多。估計上面很多地方我們也沒講明白,可能有些地方認爲簡單就沒講。所以這裏面把整個實現源碼的鏈接地址給出來 https://github.com/tuacy/java-study/tree/master/tableshard 推薦大家去看下具體源碼是怎麼實現的,有什麼疑問可以留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章