java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > ShardingProxy读写分离之原理、配置

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

作者:没事学AI

ShardingProxy是Apache ShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶颈,其核心功能包括SQL路由、负载均衡、数据一致性保障和故障转移,支持主从架构下的透明分库分表及读写分流,广泛应用于微服务和高流量业务系统

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

1.1 技术定位

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

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

1.2 读写分离核心价值

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

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

二、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语法树解析区分读写操作:

2.2.2 从库负载均衡算法

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

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.zaxxer.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客户端)

连接参数:

执行读写操作验证:

-- 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;
    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  # 匹配上述规则的SQL视为写操作

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文