前言
前阵子梳理系统瓶颈、隐患,发现几个大表愈发膨胀,单表磁盘使用量达到了400G+,数据行数超10亿。虽然经过多次SQL优化后,性能上没有多大问题(基本都是几十毫秒,最多也不超1秒),但磁盘使用量严重挤压了其他表的生存空间;而且,执行ddl语句的时长过长,甚至会因磁盘空间问题而导致索引创建失败、字段新增失败。
基于以上问题考虑,打算对头部的几个大表下手整治一番。备选的方案有以下几种:
- 基于sharding-jdbc做分库分表。
- JED:公司基于mysql的自研弹性库,支持水平分库,但不分表;
- MarxDB:公司基于CockroachDB的NewSQL数据库;
- TiDB:与CockroachDB类似的一个NewSQL数据库,公司内有刚开始试用的平台;
TiDB与CockroachDB等newSQL在未来可能会是一个趋势,但目前公司内部平台还不够成熟,最终没有选择;sharding-jdbc与jed都是比较成熟的方案,而且sharding-jdb已于2020年4月16日从Apache孵化器毕业,成为Apache顶级项目,现在叫做ShardingSphere
,涵盖了Sharding-JDBC
、Sharding-Proxy
、Sharding-Sidecar(TODO)
等组件,正如其名Sphere
,俨然已经发展成为一个分布式数据库中间件解决方案组成的生态圈。
所以我选择了JED! O(∩_∩)O哈哈~
原因也很简单:
- 头部的两个大表都是一些报表性的数据,读写并不频繁,而且查询语句相对比较简单,性能上没有多大问题,暂时没有必要shard个稀碎;
- 历史数据的迁移比较费劲,而JED是提供了数据迁移工具的,支持全量、增量的做数据迁移、校验,比较方便; 3、JED对用户屏蔽了多个库的事实,使用上几乎不用修改代码,也不用自己去维护百十张表;
虽然最终没有选择sharding-jdbc,但是在调研过程中相关使用、数据迁移的代码已经写完了,不同版本的配置差异还挺多的,特此记录一下(要不感觉白写了,指不定以后还会用到呢)。
正文
找到大表
第一步可以先大致梳理一下当前数据库状态,看看是哪些表膨胀了。关于mysql如何找到大表,可以参考如下SQL:
select table_schema as '数据库',
table_name as '表名',
table_rows as '记录数',
truncate((data_length+index_length)/1024/1024/1024, 2) as '总大小(GB)',
truncate(data_length/1024/1024, 2) as '数据大小(MB)',
truncate(index_length/1024/1024, 2) as '索引大小(MB)'
from information_schema.tables
where
table_schema='xxx_db_name'
order by (data_length+index_length) desc;
引入依赖
Sharding-JDBC可以通过Java
,YAML
,Spring命名空间
和Spring Boot Starter
四种方式配置,为适应现有系统,这里采用了Spring命名空间
的方式。
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<!--此时5.x还是待发布状态,这里选用了较新的4.x-->
<version>4.1.1</version>
</dependency>
分片配置
接下来是数据源、分片策略等配置,我这里只做了分表,未做分库。这里使用了简单的hash取模的分片算法,对表t_aa分了100张表{0..99},对表t_bb分了10张表{0..9},通过对表字段userId先做hash,再对分表数量取mod,计算出表后缀数字,获得最终的真实表名。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
xsi:schemaLocation="
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://shardingsphere.apache.org/schema/shardingsphere/sharding http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd">
<!--配置数据源连接-->
<bean id="xxx_dataSource" parent="abstractDataSource">
<property name="url" value="#{dataSourceProps['xxx_url']}"/>
<property name="username" value="#{dataSourceProps['xxx_username']}"/>
<property name="password" value="#{importantProps['xxx_password']}"/>
</bean>
<!--配置表t_aa的sharding算法-->
<bean id="t_aa_TableAlgorithm" class="com.xxx.sharding.algorithm.PreciseHashModShardingTableAlgorithm">
<!--分了100个表,sharding算法里对表数做hash取模-->
<property name="tableNumbers" value="100"/>
</bean>
<!--配置表t_bb的sharding算法-->
<bean id="t_bb_TableAlgorithm" class="com.xxx.sharding.algorithm.PreciseHashModShardingTableAlgorithm">
<!--分了10个表,sharding算法里对表数做hash取模-->
<property name="tableNumbers" value="10"/>
</bean>
<!-- 配置分表策略 -->
<sharding:standard-strategy id="t_aa_tableStrategy" sharding-column="userId" precise-algorithm-ref="t_aa_TableAlgorithm"/>
<sharding:standard-strategy id="t_bb_tableStrategy" sharding-column="userId" precise-algorithm-ref="t_bb_TableAlgorithm"/>
<!-- id生成器,这里选用了雪花算法-->
<sharding:key-generator id="shardKeyGenerator" column="id" type="SNOWFLAKE"/>
<!-- 配置ShardingSphereDataSource -->
<sharding:data-source id="xxx_shardingDataSource">
<!-- 配置分片规则 -->
<sharding:sharding-rule data-source-names="xxx_dataSource">
<sharding:table-rules>
<sharding:table-rule logic-table="t_aa"
key-generator-ref="shardKeyGenerator"
actual-data-nodes="xxx_dataSource.t_aa_$->{0..99}"
table-strategy-ref="t_aa_tableStrategy"/>
<sharding:table-rule logic-table="t_bb"
key-generator-ref="shardKeyGenerator"
actual-data-nodes="xxx_dataSource.t_bb_$->{0..9}"
table-strategy-ref="t_bb_tableStrategy"/>
</sharding:table-rules>
</sharding:sharding-rule>
<sharding:props>
<prop key="sql.show">true</prop>
</sharding:props>
</sharding:data-source>
<bean id="xxx_TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="xxx_shardingDataSource"/>
</bean>
<tx:annotation-driven transaction-manager="xxx_TransactionManager"/>
<bean id="xxx_sharding_sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="xxx_shardingDataSource"/>
<property name="typeAliasesSuperType" value="com.xxx.domain.base.IDomain"/>
<property name="mapperLocations" value="classpath*:sqlmap/xxx/*.xml"/>
<property name="configLocation" value="classpath:spring/mybatis-config.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="markerInterface" value="com.xxx.dao.base.ISqlMapper"/>
<property name="sqlSessionFactoryBeanName" value="xxx_sharding_sessionFactory"/>
<property name="basePackage" value="com.xxx.dao.xxx"/>
<property name="nameGenerator" ref="myMapperNameGenerator"/>
</bean>
</beans>
自定义sharding算法:
@Data
public final class PreciseHashModShardingTableAlgorithm implements PreciseShardingAlgorithm<String> {
private static final Logger logger = LoggerFactory.getLogger(PreciseHashModShardingTableAlgorithm.class);
private int tableNumbers;
@Override
public String doSharding(final Collection<String> tableNames, final PreciseShardingValue<String> shardingValue) {
// shardingValue.getValue() 即分表策略中指定的sharding列userId的值
String suffix = this.getSuffix(shardingValue.getValue());
for (String each : tableNames) {
if (each.endsWith(suffix)) {
return each;
}
}
logger.error("val:{}, names:{}, suffix:{}", JSON.toJSONString(shardingValue), tableNames, suffix);
throw new UnsupportedOperationException();
}
// hash取模,计算表名后缀数字
private String getSuffix(String shardingValue) {
long hash = DJBHash(shardingValue);
return "_" + (hash % tableNumbers);
}
// DJB hash算法
public static long DJBHash(String str) {
long hash = 5381;
for (int i = 0; i < str.length(); i++) {
hash = ((hash << 5) + hash) + str.charAt(i);
}
return Math.abs(hash);
}
}
关于id生成算法
我这里由于数据是定时任务单点写入,所以分布式id生成直接使用了snowflake算法,且未为其设置worker.id等参数。worker.id默认情况下为0,如果是分布式高并发环境下,则极有可能会生成重复id。 可以通过如下方式配置workId,避免不同实例生成重复id:
<bean:properties id="properties">
<prop key="worker.id">123</prop>
</bean:properties>
<sharding:key-generator id="shardKeyGenerator" column="id" type="SNOWFLAKE" props-ref="properties"/>
但实际生产环境如果应用实例较多,每个实例配置一遍是相当麻烦的(也可能是我理解不到位),而且sharding-jdbc的snowflake算法没有开放workerId的自定义分配策略,如果想动态确定workerId的值,只好重写snowflake算法。sharding-jdbc的实现全限定名为org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator
,可以自行阅读。
关于刚才说的自动分配workId的方式可以参考此文:www.bbsmax.com/A/xl56Gm2kd… 基本上就是拿实例ip做文章,极端情况下还是有重复的可能性;
Spring Bean重名问题
说点题外话,我这边在做迁移时,针对迁移前后的库同时保留了两个mybatis的mapper接口,且是同名不同包的;这样在其生成代理类时,出现了相同的bean名称,导致报错。可通过自定义BeanNameGenerator来解决,代码如下:
<!-- 自定义mybatis Mapper bean名生成器(AnnotationBeanNameGenerator)-->
<bean id="myMapperNameGenerator" class="com.xxx.dao.base.MyMapperNameGenerator"/>
public class MyMapperNameGenerator extends AnnotationBeanNameGenerator {
@Override
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
//分割类全路径
String[] strs = beanClassName.split("\\.");
//包名末段+类名 作为最后的bean名
return strs[strs.length - 2] + strs[strs.length - 1];
}
}
总结
以上就是这次sharding-jdbc的一点使用体验了,由于未在生成环境使用,故主要记录了一些使用配置,数据迁移相关的代码就不再记录了。