Sharding-Jdbc 源码分析——主体流程

510 阅读6分钟

首先让我们来介绍一下Sharding-JDBC。

什么是Sharding-JDBC?

Sharding-JDBC是一个开源的分布式数据库中间件,它是为了解决关系型数据库在大数据量、高并发情况下的性能瓶颈而设计的。该项目是ShardingSphere生态圈的一部分,致力于为用户提供分布式数据库的解决方案。

它的作用是什么?
  • 数据分片:Sharding-JDBC可以将数据库中的数据按照一定规则(如按照某个字段的取值范围)进行水平切分,将数据分散存储在多个数据库节点中,从而降低单个数据库的压力,提高系统的扩展性和性能。
  • 读写分离:它支持将读操作和写操作分发到不同的数据库节点上,以提高数据库的读写性能。
  • 路由和负载均衡:Sharding-JDBC具备强大的路由功能,能够根据SQL语句中的条件将请求路由到相应的数据库节点上,同时支持负载均衡策略,保证各个节点的负载相对均衡。
  • 分布式事务支持:它提供了分布式事务的支持,可以保证分布式环境下的数据一致性和可靠性。
  • 简化开发:通过Sharding-JDBC,开发者无需关心分布式数据库的复杂性,只需按照普通的数据库访问方式编写代码,即可享受到分布式数据库带来的性能提升和扩展性。

接下来通过一个main函数来分析下分库分表主体流程,

数据准备:

首先创建两个数据库和4张表,将user_id设置为分片键。每个数据库中有trade_order_0 trade_order_1 trade_order_2 trade_order_3四张表。

CREATE TABLE `trade_order_0` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `user_id` bigint NOT NULL DEFAULT '0' COMMENT '用户ID',
  `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态 1待支付 ',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`) USING BTREE
) 
Demo代码:
 public static void main(String[] args) throws SQLException {
        //=======一、配置数据库
        Map<String, DataSource> dataSourceMap = new HashMap<>(2);//为两个数据库的datasource
        // 配置第一个数据源
        HikariDataSource dataSource0 = new HikariDataSource();
        dataSource0.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource0.setJdbcUrl("url");
        dataSource0.setUsername("user_name");
        dataSource0.setPassword("password");
        dataSourceMap.put("m0", dataSource0);
        // 配置第二个数据源
        HikariDataSource dataSource1 = new HikariDataSource();
        dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource1.setJdbcUrl("url");
        dataSource1.setUsername("user_name");
        dataSource1.setPassword("password");
        dataSourceMap.put("m1", dataSource1);

        //=======二、配置分库分表策略
        // 配置分片规则
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        //真实表分布
        TableRuleConfiguration courseTableRuleConfig = new TableRuleConfiguration("trade_order",
                "m$->{0..1}.trade_order_$->{0..3}");
        //真实库分布
        courseTableRuleConfig.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id",
                "m$->{user_id%2}"));

        courseTableRuleConfig.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id",
                "trade_order_$->{user_id%2}"));
        shardingRuleConfig.getTableRuleConfigs().add(courseTableRuleConfig);

        //三、配置属性值
        Properties properties = new Properties();
        //打开日志输出
        properties.setProperty("sql.show", "true");
        //K1 创建ShardingSphere的数据源 ShardingDataSource
        DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, properties);

        Connection conn = null;
        try {
            //ShardingConnectioin
            conn = dataSource.getConnection();
            //ShardingStatement
            Statement statement = conn.createStatement();
            String sql = "SELECT id,order_id,user_id,order_status from trade_order where user_id = 1493538";
            //ShardingResultSet
            ResultSet result = statement.executeQuery(sql);
            while (result.next()) {
                System.out.println("result:" + result.getInt("id"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (null != conn) {
                conn.close();
            }
        }
    }
在sharding-jdbc中重要的四个对象:

ShardingDataSource:对象内最核心的字段为

  • dataSourceMap:负责管理多个数据源,这些数据源可以是主从数据源,也可以是分片数据源,每个数据源可能连接着不同的数据库实例。
  • ShardingRule:分片规则,包含了数据分片的配置信息,包括哪些表需要进行分片、分片键是什么、分片算法等。它会根据配置的规则来解析 SQL,并决定将 SQL 发送到哪个具体的数据源执行。

以下是初始化数据源主要过程:

    public static DataSource createDataSource(
            final Map<String, DataSource> dataSourceMap, final ShardingRuleConfiguration shardingRuleConfig, final Properties props) throws SQLException {
        //创建ShardingRule以及ShardingDataSource实例
        return new ShardingDataSource(dataSourceMap, new ShardingRule(shardingRuleConfig, dataSourceMap.keySet()), props);
    }



    //解析ShardingConfig,构建ShardingRule
    public ShardingRule(final ShardingRuleConfiguration shardingRuleConfig, final Collection<String> dataSourceNames) {
        Preconditions.checkArgument(null != shardingRuleConfig, "ShardingRuleConfig cannot be null.");
        Preconditions.checkArgument(null != dataSourceNames && !dataSourceNames.isEmpty(), "Data sources cannot be empty.");
        this.ruleConfiguration = shardingRuleConfig;
        //获取所有的实际数据库
        shardingDataSourceNames = new ShardingDataSourceNames(shardingRuleConfig, dataSourceNames);
        //表路由规则
        tableRules = createTableRules(shardingRuleConfig);
        //获取广播表
        broadcastTables = shardingRuleConfig.getBroadcastTables();
        //绑定表
        bindingTableRules = createBindingTableRules(shardingRuleConfig.getBindingTableGroups());
        //创建默认的分库策略
        defaultDatabaseShardingStrategy = createDefaultShardingStrategy(shardingRuleConfig.getDefaultDatabaseShardingStrategyConfig());
        //创建默认的分表策略
        defaultTableShardingStrategy = createDefaultShardingStrategy(shardingRuleConfig.getDefaultTableShardingStrategyConfig());
        //分片键
        defaultShardingKeyGenerator = createDefaultKeyGenerator(shardingRuleConfig.getDefaultKeyGeneratorConfig());
        //主从规则
        masterSlaveRules = createMasterSlaveRules(shardingRuleConfig.getMasterSlaveRuleConfigs());
        //加密规则
        encryptRule = createEncryptRule(shardingRuleConfig.getEncryptRuleConfig());
    }

ShardingConnection:主要作用是管理连接和事务,以及提供执行 SQL 操作的接口。

    //ShardingConnection中主要字段
    public ShardingConnection(final Map<String, DataSource> dataSourceMap, final ShardingRuntimeContext runtimeContext, final TransactionType transactionType) {
        this.dataSourceMap = dataSourceMap;
        this.runtimeContext = runtimeContext;
        this.transactionType = transactionType;
        shardingTransactionManager = runtimeContext.getShardingTransactionManagerEngine().getTransactionManager(transactionType);
    }

ShardingStatement:主要作用是执行普通的SQL语句,并处理与之相关的操作。

  • SQL 执行:ShardingStatement负责执行普通的 SQL 语句。与预处理语句不同,普通 SQL 语句是直接执行的,而不需要预先编译成模板。
  • SQL 路由:在执行普通 SQL 语句时,ShardingStatement会根据分片规则对 SQL 进行路由,确定应该将 SQL 发送到哪个具体的数据源或数据节点执行。
  • 结果集处理:如果 SQL 是查询语句,ShardingStatement负责处理执行结果集,并将结果返回给调用方。

在prepare方法中主要负责路由算法,重写逻辑SQL,将逻辑SQL转化为实际SQL,将真实执行sql单元保存到上下文中。

    // 分片语句的执行过程
    @Override
    public ResultSet executeQuery(final String sql) throws SQLException {
        if (Strings.isNullOrEmpty(sql)) {
            throw new SQLException(SQLExceptionConstant.SQL_STRING_NULL_OR_EMPTY);
        }
        ResultSet result;
        try {
            //这个上下文中包含了实际运行所需要的全部信息。包括真实数据源、真实SQL、真实参数
            //执行引擎的准备阶段
            executionContext = prepare(sql);
            //执行引擎的执行阶段
            List<QueryResult> queryResults = statementExecutor.executeQuery();
            //结果归并阶段
            MergedResult mergedResult = mergeQuery(queryResults);
            result = new ShardingResultSet(statementExecutor.getResultSets(), mergedResult, this, executionContext);
        } finally {
            currentResultSet = null;
        }
        currentResultSet = result;
        return result;
    }

ShardingResultSet:主要作用是负责处理执行查询操作后返回的结果集,以及结果集的归并。包括获取查询结果的行和列,以及获取各种数据类型的值。提供了操作结果集游标的方法,可以将游标移动到结果集的下一行或上一行,并获取当前行的数据。

    public ShardingResultSet(final List<ResultSet> resultSets, final MergedResult mergeResultSet, final Statement statement, final ExecutionContext executionContext) throws SQLException {
        super(resultSets, statement, executionContext);
        this.mergeResultSet = mergeResultSet;
        columnLabelAndIndexMap = createColumnLabelAndIndexMap(resultSets.get(0).getMetaData());
    }
总结:

sharding-jdbc的主体流程与 JDBC 的主体流程有相似之处,但也有一些重要的区别。

  • 获取分片数据源:应用程序通过ShardingDataSource 获取分片数据源,而不是直接获取单个数据库连接。
  • SQL 解析与路由:在执行 SQL 操作时,Sharding-JDBC 会解析 SQL,并根据分片规则对 SQL 进行路由,确定应该将 SQL 发送到哪个具体的数据源或数据节点执行。
  • 处理结果集:如果 SQL 查询涉及多个数据节点,每个数据节点都会返回一个单独的结果集。Sharding-JDBC 需要将这些结果集合并成一个整体的结果集,并返回给应用程序。而在 JDBC 中,通常只涉及单个数据库,因此只会返回一个结果集,无需合并。同样的对于 SQL 包含了排序或分页操作,并且涉及多个数据节点,Sharding-JDBC 需要对每个数据节点返回的结果集进行排序和分页,然后再进行结果集合并。