使用Sharding-JDBC集成springboot2+mybatis+druid对数据库进行分库分表操作(亲测可用)

1、Sharding-JDBC 简介

Sharding-JDBC最早是当当网外部应用的一款分库分表框架,到2017年的时候才开始对外开源,这几年在大量社区贡献者的一直迭代下,性能也逐步欠缺,现已更名为 ShardingSphere,2020年4⽉16⽇正式成为 Apache 软件基⾦会的顶级项⽬。

随着版本的一直更迭 ShardingSphere 的外围性能也变得多元化起来。从最开始 Sharding-JDBC 1.0 版本只有数据分片,到 Sharding-JDBC 2.0 版本开始提供数据库治理(注册核心、配置核心等等),再到 Sharding-JDBC 3.0版本又加分布式事务 ( Atomikos、Narayana、Bitronix、Seata)

当初的 ShardingSphere 不单单是指某个框架而是一个生态圈,这个生态圈 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 这三款开源的分布式数据库中间件解决方案所形成,ShardingSphere 的前身就是 Sharding-JDBC,所以它是整个框架中最为经典、成熟的组件,咱们先从 Sharding-JDBC 框架动手学习分库分表。

2、基本概念

在开始 Sharding-JDBC分库分表具体实战之前,咱们有必要先理解分库分表的一些基本概念。

分片

个别咱们在提到分库分表的时候,大多是以程度切分模式(程度分库、分表)为根底来说的,数据分片将本来一张数据量较大的表 t_order 拆分生成数个表构造完全一致的小数据量表 t_order_0、t_order_1、···、t_order_n,每张表只存储原大表中的一部分数据,当执行一条SQL时会通过 分库策略、分片策略 将数据扩散到不同的数据库、表内。

数据节点

数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,例如上图中order_db_1.t_order_0、order_db_2.t_order_1 就示意一个数据节点。

逻辑表

逻辑表是指一组具备雷同逻辑和数据结构表的总称。比方咱们将订单表t_order 拆分成 t_order_0 ··· t_order_9 等 10张表。此时咱们会发现分库分表当前数据库中已不在有 t_order 这张表,取而代之的是 t_order_n,但咱们在代码中写 SQL 仍然按 t_order 来写。此时 t_order 就是这些拆分表的逻辑表。

实在表

实在表也就是上边提到的 t_order_n 数据库中实在存在的物理表。

用于分片的数据库字段。咱们将 t_order 表分片当前,当执行一条SQL时,通过对字段 order_id 取模的形式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是 t_order 表的分片健。

这样以来同一个订单的相干数据就会存在同一个数据库表中,大幅晋升数据检索的性能,不仅如此 sharding-jdbc 还反对依据多个字段作为分片健进行分片。

分片算法

上边咱们提到能够用分片健取模的规定分片,但这只是比较简单的一种,在理论开发中咱们还心愿用 >=、<=、>、<、BETWEEN 和 IN 等条件作为分片规定,自定义分片逻辑,这时就须要用到分片策略与分片算法。

从执行 SQL 的角度来看,分库分表能够看作是一种路由机制,把 SQL 语句路由到咱们冀望的数据库或数据表中并获取数据,分片算法能够了解成一种路由规定。分片策略只是形象出的概念,它是由分片算法和分片健组合而成,分片算法做具体的数据分片逻辑。

分库、分表的分片策略配置是绝对独立的,能够各自应用不同的策略与算法,每种策略中能够是多个分片算法的组合,每个分片算法能够对多个分片健做逻辑判断。sharding-jdbc 并没有间接提供分片算法的实现,须要开发者依据业务自行实现。

sharding-jdbc 提供了4种分片算法:

1、准确分片算法

准确分片算法(PreciseShardingAlgorithm)用于单个字段作为分片键,SQL中有 = 与 IN 等条件的分片,须要在规范分片策略(StandardShardingStrategy )下应用。

2、范畴分片算法

范畴分片算法(RangeShardingAlgorithm)用于单个字段作为分片键,SQL中有 BETWEEN AND、>、<、>=、<= 等条件的分片,须要在规范分片策略(StandardShardingStrategy )下应用。

3、复合分片算法

复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,依据多个字段解决业务逻辑。须要在复合分片策略(ComplexShardingStrategy )下应用。

4、Hint分片算法

Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中咱们都是解析SQL 语句提取分片键,并设置分片策略进行分片。但有些时候咱们并没有应用任何的分片键和分片策略,可还想将 SQL 路由到指标数据库和表,就须要通过手动干涉指定SQL的指标数据库和表信息,这也叫强制路由。

分片策略

上边讲分片算法的时候曾经说过,分片策略是一种形象的概念,理论分片操作的是由分片算法和分片健来实现的。

1、规范分片策略

规范分片策略实用于单分片键,此策略反对 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。

其中 PreciseShardingAlgorithm 是必选的,用于解决 = 和 IN 的分片。RangeShardingAlgorithm 是可选的,用于解决BETWEEN AND, >, <,>=,<= 条件分片,如果不配置RangeShardingAlgorithm,SQL中的条件等将依照全库路由解决。

2、复合分片策略

复合分片策略,同样反对对 SQL语句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。不同的是它反对多分片键,具体调配片细节齐全由利用开发者实现。

3、行表达式分片策略

行表达式分片策略,反对对 SQL语句中的 = 和 IN 的分片操作,但只反对单分片键。这种策略通常用于简略的分片,不须要自定义分片算法,能够间接在配置文件中接着写规定。

t_order_$->{t_order_id % 4} 代表 t_order 对其字段 t_order_id取模,拆分成4张表,而表名别离是t_order_0 到 t_order_3。

4、Hint分片策略

Hint分片策略,对应上边的Hint分片算法,通过指定分片健而非从 SQL中提取分片健的形式进行分片的策略。

分布式主键

数据分⽚后,不同数据节点⽣成全局唯⼀主键是⾮常棘⼿的问题,同⼀个逻辑表(t_order)内的不同实在表(t_order_n)之间的⾃增键因为⽆法相互感知而产⽣反复主键。

只管可通过设置⾃增主键 初始值 和 步⻓ 的⽅式防止ID碰撞,但这样会使保护老本加大,乏完整性和可扩展性。如果后去须要减少分片表的数量,要逐个批改分片表的步长,运维老本十分高,所以不倡议这种形式。

为了让上手更加简略,ApacheShardingSphere 内置了UUID、SNOWFLAKE 两种分布式主键⽣成器,默认使⽤雪花算法(snowflake)⽣成64bit的⻓整型数据。不仅如此它还抽离出分布式主键⽣成器的接口,⽅便咱们实现⾃定义的⾃增主键⽣成算法。

播送表

播送表:存在于所有的分片数据源中的表,表构造和表中的数据在每个数据库中均完全一致。个别是为字典表或者配置表 t_config,某个表一旦被配置为播送表,只有批改某个数据库的播送表,所有数据源中播送表的数据都会跟着同步。

绑定表

绑定表:那些分片规定统一的主表和子表。比方:t_order 订单表和 t_order_item 订单服务项目表,都是按 order_id 字段分片,因而两张表互为绑定表关系。那绑定表存在的意义是啥呢?

通常在咱们的业务中都会应用 t_order 和 t_order_item 等表进行多表联结查问,但因为分库分表当前这些表被拆分成N多个子表。如果不配置绑定表关系,会呈现笛卡尔积关联查问,将产生如下四条SQL。

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id 

而配置绑定表关系后再进行关联查问时,只有对应表分片规定统一产生的数据就会落到同一个库中,那么只需 t_order_0 和 t_order_item_0 表关联即可。

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id 

在关联查问时 t_order 它作为整个联结查问的主表。 所有相干的路由计算都只应用主表的策略,t_order_item 表的分片相干的计算也会应用 t_order 的条件,所以要保障绑定表之间的分片键要完全相同。

3、和JDBC的猫腻

从名字上不难看出,Sharding-JDBC 和 JDBC有很大关系,咱们晓得 JDBC 是一种 Java 语言拜访关系型数据库的标准,其设计初衷就是要提供一套用于各种数据库的统一标准,不同厂家独特恪守这套规范,并提供各自的实现计划供应用程序调用。

但其实对于开发人员而言,咱们只关怀如何调用 JDBC API 来拜访数据库,只有正确应用 DataSource、Connection、Statement 、ResultSet 等 API 接口,间接操作数据库即可。所以如果想在 JDBC 层面实现数据分片就必须对现有的 API 进行性能拓展,而 Sharding-JDBC 正是基于这种思维,重写了 JDBC 标准并齐全兼容了 JDBC 标准。

对原有的 DataSource、Connection 等接口扩大成 ShardingDataSource、ShardingConnection,而对外裸露的分片操作接口与 JDBC 标准中所提供的接口完全一致,只有你相熟 JDBC 就能够轻松利用 Sharding-JDBC 来实现分库分表。

因而它实用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate,Mybatis,Spring JDBC Template 或间接应用的 JDBC。完满兼容任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP,Druid, HikariCP 等,简直对支流关系型数据库都支持。

那 Sharding-JDBC 又是如何拓展这些接口的呢?想晓得答案咱们就的从源码动手了,下边咱们以 JDBC API 中的 DataSource 为例看看它是如何被重写扩大的。

数据源 DataSource 接口的核心作用就是获取数据库连贯对象 Connection,咱们看其外部提供了两个获取数据库连贯的办法 ,并且继承了 CommonDataSource 和 Wrapper 两个接口。

public interface DataSource  extends CommonDataSource, Wrapper {

  /**
   * <p>Attempts to establish a connection with the data source that
   * this {@code DataSource} object represents.
   * @return  a connection to the data source
   */
  Connection getConnection() throws SQLException;

  /**
   * <p>Attempts to establish a connection with the data source that
   * this {@code DataSource} object represents.
   * @param username the database user on whose behalf the connection is
   *  being made
   * @param password the user's password
   */
  Connection getConnection(String username, String password)
    throws SQLException;
}

其中 CommonDataSource 是定义数据源的根接口这很好了解,而 Wrapper 接口则是拓展 JDBC 分片性能的要害。

因为数据库厂商的不同,他们可能会各自提供一些超过规范 JDBC API 的扩大性能,但这些性能非 JDBC 规范并不能间接应用,而 Wrapper 接口的作用就是把一个由第三方供应商提供的、非 JDBC 规范的接口包装成标准接口,也就是适配器模式。

既然讲到了适配器模式就多啰嗦几句,也不便后边的了解。

适配器模式个种比拟罕用的设计模式,它的作用是将某个类的接口转换成客户端冀望的另一个接口,使本来因接口不匹配(或者不兼容)而无奈在一起工作的两个类可能在一起工作。
比方用耳机听音乐,我有个圆头的耳机,可手机插孔却是扁口的,如果我想要应用耳机听音乐就必须借助一个转接头才能够,这个转接头就起到了适配作用。
举个栗子:如果咱们 Target 接口中有 hello() 和 word() 两个办法。

public interface Target {

    void hello();

    void world();
}

可因为接口版本迭代Target 接口的 word() 办法可能会被废除掉或不被反对,Adaptee 类的 greet()办法将代替hello() 办法。

public class Adaptee {

    public void greet(){

    }
    public void world(){

    }
}

但此时旧版本依然有大量 word() 办法被应用中,解决此事最好的方法就是创立一个适配器Adapter,这样就适配了 Target 类,解决了接口降级带来的兼容性问题。

public class Adapter extends Adaptee implements Target {

    @Override
    public void world() {
        
    }

    @Override
    public void hello() {
        super.greet();
    }

    @Override
    public void greet() {
        
    }
}

而 Sharding-JDBC 提供的正是非 JDBC 规范的接口,所以它也提供了相似的实现计划,也应用到了 Wrapper 接口做数据分片性能的适配。除了 DataSource 之外,Connection、Statement、ResultSet 等外围对象也都继承了这个接口。

上面咱们通过 ShardingDataSource 类源码简略看下实现过程,下图是继承关系流程图。

ShardingDataSource 类它在原 DataSource 根底上做了性能拓展,初始化时注册了分片SQL路由包装器、SQL重写上下文和后果集解决引擎,还对数据源类型做了校验,因为它要同时反对多个不同类型的数据源。到这如同也没看出如何适配,那接着向上看 ShardingDataSource的继承类 AbstractDataSourceAdapter。

@Getter
public class ShardingDataSource extends AbstractDataSourceAdapter {
    
    private final ShardingRuntimeContext runtimeContext;

    /**
     * 注册路由、SQl重写上下文、后果集解决引擎
     */
    static {
        NewInstanceServiceLoader.register(RouteDecorator.class);
        NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
        NewInstanceServiceLoader.register(ResultProcessEngine.class);
    }

    /**
     * 初始化时校验数据源类型 并依据数据源 map、分片规定、数据库类型失去一个分片上下文,用来获取数据库连贯
     */
    public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Properties props) throws SQLException {
        super(dataSourceMap);
        checkDataSourceType(dataSourceMap);
        runtimeContext = new ShardingRuntimeContext(dataSourceMap, shardingRule, props, getDatabaseType());
    }

    private void checkDataSourceType(final Map<String, DataSource> dataSourceMap) {
        for (DataSource each : dataSourceMap.values()) {
            Preconditions.checkArgument(!(each instanceof MasterSlaveDataSource), "Initialized data sources can not be master-slave data sources.");
        }
    }

    /**
     * 数据库连贯
     */
    @Override
    public final ShardingConnection getConnection() {
        return new ShardingConnection(getDataSourceMap(), runtimeContext, TransactionTypeHolder.get());
    }
}

AbstractDataSourceAdapter抽象类外部次要获取不同类型的数据源对应的数据库连贯对象,实现 AutoCloseable接口是为在应用完资源后能够主动将这些资源敞开(调用 close办法),那再看看继承类 AbstractUnsupportedOperationDataSource 。

@Getter
public abstract class AbstractDataSourceAdapter extends AbstractUnsupportedOperationDataSource implements AutoCloseable {
    
    private final Map<String, DataSource> dataSourceMap;
    
    private final DatabaseType databaseType;
    
    public AbstractDataSourceAdapter(final Map<String, DataSource> dataSourceMap) throws SQLException {
        this.dataSourceMap = dataSourceMap;
        databaseType = createDatabaseType();
    }
    
    public AbstractDataSourceAdapter(final DataSource dataSource) throws SQLException {
        dataSourceMap = new HashMap<>(1, 1);
        dataSourceMap.put("unique", dataSource);
        databaseType = createDatabaseType();
    }
    
    private DatabaseType createDatabaseType() throws SQLException {
        DatabaseType result = null;
        for (DataSource each : dataSourceMap.values()) {
            DatabaseType databaseType = createDatabaseType(each);
            Preconditions.checkState(null == result || result == databaseType, String.format("Database type inconsistent with '%s' and '%s'", result, databaseType));
            result = databaseType;
        }
        return result;
    }
    
    /**
     * 不同数据源类型获取数据库连贯
     */
    private DatabaseType createDatabaseType(final DataSource dataSource) throws SQLException {
        if (dataSource instanceof AbstractDataSourceAdapter) {
            return ((AbstractDataSourceAdapter) dataSource).databaseType;
        }
        try (Connection connection = dataSource.getConnection()) {
            return DatabaseTypes.getDatabaseTypeByURL(connection.getMetaData().getURL());
        }
    }
    
    @Override
    public final Connection getConnection(final String username, final String password) throws SQLException {
        return getConnection();
    }
    
    @Override
    public final void close() throws Exception {
        close(dataSourceMap.keySet());
    }
}

AbstractUnsupportedOperationDataSource 实现DataSource 接口并继承了 WrapperAdapter 类,它外部并没有什么具体方法只起到桥接的作用,但看着是不是和咱们前边讲适配器模式的例子形式有点类似。

public abstract class AbstractUnsupportedOperationDataSource extends WrapperAdapter implements DataSource {
    
    @Override
    public final int getLoginTimeout() throws SQLException {
        throw new SQLFeatureNotSupportedException("unsupported getLoginTimeout()");
    }
    
    @Override
    public final void setLoginTimeout(final int seconds) throws SQLException {
        throw new SQLFeatureNotSupportedException("unsupported setLoginTimeout(int seconds)");
    }
}

WrapperAdapter 是一个包装器的适配类,实现了 JDBC 中的 Wrapper 接口,其中有两个外围办法 recordMethodInvocation 用于增加须要执行的办法和参数,而 replayMethodsInvocation 则将增加的这些办法和参数通过反射执行。认真看不难发现两个办法中都用到了 JdbcMethodInvocation类。

public abstract class WrapperAdapter implements Wrapper {
    
    private final Collection<JdbcMethodInvocation> jdbcMethodInvocations = new ArrayList<>();
 
    /**
     * 增加要执行的办法
     */
    @SneakyThrows
    public final void recordMethodInvocation(final Class<?> targetClass, final String methodName, final Class<?>[] argumentTypes, final Object[] arguments) {
        jdbcMethodInvocations.add(new JdbcMethodInvocation(targetClass.getMethod(methodName, argumentTypes), arguments));
    }
    
    /**
     * 通过反射执行 上边增加的办法
     */
    public final void replayMethodsInvocation(final Object target) {
        for (JdbcMethodInvocation each : jdbcMethodInvocations) {
            each.invoke(target);
        }
    }
}

JdbcMethodInvocation 类次要利用反射通过传入的 method 办法和 arguments 参数执行对应的办法,这样就能够通过 JDBC API 调用非 JDBC 办法了。

@RequiredArgsConstructor
public class JdbcMethodInvocation {
    
    @Getter
    private final Method method;
    
    @Getter
    private final Object[] arguments;
    
    /**
     * Invoke JDBC method.
     * 
     * @param target target object
     */
    @SneakyThrows
    public void invoke(final Object target) {
        method.invoke(target, arguments);
    }
}

那 Sharding-JDBC 拓展 JDBC API 接口后,在新增的分片性能里又做了哪些事件呢?

一张表通过分库分表后被拆分成多个子表,并扩散到不同的数据库中,在不批改原业务 SQL 的前提下,Sharding-JDBC 就必须对 SQL进行一些革新能力失常执行。

大抵的执行流程:SQL 解析 -> 执⾏器优化 -> SQL 路由 -> SQL 改写 -> SQL 执⾏ -> 后果归并 六步组成,一起瞅瞅每个步骤做了点什么。

SQL 解析

SQL解析过程分为词法解析和语法解析两步,比方下边这条查问用户订单的SQL,先用词法解析将SQL拆解成不可再分的原子单元。在依据不同数据库方言所提供的字典,将这些单元归类为关键字,表达式,变量或者操作符等类型。

SELECT order_no,price FROM t_order_ where user_id = 10086 and order_status > 0

接着语法解析会将拆分后的SQL转换为形象语法树,通过对形象语法树遍历,提炼出分片所需的上下文,上下文蕴含查问字段信息(Field)、表信息(Table)、查问条件(Condition)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit)等,并标记出 SQL中有可能须要改写的地位。

执⾏器优化

执⾏器优化对SQL分片条件进行优化,解决像关键字 OR这种影响性能的条件。

SQL 路由

SQL 路由通过解析分片上下文,匹配到用户配置的分片策略,并生成路由门路。简略点了解就是能够依据咱们配置的分片策略计算出 SQL该在哪个库的哪个表中执行,而SQL路由又依据有无分片健辨别出 分片路由 和 播送路由。

有分⽚键的路由叫分片路由,细分为间接路由、规范路由和笛卡尔积路由这3种类型。

规范路由

规范路由是最举荐也是最为常⽤的分⽚⽅式,它的适⽤范畴是不蕴含关联查问或仅蕴含绑定表之间关联查问的SQL。

当 SQL分片健的运算符为 = 时,路由后果将落⼊单库(表),当分⽚运算符是BETWEEN 或IN 等范畴时,路由后果则不⼀定落⼊唯⼀的库(表),因而⼀条逻辑SQL最终可能被拆分为多条⽤于执⾏的实在SQL。

SELECT * FROM t_order  where t_order_id in (1,2)

SQL路由解决后

SELECT * FROM t_order_0  where t_order_id in (1,2)
SELECT * FROM t_order_1  where t_order_id in (1,2)

间接路由

间接路由是通过应用 HintAPI 间接将 SQL路由到指定⾄库表的一种分⽚形式,而且间接路由能够⽤于分⽚键不在SQL中的场景,还能够执⾏包含⼦查问、⾃定义函数等简单状况的任意SQL。

比方依据 t_order_id字段为条件查问订单,此时心愿在不批改SQL的前提下,加上 user_id作为分片条件就能够应用间接路由。

笛卡尔积路由

笛卡尔路由是由⾮绑定表之间的关联查问产生的,查问性能较低尽量避免走此路由模式。

无分片路由又叫做播送路由,能够划分为全库表路由、全库路由、 全实例路由、单播路由和阻断路由这 5种类型。

全库表路由

全库表路由针对的是数据库 DQL和 DML,以及 DDL等操作,当咱们执行一条逻辑表 t_order SQL时,在所有分片库中对应的实在表 t_order_0 ··· t_order_n 内逐个执行。

全库路由

全库路由次要是对数据库层面的操作,比方数据库 SET 类型的数据库治理命令,以及 TCL 这样的事务管制语句。

对逻辑库设置 autocommit 属性后,所有对应的实在库中都执行该命令。

SET autocommit=0;

全实例路由

全实例路由是针对数据库实例的 DCL 操作(设置或更改数据库用户或角色权限),比方:创立一个用户 order ,这个命令将在所有的实在库实例中执行,以此确保 order 用户能够失常拜访每一个数据库实例。

CREATE USER order@127.0.0.1 identified BY '程序员内点事';

单播路由

单播路由用来获取某一实在表信息,比方取得表的形容信息:

DESCRIBE t_order; 

t_order 的实在表是 t_order_0 ···· t_order_n,他们的形容构造相齐全同,咱们只需在任意的实在表执行一次就能够。

阻断路由

⽤来屏蔽SQL对数据库的操作,例如:

USE order_db;

这个命令不会在实在数据库中执⾏,因为 ShardingSphere 采⽤的是逻辑 Schema(数据库的组织和构造) ⽅式,所以无需将切换数据库的命令发送⾄实在数据库中。

SQL 改写

将基于逻辑表开发的SQL改写成能够在实在数据库中能够正确执行的语句。比方查问 t_order 订单表,咱们理论开发中 SQL是按逻辑表 t_order 写的。

SELECT * FROM t_order

但分库分表当前实在数据库中 t_order 表就不存在了,而是被拆分成多个子表 t_order_n 扩散在不同的数据库内,还按原SQL执行显然是行不通的,这时须要将分表配置中的逻辑表名称改写为路由之后所获取的实在表名称。

SELECT * FROM t_order_n

SQL执⾏

将路由和改写后的实在 SQL 平安且高效发送到底层数据源执行。但这个过程并不是简略的将 SQL 通过JDBC 间接发送至数据源执行,而是均衡数据源连贯创立以及内存占用所产生的耗费,它会自动化的均衡资源管制与执行效率。

后果归并

将从各个数据节点获取的多数据后果集,合并成一个大的后果集并正确的返回至申请客户端,称为后果归并。而咱们SQL中的排序、分组、分页和聚合等语法,均是在归并后的后果集上进行操作的。

4、实例验证

下面咱们联合springboot2,mybatis_plus+druid+sharding-jdbc疾速搭建一个分库分表案例。

1、筹备工作

先做筹备工作,创立两个数据库 ds-0ds-1,两个库中别离建表 t_order_0t_order_1t_order_2t_order_item_0t_order_item_1t_order_item_2t_config,不便后边验证播送表、绑定表的场景。
数据库表构造如下:

image

t_order_0 订单表

CREATE TABLE `t_order_0` (
  `id` bigint(200) NOT NULL,
  `order_no` varchar(100) DEFAULT NULL,
  `create_name` varchar(50) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

t_order_0t_order_item_0 互为关联表

CREATE TABLE `t_order_item_0` (
  `id` bigint(100) NOT NULL,
  `order_no` varchar(200) DEFAULT NULL,
  `item_name` varchar(50) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `order_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

播送表 t_config

CREATE TABLE `t_config` (
  `id` bigint(30) NOT NULL,
  `remark` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ShardingSphere 提供了4种分片配置形式:

  • Java 代码配置
  • Yaml 、properties 配置
  • Spring 命名空间配置
  • Spring Boot配置

此案例应用yaml方式配置,引入 shardingsphere 对应的 sharding-jdbc-spring-boot-starter 和 sharding-core-common 包,版本对立用的 4.0.0-RC1。

2、分片配置

<!-- for spring boot -->
<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  <version>4.0.0-RC1</version>
</dependency>
<!-- for spring namespace -->
<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>sharding-jdbc-spring-namespace</artifactId>
  <version>4.0.0-RC1</version>
</dependency>

此配置对原项目架构没有侵入性,mybatis-plus等配置信息不需要进行修改

筹备工作做完,接下来咱们逐个解读分片配置信息。咱们首先定义两个数据源 ds-0ds-1,并别离加上数据源的根底信息。

spring:
  shardingsphere:
    # 显示shardingsphere执行的sql
    props:
      sql:
        show: true
    datasource:
      names: ds-0,ds-1
      ds-0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/ds-0?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
        username: root
        password: 123456
        #初始化建立物理链接的个数
        initial-size: 5
        #最大连接池数量
        max-active: 10
        #最小连接池数量
        min-idle: 5
        #获取连接时最大等待时间,单位毫秒
        max-wait: 60000
        # 超过时间限制是否回收
        removeAbandoned: true
        # 当连接超过3分钟后会强制进行回收
        removeAbandonedTimeout: 180
        #打开PSCache,并且指定每个连接上PSCache的大小
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        #间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 600000
        #用来检测连接是否有效的sql 必须是一个查询语句。mysql中为 select 'x', oracle中为 select 1 from dual
        validation-query: select 'x'
        # validation-query-timeout: 5000
        #申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
        test-on-borrow: false
        #归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
        test-on-return: false
        test-while-idle: true
        #通过connectProperties属性来打开mergeSql功能,慢SQL记录
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
        #filters: #配置多个英文逗号分隔(统计,sql注入)
        filters: stat,wall
        #合并多个DruidDataSource的监控数据
        use-global-data-source-stat: false
        #配置stat-view-servlet
        stat-view-servlet:
          #允许开启监控
          enabled: true
          #监控面板路径
          url-pattern: /druid/*
      ds-1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/ds-1?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
        username: root
        password: 123456
        #初始化建立物理链接的个数
        initial-size: 5
        #最大连接池数量
        max-active: 10
        #最小连接池数量
        min-idle: 5
        #获取连接时最大等待时间,单位毫秒
        max-wait: 60000
        # 超过时间限制是否回收
        removeAbandoned: true
        # 当连接超过3分钟后会强制进行回收
        removeAbandonedTimeout: 180
        #打开PSCache,并且指定每个连接上PSCache的大小
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        #间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 600000
        #用来检测连接是否有效的sql 必须是一个查询语句。mysql中为 select 'x', oracle中为 select 1 from dual
        validation-query: select 'x'
        # validation-query-timeout: 5000
        #申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
        test-on-borrow: false
        #归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
        test-on-return: false
        test-while-idle: true
        #通过connectProperties属性来打开mergeSql功能,慢SQL记录
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
        #filters: #配置多个英文逗号分隔(统计,sql注入)
        filters: stat,wall
        #合并多个DruidDataSource的监控数据
        use-global-data-source-stat: false
        #配置stat-view-servlet
        stat-view-servlet:
          #允许开启监控
          enabled: true
          #监控面板路径
          url-pattern: /druid/*
    sharding:
      tables:
        # 配置分片表 t_order
        t_order:
          key-generator:
            column: id
            type: SNOWFLAKE
          #指定数据节点
          actual-data-nodes: ds-$->{0..1}.t_order_$->{0..2}
          ### 分库策略
          database-strategy:
            inline:
              #分库分片键
              sharding‐column: id
              #分库分片算法
              algorithm-expression: ds-$->{id % 2}
          # 分表策略
          table-strategy:
            inline:
              # 分表分片健
              sharding‐column: id
              # 分表算法
              algorithm-expression: t_order_$->{id % 3}
        # 配置分片表 t_order
        t_order_item:
          key-generator:
            column: id
            type: SNOWFLAKE
          #指定数据节点
          actual-data-nodes: ds-$->{0..1}.t_order_item_$->{0..2}
          ### 分库策略
          database-strategy:
            inline:
              #分库分片键
              sharding‐column: order_id
              #分库分片算法
              algorithm-expression: ds-$->{order_id % 2}
          # 分表策略
          table-strategy:
            inline:
              # 分表分片健
              sharding‐column: order_id
              # 分表算法
              algorithm-expression: t_order_item_$->{order_id % 3}

      # 绑定表关系
      binding-tables[0]: t_order,t_order_item
      # 配置播送表
      broadcast-tables: t_config
      binding-tables:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  main:
    allow-bean-definition-overriding: true

3、验证分片

启动项目,查看druid监控,这里我们看到两个数据库ds-0和ds-1

image

编写测试用例,咱们同时向 t_ordert_order_item 表插入 10条订单记录,并不给定主键 id , 字段值

这里需要注意表的主键字段需要用id,如果不是字段id,mybatis-plus在save之后不能返回主键id

@Test
public void testSharding() {
    for (int i = 0; i < 10; i++) {
        TOrder order = new TOrder();
        order.setOrderNo("A000" + String.valueOf(i));
        order.setCreateName("订单" + i);
        order.setPrice(new BigDecimal(i));
        tOrderService.save(order);

        TOrderItem orderItem = new TOrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setOrderNo(order.getOrderNo());
        orderItem.setItemName("服务项目" + i);
        orderItem.setPrice(new BigDecimal("" + i));
        tOrderItemService.save(orderItem);
    }
}

image

image

image

image

看到订单记录被胜利扩散到了不同的库表中, order_id 字段也主动生成了主键ID,根底的分片性能就实现了。

那向播送表 t_config 中插入一条数据会是什么成果呢?

@Test
public void testShardingbrock() {
    TConfig tConfig = new TConfig();
    tConfig.setRemark("我是播送表");
    tConfig.setCreateTime(LocalDateTime.now());
    tConfig.setLastModifyTime(LocalDateTime.now());
    tConfigService.save(tConfig);
}

image

image

发现所有库中 t_config 表都执行了这条SQL,播送表和 MQ播送订阅的模式很类似,所有订阅的客户端都会收到同一条音讯。

简略SQL操作验证没问通,接下来在试试简单一点的联结查问,前边咱们曾经把 t_ordert_order_item 表设为绑定表,间接联表查问执行一下。

通过控制台日志发现,逻辑表SQL 通过解析当前,只对 t_order_0t_order_item_0 表进行了关联产生一条SQL。

那如果不互为绑定表又会是什么状况呢?去掉 spring.shardingsphere.sharding.binding-tables试一下。

发现控制台解析出了 3条实在表SQL,而去掉 order_id 作为查问条件再次执行后,后果解析出了 9条SQL,进行了笛卡尔积查问。所以相比之下绑定表的长处就显而易见了。

5、总结

以上对分库分表中间件 sharding-jdbc 的根底概念做了简略梳理,疾速的搭建了一个分库分表案例,但这只是实际分库分表的第一步后边将陆续解说自定义分布式主键、分布式数据库事务、分布式服务治理,数据脱敏等。

本文的搭建操作基于https://lequ7.com/guan-yu-java-yi-wen-kuai-su-ru-men-fen-ku-fen-biao-zhong-jian-jian-shardingjdbc-bi-xiu-ke.html指导,稍作改动,在这里对作者表示感谢

posted @ 2022-08-01 11:22  十月围城小童鞋  阅读(2190)  评论(0编辑  收藏  举报