我花了一个周末,总算在Windows本机上把“基于MySQL使用ShardingSphere-Proxy做分库分表和读写分离”这条链路完整跑通了,还顺手用Python把业务侧调用也接上。之前查资料时最大的感受是:官方文档偏Linux,网上教程又大量在讲ShardingSphere-JDBC,真正讲Proxy独立部署、Windows全流程、再配合Python连接的少之又少,而且不少配置片段一抄就报错。
这篇文章就把我实际验证过的方案完整梳理出来,覆盖MySQL 8.0 + ShardingSphere-Proxy 5.4.1 + Python pymysql的完整安装、配置、启动和验证过程,包含分库分表路由演示、一主一从读写分离演示,以及我踩过的各种坑。适合三种人看:准备面试想有一个本地可复现的分库分表Demo的开发者、要在企业项目里评估中间件选型的后端工程师、以及第一次接触ShardingSphere-Proxy、被Windows环境卡住的新手。
1. 整体设计思路:为什么这个方案最适合本地复现
1.1 先搞懂Proxy和JDBC两种模式的差异
ShardingSphere有两条主流落地路径:ShardingSphere-JDBC和ShardingSphere-Proxy。JDBC模式是嵌入式SDK,应用通过DataSource直接拿连接,分片逻辑在应用进程内执行;Proxy模式则是一个独立代理服务,应用把Proxy当作普通MySQL数据库去连,分片路由在代理层完成。
如果只在本地做技术验证,我更推荐Proxy模式,原因很直接:JDBC模式改动侵入大,你得在Java工程里引入依赖、配置数据源、替换MyBatis/JPA的连接池,验证成本高;而Proxy模式只需要一个MySQL客户端或者pymysql就能连,业务侧不用改一行SQL。
打个比方:JDBC模式相当于你把“前台接待”直接安排在每个部门里,每个部门都有一套自己的接待规则;Proxy模式则像是单独租了一间前台,所有客户先到前台,再由前台按规则把人分流到不同部门。前台坏了只影响前台,部门内部不用动。
1.2 逻辑拓扑与库表规划
本地验证不需要搞很多机器,我规划了一个MySQL主实例(3306)和一个从实例(3307),主实例上建两个业务库ds0和ds1用于分库分表演示,主从两个实例各建一个app_shop库用于读写分离演示。
分库分表选择了最常见的订单场景,逻辑表t_order拆成4张物理表:
- 分库键:
user_id,按user_id % 2路由到ds0或ds1 - 分表键:
order_id,按order_id % 2路由到t_order_0或t_order_1 - 主键策略:使用雪花算法生成分布式ID,确保多个分片下的主键全局唯一
选择取模算法是为了直观。HASH_MOD、MOD这类算法在演示时能一眼看出数据分布规律,排查问题方便,后续如果需要哈希一致性算法,也只是改配置的问题。
1.3 为什么把读写分离拆成独立逻辑库
我知道ShardingSphere允许把读写分离和分库分表配置在同一个逻辑库中,数据源可以是读写分离组。但我强烈建议第一次做本地Demo时拆成两个逻辑库:sharding_db专门演分库分表,readwrite_db专门演读写分离。
原因是组合使用时,一旦数据分布不对,你得同时怀疑分片规则、读写分离规则、主从同步状态三个环节,排查链路太长。拆开后可以让每个环节变成“黑盒”,先挨个验证通过,再考虑合并。这不是能力问题,是排障效率问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Windows环境准备与ShardingSphere-Proxy安装
2.1 JDK环境与目录规划
ShardingSphere-Proxy 5.4.1基于Java构建,要求JDK 8及以上,我直接装了JDK 17。Windows下最重要的是把环境变量配好:
bash复制JAVA_HOME=D:\Java\jdk-17
# 在Path中追加
%JAVA_HOME%\bin
验证方式老生常谈,但确实有效:
bash复制java -version
如果输出显示openjdk version "17.0.x",说明环境没问题。这里有个容易忽略的点:很多开发机装了多个JDK,命令行里java可能指向旧版本,最好在cmd中重新打开一个窗口验证,避免使用了缓存PATH。
目录规划建议直接建在某个盘的根目录下,不要带中文和空格:
| 组件 | 目录 |
|---|---|
| JDK | D:\Java\jdk-17 |
| ShardingSphere-Proxy | D:\shardingsphere-proxy-5.4.1 |
| MySQL主实例 | D:\mysql-8.0\mysql-3306 |
| MySQL从实例 | D:\mysql-8.0\mysql-3307 |
路径中带中文或空格,在启动脚本解析时经常出诡异问题,这个坑我踩过一次之后就长记性了。
2.2 下载安装ShardingSphere-Proxy
从Apache ShardingSphere官网下载二进制发布包,选择apache-shardingsphere-proxy-5.4.1-bin.tar.gz对应Windows的ZIP包,解压到上述目录。
解压后目录结构里重点关注两个目录:conf存放配置,bin存放启停脚本,lib存放依赖包。
这里有个从4.x流传到5.x的大坑:官方发布包默认不一定带MySQL驱动。如果启动后代理能起来、但连接逻辑库时报找不到驱动或者无法加载数据源,先去检查lib目录里有没有mysql-connector-j*.jar。如果没有,从Maven中央仓库下载mysql-connector-j-8.0.33.jar,直接丢进lib目录。注意版本要和后端MySQL 8.0匹配,别用5.x的老驱动去连MySQL 8。
2.3 准备两个MySQL实例模拟主从
读写分离必须要有一主一从两个真实MySQL实例,但Windows本机装两个MySQL实例并不复杂。
我准备了两个独立目录mysql-3306和mysql-3307,各自维护一份my.ini。主实例的my.ini核心配置:
ini复制[mysqld]
port=3306
basedir=D:/mysql-8.0/mysql-3306
datadir=D:/mysql-8.0/mysql-3306/data
server-id=1
log-bin=mysql-bin
binlog-format=ROW
从实例的my.ini核心配置:
ini复制[mysqld]
port=3307
basedir=D:/mysql-8.0/mysql-3307
datadir=D:/mysql-8.0/mysql-3307/data
server-id=2
两个目录初始化完成后,分别注册为Windows服务并启动:
bash复制mysqld --install MySQL3306 --defaults-file="D:\mysql-8.0\mysql-3306\my.ini"
mysqld --install MySQL3307 --defaults-file="D:\mysql-8.0\mysql-3307\my.ini"
net start MySQL3306
net start MySQL3307
然后分别在两个实例上创建业务库:
sql复制-- 3306实例
CREATE DATABASE ds0 DEFAULT CHARACTER SET utf8mb4;
CREATE DATABASE ds1 DEFAULT CHARACTER SET utf8mb4;
CREATE DATABASE app_shop DEFAULT CHARACTER SET utf8mb4;
-- 3307实例
CREATE DATABASE app_shop DEFAULT CHARACTER SET utf8mb4;
从库不需要创建ds0和ds1,因为分库分表演示时Proxy连接的是3306上的两个库,从库只服务读写分离场景。
2.4 修改配置并启动Proxy
编辑conf/server.yaml,先改成Standalone模式,并修改前端端口和账号:
yaml复制mode:
type: Standalone
repository:
type: File
props:
path: ./
overwrite: false
rules:
- !AUTHORITY
users:
- root@%:root
provider:
type: ALL_PRIVILEGES_PERMITTED
- !TRANSACTION
defaultType: LOCAL
props:
proxy-frontend-port: 3309
proxy-frontend-database-protocol-type: MySQL
然后准备分库分表配置文件conf/config-sharding.yaml和读写分离配置文件conf/config-readwrite-splitting.yaml,这两个文件的具体内容我会在下面两章专门讲,先保证能把Proxy启动起来。
进入bin目录执行启动脚本:
bash复制cd D:\shardingsphere-proxy-5.4.1\bin
start.bat
启动后不要着急连接,先确认端口在监听:
bash复制netstat -ano | findstr 3309
然后用本机MySQL客户端连一下:
bash复制mysql -h127.0.0.1 -P3309 -uroot -proot
如果能连上并执行SHOW DATABASES看到逻辑库,说明Proxy已经起来了。如果连不上,立刻看logs目录下的stdout.log和error.log,排障思路我在最后一章展开。
3. 分库分表核心配置与路由验证
3.1 分片规则配置解读
在conf/config-sharding.yaml中写入以下内容,这里重点注意缩进和表达式写法:
yaml复制databaseName: sharding_db
dataSources:
ds0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds0?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
ds1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds$->{0..1}.t_order_$->{0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table_inline
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
shardingAlgorithms:
db_inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table_inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
几个关键点解释一下:
actualDataNodes表示真实节点分布,ds$->{0..1}.t_order_$->{0..1}意思是逻辑表t_order背后有4张物理表,分布在ds0和ds1两个库中。databaseStrategy控制选库,tableStrategy控制选表。
分库键选择user_id,分表键选择order_id,这是合理的设计:同一个用户的订单可以分布在不同的表里,但通过user_id可以快速定位到某个库,再通过order_id定位到具体表。
keyGenerateStrategy配置了order_id使用雪花算法生成。这里要特别注意:分表键本身通常也是主键,如果你不配置分布式ID策略,多个分片的自增主键一定会撞车。让代理层统一生成ID,业务侧在插入时不需要显式传order_id。
3.2 逻辑库建表与数据分布验证
配置好以后重启Proxy,用MySQL客户端连上3309端口,进入sharding_db逻辑库,然后执行建表SQL:
sql复制CREATE TABLE t_order (
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
order_amount DECIMAL(10, 2) DEFAULT NULL,
PRIMARY KEY (order_id)
);
这条DDL会被代理解析并下发到所有实际节点,执行完后到3306实例上看,会发现ds0和ds1两个库里各自生成了t_order_0和t_order_1共4张物理表。
插入数据验证路由效果:
sql复制INSERT INTO t_order (user_id, order_amount) VALUES (1, 100.00);
INSERT INTO t_order (user_id, order_amount) VALUES (2, 200.00);
INSERT INTO t_order (user_id, order_amount) VALUES (3, 300.00);
INSERT INTO t_order (user_id, order_amount) VALUES (4, 400.00);
回到物理库检查,你会看到user_id为奇数的订单进入ds0库,偶数的进入ds1库;而order_id奇偶决定落在t_order_0还是t_order_1。整个路由是自动的,业务侧完全无感知。
3.3 必知的路由细节与常见误区
第一个误区:查询不带分片键会导致全路由。比如SELECT * FROM t_order WHERE order_amount > 100,代理不知道去哪个分片找,只能发到所有物理表再合并结果。本地数据量小看不出问题,生产环境这是灾难。强制要求业务SQL必须携带分片键,这是一个重要的设计约束。
第二个误区:INLINE表达式在YAML里的写法。$->{...}在YAML解析时可能会被特殊处理,必须用单引号包裹整个表达式,写成:
yaml复制algorithm-expression: 'ds$->{user_id % 2}'
我第一次没加引号,启动时控制台不报错,但执行SQL时报算法错误,查了半天才发现是表达式被YAML转义了。
第三个误区:建表和DDL语句里不要直接指定物理表名。在逻辑库中只能操作逻辑表t_order,不要写CREATE TABLE t_order_0,代理不会帮你做这类映射。
第四个值得说的点是PREVIEW语法,5.x版本可以用它查看SQL实际路由到哪个节点:
sql复制PREVIEW SELECT * FROM t_order WHERE user_id = 1;
结果集会显示真实数据源和改写后的SQL,这是排查分片问题最好用的工具,比对着日志猜高效得多。
4. 读写分离:从主从复制到负载均衡验证
4.1 MySQL主从复制搭建
读写分离的前提是主从数据一致,这依赖MySQL原生复制能力。先在主库创建复制账号:
sql复制CREATE USER 'repl'@'%' IDENTIFIED BY 'repl123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
查看主库当前binlog位置:
sql复制SHOW MASTER STATUS;
记录下File和Position的值,然后在从库执行:
sql复制CHANGE MASTER TO
MASTER_HOST='127.0.0.1',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl123',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=4;
START SLAVE;
查看同步状态:
sql复制SHOW SLAVE STATUS\G;
关注两个关键字段:Slave_IO_Running和Slave_SQL_Running,两个都显示Yes才算正常。
这里有一个从库起不来最常见的坑:如果从库实例是直接复制主库安装目录而来,可能导致server-uuid冲突。解决办法是停掉从库,删掉其数据目录下的auto.cnf文件,再重启从库,MySQL会自动生成新的UUID。
4.2 Proxy读写分离配置
在conf/config-readwrite-splitting.yaml中写入:
yaml复制databaseName: readwrite_db
dataSources:
write_ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/app_shop?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
read_ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3307/app_shop?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
rules:
- !READWRITE_SPLITTING
dataSourceGroups:
rw_group:
writeDataSourceName: write_ds
readDataSourceNames:
- read_ds
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
这个配置的意思是:逻辑库readwrite_db下有一组数据源rw_group,写流量强制进write_ds(3306主库),读流量在read_ds(3307从库)上分配,负载均衡算法是轮询。
需要注意命名规范:writeDataSourceName和readDataSourceNames里填的必须是dataSources中定义的数据源名字,别想当然填主机名或者库名。
4.3 用“停同步法”验证读流量真正走了从库
很多人在这一步犯迷糊:明明配好了读写分离,却不知道怎么验证读流量确实走到了从库。我分享一个最简单的实验方法:先停掉主从复制,再插入一条数据,看能不能通过Proxy查到。
操作步骤如下:
sql复制-- 从库执行,暂停同步
STOP SLAVE;
-- 主库直接插入一条数据
USE app_shop;
CREATE TABLE t_user (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
INSERT INTO t_user (id, name) VALUES (1, '从库看不到这条');
-- 通过Proxy查询
USE readwrite_db;
SELECT * FROM t_user;
如果配置正确,这条查询返回空,原因是插入的数据写进了3306主库,但同步停了,3307从库没有这条记录,而Proxy把读请求路由到了从库。
再把同步打开:
sql复制START SLAVE;
再通过Proxy查询,数据就出现了。这个实验做一次,你对读写分离的理解会完全不同。
另一个必须知道的规则:事务内的读默认走主库。因为事务中如果先写后读,读走从库可能读到旧数据,破坏事务隔离性。ShardingSphere-Proxy碰到事务内读请求时,会优先路由到主库保证一致性。本地验证时如果发现查询没走从库,先确认是不是在事务里。
5. Python调用与业务接入建议
5.1 pymysql直连Proxy
Python调用不需要额外引入ShardingSphere的任何SDK,直接通过pymysql把Proxy当成普通MySQL即可。
先安装依赖:
bash复制pip install pymysql
然后编写测试脚本:
python复制import pymysql
conn = pymysql.connect(
host='127.0.0.1',
port=3309,
user='root',
password='root',
database='sharding_db',
charset='utf8mb4'
)
try:
with conn.cursor() as cursor:
# 插入测试数据
for i in range(1, 6):
cursor.execute(
"INSERT INTO t_order (user_id, order_amount) VALUES (%s, %s)",
(i, 100.00 + i)
)
conn.commit()
# 按分片键查询
cursor.execute("SELECT order_id, user_id, order_amount FROM t_order WHERE user_id = %s", (3,))
for row in cursor.fetchall():
print(row)
finally:
conn.close()
注意port=3309是ShardingSphere-Proxy的端口,不是MySQL的3306。database='sharding_db'是逻辑库名,不是真实库名。这两点写错就会连不上。
跑完脚本后,回到3306实例看ds0.t_order_0、ds0.t_order_1等表,会发现数据按规则分布到了不同物理表,代码层面完全不感知分片。
5.2 分片键与事务使用要点
Python端使用这个Proxy时,有几个建议值得记下来:
第一,业务SQL尽量携带分片键。带WHERE user_id = ?可以精准路由;不带分片键虽然能查,但会触发全分片扫描,代理层要做结果合并,性能差异明显。
第二,连接池配置好。pymysql本身没有连接池,配合DBUtils或者SQLAlchemy使用更符合生产习惯。但注意连接池中的连接如果长时间空闲,Proxy可能因为空闲超时断开,要做好自动重连的兜底。
第三,谨慎使用长事务。事务中所有读都会路由到主库,如果把大查询包在事务里,从库负载能力就浪费了。读多写少的场景,尽量用短事务或者干脆自动提交。
5.3 可视化工具连接与检查清单
Navicat、DBeaver同样可以连接Proxy。新建连接时,主机填127.0.0.1,端口填3309,用户名和密码填server.yaml中配置的账号,MySQL版本选8.0。
如果你发现图形工具连接报Public Key Retrieval is not allowed,在连接参数中增加allowPublicKeyRetrieval=true。这个报错常见于MySQL 8.0默认的caching_sha2_password认证插件,Proxy连接真实库时也容易遇到,建议在config文件的所有jdbcUrl中都加上这个参数。
可视化工具连接失败时,按这个顺序排查:
| 检查项 | 操作 |
|---|---|
| Proxy进程是否存活 | `netstat -ano |
| 端口是否冲突 | 换proxy-frontend-port为其他端口 |
| 账号权限是否匹配 | server.yaml中AUTHORITY的账号密码 |
| 驱动版本兼容性 | 图形工具使用最新MySQL驱动 |
| 是否超时 | 关闭代理连接时的SSL选项 |
6. 常见问题排查与实操避坑
6.1 启动失败与日志排查
启动Proxy最容易遇到的是start.bat一闪而过,这时候不要慌,去logs目录看日志。stdout.log记录应用输出,error.log记录异常堆栈,大多数问题在日志里都有明确提示。
常见原因有三类:
- JAVA_HOME没配好,日志提示找不到Java命令。
- 配置文件YAML语法错误,日志会提示解析失败,比如缩进不一致、引号缺失。
- 端口被占用,日志报
address already in use。
YAML缩进是新手最容易翻车的地方,尤其是rules:下每个规则项都要有- !开头,属性缩进要完全对齐。我习惯用带YAML校验的编辑器写配置,保存前先格式化。
6.2 连接认证与驱动报错
连接Proxy时如果报Unable to load authentication plugin 'caching_sha2_password',说明客户端驱动版本过旧,升级驱动即可。
如果启动日志报找不到MySQL驱动类,把mysql-connector-j-8.0.33.jar放到lib目录,重启。
如果报后端数据源连接失败,检查config文件里jdbcUrl的allowPublicKeyRetrieval=true参数,以及数据库账号是否有权限访问对应库。我遇到过一次数据源连接失败,结果只是3306实例的root账号密码写错了,低级但隐蔽。
6.3 数据分布不对的路由排查
插入的数据没有按预期分布,先用PREVIEW SELECT * FROM t_order WHERE user_id = 1;看路由结果,确认代理认为该走哪个数据源和物理表。如果路由正确但物理表数据不对,检查是不是手动改过真实库的数据;如果路由本身不对,检查分片表达式是否写错,尤其注意%运算是否符合预期。
对于INLINE表达式,可以先用一个简单规则验证,比如固定表达式ds$->{user_id % 2},插入user_id=1,2,3,4四条数据,理论上1,3进ds0,2,4进ds1,如果结果不符合,优先怀疑表达式字符串中的转义问题。
6.4 稳定性和性能建议
本地Demo和生产的差距要心里有数。Proxy毕竟是独立进程,多一次网络跳转,延迟比直连MySQL高,但换来的是对业务透明的分片能力。生产环境建议使用Linux部署,把Proxy放在离应用较近的机房,最好用官方Docker镜像,减少环境差异。
如果要上生产,配置中心(ZooKeeper或Nacos)是必须考虑的,Standalone模式适合学习和测试,不适合动态配置管理和高可用场景。另一个建议是从一开始就用官方推荐的DistSQL管理规则,不要把全部希望寄托在手工维护YAML文件上。
我个人在实际操作中体会最深的一点是:ShardingSphere-Proxy这套东西,配置难度并不高,真正考验理解的是“流量路径”。你脑子里能随时画出这条SQL从客户端出发,经过Proxy,最终落到哪台机器的哪张表,排障效率会指数级提升。所以如果你也是第一次接触,强烈建议在本地把分库分表和读写分离各跑一遍,亲手观察数据分布、亲手做一次停同步验证,这比看十篇博客都有用。
这个Demo后续还可以再扩展,比如把分库分表和读写分离合并到同一个逻辑库,或者引入分布式事务、数据迁移、弹性扩缩容这些进阶功能。先把基础链路打通,后面每一步都会顺很多。
