前言:
由于业务订单增长,设计之初没有考虑到订单量会达到亿级以上,从而导致现在订单服务查询耗时太久,为解决该问题,并结合业务实际情况,通过调研最终采用shardingsphere分库分表架构对订单数据按年分库,按业务code和月分表,最终分布到每张表的数据量尽可能的减少到百万以内,优化查询效果。
关于shardingsphere的介绍和基本使用可参考ShardingSphere介绍与使用_还没秃的小菜鸡的博客-CSDN博客_shardingsphere
官网地址: 概览 :: ShardingSphere
目前shardingshpere支持5.0以上版本,为了避免高版本带来的一些坑,作者这里选用4.1.1 的springboot起步依赖包
pom依赖如下:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
数据库表结构:
CREATE TABLE `insurance_order` (
`subOrderId` varchar(128) NOT NULL COMMENT '子订单',
`orderId` varchar(128) NOT NULL COMMENT '保险订单号',
`otaOrderId` varchar(128) NOT NULL COMMENT 'ota的主订单',
`merchantId` varchar(32) NOT NULL COMMENT '商户Id',
`insurCompanyCode` varchar(32) NOT NULL COMMENT '保险公司id',
`policyNo` varchar(128) NOT NULL DEFAULT '' COMMENT '保单号',
`proposalNo` varchar(128) NOT NULL DEFAULT '' COMMENT '核保订单号',
`insurType` varchar(32) NOT NULL DEFAULT '' COMMENT '保险类别',
`productCode` varchar(32) NOT NULL DEFAULT '' COMMENT '保险产品代码',
`premium` double(10,2) NOT NULL DEFAULT '0.00' COMMENT '总保费',
`amount` double(20,2) NOT NULL DEFAULT '0.00' COMMENT '保额',
`createTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间'
。。。部分省略
PRIMARY KEY (`subOrderId`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='保险订单';
核心代码:
@SpringBootConfiguration
public class InsuranceOrderShardingConfig {
//逻辑表名称
private final String LOGICAL_TABLE = "insurance_order";
//数据库分片键
private final String DATABASE_SHARDING_COLUMN = "createTime";
//表分片键
private final String TABLE_SHARDING_COLUMN = "createTime,insurCompanyCode";
@Bean
DataSource getShardingDataSource() throws SQLException {
// 分片规则配置对象
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
// 规则配置
shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
// 默认库,如果存在广播表和绑定表也可在此配置
shardingRuleConfig.setDefaultDataSourceName("default_dataSource");
final Properties properties = new Properties();
// 开启日志打印
properties.setProperty("sql.show", "true");
// 数据源配置
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, properties);
}
//定义物理表建表规则
// 关于inline表达式,可查看官方文档 https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/sharding/other-features/inline-expression/
private TableRuleConfiguration getOrderTableRuleConfiguration() {
String expression = "2020." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2020_${1..9}," +
"2020." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2020_${10..12}," +
"2021." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2021_${1..9}," +
"2021." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2021_${10..12}," +
"2022." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2022_${1..9}," +
"2022." + LOGICAL_TABLE + "_${['pingan','taibao','yangguang','renbao','renshou']}" + "_2022_${10..12}";
TableRuleConfiguration ruleConfiguration = new TableRuleConfiguration("insurance_order", expression);
ruleConfiguration.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(DATABASE_SHARDING_COLUMN,new DatabaseByYearShardingAlgorithm(), new RangeDatabaseShardingAlgorithm()));
ruleConfiguration.setTableShardingStrategyConfig(new ComplexShardingStrategyConfiguration(TABLE_SHARDING_COLUMN, new TableByCodeAndMonthShardingAlgorithm()));
return ruleConfiguration;
}
private Map<String, DataSource> createDataSourceMap() {
// key为数据源名称,后面分片算法取得就是这个,value为具体的数据源
final HashMap<String, DataSource> shardingDataSourceMap = new HashMap<>();
shardingDataSourceMap.put("default_dataSource", DataSourceUtil.createDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/sharding?userUnicode=ture&characterEncoding=utf8&serverTimezone=GMT%2B8",
"root",
"123456"));
shardingDataSourceMap.put("2020", DataSourceUtil.createDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/2020_insurance_order?userUnicode=ture&characterEncoding=utf8&serverTimezone=GMT%2B8",
"root",
"123456"));
shardingDataSourceMap.put("2021", DataSourceUtil.createDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/2021_insurance_order?userUnicode=ture&characterEncoding=utf8&serverTimezone=GMT%2B8",
"root",
"123456"));
shardingDataSourceMap.put("2022", DataSourceUtil.createDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/2022_insurance_order?userUnicode=ture&characterEncoding=utf8&serverTimezone=GMT%2B8",
"root",
"123456"));
return shardingDataSourceMap;
}
}
DatabaseByYearShardingAlgorithm,RangeDatabaseShardingAlgorithm为按年分库的分片算法实现类,这里采用的是shardingsphere的StandardShardingStrategyConfiguration标准分片策略
PreciseShardingAlgorithm精确分片算法
**
* @desc 按年分库精确分片算法
*/
public class DatabaseByYearShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Date> preciseShardingValue) {
Date createTime = preciseShardingValue.getValue();
int year = DateUtil.year(createTime);
for (String database : collection) {
if (Objects.equals(database,String.valueOf(year))) {
return database;
}
}
throw new UnsupportedOperationException();
}
}
RangeShardingAlgorithm范围分片算法
/**
* @Description: 数据库范围分片算法
*/
public class RangeDatabaseShardingAlgorithm implements RangeShardingAlgorithm<Date> {
/**
* 范围分片算法
*
* @param availableTargetNames 所有配置的库列表
* @param rangeShardingValue 分片值,也就是createTime的值,范围分片算法必须提供开始时间和结束时间
* @return 所匹配库的结果
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> rangeShardingValue) {
final ArrayList<String> result = new ArrayList<>();
Range<Date> range = rangeShardingValue.getValueRange();
Date startTime = range.lowerEndpoint();
Date endTime = range.upperEndpoint();
// 起始年和结束年
int startYear = DateUtil.year(startTime);
int endYear = DateUtil.year(endTime);
Collection<String> databaseList = startYear == endYear ? theSameYear(String.valueOf(startYear), availableTargetNames, result) : differentYear(startYear, endYear, availableTargetNames, result);
return databaseList;
}
// 起始年跟结束年在同一年,只需要查一个数据库
private Collection<String> theSameYear(String startTime, Collection<String> availableTargetNames, ArrayList<String> result) {
for (String availableTargetName : availableTargetNames) {
if (availableTargetName.endsWith(startTime)) {
result.add(availableTargetName);
}
}
return result;
}
// 跨年,查多个数据库
private Collection<String> differentYear(int startYear, int endYear, Collection<String> availableTargetNames, ArrayList<String> result) {
for (String availableTargetName : availableTargetNames) {
for (int i = startYear; i <= endYear; i++) {
if (availableTargetName.endsWith(String.valueOf(i))) {
result.add(availableTargetName);
}
}
}
return result;
}
}
TableByCodeAndMonthShardingAlgorithm是通过insurCompanyCode和createTime两个字段的复合分片键进行分表的分片算法实现类,这里采用的是shardingsphere的ComplexShardingStrategyConfiguration复合分片算法策略
ComplexKeysShardingAlgorithm复合分片算法
public class TableByCodeAndMonthShardingAlgorithm implements ComplexKeysShardingAlgorithm {
@Override
public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue complexKeysShardingValue) {
Map<String, Collection> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
ArrayList<String> monthList = new ArrayList<>();
ArrayList<String> codeList = new ArrayList<>();
ArrayList<String> columnNameAndShardingList = new ArrayList<>();
ArrayList<String> tables = new ArrayList<>();
//精确分片
if (CollectionUtil.isNotEmpty(columnNameAndShardingValuesMap)) {
Collection<Date> dateCollection = (Collection) columnNameAndShardingValuesMap.get("createtime");
if (CollectionUtil.isEmpty(dateCollection)) {
dateCollection = (Collection) columnNameAndShardingValuesMap.get("createTime");
}
if (CollectionUtil.isNotEmpty(dateCollection)) {
for (Date date : dateCollection) {
monthList.add(DateUtil.year(date) + "_" + (DateUtil.month(date) + 1));
}
}
Collection<String> insurCompanyCodeList = (Collection) columnNameAndShardingValuesMap.get("insurcompanycode");
if (CollectionUtil.isEmpty(insurCompanyCodeList)) {
insurCompanyCodeList = (Collection) columnNameAndShardingValuesMap.get("insurCompanyCode");
}
//再通过业务code过滤
if (CollectionUtil.isNotEmpty(insurCompanyCodeList)) {
for (String code : insurCompanyCodeList) {
codeList.add(code);
}
}
if (CollectionUtil.isNotEmpty(codeList) && CollectionUtil.isNotEmpty(monthList)) {
for (String code : codeList) {
for (String month : monthList) {
columnNameAndShardingList.add(code + "_" + month);
}
}
} else if (CollectionUtil.isNotEmpty(codeList)) {
for (String code : codeList) {
columnNameAndShardingList.add(code);
}
} else if (CollectionUtil.isNotEmpty(monthList)) {
for (String month : monthList) {
columnNameAndShardingList.add(month);
}
} else {
logger.error("未按分片键查询,不支持此类操作");
throw new UnsupportedOperationException();
}
if (CollectionUtil.isNotEmpty(columnNameAndShardingList)) {
for (Object tableName : availableTargetNames) {
if (tableName == null) {
continue;
}
for (String target : columnNameAndShardingList) {
String lowerCase = target.toLowerCase();
if (tableName.toString().contains(lowerCase)) {
tables.add(tableName.toString());
break;
}
}
}
}
}
Map<String, Range<Date>> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
if (CollectionUtil.isNotEmpty(columnNameAndRangeValuesMap)) {
Range<Date> dateRange = columnNameAndRangeValuesMap.get("createTime");
Date start = dateRange.lowerEndpoint();
Date end = dateRange.upperEndpoint();
int startYear = DateUtil.year(start);
int endYear = DateUtil.year(end);
int startMonth = DateUtil.month(start) + 1;
int endMonth = DateUtil.month(end) + 1;
if (startYear > endYear) {
throw new UnsupportedOperationException();
}
List<String> finalTables = null;
//通过精确分片过滤的数据库表集合不为空
if (CollectionUtil.isNotEmpty(tables)) {
finalTables = getResults(tables, startYear, endYear, startMonth, endMonth);
} else {
for (Object targetName : availableTargetNames) {
if (targetName == null) {
continue;
}
tables.add(targetName.toString());
}
//无精确分片过滤的表集合
finalTables = getResults(tables, startYear, endYear, startMonth, endMonth);
}
return finalTables;
}
return tables;
}
private List<String> getResults(ArrayList<String> tables, int startYear, int endYear, int startMonth, int endMonth) {
List<String> finalTables = new ArrayList<>();
if (startYear == endYear) {
//同年
for (String tableName : tables) {
for (int i = startMonth; i <= endMonth; i++) {
if (tableName.endsWith("_" + i)) {
finalTables.add(tableName);
break;
}
}
}
return finalTables;
} else if ((endYear - startYear) == 1) {
//跨一年
String tableNameOne = tables.get(0);
if (tableNameOne.contains(startYear + "")) {
for (String tableName : tables) {
for (int i = startMonth; i <= 12; i++) {
if (tableName.endsWith("_" + i)) {
finalTables.add(tableName);
break;
}
}
}
return finalTables;
} else if (tableNameOne.contains(endYear + "")) {
for (String tableName : tables) {
for (int i = 1; i <= endMonth; i++) {
if (tableName.endsWith("_" + i)) {
finalTables.add(tableName);
break;
}
}
}
return finalTables;
}
} else {
//跨两年及以上
logger.error("跨两年及以上的多数据库查询,影响性能,暂不支持此操作");
throw new UnsupportedOperationException();
}
return null;
}
}
最终效果:
由于shardingsphere不能自动数据库和表,在使用前,必须先创建好数据库,然后写个测试类,写入建表脚本,这里就可以根据你的规则创建好相应的物理表了,然后新增一些测试数据,可以看到数据会按照你相应的规则插入到指定的物理表上啦,这样按照不通维度的分库分表,能减少落到每张表的数据大大减少,查询的时候一定要注意,查询条件一定要带上你分表的分片键,这样才能路由到指定的表,缩小查询范围,提高查询效率。
更多的使用技能请移步官网:概览 :: ShardingSphere
如有不妥,望各路大神批评指正。