分库分表中间件 Sharding-JDBC

1. 简介

Sharding-JDBC 是当当开源的数据库分库分表中间件,同时也支持读写分离。

Sharding-JDBC 定位为轻量级 java 框架,使用客户端直连数据库,以 jar 包形式提供服务,未使用中间层,无需额外部署,无其他依赖,DBA 也无需改变原有的运维方式,可理解为增强版的 JDBC 驱动,旧代码迁移成本几乎为零。

Sharding-JDBC 架构的核心逻辑为分片规则配置、SQL解析、SQL路由、SQL改写、SQL执行以及结果归并模块。

Sharding-JDBC 架构图

2. 客户端配置

2.1 添加 maven 依赖
        <dependency> 
            <groupId>com.dangdang</groupId> 
            <artifactId>sharding-jdbc-core</artifactId> 
            <version>2.0.3</version> 
        </dependency> 
2.2 分片算法

Sharding-JDBC 提供了以下 5 种分片策略,都继承至ShardingStrategy:

StandardShardingStrategy

标准分片策略(最常用)。提供对 SQL语 句中的 =, IN 和 BETWEEN AND的分片操作支持。

StandardShardingStrategy 只支持单分片键,提供PreciseShardingAlgorithmRangeShardingAlgorithm两个分片算法。

  • PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。
  • RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND 分片,如果不配置RangeShardingAlgorithm,SQL中的 BETWEEN AND 将按照全库路由处理。

ComplexShardingStrategy

复合分片策略。提供对 SQL 语句中的 =, IN 和 BETWEEN AND 的分片操作支持。

ComplexShardingStrategy 支持多分片键,由于多分片键之间的关系复杂,因此 Sharding-JDBC 并未做过多的封装,而是直接将分片键值组合以及分片操作符交于算法接口,完全由应用开发者实现,提供最大的灵活度。

InlineShardingStrategy

Inline 表达式分片策略。使用 Groovy 的 Inline 表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持。

InlineShardingStrategy 只支持单分片键,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的 Java 代码开发,如: tuser${user_id % 8} 表示 t_user 表按照 user_id 对 8 取模分成 8 个表,表名称为 t_user_0 到 t_user_7。

HintShardingStrategy

通过 Hint 而非 SQL 解析的方式分片的策略。

NoneShardingStrategy

不分片的策略。

2.3 自定义分片算法

Sharding-JDBC 提供了以下4种算法接口:

  • PreciseShardingAlgorithm
  • RangeShardingAlgorithm
  • HintShardingAlgorithm
  • ComplexKeysShardingAlgorithm

现在我们有个分表需求,将时间字段,如repay_date(yyyy-MM-dd)按照季度分表,例如:“table_2019q1,table_2019q2,table_2019q3,table_2019q4,...”。由于时间的比较方式通常为区间比较,于是我们可以采用StandardShardingStrategy下的分片策略。

a、PreciseShardingAlgorithm 实现:(Precise 处理 = 和 in 的路由)

public class DatePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
        Assert.notNull(shardingValue.getValue(), "分表键不能为空");
        String quarterMonth = DateShardingUtils.getYearQuarter(shardingValue.getValue());

        for (String availableTarget : availableTargetNames){
            if(availableTarget.endsWith(quarterMonth)){
                return availableTarget;
            }
        }
        throw new IllegalArgumentException("分表不存在,shardingValue="+quarterMonth);
    }
}

b、 RangeShardingAlgorithm 实现:(Range 处理 Between And 的路由)

public class DateRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {
        Range<Date> dateRange = shardingValue.getValueRange();
        Assert.notNull(dateRange.lowerEndpoint(), "分表键开始时间不能为空");
        Assert.notNull(dateRange.upperEndpoint(), "分表键截止时间不能为空");

        Date start = DateTimeUtils.getStartOfMonth(dateRange.lowerEndpoint());
        Date end = DateTimeUtils.getStartOfMonth(dateRange.upperEndpoint());

        Collection<String> tables = new HashSet<String>();
        for( Date date = start; date.compareTo(end) <= 0; date = DateUtil.addMonths(date, 1)){
            String tableSuffix = DateShardingUtils.getYearQuarter(date);
            for (String each : availableTargetNames) {
                if (each.endsWith(tableSuffix)) {
                    tables.add(each);
                    break;
                }
            }
        }
        return tables;
    }

}

如果有多个分片键的需求,如order_no,subject_no两个分片键。我们希望其按照值的后两位尾数进行分表,并且表的区间为16,如表 ”table_00,table_16,table_32,table_48,...“。假如 order_no = 1000018,其会在表 t_order_16 表中。此时就需要使用ComplexShardingStrategy策略:

ComplexKeysShardingAlgorithm 实现

public class MyComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm {

    private final String ORDER_NO = "order_no";

    private final String SUBJECT_NO = "subject_no";

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue> shardingValues) {
        ShardingValue shardingValue = null;
        if( (shardingValue = getByColumnKey(shardingValues, ORDER_NO)) != null){
            return getTableByNo(availableTargetNames, shardingValue);
        }else if( (shardingValue = getByColumnKey(shardingValues, SUBJECT_NO)) != null){
            return getTableByNo(availableTargetNames, shardingValue);
        }
        }
        throw new IllegalArgumentException("Unsupported shardingValues: " + JSON.toJSONString(shardingValues));
    }

    private ShardingValue getByColumnKey(Collection<ShardingValue> shardingValues, final String columnKey){
        for(ShardingValue shardingValue : shardingValues){
            if(shardingValue.getColumnName().toLowerCase().equals(columnKey)){
                return shardingValue;
            }
        }
        return null;
    }

    /**
     * 尾数即为分表位 算法
     * 截取去最后2位分表位
     * @param availableTargetNames
     * @param shardingValue
     * @return
     */
    private Collection<String> getTableByNo(Collection<String> availableTargetNames, ShardingValue shardingValue){
        if(shardingValue instanceof ListShardingValue){
            Collection<String> noList = ((ListShardingValue) shardingValue).getValues();
            Collection<String> tables = new HashSet<String>();
            for(String no : noList){
                //1、截取最后两位
                int tableIndex = Integer.valueOf(StringUtils.substring(no, no.length() - TsShardingConsts.TABLE_SEQ_LENGTH));
                // 分表算法,16的倍数
                int actualTableSeq = tableIndex / 16 * 16 ;
                //3、查找分表
                for (String each : availableTargetNames) {
                    if (each.endsWith(actualTableSeq)) {
                        tables.add(each);
                        break;
                    }
                }
            }
            return tables;
        }
        throw new IllegalArgumentException("ShardingValue must be instanceof ListShardingValue.");
    }
}

这里需要注意的是,= 和 in 的分片值都是ListShardingValue

2.4 分片配置

基于 xml 的配置

  • 分片规则配置 sharding-jdbc.xml
    <sharding:complex-strategy id="myComplexKeysShardingAlgorithm" sharding-columns="order_no,subject_no"
                               algorithm-class="com.xxx.MyComplexKeysShardingAlgorithm" />

    <sharding:standard-strategy id="repayDateTableStrategy" sharding-column="repay_date"
                                precise-algorithm-class="com.xxx.DatePreciseShardingAlgorithm"
                                range-algorithm-class="com.xxx.DateRangeShardingAlgorithm"/>

    <sharding:data-source id="shardingDataSource">
        <sharding:sharding-rule data-source-names="dataSource" default-data-source-name="dataSource">
            <sharding:table-rules>
                <sharding:table-rule logic-table="table" actual-data-nodes="dataSource.table_${[00,16,32,48,64,80,96]}"
                                     table-strategy-ref="myComplexKeysShardingAlgorithm" />             

                <!-- 时间分表,按季度分: 2019q4 -->
                <sharding:table-rule logic-table="table_date" actual-data-nodes="dataSource.table_20${[19,20,21,22]}q${[1,2,3,4]}"
                                     table-strategy-ref="repayDateTableStrategy" />
            </sharding:table-rules>
</beans>
  • 数据源配置 jdbc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url">
            <value>
                ${jdbc.druid.url}
            </value>
        </property>
        <property name="username">
            <value>${jdbc.druid.user}</value>
        </property>
        <property name="password">
            <value>"${jdbc.druid.password}"</value>
        </property>
        <property name="filters">
            <value>${jdbc.druid.filters}</value>
        </property>
        <property name="maxActive">
            <value>${jdbc.druid.maxActive}</value>
        </property>
        <property name="initialSize">
            <value>${jdbc.druid.initialSize}</value>
        </property>
        <property name="maxWait">
            <value>${jdbc.druid.maxWait}</value>
        </property>
        <property name="minIdle">
            <value>${jdbc.druid.minIdle}</value>
        </property>
        <property name="timeBetweenEvictionRunsMillis">
            <value>${jdbc.druid.timeBetweenEvictionRunsMillis}</value>
        </property>
        <property name="minEvictableIdleTimeMillis">
            <value>${jdbc.druid.minEvictableIdleTimeMillis}</value>
        </property>
        <property name="validationQuery">
            <value>${jdbc.druid.validationQuery}</value>
        </property>
        <property name="testWhileIdle">
            <value>${jdbc.druid.testWhileIdle}</value>
        </property>
        <property name="testOnBorrow">
            <value>${jdbc.druid.testOnBorrow}</value>
        </property>
        <property name="testOnReturn">
            <value>${jdbc.druid.testOnReturn}</value>
        </property>
        <property name="poolPreparedStatements">
            <value>${jdbc.druid.poolPreparedStatements}</value>
        </property>
        <property name="maxOpenPreparedStatements">
            <value>${jdbc.druid.maxOpenPreparedStatements}</value>
        </property>
    </bean>

    <!-- myBatis文件 -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/*.xml"/>
        <!-- 将 shrading-jdbc 作为数据源 -->
        <property name="dataSource" ref="shardingDataSource"/>
    </bean>

    <import resource="classpath:META-INF/spring/sharding-jdbc.xml" />

</beans>

基于 Spring Boot 的配置

sharding.jdbc.datasource.names=ds

sharding.jdbc.datasource.ds.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds.url=jdbc:mysql://localhost:3306/ds
sharding.jdbc.datasource.ds.username=root
sharding.jdbc.datasource.ds.password=123456

sharding.jdbc.config.sharding.tables.table(表名).logic-table=table
sharding.jdbc.config.sharding.tables.table.actual-data-nodes=ds.table_${[00,16,32,48,64,80,96]}
sharding.jdbc.config.sharding.tables.table.table-strategy.complex.sharding-columns=order_no,subject_no
sharding.jdbc.config.sharding.tables.table.table-strategy.complex.algorithm-class-name=com.xxx. MyComplexKeysShardingAlgorithm

sharding.jdbc.config.sharding.tables.table.logic-table=table_date
sharding.jdbc.config.sharding.tables.table.actual-data-nodes=ds.table_20${[19,20,21,22]}q${[1,2,3,4]}
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=repay_date
sharding.jdbc.config.sharding.tables.table.table-strategy.standard.precise-algorithm-class-name=com.xxx.DatePreciseShardingAlgorithm
sharding.jdbc.config.sharding.tables.table.table-strategy.standard.range-algorithm-class-name=com.xxx. DateRangeShardingAlgorithm

至此,Sharding-JDBC 的配置已经讲完,同学们可以自己实践一下,相对来说很是很简单的。后面,我们将深入源码,了解其底层的实现机制,尽请关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342