分库分表自定义策略¶
首先捋一下整体的分库分表的思路:
针对link_group分组的数据,根据用户的account_no字段进行分库,前面有提到过用户的account_no字段是一串数字,所以根据其对2取余得到的结果进行分库
针对short_link短链的数据,根据短链码code进行分库分表,在进行短链码生成的时候可以在其前后分别添加一位,前面一位用于标识库位,后面一位用于标识表位。
至于分库分表的逻辑则是在DataSourceConfig数据源配置中进行配置
首先创建两个数据源配置
private DataSource createDataSource0() {
com.zaxxer.hikari.HikariDataSource ds = new com.zaxxer.hikari.HikariDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl(dbUrl1);
ds.setUsername(dbUser);
ds.setPassword(dbPassword);
// 添加连接池优化配置 [8](@ref)
ds.setMinimumIdle(5); // 最小空闲连接数
ds.setMaximumPoolSize(20); // 最大连接数
ds.setIdleTimeout(30000); // 空闲连接超时时间(毫秒)
ds.setMaxLifetime(1800000); // 连接最大生命周期(毫秒)
ds.setConnectionTimeout(30000); // 连接获取超时时间(毫秒)
ds.setLeakDetectionThreshold(60000); // 连接泄漏检测阈值(毫秒)
ds.setConnectionTestQuery("SELECT 1"); // 连接验证查询
return ds;
}
private DataSource createDataSource1() {
com.zaxxer.hikari.HikariDataSource ds = new com.zaxxer.hikari.HikariDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl(dbUrl2);
ds.setUsername(dbUser);
ds.setPassword(dbPassword);
// 添加连接池优化配置 [8](@ref)
ds.setMinimumIdle(5); // 最小空闲连接数
ds.setMaximumPoolSize(20); // 最大连接数
ds.setIdleTimeout(30000); // 空闲连接超时时间(毫秒)
ds.setMaxLifetime(1800000); // 连接最大生命周期(毫秒)
ds.setConnectionTimeout(30000); // 连接获取超时时间(毫秒)
ds.setLeakDetectionThreshold(60000); // 连接泄漏检测阈值(毫秒)
ds.setConnectionTestQuery("SELECT 1"); // 连接验证查询
return ds;
}
整体的分库分表逻辑配置
// 1. 配置数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 通过put,可以配置多个数据源
dataSourceMap.put("ds0", createDataSource0());
dataSourceMap.put("ds1", createDataSource1());
// 2. 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getLinkGroupRuleConfiguration());
shardingRuleConfig.getTableRuleConfigs().add(getShortLinkRuleConfiguration());
// 添加多个表的分片规则,可以配置多个方法
// shardingRuleConfig.getTableRuleConfigs().add(getTableRuleConfiguration2());
Properties properties = new Properties();
// 自定义方法生成workId
properties.setProperty("worker.id", snowFlakeWordIdConfig.getWorkerId()); // 设置工作节点ID
// 配置主键生成规则
KeyGeneratorConfiguration keyGeneratorConfiguration = new KeyGeneratorConfiguration(
"SNOWFLAKE", // 内置雪花算法类型生成id
"id", // 主键字段名
properties // 配置属性
);
// 直接设置配置对象,而非生成器实例
shardingRuleConfig.setDefaultKeyGeneratorConfig(keyGeneratorConfiguration);
// 3. 创建 ShardingSphere 数据源
return ShardingDataSourceFactory.createDataSource(
dataSourceMap,
shardingRuleConfig,
new Properties()
);
针对LinkGroup和ShortLink的分库分表规则
private TableRuleConfiguration getLinkGroupRuleConfiguration() {
// 使用正确的分表表达式
TableRuleConfiguration result = new TableRuleConfiguration(
"link_group",
"ds$->{0..1}.link_group"
);
// 设置分库策略,根据account_no字段进行分库
result.setDatabaseShardingStrategyConfig(
new InlineShardingStrategyConfiguration("account_no", "ds$->{account_no % 2}")
);
return result;
}
private TableRuleConfiguration getShortLinkRuleConfiguration() {
StandardShardingStrategyConfiguration databaseShardingStrategy =
new StandardShardingStrategyConfiguration(
"code",
new CustomDBPreciseShardingAlgorithm() // 实例化您的自定义分库算法
);
StandardShardingStrategyConfiguration tableShardingStrategy =
new StandardShardingStrategyConfiguration(
"code",
new CustomTablePreciseShardingAlgorithm() // 实例化您的自定义分库算法
);
// 关键修复:使用正确的分表表达式
TableRuleConfiguration result = new TableRuleConfiguration(
"short_link",
"ds$->{0..1}.short_link_$->{0..1}"
);
// 设置分库策略,根据短链码code字段进行分库
result.setDatabaseShardingStrategyConfig(
databaseShardingStrategy
);
// 设置分表策略,根据短链码code字段进行分表
result.setTableShardingStrategyConfig(
tableShardingStrategy
);
return result;
}
// ShortLink的自定义分片规则
// 分库规则
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
*
* @param collection 数据源集合
* 在分库时值为所有分片库的集合 databaseNames
* 分表时为对应分库中所有分片表的集合 tablesNames
* 分库场景:值为所有分片库的名称集合(如 ["ds0", "ds1", "ds2"])。
* 分表场景:值为当前分片库中所有分片表的名称集合(如 ["t_order_0", "t_order_1"])。
* @param preciseShardingValue 分片的属性
* logicTableName(逻辑表名,如 t_order)
* columnName(分片键字段名,如 short_code)
* value(分片键的具体值,如 "A123456")
* @return
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
// 获取短链码第一位,即库位
String codePrefix = preciseShardingValue.getValue().substring(0,1);
for (String targetName:collection) {
//获取库名的最后一位
String targetNameSuffix = targetName.substring(targetName.length()-1);
if (codePrefix.equals(targetNameSuffix)){
return targetName;
}
}
return null;
}
}
// 分表规则
public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
*
* @param availableTargetNames 数据源集合
* 在分库时值为所有分片库的集合 databaseNames
* 分表时为对应分库中所有分片表的集合 tablesNames
* 分库场景:值为所有分片库的名称集合(如 ["ds0", "ds1", "ds2"])。
* 分表场景:值为当前分片库中所有分片表的名称集合(如 ["t_order_0", "t_order_1"])。
* @param shardingValue 分片的属性
* logicTableName(逻辑表名,如 t_order)
* columnName(分片键字段名,如 short_code)
* value(分片键的具体值,如 "A123456")
* @return
*/
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<String> shardingValue) {
// 1. 获取分片键值(短链码)
String code = shardingValue.getValue();
// 2. 提取表后缀(如取最后一位)
String tableSuffix = code.substring(code.length() - 1); // 示例:code="ABC123" → "3"
// 3. 构建目标表名基础(逻辑表名_后缀)
String targetBaseName = shardingValue.getLogicTableName() + "_" + tableSuffix;
// 4. 遍历所有可用物理表名,匹配后缀
for (String physicalTable : availableTargetNames) {
// physicalTable 格式:ds0.short_link_1
if (physicalTable.endsWith(targetBaseName)) { // 后缀匹配
return physicalTable; // 返回完整物理表名
}
}
throw new IllegalStateException("无法路由到分片表:" + code);
}
}
在进行分库分表后,会有几个问题;一是库表扩容后,数据分布不均匀的问题、二是分库分表后进行数据查询时如果进行全库表查询的话,会很耗费性能
针对问题一:最直接的方法是增加新建库表的权重比,可以通过负载均衡端实现,也可自定义算法实现。我这边是使用的第二个方法
在进行库表名拼接的时候是随机返回标识,0/1,根据返回的标识进行库表名的拼接。增加权重比的话,直接在list中新增对应的0/1,增加对应表的权重比
private static final List<String> DBPrefixList = new ArrayList<>();
static {
DBPrefixList.add("0");
DBPrefixList.add("1");
}
private static Random random = new Random();
public static String getRandomPrefix(){
int index = random.nextInt(DBPrefixList.size());
return DBPrefixList.get(index);
}
private static final List<String> TablePrefixList = new ArrayList<>();
static {
TablePrefixList.add("0");
TablePrefixList.add("1");
}
private static Random random = new Random();
public static String getRandomPrefix(){
int index = random.nextInt(TablePrefixList.size());
return TablePrefixList.get(index);
}
针对问题二:可以引入nosql,比如es,对数据全量冗余存储。在进行查询时,不通过数据库,直接在es中进行查询。也可进行双向冗余,比如订单系统,针对商家和客户两个维度将订单在两个地方都存储一份。在查询时,既可根据客户主键查询订单,也可通过商户逐渐查询订单,通过空间换取查询时的高效性能。