开发者

ShardingProxy读写分离之原理、配置与实践过程

开发者 https://www.devze.com 2025-08-30 10:21 出处:网络 作者: 没事学AI
目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二、ShardingProxy读写分离核心原理2.1 整体工作流程2.2 关键技术点2.2.1 SQL类型判断2.2.2 从库负载均衡算法2.2http://www.devze
目录
  • 一、ShardingProxy技术定位与读写分离核心价值
    • 1.1 技术定位
    • 1.2 读写分离核心价值
  • 二、ShardingProxy读写分离核心原理
    • 2.1 整体工作流程
    • 2.2 关键技术点
      • 2.2.1 SQL类型判断
      • 2.2.2 从库负载均衡算法
      • 2.2http://www.devze.com.3 主从数据一致性保障
  • 三、ShardingProxy读写分离环境搭建与配置
    • 3.1 前置环境准备
      • mysql主从复制验证
    • 3.2 ShardingProxy核心配置
      • 3.2.1 server.yaml配置(全局基础配置)
      • 3.2.2 config-sharding.yaml配置(读写分离核心配置)
    • 3.3 ShardingProxy启动与验证
      • 3.3.1 启动ShardingProxy
      • 3.3.2 连接验证(使用Navicat/MySQL客户端)
  • 四、工程实践:SpringBoot整合ShardingProxy读写分离
    • 4.1 项目依赖配置(pom.XML)
      • 4.2 数据库配置(application.yml)
        • 4.3 业务代码实现(用户模块示例)
          • 4.3.1 实体类(User.Java)
          • 4.3.2 DAO层(UserDao.java)
        • 4.4 测试代码(UserDaoTest.java)
          • 5.2 从库故障导致读失败
            • 问题表现
            • 解决方案
          • 5.3 SQL路由错误
          • 总结

            一、ShardingProxy技术定位与读写分离核心价值

            1.1 技术定位

            ShardingProxy是Apache ShardingSphere生态中的数据库中间件,采用客户端-代理-数据库的三层架构模式,对应用程序透明(无需修改应用代码),负责接收应用的SQL请求并进行路由转发。其核心定位是:作为分布式数据库的“流量入口”,解决单机数据库在高并发场景下的性能瓶颈,支持读写分离、分库分表、数据脱敏等核心能力,在微服务架构、高流量业务系统(如电商订单、用户中心)中广泛应用。

            在整个技术体系中,ShardingProxy处于“应用层与数据库层之间的中间件层”,上接应用服务的SQL请求,下连主从架构的数据库集群,承担SQL解析、路由决策、结果合并等关键职责,是实现数据库水平扩展的核心组件之一。

            1.2 读写分离核心价值

            在传统单机数据库架构中,“读”和“写”操作均依赖同一台数据库,当业务流量增长(如秒杀活动、高频查询场景)时,读请求会大量占用数据库资源,导致写操作响应延迟,甚至引发数据库性能雪崩。

            ShardingProxy读写分离的核心价值在于拆分读写流量

            • 将“写操作”(INSERT/UPDATE/DELETE)路由至主库,保证数据一致性;
            • 将“读操作”(SELECT)路由至从库,利用从库集群分担读压力;
            • 支持从库负载均衡,避免单从库成为新瓶颈;
            • 提供故障转移能力,当某台从库下线时,自动将读流量切换至其他可用从库。

            二、ShardingProxy读写分离核心原理

            2.1 整体工作流程

            ShardingProxy实现读写分离的核心流程分为5个步骤,具体如下:

            步骤核心操作说明
            1SQL接收应用通过JDBC/ODBC协议连接ShardingProxy(代理地址与端口),发送SQL请求
            2SQL解析ShardingProxy对SQL进行语法解析、语义分析,判断SQL类型(读/写)
            3路由决策根据预设的读写分离规则,决定将请求路由至主库(写操作)或从库(读操作);若为读操作,还需通过负载均衡算法选择具体从库
            4请求转发将解析后的SQL转发至目标数据库(主库/从库)
            5结果返回接收目标数据库的执行结果,若涉及多从库(如分页查询合并)则进行结果处理,最终返回给应用

            2.2 关键技术点

            2.2.1 SQL类型判断

            ShardingProxy通过SQL语法树解析区分读写操作:

            • 写操作判定:包含INSERT、UPDATE、DELETE、CREATE、ALTER等会修改数据或表结构的SQL;
            • 读操作判定:仅包含SELECT的SQL(特殊场景如SELECT … FOR UPDATE会被判定为写操作,需单独配置)。

            2.2.2 从库负载均衡算法

            ShardingProxy支持3种常用负载均衡算法,满足不同业务场景需求:

            • 轮询算法(ROUND_ROBIN):按从库顺序依次分配读请求,适用于各从库配置一致的场景;
            • 随机算法(RANDOM):随机选择从库分配请求,适用于从库性能存在轻微差异的场景;
            • 权重算法(WEIGHT):根据从库权重分配请求(权重越高,接收请求越多),适用于从库配置差异较大的场景(如高性能从库权重设为10,普通从库设为5)。

            2.2.3 主从数据一致性保障

            由于主从复制存在延迟(MySQL默认异步复制),可能导致“写主库后立即读从库”出现数据不一致问题。ShardingProxy提供2种解决方案:

            1. 强制路由主库:对核心业务读请求(如用户下单后查询订单状态),通过Hint语法强制路由至主库;
            2. 主从延迟控制:配置从库延迟阈值(如500ms),当从库延迟超过阈值时,自动将读请求路由至主库,避免脏读。

            三、ShardingProxy读写分离环境搭建与配置

            3.1 前置环境准备

            搭建ShardingProxy读写分离需满足以下环境依赖,以MySQL主从架构为例:

            组件版本要求说明
            JDK1.8及以上ShardingProxy基于Java开发,需配置JAVA_HOME
            MySQL5.7/8.0需提前搭建MySQL主从复制(异步/半同步均可)
            ShardingProxy4.1.1(稳定版)从Apache官网下载

            MySQL主从复制验证

            确保主从复制正常运行,在主库执行以下SQL创建测试表并插入数据,从库需能同步数据:

            -- 主库创建测试库
            CREATE DATABASE IF NOT EXISTS sharding_db;
            USE sharding_db;
            
            -- 创建用户表
            CREATE TABLE `t_user` (
              `id` BIGINT NOT NULL AUTO_INCREMENT,
              `username` VARCHAR(50) NOT NULL,
              `age` INT NOT NULL,
              PRIMARY KEY (`id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
            
            -- 插入测试数据(从库需同步该数据)
            INSERT INTO t_user (username, age) VALUES ('zhangsan', 25), ('lisi', 30);
            

            3.2 ShardingProxy核心配置

            ShardingProxy的配置文件位于conf目录下,核心配置文件为server.yaml(全局配置)和config-sharding.yaml(读写分离+分库分表配置)。

            3.2.1 server.yaml配置(全局基础配置)

            # 服务端口(应用连接ShardingProxy的端口)
            serverPort: 3307
            # 管理端口(用于监控和运维)
            adminPort: 8088
            # 数据库驱动类名
            driverClassName: com.mysql.cj.jdbc.Driver
            # 认证配置(应用连接ShardingProxy的账号密码)
            authentication:
              users:
                root:
                  password: 123456  # 应用连接密码
                sharding:
                  password: sharding 
                  authorizedSchemas: sharding_db  # 授权访问的数据库
            
            # 日志配置(输出SQL路由日志,便于调试)
            props:
              max.connections.size.per.query: 1
              acceptor.size: 16  # 用于接收连接的线程数
              executor.size: 16  # 用于处理SQL的线程数
              proxy.frontend.flush.threshold: 128  # 前端刷盘阈值
              proxy.transaction.type: LOCAL
              proxy.opentracing.enabled: false
              query.with.cipher.column: true
              sql.show: true  # 开启SQL显示(打印路由后的SQL,调试必备)
            

            3.2.2 config-sharding.yaml配置(读写分离核心配置)

            # 数据源配置(主库+从库)
            dataSources:
              # 主库数据源
              master_ds:
                type: com.zaxphpxer.hikari.HikariDataSource
                driverClassName: com.mysql.cj.jdbc.Driver
                jdbcUrl: jdbc:mysql://192.168.1.100:3306/sharding_db?useSSL=false&serverTimezone=UTC&allowpublicKeyRetrieval=true
                username: root
                password: root123
                connectionTimeout: 30000
                idleTimeout: 60000
                maxLifetime: 1800000
                maximumPoolSize: 50
              
              # 从库1数据源
              slave_ds_0:
                type: com.zaxxer.hikari.HikariDataSource
                driverClassName: com.mysql.cj.jdbc.Driver
                jdbcUrl: jdbc:mysql://192.168.1.101:3306/sharding_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
                username: root
                password: root123
                connectionTimeout: 30000
                idleTimeout: 60000
                maxLifetime: 1800000
                maximumPoolSize: 50
              
              # 从库2数据源(可扩展多个从库)
              slave_ds_1:
                type: com.zaxxer.hikari.HikariDataSource
                driverClassName: com.mysql.cj.jdbc.Driver
                jdbcUrl: jdbc:mysql://192.168.1.102:3306/sharding_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
                username: root
                password: root123
                connectionTimeout: 30000
                idleTimeout: 60000
                maxLifetime: 1800000
                maximumPoolSize: 50
            
            # 读写分离规则配置
            rules:
            - !REAdwRITE_SPLITTING
              dataSources:
                # 读写分离数据源名称(应用实际访问的数据源名)
                sharding_rw_ds:
                  type: Static
                  props:
                    write-data-source-name: master_ds  # 写操作路由至主库
                    read-data-source-names: slave_ds_0,slave_ds_1  # 读操作路由至两个从库
                    load-balance-algorithm-type: ROUND_ROBIN  # 从库负载均衡算法(轮询)
                    # 可选:主从延迟控制(超过500ms则读主库)
                    # read-write-splitting-delay-threshold: 500
            
            # 绑定表规则(无分表时可省略,此处仅作示例)
            bindingTables:
            - t_user
            
            # 默认数据源(当SQL未匹配任何规则时,路由至该数据源)
            defaultDataSourceName: sharding_rw_ds
            

            3.3 ShardingProxy启动与验证

            3.3.1 启动ShardingProxy

            进入ShardingProxy的bin目录;

            执行启动脚本(Windows用start.BAT,linux用start.sh):

            # Linux启动命令(指定配置文件目录)
            ./start.sh ../conf
            

            验证启动状态:查看logs/stdout.log日志,若出现以下内容则启动成功:

            [INFO ] 2024-05-20 10:编程客栈00:00.000 [main] o.a.s.p.frontend.ShardingSphereProxy - ShardingSphere-Proxy start success.
            

            3.3.2 连接验证(使用Navicat/MySQL客户端)

            连接参数:

            • 主机:ShardingProxy所在服务器IP(如192.168.1.103);
            • 端口:3307(对应server.yaml中的serverPort);
            • 账号:root(对应server.yaml中的认证用户);
            • 密码:123456(对应server.yaml中的认证密码);
            • 数据库:sharding_db(授权数据库)。

            执行读写操作验证:

            -- 1. 写操作(INSERT):查看ShardingProxy日志,确认路由至master_ds
            INSERT INTO t_user (username, age) VALUES ('wangwu', 35);
            
            -- 2. 读操作(SELECT):查看日志,确认轮询路由至slave_ds_0、slave_ds_1
            SELECT * FROM t_user;
            
            -- 3. 强制读主库(Hint语法):核心业务读请求用此方式保障一致性
            /*!SHARDINGSPHERE_HINT: DATA_SOURCE_NAME=master_ds */ 
            SELECT * FROM t_user WHERE id = 3;
            

            四、工程实践:SpringBoot整合ShardingProxy读写分离

            4.1 项目依赖配置(pom.xml)

            在SpringBoot项目中添加MySQL驱动和JDBC依赖(无需添加ShardingSphere客户端依赖,因应用仅需连接ShardingProxy,无需感知中间件细节):

            <dependencies>
                <!-- SpringBoot JDBC Starter -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jdbc</artifactId>
                </dependency>
                
                <!-- MySQL驱动 -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.32</version>
                    <scope>runtime</scope>
                </dependency>
                
                <!-- 数据库连接池(HikariCP) -->
                <dependency>
                    <groupId>com.zaxxer</groupId>
                    <artifactId>HikariCP</artifactId>
                </dependency>
                
                <!-- SpringBoot Test(用于测试) -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-test</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
            

            4.2 数据库配置(application.yml)

            应用连接ShardingProxy的配置,与连接普通MySQL数据库一致(无需修改代码,完全透明):

            spring:
              datasource:
                type: com.zaxxer.hikari.HikariDataSource
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://192.168.1.103:3307/sharding_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
                username: root
                password: 123456
                hikari:
                  maximum-pool-size: 20
                  minimum-idle: 5
                  connection-timeout: 30000
                  idle-timeout: 600000
            

            4.3 业务代码实现(用户模块示例)

            4.3.1 实体类(User.java)

            public class User {
                private Long id;
                private String username;
             js   private Integer age;
            
                // 无参构造、有参构造、getter、setter
                public User() {}
                public User(String username, Integer age) {
                    this.username = username;
                    this.age = age;
                }
            
                // getter和setter
                public Long getId() { return id; }
                public void setId(Long id) { this.id = id; }
                public String getUsername() { return username; }
                public void setUsername(String username) { this.username = username; }
                public Integer getAge() { return age; }
                public void setAge(Integer age) { this.age = age; }
            }
            

            4.3.2 DAO层(UserDao.java)

            使用JdbcTemplate实现数据访问(MyBatis/MyBatis-Plus用法一致,仅需配置数据源):

            import org.springframework.jdbc.core.BeanPropertyRowMapper;
            import org.springframework.jdbc.core.JdbcTemplate;
            import org.springframework.stereotype.Repository;
            import javax.annotation.Resource;
            import java.util.List;
            
            @Repository
            public class UserDao {
                @Resource
                private JdbcTemplate jdbcTemplate;
            
                // 写操作:新增用户(路由至主库)
                public int addUser(User user) {
                    String sql = "INSERT INTO t_user (username, age) VALUES (?, ?)";
                    return jdbcTemplate.update(sql, user.getUsername(), user.getAge());
                }
            
                // 读操作:查询所有用户(路由至从库,轮询)
                public List<User> listAllUsers() {
                    String sql = "SELECT id, username, age FROM t_user";
                    return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
                }
            
                // 读操作:强制读主库(核心业务,保障数据一致性)
                public User getUserById(Long id) {
                    // Hint语法:强制路由至主库
                    String sql = "/*!SHARDINGSPHERE_HINT: DATA_SOURCE_NAME=master_ds */ " +
                                 "SELECT id, username, age FROM t_user WHERE id = ?";
                    return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id);
                }
            }
            

            4.4 测试代码(UserDaoTest.java)

            通过单元测试验证读写分离效果:

            import org.junit.jupiter.api.Test;
            import org.springframework.boot.test.context.SpringBootTest;
            import javax.annotation.Resource;
            import java.util.List;
            import static org.junit.jupiter.api.Assertions.*;
            
            @SpringBootTest
            public class UserDaoTest {
                @Resource
                private UserDao userDao;
            
                // 测试写操作(路由至主库)
                @Test
                public void testAddUser() {
                    User user = new User("zhaoliu", 40);
                    int rows = userDao.addUser(user);
                    assertEquals(1, rows); // 新增成功返回1
                }
            
                // 测试读操作(路由至从库,轮询)
                @Test
                public void testListAllUsers() {
                    List<User> userList = userDao.listAllUsers();
                    assertNotNull(userList);
                    System.out.println("查询用户数量:" + userList.size());
                    // 连续查询3次,观察ShardingProxy日志,确认从库轮询(slave_ds_0 → slave_ds_1 → slave_ds_0)
                    for (int i = 0; i < 3; i++) {
                        userDao.listAllUsers();
                    }
                }
            
                // 测试强制读主库(保障数据一致性)
                @Test
                public void testGetUserById() {
                    // 先新增用户(写主库)
                    User user = new User("qianqi", 28);
                    userDao.addUser(user);
                    // 立即查询刚新增的用户(强制读主库,避免主从延迟导致查不到数据)
                    User result = userDao.getUserById(user.getId());
                    assertNotNull(result);
                    assertEquals("qianqi", result.getUsername());
                }
            }
            
            
            ## 五、常见问题与解决方案
            ### 5.1 主从数据不一致
            #### 问题表现
            应用执行“写主库后立即读从库”时,可能查询不到最新数据(因主从复制存在延迟)。
            
            #### 解决方案
            1. **强制路由主库**:对核心业务场景(如用户注册后立即查询用户信息),使用Hint语法强制读主库:
               ```sql
               /*!SHARDINGSPHERE_HINT: DATA_SOURCE_NAME=master_ds */ 
               SELECT * FROM t_user WHERE id = ?
            

            配置延迟阈值:在config-sharding.yaml中设置read-write-splitting-delay-threshold(单位:毫秒),当从库延迟超过阈值时,自动路由至主库:

            props:
              read-write-splitting-delay-threshold: 500  # 从库延迟>500ms时读主库
            

            5.2 从库故障导致读失败

            问题表现

            某台从库宕机或网络故障时,ShardingProxy若仍将请求路由至该从库,会导致读操作失败。

            解决方案

            ShardingProxy默认支持从库故障检测与自动剔除,需在config-sharding.yaml中配置健康检查参数:

            dataSources:
              slave_ds_0:
                # 其他配置省略...
                health:
                  check:
                    enable: true  # 开启健康检查
                    timeout: 3000  # 检查超时时间(毫秒)
                    interval: 5000  # 检查间隔(毫秒)
              slave_ds_1:
                # 同上配置...
            

            配置后,当从库连续3次健康检查失败,ShardingProxy会将其从可用列表中剔除;恢复正常后,自动重新加入。

            5.3 SQL路由错误

            问题表现

            某些特殊SQL(如SELECT ... FOR UPDATE)被误判为读操作,路由至从库导致执行失败(从库默认只读)。

            解决方案

            config-sharding.yaml中配置SQL路由规则,修正特殊SQL的路由逻辑:

            rules:
            - !READWRITE_SPLITTING
              dataSources:
                sharding_rw_ds:
                  # 其他配置省略...
                  props:
                    # 自定义SQL路由规则:将包含FOR UPDATE的SELECT视为写操作
                    sql-parser-rule: "SELECT.*FOR UPDATE"
                    sql-parser-rule-type: WRITE  # 匹配上述规则的SjsQL视为写操作
            

            总结

            以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

            0

            精彩评论

            暂无评论...
            验证码 换一张
            取 消

            关注公众号