開發過程中可能會碰到分表的場景,數據庫的數據量相當大的時候可能需要按天分表或者按月分表啥的(分表策略)。接下來就教大家用最簡單的方式實現這一需求。
咱們接下來主要實現以下兩個大功能:
- 自動建表,當表不存在的時候自動把表創建出來。
- 自動分表,根據操作數據庫的某個參數進行分表。
自動建表,自動分表核心思想在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 推薦大家去看下具體源碼是怎麼實現的,有什麼疑問可以留言。