目录
- 一、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个步骤,具体如下:
步骤 | 核心操作 | 说明 |
---|---|---|
1 | SQL接收 | 应用通过JDBC/ODBC协议连接ShardingProxy(代理地址与端口),发送SQL请求 |
2 | SQL解析 | 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种解决方案:
- 强制路由主库:对核心业务读请求(如用户下单后查询订单状态),通过Hint语法强制路由至主库;
- 主从延迟控制:配置从库延迟阈值(如500ms),当从库延迟超过阈值时,自动将读请求路由至主库,避免脏读。
三、ShardingProxy读写分离环境搭建与配置
3.1 前置环境准备
搭建ShardingProxy读写分离需满足以下环境依赖,以MySQL主从架构为例:
组件 | 版本要求 | 说明 |
---|---|---|
JDK | 1.8及以上 | ShardingProxy基于Java开发,需配置JAVA_HOME |
MySQL | 5.7/8.0 | 需提前搭建MySQL主从复制(异步/半同步均可) |
ShardingProxy | 4.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)。
精彩评论