分布式专题——10.4 ShardingSphere-Proxy服务端分库分表

1 为什么要有服务端分库分表?

  • ShardingSphere-Proxy 是 ShardingSphere 提供的服务端分库分表工具,定位是"透明化的数据库代理"。

    • 它模拟 MySQL 或 PostgreSQL 的数据库服务,应用程序(Application)只需像访问单个数据库一样访问 ShardingSphere-Proxy;

    • 实际的分库分表逻辑,由 ShardingSphere-Proxy 在代理层完成,对应用程序完全透明;

  • ShardingSphere-JDBC 已经能实现分库分表,为什么还需要 ShardingSphere-Proxy?

    • 对 ORM 框架更友好

      • ShardingSphere-JDBC 的问题:需要在业务代码中直接写分库分表逻辑 (比如代码里写:数据该路由到哪个库/表)。如果项目用了 ORM 框架(如 MyBatis、Hibernate),这种代码侵入式的逻辑容易和 ORM 冲突;

      • ShardingSphere-Proxy 的解决:作为第三方服务端代理 ,分库分表逻辑在代理层(ShardingSphere-Proxy)处理 ,应用程序完全不用关心。应用只需按访问单库的方式写代码,无缝对接 ORM 框架;

    • 对 DBA(数据库管理员)更友好

      • ShardingSphere-JDBC 的问题:DBA 要管理分库分表后的数据库,得先了解 ShardingSphere 的专属功能和 API,学习成本高;

      • ShardingSphere-Proxy 的解决:对 DBA 完全透明 。DBA 可以直接操作原始数据库(像平时管理 MySQL/PostgreSQL 一样),无需了解 ShardingSphere 的技术细节,简化了 DBA 的工作;

    • 避免业务代码侵入分库分表逻辑

      • ShardingSphere-JDBC 的问题:分库分表的规则配置(比如"按用户 ID 分库"的规则)要写在业务代码里,会让业务代码变得臃肿。如果规则变更(比如从"按用户 ID 分"改成"按订单 ID 分"),要修改大量业务代码,维护成本高;

      • ShardingSphere-Proxy 的解决:通过外部配置文件管理分库分表规则,业务代码完全不用关心这些逻辑。规则变更时,只需改配置,不影响业务代码;

    • 实现"无中心化"数据治理

      • ShardingSphere-JDBC 的局限:多数据源的治理(比如跨库监控、报警、统一管理)难以统一;

      • ShardingSphere-Proxy 的解决:多个数据源可以注册到同一个 ShardingSphere-Proxy 代理服务 中。基于代理,能实现跨数据源 的数据治理(比如统一监控、报警、数据管理),特别适合大规模微服务系统的运维。

2 基本使用

2.1 部署 ShardingSphere-Proxy

  • 下载并解压(注意不要有中文,此处选择的版本:Apache Archive Distribution Directory);

  • ShardingSphere-Proxy 是一个典型的 Java 应用程序,解压后目录结构如下:

  • 解压完成后,如果要连接 MySQL 数据库,那么需要手动将 JDBC 的驱动包mysql-connector-java-8.0.20.jar复制到 ShardingSphere-Proxy 的lib目录下(ShardingSphere-Proxy 默认只附带了 PostgreSQL 的 JDBC 驱动包,没有包含 MySQL 的 JDBC 驱动包);

  • 打开conf目录,在这个目录下就有 ShardingSphere-Proxy 的所有配置文件(不同版本的会不一样):

  • 打开server.yaml,把其中的rules部分和props部分的注释取消:

    yaml 复制代码
    rules:
     # 权限控制规则配置
     - !AUTHORITY
       users:
         # 定义用户权限:root用户可从任何主机(%)访问,拥有root角色;sharding用户可从任何主机访问,拥有sharding角色
         - root@%:root
         - sharding@:sharding
       provider:
         # 权限提供者类型:ALL_PERMITTED表示允许所有操作(无权限限制)
         type: ALL_PERMITTED
     # 事务管理配置
     - !TRANSACTION
       # 默认事务类型:XA分布式事务
       defaultType: XA
       # 事务管理器提供者:Atomikos(一种XA事务管理器实现)
       providerType: Atomikos
     # SQL解析器配置
     - !SQL_PARSER
       # 启用SQL注释解析功能
       sqlCommentParseEnabled: true
       sqlStatementCache:
         # SQL语句缓存初始容量
         initialCapacity: 2000
         # SQL语句缓存最大容量
         maximumSize: 65535
       parseTreeCache:
         # 解析树缓存初始容量
         initialCapacity: 128
         # 解析树缓存最大容量
         maximumSize: 1024
    
    # 属性配置
    props:
     # 每个查询允许的最大连接数
     max-connections-size-per-query: 1
     # 内核线程池大小(默认无限制)
     kernel-executor-size: 16
     # 代理前端刷新阈值(网络数据包刷新条件)
     proxy-frontend-flush-threshold: 128
     # 是否启用Hint功能(强制路由提示)
     proxy-hint-enabled: false
     # 是否在日志中显示SQL语句
     sql-show: false
     # 是否启用表元数据一致性检查
     check-table-metadata-enabled: false
     # 代理后端查询获取大小:-1表示使用不同JDBC驱动程序的最小值
     proxy-backend-query-fetch-size: -1
     # 代理前端执行线程数:0表示由Netty自动决定
     proxy-frontend-executor-size: 
     # 代理后端执行模式:OLAP(联机分析处理)或OLTP(联机事务处理)
     proxy-backend-executor-suitable: OLAP
     # 代理前端最大连接数:0或负数表示无限制
     proxy-frontend-max-connections: 0
     # SQL联邦查询类型:NONE(禁用),ORIGINAL(原始),ADVANCED(高级)
     sql-federation-type: NONE
     # 代理后端驱动类型:JDBC(标准)或ExperimentalVertx(实验性Vert.x)
     proxy-backend-driver-type: JDBC
     # 默认MySQL版本号(当缺少schema信息时使用)
     proxy-mysql-default-version: 8.0.20
     # 代理服务默认端口
     proxy-default-port: 3307
     # Netty网络连接后备队列长度
     proxy-netty-backlog: 1024
    • 注意:为了与 MySQL JDBC 的驱动包mysql-connector-java-8.0.20.jar兼容,将proxy-mysql-default-version修改为8.0.20
  • 启动:

  • 然后就可以使用 MySQL 的客户端(比如 DataGrip)去连接 ShardingSphere-Proxy 了,且可以像用 MySQL 一样去使用 ShardingSphere-Proxy:

    sql 复制代码
    mysql> show databases;
    +--------------------+
    | schema_name        |
    +--------------------+
    | shardingsphere     |
    | information_schema |
    | performance_schema |
    | mysql              |
    | sys                |
    +--------------------+
    5 rows in set (0.01 sec)
    
    mysql> use shardingsphere
    Database changed
    mysql> show tables;
    +---------------------------+------------+
    | Tables_in_shardingsphere  | Table_type |
    +---------------------------+------------+
    | sharding_table_statistics | BASE TABLE |
    +---------------------------+------------+
    1 row in set (0.01 sec)
    
    mysql> select * from sharding_table_statistics;
    Empty set (1.25 sec)
  • 不过要注意,此时 ShardingSphere-Proxy 只是一个虚拟库,所以并不能像 MySQL 一样去随意建表或修改数据,比如下面就建表失败:

    sql 复制代码
    mysql> CREATE TABLE test  (id varchar(255) NOT NULL);
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> select * from test;
    30000 - Unknown exception: At line 0, column 0: Object 'test' not found
    mysql> show tables;
    +---------------------------+------------+
    | Tables_in_shardingsphere  | Table_type |
    +---------------------------+------------+
    | sharding_table_statistics | BASE TABLE |
    +---------------------------+------------+
    1 row in set (0.01 sec)

2.2 常用的分库分表配置策略

  • 打开config-sharding.yaml,配置逻辑表course

    yaml 复制代码
    # 逻辑数据库名称(客户端连接时使用的虚拟数据库名)
    databaseName: sharding_db
    
    # 数据源配置(定义实际的后端数据库连接)
    dataSources:
      # 第一个数据源,命名为m0
      m0:
        # MySQL数据库连接URL,指向192.168.65.212服务器的3306端口,数据库名为shardingdb1
        url: jdbc:mysql://192.168.65.212:3306/shardingdb1?serverTimezone=UTC&useSSL=false
        username: root
        password: root
        # 连接超时时间(毫秒)
        connectionTimeoutMilliseconds: 30000
        # 连接空闲超时时间(毫秒)
        idleTimeoutMilliseconds: 60000
        # 连接最大生命周期(毫秒)
        maxLifetimeMilliseconds: 1800000
        # 连接池最大大小
        maxPoolSize: 50
        # 连接池最小大小
        minPoolSize: 1
      # 第二个数据源,命名为m1
      m1:
        url: jdbc:mysql://192.168.65.212:3306/shardingdb2?serverTimezone=UTC&useSSL=false
        username: root
        password: root
        connectionTimeoutMilliseconds: 30000
        idleTimeoutMilliseconds: 60000
        maxLifetimeMilliseconds: 1800000
        maxPoolSize: 50
        minPoolSize: 1
    
    # 分片规则配置
    rules:
    - !SHARDING  # 分片规则标识
      tables:
        # 配置course表的分片规则
        course:
          # 实际数据节点:分布在m0和m1数据库的course_1和course_2表中
          actualDataNodes: m${0..1}.course_${1..2}
          # 数据库分片策略
          databaseStrategy:
            standard:  # 标准分片策略
              shardingColumn: cid  # 分片列(根据此字段的值决定数据路由到哪个数据库)
              shardingAlgorithmName: course_db_alg  # 使用的分片算法名称
          # 表分片策略
          tableStrategy:
            standard:  # 标准分片策略
              shardingColumn: cid  # 分片列(根据此字段的值决定数据路由到哪个表)
              shardingAlgorithmName: course_tbl_alg  # 使用的分片算法名称
          # 主键生成策略
          keyGenerateStrategy:
            column: cid  # 需要生成主键的列名
            keyGeneratorName: alg_snowflake  # 使用的主键生成器名称
      # 分片算法定义
      shardingAlgorithms:
        # 数据库分片算法:取模算法
        course_db_alg:
          type: MOD  # 取模分片算法
          props:
            sharding-count: 2  # 分片数量(2个数据库)
        # 表分片算法:行表达式算法
        course_tbl_alg:
          type: INLINE  # 行表达式分片算法
          props:
            # 算法表达式:根据cid字段的值取模2再加1,得到表后缀(1或2)
            algorithm-expression: course_${cid%2+1}
      # 主键生成器定义
      keyGenerators:
        # 雪花算法生成器,用于生成分布式唯一ID
        alg_snowflake:
          type: SNOWFLAKE  # 使用雪花算法
  • 重新启动 ShardingSphere-Proxy 服务:

    sql 复制代码
    mysql> show databases;
    +--------------------+
    | schema_name        |
    +--------------------+
    | information_schema |
    | performance_schema |
    | sys                |
    | shardingsphere     |
    | sharding_db        |
    | mysql              |
    +--------------------+
    6 rows in set (0.02 sec)
     
    mysql> use sharding_db;
    Database changed
    mysql> show tables;
    +-----------------------+------------+
    | Tables_in_sharding_db | Table_type |
    +-----------------------+------------+
    | course                | BASE TABLE |
    | user_2                | BASE TABLE |
    | user                  | BASE TABLE |
    | user_1                | BASE TABLE |
    +-----------------------+------------+
    4 rows in set (0.02 sec)
     
    mysql> select * from course;
    +---------------------+-------+---------+---------+
    | cid                 | cname | user_id | cstatus |
    +---------------------+-------+---------+---------+
    | 1017125767709982720 | java  |    1001 | 1       |
    | 1017125769383510016 | java  |    1001 | 1       |
    
    mysql> select * from course_1;
    +---------------------+-------+---------+---------+
    | cid                 | cname | user_id | cstatus |
    +---------------------+-------+---------+---------+
    | 1017125767709982720 | java  |    1001 | 1       |
    | 1017125769383510016 | java  |    1001 | 1       |
    • 可以看到多了一个sharding_db库。

3 ShardingSphere-Proxy 中的分布式事务机制

3.1 为什么需要分布式事务?

  • XA 事务 :: ShardingSphere

  • 2.1 部署 ShardingSphere-Proxy中配置server.yaml时,有一个TRANSACTION配置项:

    yaml 复制代码
    rules:
    - !TRANSACTION
       defaultType: XA
       providerType: Atomikos
  • 为什么 ShardingSphere-Proxy 需要分布式事务?

    • ShardingSphere(尤其是 ShardingSphere-Proxy)要操作分布式的数据库集群(多个库、多个节点)。而数据库的本地事务只能保证单个数据库内的事务安全,无法覆盖跨多个数据库的场景;

    • 因此,必须引入分布式事务管理机制 ,才能保证 ShardingSphere-Proxy 中 SQL 执行的原子性。开启分布式事务后,开发者不用再手动考虑跨库 SQL 的事务问题。

3.2 什么是 XA 事务?

  • XA 是由 X/Open Group 组织定义的分布式事务标准,主流关系型数据库(如 MySQL、Oracle 等)都实现了 XA 协议(比如 MySQL 5.0.3+ 的 InnoDB 引擎支持 XA);

  • XA 事务的核心是两阶段提交(2PC) ,通过**准备(Prepare)提交(Commit)**两个阶段,保证跨多个数据库的事务一致性;

  • XA 分布式事务操作示例:

    sql 复制代码
    -- 1. 开启XA事务,'test'是事务标识符(XID),将事务状态设置为ACTIVE(活跃)
    -- 事务标识符用于在分布式环境中唯一标识一个事务
    XA START 'test';
    
    -- 2. 在ACTIVE状态的XA事务中执行业务SQL语句
    -- 这些操作是事务的一部分,但尚未最终提交
    INSERT INTO dict VALUES(1, 't', 'test');
    
    -- 3. 结束XA事务,将事务状态从ACTIVE改为IDLE(空闲)
    -- 表示事务内的所有操作已完成,准备进入准备阶段
    XA END 'test';
    
    -- 4. 准备提交事务,将事务状态从IDLE改为PREPARED(已准备)
    -- 此阶段事务管理器会确保所有资源管理器都已准备好提交
    XA PREPARE 'test';
    
    -- 5. 列出所有处于PREPARED状态的XA事务
    -- 返回的信息包含:gtrid(全局事务ID)、bqual(分支限定符)、formatID(格式ID)和data(数据)
    XA RECOVER;
    
    -- 6. 提交已准备的XA事务,使所有更改永久生效
    -- 这是两阶段提交的第二阶段(提交阶段)
    XA COMMIT 'test';
    
    -- 6. 替代方案:回滚已准备的XA事务,撤销所有更改
    -- 在PREPARED状态下也可以选择回滚而不是提交
    XA ROLLBACK 'test';
    
    -- 注意:也可以使用单条命令直接提交,将预备和提交合并操作
    -- XA COMMIT 'test' ONE PHASE;
  • ShardingSphere 集成了多个 XA 实现框架(Atomikos、Bitronix、Narayana)。在 ShardingSphere-Proxy 中,默认只集成了 Atomikos (所以配置里 providerType: Atomikos)。

3.3 实战理解 XA 事务

  • 引入 Maven 依赖:

    xml 复制代码
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-transaction-xa-core</artifactId>
        <version>5.2.1</version>
        <exclusions>
            <exclusion>
                <artifactId>transactions-jdbc</artifactId>
                <groupId>com.atomikos</groupId>
            </exclusion>
            <exclusion>
                <artifactId>transactions-jta</artifactId>
                <groupId>com.atomikos</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 版本滞后了 -->
    <dependency>
        <artifactId>transactions-jdbc</artifactId>
        <groupId>com.atomikos</groupId>
        <version>5.0.8</version>
    </dependency>
    <dependency>
        <artifactId>transactions-jta</artifactId>
        <groupId>com.atomikos</groupId>
        <version>5.0.8</version>
    </dependency>
    <!-- 使用XA事务时,可以引入其他几种事务管理器 -->
    <!--        <dependency>-->
    <!--            <groupId>org.apache.shardingsphere</groupId>-->
    <!--            <artifactId>shardingsphere-transaction-xa-bitronix</artifactId>-->
    <!--            <version>5.2.1</version>-->
    <!--        </dependency>-->
    <!--        <dependency>-->
    <!--            <groupId>org.apache.shardingsphere</groupId>-->
    <!--            <artifactId>shardingsphere-transaction-xa-narayana</artifactId>-->
    <!--            <version>5.2.1</version>-->
    <!--        </dependency>-->
  • 配置事务管理器:

    java 复制代码
    @Configuration
    @EnableTransactionManagement
    public class TransactionConfiguration {
        
        @Bean
        public PlatformTransactionManager txManager(final DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
  • 测试案例:

    java 复制代码
    public class MySQLXAConnectionTest {
        public static void main(String[] args) throws SQLException {
            // 是否打印XA相关命令,用于调试目的
            boolean logXaCommands = true;
            
            // ============== 初始化阶段:建立数据库连接 ==============
            // 获取第一个数据库连接(资源管理器RM1)
            Connection conn1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/coursedb?serverTimezone=UTC", "root", "root");
            // 创建XA连接包装器,用于支持分布式事务
            XAConnection xaConn1 = new MysqlXAConnection((com.mysql.cj.jdbc.JdbcConnection) conn1, logXaCommands);
            // 获取XA资源接口,用于控制分布式事务
            XAResource rm1 = xaConn1.getXAResource();
            
            // 获取第二个数据库连接(资源管理器RM2)
            Connection conn2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/coursedb2?serverTimezone=UTC", "root", "root");
            // 创建第二个XA连接包装器
            XAConnection xaConn2 = new MysqlXAConnection((com.mysql.cj.jdbc.JdbcConnection) conn2, logXaCommands);
            // 获取第二个XA资源接口
            XAResource rm2 = xaConn2.getXAResource();
            
            // 生成全局事务ID(GTrid,是全局事务标识符,在整个事务生命周期中唯一)
            byte[] gtrid = "g12345".getBytes();
            int formatId = 1; // 格式标识符,通常为1,表示标准XID格式
            
            try {
                // ============== 分别执行RM1和RM2上的事务分支 ====================
                
                // 为第一个资源管理器生成分支事务ID(BQual)
                byte[] bqual1 = "b00001".getBytes();
                // 创建完整的事务ID(XID),包含全局ID和分支ID
                Xid xid1 = new MysqlXid(gtrid, bqual1, formatId);
                // 开始第一个分支事务,TMNOFLAGS表示新建一个事务(不是加入或恢复现有事务)
                rm1.start(xid1, XAResource.TMNOFLAGS);
                // 在第一个分支事务中执行SQL操作
                PreparedStatement ps1 = conn1.prepareStatement("INSERT INTO `dict` VALUES (1, 'T', '测试1');");
                ps1.execute();
                // 结束第一个分支事务,TMSUCCESS表示事务执行成功
                rm1.end(xid1, XAResource.TMSUCCESS);
                
                // 为第二个资源管理器生成分支事务ID
                byte[] bqual2 = "b00002".getBytes();
                // 创建第二个事务ID
                Xid xid2 = new MysqlXid(gtrid, bqual2, formatId);
                // 开始第二个分支事务
                rm2.start(xid2, XAResource.TMNOFLAGS);
                // 在第二个分支事务中执行SQL操作
                PreparedStatement ps2 = conn2.prepareStatement("INSERT INTO `dict` VALUES (2, 'F', '测试2');");
                ps2.execute();
                // 结束第二个分支事务
                rm2.end(xid2, XAResource.TMSUCCESS);
                
                // =================== 两阶段提交流程 ============================
                
                // 第一阶段:准备阶段 - 询问所有资源管理器是否准备好提交事务
                int rm1_prepare = rm1.prepare(xid1);
                int rm2_prepare = rm2.prepare(xid2);
                
                // 第二阶段:提交阶段 - 根据准备阶段的结果决定提交或回滚
                boolean onePhase = false; // 设置为false表示使用标准的两阶段提交
                
                // 检查所有资源管理器是否都准备成功
                if (rm1_prepare == XAResource.XA_OK
                        && rm2_prepare == XAResource.XA_OK
                ) {
                    // 所有分支都准备成功,提交所有事务
                    rm1.commit(xid1, onePhase);
                    rm2.commit(xid2, onePhase);
                } else {
                    // 如果有任何一个分支准备失败,回滚所有事务
                    rm1.rollback(xid1);
                    rm2.rollback(xid2);
                }
            } catch (XAException e) {
                // 处理XA异常,打印异常信息
                e.printStackTrace();
            }
        }
    }
  • XA标准规范了事务XID 的格式,它由三个部分组成:gtrid [, bqual [, formatID ]],具体含义如下:

    • gtrid(全局事务标识符)

      • global transaction identifier,是用于标识一个全局事务的唯一值 。在分布式事务场景下,多个数据库节点参与同一个事务,通过这个全局事务标识符可以将这些操作关联到一起,明确它们属于同一个事务;
      • 比如在一个涉及电商系统多个数据库(订单库、库存库、支付库)的分布式事务中,通过相同的gtrid就能确认这些数据库上的操作都属于同一次购物流程对应的事务;
    • bqual(分支限定符)

      • branch qualifier ,用来进一步限定事务分支。如果没有提供,会使用默认值即一个空字符串;
      • 例如,在一个分布式事务中有多个数据库节点,每个节点上的操作可以看作事务的一个分支,bqual 就可以用来区分和标识这些不同的分支,以便更精确地管理事务在各个节点上的执行情况;
    • formatID(格式标识符)

      • 是一个数字,用于标记gtrid和bqual值的格式,是一个正整数,最小为0,默认值是1 ;
      • 它主要用于在不同的系统或数据库之间进行交互时,明确全局事务标识符和分支限定符的格式规则,以确保各方能够正确解析和处理事务标识信息。
  • 使用XA事务时的注意事项

    • 无法自动提交 :与本地事务在满足一定条件下可以自动提交不同,XA事务需要开发者显式地调用提交指令(如XA COMMIT) 。这就要求开发者在编写代码时,要准确把控事务提交的时机,避免因疏忽未提交事务而导致数据不一致等问题。例如在编写一个涉及多个数据库操作的业务逻辑时,开发者需要在所有操作都成功执行后,手动调用提交语句来完成事务,否则事务会一直处于未提交状态;

    • 效率低下:XA事务执行效率远低于本地事务,通常耗时能达到本地事务的10倍。这是因为在XA事务中,全局事务的状态都需要持久化,涉及到多个数据库节点之间的协调和通信,比如在两阶段提交过程中,准备阶段各个节点要记录事务状态等信息,提交阶段又需要进行多次确认和交互,这些额外的操作增加了系统开销,降低了事务处理的速度。这就意味着在对性能要求极高的场景中,使用XA事务可能并不合适,需要谨慎评估;

    • 故障隔离困难:在XA事务提交前如果出现故障(如某个数据库节点宕机、网络中断等),很难将问题隔离开 。由于XA事务涉及多个数据库节点的协同工作,一个节点出现故障可能会影响到整个事务的执行,而且故障排查和恢复相对复杂。例如在一个跨地域的分布式数据库系统中,如果某个地区的数据库节点在XA事务准备阶段出现故障,不仅要处理该节点自身的数据恢复问题,还要协调其他节点来处理事务状态,以保证整个系统的数据一致性。

3.4 如何在 ShardingSphere-Proxy 中使用其他事务管理器?

  • ShardingSphere-Proxy 支持多种分布式事务管理器(如 Atomikos、Narayana、Bitronix)。默认集成的是 Atomikos,若要改用其它两种事务管理器,需手动配置;

  • 具体操作(以 Narayana 为例)

    • 添加 Narayana 的依赖包:

      • 将 Narayana 事务集成的 Jar 包(如 shardingsphere-transaction-xa-narayana-5.2.1.jar),放到 ShardingSphere-Proxy 的 lib 目录下;
      • 该 Jar 包可通过 Maven 依赖下载(需要在项目的 Maven 配置中引入对应依赖,构建后获取 Jar 包);
    • server.yamlrules 部分,将事务的 providerType 配置为 Narayana。配置示例:

      yaml 复制代码
      rules:
        - !TRANSACTION
          defaultType: XA
          providerType: Narayana
  • 注意事项 :ShardingSphere 版本更新快,部分依赖可能未及时维护,存在组件版本冲突的风险。使用前需确认依赖包与 ShardingSphere-Proxy 版本的兼容性。

4 ShardingSphere-Proxy 集群化部署

4.1 ShardingSphere-Proxy 的运行模式

  • server.yaml 文件中,有一段被注释的 mode 配置。这段配置的作用是指定 ShardingSphere 的运行模式,决定 ShardingSphere 如何管理复杂的配置信息:

    yaml 复制代码
    #mode:
    #  type: Cluster
    #  repository:
    #    type: ZooKeeper
    #    props:
    #      namespace: governance_ds
    #      server-lists: localhost:2181
    #      retryIntervalMilliseconds: 500
    #      timeToLiveSeconds: 60
    #      maxRetries: 3
    #      operationTimeoutMilliseconds: 500
  • ShardingSphere 支持 Standalone(独立模式)Cluster(集群模式) 两种运行模式,核心差异如下:

    • Standalone(独立模式)

      • ShardingSphere 不需要考虑其他实例的影响,直接在内存中管理核心配置规则

      • 是 ShardingSphere 默认的运行模式,配置简单,无需依赖第三方组件。

      • 适用场景:小型项目,或对性能要求较高 的场景(因为没有额外的配置同步开销)。通常配合 ShardingJDBC 使用(ShardingJDBC 是客户端分库分表,更侧重性能);

    • Cluster(集群模式)

      • ShardingSphere 不仅要管理自身配置,还要与集群中其他实例同步配置规则 。因此需要引入第三方配置中心(如 Zookeeper、etcd、Nacos 等)来实现配置同步;

      • 推荐的配置中心:在分库分表场景下,配置信息变动少、访问频率低,因此基于 CP 架构的 Zookeeper 最推荐(CP 架构保证数据一致性,适合配置这类强一致需求的场景);

      • 优先级:如果应用本地和 Zookeeper 都有配置,ShardingSphere 以 Zookeeper 中的配置为准(保证集群配置统一);

      • 适用场景:大规模集群环境,需要多实例协同工作,通常配合 ShardingSphere-Proxy 使用;

  • 模式选择建议

    • Standalone:小型项目、性能敏感场景 → 配合 ShardingJDBC;

    • Cluster:大规模集群环境 → 配合 ShardingSphere-Proxy,且推荐用 Zookeeper 作为配置中心。

4.2 使用 Zookeeper 进行集群部署

  • 接下来基于 Zookeeper 部署一下 ShardingSphere-Proxy 集群;

  • 首先在本地部署一个 Zookeeper,然后将server.yaml中的mode部分解除注释:

    yaml 复制代码
    # ShardingSphere 运行模式配置
    mode:
      # 运行模式类型:Cluster 表示集群模式(支持多个Proxy实例组成集群)
      type: Cluster
      # 集群元数据存储仓库配置
      repository:
        # 仓库类型:ZooKeeper(使用ZooKeeper作为分布式协调服务)
        type: ZooKeeper
        # ZooKeeper连接配置属性
        props:
          # ZooKeeper命名空间:用于在ZooKeeper中隔离不同环境的配置
          namespace: governance_ds
          # ZooKeeper服务器地址列表:支持多个服务器地址,用逗号分隔
          server-lists: 192.168.65.212:2181
          # 重试间隔时间(毫秒):连接失败后重试的等待时间
          retryIntervalMilliseconds: 500
          # 节点存活时间(秒):在ZooKeeper中创建的临时节点的存活时间
          timeToLiveSeconds: 60
          # 最大重试次数:连接ZooKeeper失败时的最大重试次数
          maxRetries: 3
          # 操作超时时间(毫秒):ZooKeeper操作(如创建、读取节点)的超时时间
          operationTimeoutMilliseconds: 500
  • 启动 ShardingSphere-Proxy 服务,在 Zookeeper 注册中心可以看到 ShardingSphere 集群模式下的节点结构如下:

    yaml 复制代码
    # ShardingSphere 集群模式下的 ZooKeeper 节点结构详解
    
    # 根命名空间(用于环境隔离,避免不同环境配置相互干扰)
    namespace: governance_ds
    # 全局配置节点
    ├── rules                    # 存储全局规则配置(如分片规则、加密规则等)
    ├── props                    # 存储全局属性配置(如SQL显示开关、执行模式等)
    # 元数据管理节点
    ├── metadata                 # 元数据配置根目录
    │   ├── ${databaseName}      # 逻辑数据库名称(如:sharding_db)
    │   │   ├── schemas          # 逻辑Schema列表
    │   │   │   ├── ${schemaName} # 逻辑Schema名称
    │   │   │   │   ├── tables   # 表结构配置
    │   │   │   │   │   ├── ${tableName}  # 具体表的结构定义
    │   │   │   │   │   └── ...  # 其他表
    │   │   │   │   └── views    # 视图结构配置
    │   │   │   │       ├── ${viewName}   # 具体视图的定义
    │   │   │   │       └── ...  # 其他视图
    │   │   │   └── ...          # 其他Schema
    │   │   └── versions         # 元数据版本管理
    │   │       ├── ${versionNumber} # 具体版本号(如:v1, v2)
    │   │       │   ├── dataSources # 该版本的数据源配置
    │   │       │   └── rules       # 该版本的规则配置
    │   │       ├── active_version  # 当前激活的元数据版本号
    │   │       └── ...          # 其他版本
    # 计算节点管理(Proxy和JDBC实例管理)
    ├── nodes
    │   ├── compute_nodes        # 计算节点管理根目录
    │   │   ├── online           # 在线实例列表
    │   │   │   ├── proxy        # Proxy模式实例
    │   │   │   │   ├── UUID     # 每个Proxy实例的唯一标识(如:生成的实际UUID)
    │   │   │   │   └── ...      # 其他Proxy实例
    │   │   │   └── jdbc         # JDBC模式实例
    │   │   │       ├── UUID     # 每个JDBC实例的唯一标识
    │   │   │       └── ...      # 其他JDBC实例
    │   │   ├── status           # 实例状态信息
    │   │   │   ├── UUID         # 具体实例的状态数据
    │   │   │   └── ...          # 其他实例状态
    │   │   ├── worker_id        # 工作节点ID分配
    │   │   │   ├── UUID         # 实例分配的工作ID
    │   │   │   └── ...          # 其他实例的工作ID
    │   │   ├── process_trigger  # 进程触发管理
    │   │   │   ├── process_list_id:UUID  # 进程列表与实例的关联
    │   │   │   └── ...          # 其他进程触发信息
    │   │   └── labels           # 实例标签管理
    │   │       ├── UUID         # 具体实例的标签配置
    │   │       └── ...          # 其他实例标签
    # 存储节点管理(实际数据源管理)
    │   └── storage_nodes        # 存储节点管理根目录
    │       ├── ${databaseName.groupName.ds}  # 数据源节点命名格式:逻辑库名.组名.数据源名
    │       └── ${databaseName.groupName.ds}  # 另一个数据源节点
  • server.yaml中的rules部分同2.1 部署 ShardingSphere-Proxy

  • 分库分表配置同2.2 常用的分库分表配置策略

4.3 统一 JDBC 和 Proxy 配置信息

  • ShardingSphere-JDBC 也能像 ShardingSphere-Proxy 一样,通过 Zookeeper 同步配置信息 ,从而实现两者配置的统一管理(减少配置分散,便于集群化维护)。

4.3.1 通过注册中心同步配置

  • application.properties 中,删除之前 ShardingSphere-JDBC 相关的分库分表规则等配置,只配置Zookeeper 的连接和集群模式。示例配置:

    properties 复制代码
    # 指定运行模式为"集群模式(Cluster)"
    spring.shardingsphere.mode.type=Cluster
    # 指定配置中心类型为 Zookeeper
    spring.shardingsphere.mode.repository.type=Zookeeper
    # Zookeeper 的命名空间(用于隔离不同配置,自定义即可)
    spring.shardingsphere.mode.repository.props.namespace=governance_ds
    # Zookeeper 的地址和端口
    spring.shardingsphere.mode.repository.props.server-lists=localhost:2181
    # 重试间隔(毫秒)
    spring.shardingsphere.mode.repository.props.retryIntervalMilliseconds=600
    # 存活时间(秒)
    spring.shardingsphere.mode.repository.props.timeToLiveSeconds=60
    # 最大重试次数
    spring.shardingsphere.mode.repository.props.maxRetries=3
    # 操作超时时间(毫秒)
    spring.shardingsphere.mode.repository.props.operationTimeoutMilliseconds=500
  • 配置完成后,可继续验证对表(如 course 表)的分库分表操作,此时 ShardingSphere-JDBC 会从 Zookeeper 中拉取配置信息来执行分库分表逻辑;

  • 注意:

    • 如果在 ShardingSphere-JDBC 中读取配置中心(Zookeeper)的配置,需要用 spring.shardingsphere.database.name 指定对应的虚拟库
    • 若不配置该参数,默认值为 logic_db
    • 这个虚拟库是 ShardingSphere 中逻辑上的数据库概念,用于统一管理分库分表后的逻辑结构。

4.3.2 使用ShardingSphere-Proxy提供的JDBC驱动读取配置文件

  • ShardingSphere 过去是通过兼容 MySQL 或 PostgreSQL 服务的方式,提供分库分表功能:

    • 应用端需要用 MySQL/PostgreSQL 的 JDBC 驱动 ,去访问 ShardingSphere 封装的数据源(ShardingSphereDataSource);

    • 这种方式相当于模拟成 MySQL/PostgreSQL 数据库,让应用无感知地使用分库分表能力,但依赖的是第三方数据库的 JDBC 驱动;

  • 在当前版本中,ShardingSphere 直接提供了自己的 JDBC 驱动

    • 这意味着应用可以不再依赖 MySQL/PostgreSQL 的 JDBC 驱动,直接用 ShardingSphere 专属的驱动来连接和操作数据;

    • 优势是更原生、更直接地支持 ShardingSphere 的特性(分库分表、分布式事务等),减少对第三方驱动的依赖,也能更灵活地扩展功能;

  • 比如在 ShardingSphere-JDBC 中,可在类路径(classpath)下增加 config.yaml 文件,将之前 ShardingSphere-Proxy中的关键配置整合到这个文件中 。这样 ShardingSphere-JDBC 就能通过自身的 JDBC 驱动,读取 config.yaml 里的配置(分库分表规则、数据源等),实现和 ShardingSphere-Proxy 类似的分库分表逻辑:

    yaml 复制代码
    rules:
      - !AUTHORITY
        users:
          - root@%:root
          - sharding@:sharding
        provider:
          type: ALL_PERMITTED
      - !TRANSACTION
        defaultType: XA
        providerType: Atomikos
      - !SQL_PARSER
        sqlCommentParseEnabled: true
        sqlStatementCache:
          initialCapacity: 2000
          maximumSize: 65535
        parseTreeCache:
          initialCapacity: 128
          maximumSize: 1024
      - !SHARDING
        tables:
          course:
            actualDataNodes: m${0..1}.course_${1..2}
            databaseStrategy:
              standard:
                shardingColumn: cid
                shardingAlgorithmName: course_db_alg
            tableStrategy:
              standard:
                shardingColumn: cid
                shardingAlgorithmName: course_tbl_alg
            keyGenerateStrategy:
              column: cid
              keyGeneratorName: alg_snowflake
    
        shardingAlgorithms:
          course_db_alg:
            type: MOD
            props:
              sharding-count: 2
          course_tbl_alg:
            type: INLINE
            props:
              algorithm-expression: course_$->{cid%2+1}
        keyGenerators:
          alg_snowflake:
            type: SNOWFLAKE
    
    props:
      max-connections-size-per-query: 1
      kernel-executor-size: 16
      proxy-frontend-flush-threshold: 128
      proxy-hint-enabled: false
      sql-show: false
      check-table-metadata-enabled: false
      proxy-backend-query-fetch-size: -1
      proxy-frontend-executor-size: 0
      proxy-backend-executor-suitable: OLAP
      proxy-frontend-max-connections: 0 
      sql-federation-type: NONE
      proxy-backend-driver-type: JDBC
      proxy-mysql-default-version: 8.0.20
      proxy-default-port: 3307
      proxy-netty-backlog: 1024
    
    databaseName: sharding_db
    dataSources:
      m0:
        dataSourceClassName: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://127.0.0.1:3306/coursedb?serverTimezone=UTC&useSSL=false
        username: root
        password: root
        connectionTimeoutMilliseconds: 30000
        idleTimeoutMilliseconds: 60000
        maxLifetimeMilliseconds: 1800000
        maxPoolSize: 50
        minPoolSize: 1
      m1:
        dataSourceClassName: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://127.0.0.1:3306/coursedb2?serverTimezone=UTC&useSSL=false
        username: root
        password: root
        connectionTimeoutMilliseconds: 30000
        idleTimeoutMilliseconds: 60000
        maxLifetimeMilliseconds: 1800000
        maxPoolSize: 50
        minPoolSize: 1
  • 然后,可以直接用 JDBC 的方式访问带有分库分表的虚拟库:

    java 复制代码
    public class ShardingJDBCDriverTest {
    
        @Test
        public void test() throws ClassNotFoundException, SQLException {
            // 定义ShardingSphere JDBC驱动类全限定名
            // 这是ShardingSphere提供的专用JDBC驱动,用于拦截和路由SQL请求
            String jdbcDriver = "org.apache.shardingsphere.driver.ShardingSphereDriver";
            
            // JDBC连接URL,指定使用ShardingSphere驱动和配置文件路径
            // classpath:config.yaml 表示配置文件位于类路径下的config.yaml文件
            String jdbcUrl = "jdbc:shardingsphere:classpath:config.yaml";
            
            // 要执行的SQL查询语句
            // 这里查询的是逻辑数据库sharding_db中的course表
            // ShardingSphere会根据配置将查询路由到实际的物理表
            String sql = "select * from sharding_db.course";
    
            // 加载ShardingSphere JDBC驱动类
            // 这是使用JDBC驱动的标准方式,驱动会自动注册到DriverManager
            Class.forName(jdbcDriver);
            
            // 使用try-with-resources语句确保连接正确关闭
            try(Connection connection = DriverManager.getConnection(jdbcUrl);) {
                // 创建Statement对象用于执行SQL语句
                Statement statement = connection.createStatement();
                
                // 执行SQL查询并返回ResultSet结果集
                // ShardingSphere会在此处拦截SQL,根据分片规则路由到正确的数据节点
                ResultSet resultSet = statement.executeQuery(sql);
                
                // 遍历结果集,处理查询结果
                while (resultSet.next()){
                    // 从结果集中获取cid字段的值并输出
                    // ShardingSphere会自动合并来自多个分片的结果
                    System.out.println("course cid= "+resultSet.getLong("cid"));
                }
                // try-with-resources会自动关闭statement和resultSet
            }
            // try-with-resources会自动关闭connection,释放数据库连接资源
        }
    }
  • 官方的说明是 ShardingSphereDriver 读取config.yaml时, 这个config.yaml配置信息与 ShardingSphere-Proxy 中的配置文件完全是相同的,甚至可以直接将 ShardingSphere-Proxy 中的配置文件拿过来用。但是从目前版本来看,还是有不少小问题的,静待后续版本跟踪吧;

  • 到这里,对于之前讲解过的 ShardingSphere 的混合架构,有没有更新的了解?

5 ShardingSphere-Proxy功能扩展

  • ShardingSphere-Proxy 支持通过 SPI(Service Provider Interface)机制 进行自定义扩展,只要将自定义的扩展功能(如自定义算法、策略等)按照 SPI 规范打成 Jar 包,放入 ShardingSphere-Proxy 的 lib 目录,就能被 ShardingSphere-Proxy 加载并使用;

  • 自定义主键生成器为例:

    • 实现 KeyGeneratorAlgorithm 接口,用于生成自定义格式的主键;
    • 生成主键时,结合当前时间戳 (格式化为 yyyyMMddHHmmssSSS)和自增原子变量,保证主键的唯一性和有序性;
    java 复制代码
    public class MyKeyGeneratorAlgorithm implements KeyGeneratorAlgorithm {
        private AtomicLong atom = new AtomicLong(0);
        private Properties props;
    
        @Override
        public Comparable<?> generateKey() {
            LocalDateTime ldt = LocalDateTime.now();
            // 格式化时间为 "年月日时分秒毫秒" 格式
            String timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(ldt);
            // 结合自增数生成主键(时间戳 + 自增数)
            return Long.parseLong(timestamp + atom.incrementAndGet());
        }
    
        @Override
        public Properties getProps() {
            return this.props;
        }
    
        @Override
        public String getType() {
            // 自定义算法的类型标识,配置时会用到
            return "MYKEY";
        }
    
        @Override
        public void init(Properties props) {
            this.props = props;
        }
    }
  • 要将自定义类和对应的 SPI 文件打成 Jar 包,需在 pom.xml 中配置 maven-jar-plugin

    • 指定要打包的自定义类(com/your/package/algorithm/**)和 SPI 配置文件(META-INF/services/*),生成包含扩展功能的 Jar 包;
    xml 复制代码
    <build>
        <plugins>
            <!-- 将 SPI 扩展功能单独打成 Jar 包 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <id>shardingJDBDemo</id>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                        <configuration>
                            <classifier>spi-extention</classifier>
                            <includes>
                                <!-- 包含自定义算法的包路径 -->
                                <include>com/your/package/algorithm/**</include>
                                <!-- 包含 SPI 配置文件(META-INF/services/ 下的文件) -->
                                <include>META-INF/services/*</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • 将打包好的 Jar 包,放入 ShardingProxy 的 lib 目录 。之后在 ShardingProxy 的配置中,就可以像使用内置组件一样,引用这个自定义的主键生成算法(如配置 keyGeneratorName: MYKEY)。

6 分库分表数据迁移方案

  • 分库分表场景下,数据迁移面临两大难题:

    • 海量数据迁移难度大:旧项目无分库分表,或需调整分片规则时,数据量庞大,迁移工程浩大;

    • 业务无感知难度高:迁移过程中要保证业务正常运行,不能因迁移导致服务中断;

  • 为平衡数据迁移业务连续性 (在数据转移的过程中,保证不影响业务正常进行),采用冷热数据分离迁移

    • 冷数据

      • 即历史存量数据(数据量大,无法一次性迁移);
      • 策略:通过定时任务逐步迁移,减少对业务的瞬间压力;
    • 热数据

      • 即业务实时产生的新数据;
      • 策略:保证双写(同时写入旧库和新库),确保迁移过程中数据实时同步,业务不受影响;
  • 基于 ShardingSphere 的迁移方案

    • 热数据双写(ShardingJDBC 数据双写库)

      • 核心逻辑:用 ShardingSphereDataSource 替换旧的 DataSource,配置双写规则(核心业务表同时写入旧库和新库);

      • 实现方式:在 ShardingJDBC 双写库中,针对需迁移的核心表配置分片规则,通过定制分片算法 实现一份数据写两份库

    • 新库分片写入(ShardingJDBC 数据写入库)

      • 核心逻辑:针对新数据库集群,配置专门的 ShardingJDBC 写入库,完整实现新的分片规则(即分库分表的最终逻辑);

      • 关联方式:将这个写入库 配置到之前的双写库中,保证新数据按新规则写入新库;

    • 冷数据增量迁移(定时任务 + ShardingProxy)

      • 冷数据特点:量大、迁移慢,需分批增量迁移;

      • 实现方式:

        • ElasticJob 等定时任务框架,分批读取旧库冷数据,按新分片规则写入新库;
        • 借助 ShardingProxy 服务,简化新库的分片逻辑(定时任务只需从旧库读、往新库写,分片规则由 ShardingProxy 统一管理);
        • 规则同步:通过 ShardingSphere Governance Center(如 Zookeeper),保证 ShardingProxy 和 ShardingJDBC 写入库的分片规则一致
    • 迁移完成后切换

      • 核心逻辑:冷数据迁移完成后,删除双写库和旧库依赖,只保留 ShardingJDBC 写入库(新库的分片逻辑);

      • 业务影响:应用层只需访问一个 DataSource(底层由 ShardingSphere 切换为新库逻辑),业务代码几乎无需修改;

  • 注意:迁移过程中,需梳理业务 SQL,确保 SQL 符合 ShardingSphere(尤其是 ShardingSphere-JDBC)的支持范围,避免使用不兼容的 SQL 语法导致迁移或业务异常。

相关推荐
Bellafu6663 小时前
spring项目部署后为什么会生成 logback-spring.xml文件
java
递归不收敛3 小时前
一、Java 基础入门:从 0 到 1 认识 Java(详细笔记)
java·开发语言·笔记
沐浴露z4 小时前
【Java SpringAI智能体开发学习 | 2】SpringAI 实用特性:自定义Advisor,结构化输出,对话记忆持久化,prompt模板,多模态
java·spring·springai
小沈同学呀4 小时前
创建一个Spring Boot Starter风格的Basic认证SDK
java·spring boot·后端
码农小伙4 小时前
通俗易懂地讲解JAVA的BIO、NIO、AIO
java·nio
不要再敲了5 小时前
JDBC从入门到面试:全面掌握Java数据库连接技术
java·数据库·面试
潇I洒6 小时前
若依4.8.1打包war后在Tomcat无法运行,404报错的一个解决方法
java·tomcat·ruoyi·若依·404
方圆想当图灵6 小时前
如何让百万 QPS 下的服务更高效?
分布式·后端
Funcy6 小时前
XxlJob 源码分析05:执行器注册流程
java