sharding-jdbc应用笔录

1,076 阅读4分钟

前言

前阵子梳理系统瓶颈、隐患,发现几个大表愈发膨胀,单表磁盘使用量达到了400G+,数据行数超10亿。虽然经过多次SQL优化后,性能上没有多大问题(基本都是几十毫秒,最多也不超1秒),但磁盘使用量严重挤压了其他表的生存空间;而且,执行ddl语句的时长过长,甚至会因磁盘空间问题而导致索引创建失败、字段新增失败。

基于以上问题考虑,打算对头部的几个大表下手整治一番。备选的方案有以下几种:

  1. 基于sharding-jdbc做分库分表。
  2. JED:公司基于mysql的自研弹性库,支持水平分库,但不分表;
  3. MarxDB:公司基于CockroachDB的NewSQL数据库;
  4. TiDB:与CockroachDB类似的一个NewSQL数据库,公司内有刚开始试用的平台;

TiDB与CockroachDB等newSQL在未来可能会是一个趋势,但目前公司内部平台还不够成熟,最终没有选择;sharding-jdbc与jed都是比较成熟的方案,而且sharding-jdb已于2020年4月16日从Apache孵化器毕业,成为Apache顶级项目,现在叫做ShardingSphere,涵盖了Sharding-JDBCSharding-ProxySharding-Sidecar(TODO)等组件,正如其名Sphere,俨然已经发展成为一个分布式数据库中间件解决方案组成的生态圈。

所以我选择了JED! O(∩_∩)O哈哈~

原因也很简单:

  1. 头部的两个大表都是一些报表性的数据,读写并不频繁,而且查询语句相对比较简单,性能上没有多大问题,暂时没有必要shard个稀碎;
  2. 历史数据的迁移比较费劲,而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可以通过JavaYAMLSpring命名空间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的一点使用体验了,由于未在生成环境使用,故主要记录了一些使用配置,数据迁移相关的代码就不再记录了。

附录