springboot+shardingsphere+druid 实现基于时间的水平分表
第一章 引入 shardingsphere
`
文章目录
前言
例如:随数据量不断增加,我们不得不想办法解决单表性能瓶颈。最好的方法是使用现有中间件实现分表以突破单表的性能瓶颈
一、分表是什么?
分表是指将一个大表拆分成多个小表,每个小表只包含部分数据。分表的目的是为了提高数据库的性能和可扩展性。
二、 分表方式
垂直分表:将一个表按照字段的含义或关系拆分成多个子表。比如,将一个包含用户信息和订单信息的表分为一个用户表和一个订单表。
水平分表:将一个表按照数据行的范围或规则拆分成多个子表。常见的水平分表方式包括基于范围的分表、基于散列的分表和基于时间的分表。
分库分表:将一个数据库按照一定的规则拆分成多个子数据库,每个子数据库中包括多个子表。这种方式适用于数据量非常大的情况,可以通过分布式架构来提升系统的性能和扩展性。
这个系列我们主要讲基于时间的水平分表
三、shardingsphere是什么?
ShardingSphere是一个开源的数据库分片中间件,旨在帮助用户实现数据库的水平拆分和分布式存储。它提供了一套完整的分片、分库、分表的解决方案,支持多种数据库,包括MySQL、Oracle、SQL Server等。
四、使用
1.引入包
<!--分库分表中间件-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
2.配置
spring:
shardingsphere:
datasource:
#数据源名称
names: d0
#该数据源的分片配置
d0:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
#oracel和mysql这种数据库 驱动会根据 url获取到
driver-class-name: oracle.jdbc.driver.OracleDriver
username: name
password: password
#默认数据源,未分片的表默认执行库
sharding:
default-data-source-name: d0
#详细的分片
rules:
d0:
#这里只设置了默认的算法 具体的配置在启动时进行,当然可以直接获取配置
defaultTableStrategy:
standard:
shardingColumn: field001
shardingAlgorithmName: createTimeDayTableShardingAlgorithm
enabled: true
# 展示修改以后的sql语句
props:
sql-show: true
datasource:
dynamic:
primary: master
strict: false
p6spy: false
datasource:
url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
username: name
password: password
3.将sharding加入dynamic数据源
@Configuration
@AutoConfigureBefore({DynamicDataSourceAutoConfiguration.class,
SpringBootConfiguration.class})
public class DataSourceConfiguration {
/**
* 动态数据源配置项
* 这里会根据yml文件的配置自动加载配置,将多个数据源信息放到datasourceMap中
*/
@Autowired
private DynamicDataSourceProperties properties;
/**
* 使用shardingSphereDataSource 自动装载的 DataSource
* 5.1.1版本自动装载的shardingSphereDataSource beanName="shardingSphereDataSource"
* 要加@Lazy
*/
@Lazy
@Autowired
private DataSource shardingSphereDataSource;
@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new AbstractDataSourceProvider() {
@Override
public Map<String, DataSource> loadDataSources() {
Map<String, DataSource> dataSourceMap = createDataSourceMap(datasourceMap);
// 将 shardingjdbc 管理的数据源也交给动态数据源管理
dataSourceMap.put(InitActualDataNodesAO.SHARDING_DB_NAME, shardingSphereDataSource);
return dataSourceMap;
}
};
}
/**
* 将动态数据源设置为首选的
* 当spring存在多个数据源时, 自动注入的是首选的对象
* 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了
*
* @return
*/
@Primary
@Bean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
}
4.启动时加载节点数据
初始化流程:
加载shardingSphereDataSource的contextManager
获取原有的动态节点
更新contextManager
数据更新流程:
创建对应的Node
更新contextManager
@Slf4j
@Component
public class DynamicNodeUtil{
@Resource
private ShardingSphereDataSource shardingSphereDataSource;
@Resource
private Collection<RuleConfiguration> ruleConfigs;
@Lazy
@Autowired
private IReportFormShardingDataService reportFormShardingDataService;
/**
* 数据源的表名
*/
public static final String SHARDING_DB_NAME = "sharding";
public static final String AUTO_TABLES_PREFIX = SHARDING_DB_NAME+".";
/**
* sharding-jdcb中datasource的name
*/
private static final String schemaName = "logic_db";
private ContextManager contextManager ;
private AlgorithmProvidedShardingRuleConfiguration apsrc;
private Collection<ShardingTableRuleConfiguration> tables = new LinkedList<>();
/**
* 初始化所有表
*/
@PostConstruct
public void init() throws Exception {
//加载shardingSphereDataSource的contextManager
initShardingJdbcContextManager();
//获取AlgorithmProvidedShardingRuleConfiguration
initConfiguration();
//查询所有分表的数据
//循环添加至tables
//更新
//查询表并生成对应的动态节点 循环加入并更新
List<ReportFormShardingData> reportFormShardingDataList = reportFormShardingDataService.findAllReportFormShardingData();
updateShardRulConfigByReportFormShardingDataList(reportFormShardingDataList);
}
/**
* 初始化时获取contextManager
*/
private void initShardingJdbcContextManager() throws Exception {
Class clazz = shardingSphereDataSource.getClass();
Field contextManagerF = clazz.getDeclaredField("contextManager");
contextManagerF.setAccessible(true);
contextManager = (ContextManager) contextManagerF.get(shardingSphereDataSource);
//若没有首次的configurations刷新contextManager 会导致contextManager只存在动态分片节点 其他表会被清空导致链接普通表查询异常
Collection<RuleConfiguration> configurations = contextManager.getMetaDataContexts()
.getMetaData().getDatabase(schemaName)
.getRuleMetaData()
.getConfigurations();
contextManager.alterRuleConfiguration(schemaName,configurations);
}
/**
* 初始化获取AlgorithmProvidedShardingRuleConfiguration
*/
private void initConfiguration() {
apsrc = (AlgorithmProvidedShardingRuleConfiguration) ruleConfigs.stream().findFirst().orElseGet(() -> null);
}
public void updateShardRulConfigByReportFormShardingDataList(List<ReportFormShardingData> reportFormShardingDataList ) {
for(ReportFormShardingData reportFormShardingData: reportFormShardingDataList ){
addShardingTableRuleConfiguration(reportFormShardingData);
}
updateShardRuleActualDataNodes();
}
/**
* 单库分片节点更新
* @content更新sharding-jdbc的分片
*
*/
public void updateShardRulConfig( String tableName,String startYear,String endYear){
addShardingTableRuleConfiguration(tableName, startYear, endYear);
updateShardRuleActualDataNodes();
}
/**
* 创建ShardingTableRuleConfiguration并添加如tablse
* @param tableName
* @param startYear
* @param endYear
*/
private void addShardingTableRuleConfiguration(String tableName, String startYear, String endYear){
tables.add( createShardingTableRuleCOnfiguration(tableName, startYear, endYear));
}
private void addShardingTableRuleConfiguration(ReportFormShardingData reportFormShardingData){
tables.add( createShardingTableRuleCOnfiguration(reportFormShardingData.getTableName(), reportFormShardingData.getStartYear(), reportFormShardingData.getEndYear()));
}
/**
* 创建ShardingTableRuleConfiguration
*/
private ShardingTableRuleConfiguration createShardingTableRuleCOnfiguration(String tableName, String startYear, String endYear) {
String logicTable = tableName;
//结果 sharding.tableName_1_$->{2022..2023}
String actualDataNodes = AUTO_TABLES_PREFIX+ tableName +"_$->{"+ startYear +".."+ endYear +"}";
return new ShardingTableRuleConfiguration(logicTable,actualDataNodes);
}
/**
*
* 更新节点
*
*/
private void updateShardRuleActualDataNodes( ) {
apsrc.setTables(tables);
contextManager.alterRuleConfiguration(schemaName,ruleConfigs);
}
public void test(){
//将tableName_1的两个分片2022 和2023加载至动态节点
updateShardRulConfig("tableName_1","2022","2023");
}
}
对应的实体
@Data
@NoArgsConstructor
public class ReportFormShardingData extends BaseDomain {
private static final long serialVersionUID = 1123123465465465L;
/**
* 开始年份
*/
@Excel(name = "开始年份")
private String startYear;
/**
* 结束年份
*/
@Excel(name = "结束年份")
private String endYear;
/**
* 表名
*/
@Excel(name = "表名")
private String tableName;
/**
* id
*/
private String id;
public ReportFormShardingData(String satrtYear, String endYear, String tableName ) {
this.id = SequenceUtil.makeStringId();
this.tableName = tableName;
this.startYear = satrtYear;
this.endYear = endYear;
}
}
5、分片算法
sharding或加载集成StandardShardingAlgorithm 的bean,对应的算法名称就是bean的名称createTimeDayTableShardingAlgorithm
@Component
public class CreateTimeDayTableShardingAlgorithm implements StandardShardingAlgorithm<String> {
/**
* 分隔符
*/
private final static String CUT = "_";
/**
* 初始配置
*/
private Properties props = new Properties();
/**
* @Description 精确分片算法类名称,用于=和IN
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
String value = preciseShardingValue.getValue();
if (collection == null || (collection.stream().findFirst().orElse(null)) == null || value == null) {
throw new IllegalArgumentException("sharding jdbc not find logic table,please check config");
}
//查询表是否存在,不存在则新建并更新
String targetTableName = createTableName(preciseShardingValue.getLogicTableName(), value);
if (collection.contains(targetTableName)) {
return targetTableName;
} else {
throw new IllegalArgumentException("sharding jdbc not find logic table,No date specified,(single) " + value);
}
}
/**
* @Description 范围分片算法类名称,用于BETWEEN等
*/
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<String> rangeShardingValue) {
return new ArrayList<>();
}
/**
* 根据源表名和路由生成目标表名
*
* @param logicTable 源表名
* @param route 路由,如年份规则2023
* @return 目标表名
*/
public static String createTableName(String logicTable, String route) {
// route.substring(0,4);
return String.format("%s%s%s", logicTable, CUT, route);
}
@Override
public String getType() {
return "CLASS_BASED";
}
@Override
public Properties getProps() {
return this.props;
}
@Override
public void init(Properties properties) {
this.props = properties;
}
@PostConstruct
public void init(){
//这两个不写会直接报错
this.props.put("strategy", "STANDARD");
this.props.put("algorithmClassName",this.getClass().getCanonicalName());
}
}
6、使用
因为使用了dynamic且主数据源不变,所以正常节点的sql不需要改变其他的sql 需要使用分片的sql在service接口添加多数据源标志@DS(“sharding”)
@DS("sharding")
public List<Map<String, Object>> findData(String sql) {
return mapper.findData(sql);
}
总结
使用shardingsphere中间件可以方便的进行分表查询,但是对应分页和其他复杂sql需要进行调整;
通过更新contextManager实现动态分片时更新时间过长,后续尝试优化。