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

2025-08-30 02:50

本文主要是介绍ShardingProxy读写分离之原理、配置与实践过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶...

一、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目录;

执行启动脚本Windowsstart.BATlinuxstart.sh):

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

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

[INFO ] 2024-05-20 10:China编程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视为写操作

总结

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

这篇关于ShardingProxy读写分离之原理、配置与实践过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155874

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Linux云服务器手动配置DNS的方法步骤

《Linux云服务器手动配置DNS的方法步骤》在Linux云服务器上手动配置DNS(域名系统)是确保服务器能够正常解析域名的重要步骤,以下是详细的配置方法,包括系统文件的修改和常见问题的解决方案,需要... 目录1. 为什么需要手动配置 DNS?2. 手动配置 DNS 的方法方法 1:修改 /etc/res

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、