目录
- 一、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)。
加载中,请稍侯......
精彩评论