一、背景
随着项目的不断运行数据库中单表数据不断的在增大,已经达到了千万级别以上。此时单表数据的查询与修改都很困难,系统查询时间严重超时,客户端接口返回经常出现错误。newsql(TIDB,OCEANBASE)成本高。故选择分库分表方案,降低单表的数据量
二、方案
动态数据源 切换与sharding jdbc 相结合,实现可选择的动态切分数据表,
三、动态数据源
-
调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map,并将新的数据源信息添加到map中,并替换targetdatasources中的map
-
切换数据源时可以使用@DataSource(value = “数据源名称”),或者DynamicDataSourceContextHolder.setContextKey(“数据源名称”)
package com.dwl.datasource.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
/**
* 动态数据源
* 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map,并将新的数据源信息添加到map中,并替换targetdatasources中的map
* 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称")
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
//备份所有数据源信息,
private Map<Object, Object> defineTargetDataSources;
/**
* 决定当前线程使用哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}
}
package com.dwl.datasource.config;
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;
/**
-
数据源切换处理
-
@author dongwei */ @Slf4j public class DynamicDataSourceHolder { ////
-
保存动态数据源名称 */ private static final ThreadLocal DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
-
设置/切换数据源,决定当前线程使用哪个数据源 */ public static void setDynamicDataSourceKey(String key){ log.info(“数据源切换为:{}”,key); DYNAMIC_DATASOURCE_KEY.set(key); }
-
获取动态数据源名称,默认使用mater数据源 */ public static String getDynamicDataSourceKey(){ String key = DYNAMIC_DATASOURCE_KEY.get(); return key == null ? “default” : key; }
-
移除当前数据源 */ public static void removeDynamicDataSourceKey(){ log.info(“移除数据源:{}”,DYNAMIC_DATASOURCE_KEY.get()); DYNAMIC_DATASOURCE_KEY.remove(); } }
-
动态注册sharding数据源
将 ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, properties);生成的数据源注入到DynamicDataSource dynamicDataSource中,利用spring aop管理数据源,使得利用注解可以切换数据源
三、分表策略
1、建表
create table sharding.sharding_data_source
(
id int auto_increment comment '自增主键'
primary key,
rule_name varchar(255) null comment '环境前缀',
source_type varchar(255) null comment '数据源类型,(sharding,general)',
database_type varchar(255) null comment '0:Mysql 1:SQLServer',
url varchar(255) null comment '连接地址',
user_name varchar(255) null comment '数据源 用户名',
password varchar(255) null comment '数据源密码',
driver_class varchar(255) null comment '驱动全类名',
master_slave_type varchar(255) null comment '主(master) 从(slave)',
sharding_column varchar(255) null comment '分库的列',
data_node_name varchar(255) null comment '数据节点名称'
)
charset = utf8mb3;
create table sharding.sharding_data_source_detail
(
id int auto_increment comment '自增id'
primary key,
data_source_id int null comment '数据源id',
logical_table varchar(255) null comment '逻辑表名',
sharding_column varchar(255) null comment '分表列',
actual_table varchar(255) null comment '物理表名'
)
charset = utf8mb3;
sharding_data_source 是数据源表,通过此表中的数据来建立新的数据源 sharding.sharding_data_source_detail为具体分表策略表,将分表的字段与规则加入到此表中。当项目启动时,利用CommandLineRunner 将表中数据加载到 jvm中。
四、分表策略
项目是以租户的saas项目,结合项目实际需要,利用租户id字段 domain_id与时间自由组合分表。采用sharding jdbc 的 ComplexKeysShardingAlgorithm 分片策略,实现了组合id与其他建的 笛卡尔积式的分表。
package com.dwl.dwlshardingjdbc.config.algorithm;
import cn.hutool.core.date.DateUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;
import java.util.*;
public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {
@Override
public Collection<String> doSharding(Collection collection, ComplexKeysShardingValue complexKeysShardingValue) {
String logicTableName = complexKeysShardingValue.getLogicTableName();
Map<String,Range> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
Map<String,Collection> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
Set<String> valueKeys = columnNameAndShardingValuesMap.keySet();
Map<String,List<String>> keyValueMap = Maps.newConcurrentMap();
for (String valueKey : valueKeys) {
List<String> suffixList = keyValueMap.get(valueKey);
if(suffixList==null){
suffixList = Lists.newArrayList();
}
Collection collection1 = columnNameAndShardingValuesMap.get(valueKey);
for (Object o : collection1) {
if(o instanceof Date){
if(!suffixList.contains(DateUtil.format((Date) o, "yyyyMM"))){
suffixList.add(DateUtil.format((Date) o, "yyyyMM"));
}
}else {
if(!suffixList.contains(o.toString())){
suffixList.add(o.toString());
}
}
}
keyValueMap.put(valueKey,suffixList);
}
Set<String> rangeKeyStrings = columnNameAndRangeValuesMap.keySet();
for (String rangeKeyString : rangeKeyStrings) {
List<String> suffixList = keyValueMap.get(rangeKeyString);
if(suffixList==null){
suffixList = Lists.newArrayList();
}
Range range = columnNameAndRangeValuesMap.get(rangeKeyString);
for (Object s : collection) {
String autualTableName = s.toString();
String replace = autualTableName.replace(logicTableName, "");
//如果有上限
if(range.hasUpperBound()){
Comparable comparable = range.upperEndpoint();
if(comparable instanceof Date){
comparable = DateUtil.format((Date) comparable, "yyyyMM");
}
String suffix = comparable.toString();
for (String s1 : replace.split("_")) {
if(suffix.length() == s1.length() && 0 <= suffix.compareTo(s1)){
if(!suffixList.contains(s1)){
suffixList.add(s1);
}
}
}
}
//如果有下限
if(range.hasLowerBound()){
Comparable comparable = range.lowerEndpoint();
if(comparable instanceof Date){
comparable = DateUtil.format((Date) comparable, "yyyyMM");
}
String suffix = comparable.toString();
for (String s1 : replace.split("_")) {
if(suffix.length() == s1.length() && 0 >= suffix.compareTo(s1)){
if(!suffixList.contains(s1)){
suffixList.add(s1);
}
}
}
}
}
keyValueMap.put(rangeKeyString,suffixList);
}
// 利用笛卡尔积 求返回的结果表
List<List<String>> tempValueList = Lists.newArrayList(keyValueMap.values());
List<List<String>> result = Lists.cartesianProduct(tempValueList);
// List<List<String>> result = Lists.newArrayList();
// this.descartes(tempValueList,result,0,Lists.newArrayList());
List<String> shardingSuffix = Lists.newArrayList();
for (List<String> strings : result) {
List<String> tempList = Lists.newArrayList();
tempList.addAll(strings);
String suffix = String.join("_",tempList);
Collections.reverse(tempList);
String suffixReverse = String.join("_", tempList);
for (Object o : collection) {
String actualTable =o.toString();
if(actualTable.contains(suffix)|| actualTable.contains(suffixReverse)){
shardingSuffix.add(actualTable);
}
}
}
return shardingSuffix;
}
/**
* 求笛卡尔积
* @param dimvalue
* @param result
* @param layer
* @param curList
*/
private void descartes(List<List<String>> dimvalue, List<List<String>> result, int layer, List<String> curList) {
if (layer < dimvalue.size() - 1) {
if (dimvalue.get(layer).isEmpty()) {
descartes(dimvalue, result, layer + 1, curList);
} else {
for (int i = 0; i < dimvalue.get(layer).size(); i++) {
List<String> list = Lists.newArrayList(curList);
list.add(dimvalue.get(layer).get(i));
descartes(dimvalue, result, layer + 1, list);
}
}
} else if (layer == dimvalue.size() - 1) {
if (dimvalue.get(layer).isEmpty()) {
result.add(curList);
} else {
for (int i = 0; i < dimvalue.get(layer).size(); i++) {
List<String> list = Lists.newArrayList(curList);
list.add(dimvalue.get(layer).get(i));
result.add(list);
}
}
}
}
}
五、事务问题
在采用里分表的方法上增加@ShardingTransactionType(TransactionType.LOCAL) @Transactional(rollbackFor = Exception.class)实现事务的管理。也可以采用分布式事务 seata
六、历史数据迁移问题
利用mybatis plus 的流查询 查询出表中所有数据,然后对数据进行一一处理
@Select("select * from ${tableName} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(Map.class)
void getOrgWithBigData(@Param("tableName") String tableName, ResultHandler handler);
每1000条数据为一组插入到目标中去,注意最后一次不足1000条,需要格外处理。(查询出所有的记录数,与处理进度进行对比。别采用 information_schema中的记录数,不准确)
七、表结构变更与索引创建问题
通过提供的接口走数据源进行sql执行,经测试会自动分库分表