没啥好说的,领导叫我水平分表,将业务订单数据水平分表,减少一个表中的数据量,加快数据查询。
除了shardingsphere外,其他3项都是项目中已经整合了的技术。写这篇文章主要是为了记录shardingsphere整合其他3项技术的过程。
shardingsphere定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用上来说,在你的客户端通过解析你的sql来判断你访问的某个表x是不是已经分成了形如x_1、x_2、x_3这种命名规则的表。
shardingsphere
Druid
这里只列出关键信息:
CREATE TABLE `task_info_202001` (
`id_` char(32) NOT NULL COMMENT '无业务意义主键',
`content_` json NOT NULL COMMENT '订单内容',
`create_date_` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`update_date_` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (`id_`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务表';
CREATE TABLE `task_result_202001` (
`id_` char(32) NOT NULL,
`task_id_` char(32) DEFAULT NULL COMMENT '任务ID',
`error_code_` varchar(8) DEFAULT NULL COMMENT '业务错误码',
`message_` text COMMENT '响应消息',
`result_` json DEFAULT NULL COMMENT '业务处理结果(数据)',
`create_date_` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
PRIMARY KEY (`id_`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务处理结果表';
其他月份更改表名即可。
springboot 选2.2.7的版本,2.3.0还存在一些问题。
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.7.RELEASEversion>
<relativePath/>
parent>
其他的一些选择:
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.2version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.1version>
dependency>
<dependency>
<groupId>org.apache.shardingspheregroupId>
<artifactId>sharding-jdbc-spring-boot-starterartifactId>
<version>4.1.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.22version>
dependency>
spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://****
username: *******
password: *******
filters: stat
maxActive: 20
initialSize: 1
maxWait: 1000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 1 from dual
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
sharding:
tables:
task_info:
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
actual-data-nodes: ds0.task_info_$->{2020}$->{['01','02','03','04','05','06','07','08','09','10','11','12']}
#用于单分片键的标准分片场景
table-strategy:
complex:
sharding-columns: id_,create_date_
#复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
algorithmClassName: com.example.privatebatistest.config.MonthTableComplexShardingAlgorithm
task_result:
actual-data-nodes: ds0.task_result_$->{2020}$->{['01','02','03','04','05','06','07','08','09','10','11','12']}
table-strategy:
standard:
sharding-column: task_id_
precise-algorithm-class-name: com.example.mybatistest.config.MonthTableShardingAlgorithm
binding-tables:
- task_info,task_result
default-data-source-name: ds0
defaultTableStrategy: #默认表分片策略,同分库策略
none:
props:
sql:
show: true
executor:
size: 8
server:
port: 10489
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: true
logging:
level:
root: info
ShardingSphere-SQL: debug
这里注意几点问题:
- names: ds0必须要配,多个数据源用逗号分隔。
- binding-tables的配置是为了同步task_info和task_result的变化,即限制如
task_info_202001 left join task_result_202001
同月份关联查询,而不出现如task_info_202001 left join task_result_202002
这种没有意义的关联查询。- table-strategy只能5选1,不能混合多种配置。否则初始化报错。
关于这些参数都有什么意义,可以参考这里
我将数据分片部分摘抄下来,防止这个网址失效:
dataSources: #数据源配置,可配置多个data_source_name
: # `!!`表示实例化该类
driverClassName: #数据库驱动类名
url: #数据库url连接
username: #数据库用户名
password: #数据库密码
# ... 数据库连接池的其它属性
shardingRule:
tables: #数据分片规则配置,可配置多个logic_table_name
: #逻辑表名称
actualDataNodes: #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
databaseStrategy: #分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
standard: #用于单分片键的标准分片场景
shardingColumn: #分片列名称
preciseAlgorithmClassName: #精确分片算法类名称,用于=和IN。。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
rangeAlgorithmClassName: #范围分片算法类名称,用于BETWEEN,可选。。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
complex: #用于多分片键的复合分片场景
shardingColumns: #分片列名称,多个列以逗号分隔
algorithmClassName: #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
inline: #行表达式分片策略
shardingColumn: #分片列名称
algorithmInlineExpression: #分片算法行表达式,需符合groovy语法
hint: #Hint分片策略
algorithmClassName: #Hint分片算法类名称。该类需实现HintShardingAlgorithm接口并提供无参数的构造器
none: #不分片
tableStrategy: #分表策略,同分库策略
keyGenerator:
column: #自增列名称,缺省表示不使用自增主键生成器
type: #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
: 属性名称
bindingTables: #绑定表规则列表
- , logic_table_name2, ...>
- , logic_table_name4, ...>
- , logic_table_name_y, ...>
broadcastTables: #广播表规则列表
- table_name1
- table_name2
- table_name_x
defaultDataSourceName: #未配置分片规则的表将通过默认数据源定位
defaultDatabaseStrategy: #默认数据库分片策略,同分库策略
defaultTableStrategy: #默认表分片策略,同分库策略
defaultKeyGenerator: #默认的主键生成算法 如果没有设置,默认为SNOWFLAKE算法
type: #默认自增列值生成器类型,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
props:
: #自增列值生成器属性配置, 比如SNOWFLAKE算法的worker.id与max.tolerate.time.difference.milliseconds
masterSlaveRules: #读写分离规则,详见读写分离部分
: #数据源名称,需要与真实数据源匹配,可配置多个data_source_name
masterDataSourceName: #详见读写分离部分
slaveDataSourceNames: #详见读写分离部分
loadBalanceAlgorithmType: #详见读写分离部分
props: #读写分离负载算法的属性配置
: #属性值
props: #属性配置
sql.show: #是否开启SQL显示,默认值: false
executor.size: #工作线程数量,默认值: CPU核数
max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
首先在@SpringBootApplication
存在的主类上配置
@SpringBootApplication(exclude = {
DruidDataSourceAutoConfigure.class,
DataSourceAutoConfiguration.class,
})
@MapperScan("com.example.mybatistest.mapper")
public class MybatistestApplication {
public static void main(String[] args) {
SpringApplication.run(MybatistestApplication.class, args);
}
}
解释一下, 排除DruidDataSourceAutoConfigure.class是为了防止出现如下错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
因为DruidDataSourceAutoConfigure默认用了SpringBoot顶级的配置,如下:
spring:
datasource:
url:
如果不存在,Druid会启动不成功。我们需要排除它。不过别担心,shardingsphere会帮助我们完成Druid的生成,在前面的配置中可见。
另外一个MapperScan是用来扫描Mybatis的mapper,这是MyBatis的常规玩法。
配置中我们用到了一个算法,用来对task_result精确分片:
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
public class MonthTableShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
// 根据配置的分表规则生成目标表的后缀
String tableExt = shardingValue.getValue().substring(26);
for (String availableTableName : availableTargetNames) {
if (availableTableName.endsWith(tableExt)) {
// 匹配成功返回正确表名
return availableTableName;
}
}
return null;
}
}
以上代码利用查询时的shardingColumn的取值,判断正确的表名,返回给上层做决策。
另外一个复杂点的复合分片算法,是为了更好地处理 task_info 表的 id_ 和 create_date_ 字段:
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;
import org.springframework.util.ObjectUtils;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MonthTableComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
private final static DateTimeFormatter createDateDf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final static DateTimeFormatter targetNamesDateDf = DateTimeFormatter.ofPattern("yyyyMMdd");
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Set<String> finalTargetNames = new HashSet<>();
//拿到逻辑表名的长度,比如task_info的长度,加上1个下划线的长度,方便后面获取最后的年月
int tablePrefixLength = shardingValue.getLogicTableName().length() + 1;
if (shardingValue.getColumnNameAndShardingValuesMap().size() > 0) {
/**
* 以下是对精确分片的处理,比如= 或者 in
*/
Map<String, Collection<Comparable<?>>> shardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
for (String availableTargetName : availableTargetNames) {
LocalDate targetNamesDt = getLocalDateFromTableName(tablePrefixLength, availableTargetName);
if (!ObjectUtils.isEmpty(shardingValuesMap.get("create_date_"))) {
if (shardingValuesMap.get("create_date_").size() > 0) {
Collection<Comparable<?>> createDateStr = shardingValuesMap.get("create_date_");
for (Comparable<?> date :
createDateStr) {
if (date instanceof Timestamp) {
LocalDate timestamp = ((Timestamp) date).toLocalDateTime().toLocalDate();
if (isSameMonth(timestamp, targetNamesDt)) {
finalTargetNames.add(availableTargetName);
}
}
}
}
} else if (!ObjectUtils.isEmpty(shardingValuesMap.get("id_"))) {
if (shardingValuesMap.get("id_").size() > 0) {
Collection<Comparable<?>> idStr = shardingValuesMap.get("id_");
for (Comparable<?> id :
idStr) {
if (id instanceof String) {
String tableExt = ((String) id).substring(26);
if (availableTargetName.endsWith(tableExt)) {
finalTargetNames.add(availableTargetName);
}
}
}
}
}
}
} else if (shardingValue.getColumnNameAndRangeValuesMap().size() > 0) {
/**
* 以下是对范围分片的处理,比如>、<等
* 这里主要处理create_date_,通过create_date_来确认是那些表
*/
Map<String, Range<Comparable<?>>> rangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap();
Range<Comparable<?>> createDate = rangeValuesMap.get("create_date_");
//2020-05-19 00:00:00
LocalDate lowerEndpointDt = null, upperEndpointDt = null;
if (createDate.hasLowerBound()) {
lowerEndpointDt = LocalDateTime.parse((String) createDate.lowerEndpoint(), createDateDf).toLocalDate();
}
if (createDate.hasUpperBound()) {
upperEndpointDt = LocalDateTime.parse((String) createDate.upperEndpoint(), createDateDf).toLocalDate();
}
Set<String> lowerSet = new HashSet<>();
Set<String> upperSet = new HashSet<>();
for (String availableTargetName : availableTargetNames) {
//例如202002
LocalDate targetNamesDt = getLocalDateFromTableName(tablePrefixLength, availableTargetName);
if (lowerEndpointDt != null &&
(targetNamesDt.isAfter(lowerEndpointDt) ||
isSameMonth(lowerEndpointDt, targetNamesDt)
)
) {
lowerSet.add(availableTargetName);
}
if (upperEndpointDt != null &&
(targetNamesDt.isBefore(upperEndpointDt) ||
(isSameMonth(upperEndpointDt, targetNamesDt))
)
) {
upperSet.add(availableTargetName);
}
}
lowerSet.retainAll(upperSet);
finalTargetNames = lowerSet;
}
return finalTargetNames;
}
/**
* 是否是同一个月
* @param date 待比较日期
* @param targetDate 目标日期
* @return 真为是同一个月
*/
private boolean isSameMonth(LocalDate date, LocalDate targetDate) {
return targetDate.getYear() == date.getYear() && targetDate.getMonthValue() == date.getMonthValue();
}
/**
*
* @param tablePrefixLength 表名前面相同部分的前缀长度
* @param availableTargetName 可用表名
* @return
*/
private LocalDate getLocalDateFromTableName(int tablePrefixLength, String availableTargetName) {
String targetNamesDateStr = availableTargetName.substring(tablePrefixLength);
return LocalDate.parse(targetNamesDateStr + "01", targetNamesDateDf);
}
}
Id生成算法如下,主要为给新增数据添加ID:
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
import java.util.Date;
import java.util.Properties;
import java.util.Random;
public final class ShardingIdGenerator implements ShardingKeyGenerator, IdentifierGenerator {
private final static SnowflakeShardingKeyGenerator snowflakeShardingKeyGenerator;
static {
snowflakeShardingKeyGenerator = new SnowflakeShardingKeyGenerator();
}
private static long getRandom(long n) {
long min = 1, max = 9;
for (int i = 1; i < n; i++) {
min *= 10;
max *= 10;
}
return (((long) (new Random().nextDouble() * (max - min)))) + min;
}
@Override
public Comparable<?> generateKey() {
Comparable<?> key = snowflakeShardingKeyGenerator.generateKey();
String date = DateFormatUtil.format(new Date(), "yyyyMM");
return String.valueOf(getRandom(8)) + key + date;
}
@Override
public String getType() {
return "DATEFLAKE";
}
@Override
public Properties getProperties() {
return snowflakeShardingKeyGenerator.getProperties();
}
@Override
public void setProperties(Properties properties) {
snowflakeShardingKeyGenerator.setProperties(properties);
}
@Override
public Number nextId(Object entity) {
return 1;
}
@Override
public String nextUUID(Object entity) {
return (String) generateKey();
}
}
TaskInfo:
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("task_info")
public class TaskInfo {
/**
* 表ID
*/
@TableId(value = "id_", type = IdType.ASSIGN_UUID)
private String id;
……
}
TaskResult:
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("task_result")
public class TaskResult {
@TableId(value = "id_", type = IdType.ASSIGN_UUID)
private String id;
@TableField("task_id_")
private String taskId;
……
}
其他代码如Mapper、Service、Controller等可以自己实现,这些东西和平常使用上不会有变化。
public interface TaskInfoMapper extends BaseMapper<TaskInfo> {
/**
*
* 查询 : 根据state状态查询用户列表,分页显示
*
*
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象)
* @return 分页对象
*/
@Select("select i.id_ as id, " +
" i.content_ as content " +
" from task_info i " +
" left join task_result r on i.id_ = r.task_id_ ${ew.customSqlSegment} " +
" order by i.create_date_ desc ")
Page<Map<String, Object>> findTaskByCondition(@Param(Constants.WRAPPER) Wrapper<Map<String, Object>> wrapper,
Page<?> page);
@Insert("insert into task_info (content_) values (#{content})")
int insertX(TaskInfo taskInfo);
}
可以看到 task_info 和 task_result 并不需要我们刻意加上_202005这样的后缀。在org.apache.shardingsphere.sharding.route.engine.ShardingRouteDecorator#decorate
方法中有详细的从逻辑表到真实表路由规则。可以对源码位置进行调试即可理清一些逻辑。
MyBatis需要分页插件,已经自己写的IdentifierGenerator生成数据库ID:
@EnableTransactionManagement
@Configuration
@MapperScan("com.example.privatebatistest.mapper")
public class MybatisPlusConfig {
/**
* 将shardingsphere的id生成器拿过来用
* 如果不想这样子
* 请使用SPI独立注入到shardingsphere
* @return
*/
@Bean
public IdentifierGenerator idGenerator() {
return new ShardingIdGenerator();
}
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
如果想让ShardingIdGenerator
只在shardingsphere中使用,那么需要按官方提供的 Service Provider Interface (SPI)1 去完成配置操作。
操作为:在resources目录下创建META-INF目录,接着在META-INF目录下创建services目录,然后在services目录中创建名为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator
文件,内容为com.example.privatebatistest.util.ShardingIdGenerator
。这个内容指向ShardingIdGenerator完整类路径。配合配置文件完成注入到shardingsphere中的任务。
如图所示:
以上代码虽然挺多的,但是有几个是MyBatis需要的类,ShardingSphere的主要功能都在配置中完成。
草草写完,还有待完善。如遇到问题请各位大神锤锤小弟我,不吝赐教,感谢。
https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/spi/ ↩︎