📌 关键词:ProxySQL、数据库中间件、读写分离、MyCAT、ShardingSphere、主从复制、查询路由、高可用
👋 大家好,我是数据库小学妹!
我们前面学完主从复制、读写分离,感觉动态数据源、AOP 注解、强制读主,这条路终于走通了。后面发现在Spring 配了两套数据源,事务里还要手动处理,加个从库就得改代码重新发版。连接池管理、故障切换这些更麻烦的事,我压根没考虑到。
最近我就把市面上主流的数据库中间件翻了个遍,最后选了 ProxySQL。今天把选型和踩坑的过程捋一遍,省得你再走我的弯路。
一、手写读写分离的痛点
初学读写分离时,手写代码确实轻量好用。但系统跑起来之后,问题一个个冒出来:
| 痛点 | 手写代码 | 中间件 |
|---|---|---|
| 新增从库 | 改代码、发版、重启 | 改配置,秒级生效 |
| 从库故障 | 代码判断连接失败再切主库 | 自动摘除故障节点 |
| 连接池 | 每个数据源单独配 | 统一管控 |
| 多语言 | Java 写一套,Python 再写一套 | 任何语言连过来都行 |
| 负载均衡 | 自己实现轮询/权重 | 内置支持 |
说白了,中间件就是在应用和数据库之间加一层代理,脏活累活它全包了。
二、选型:为什么是 ProxySQL?
市面上做读写分离的中间件我重点看了三个:ProxySQL、MyCAT、ShardingSphere。
| 维度 | ProxySQL | MyCAT | ShardingSphere |
|---|---|---|---|
| 定位 | 轻量级 MySQL 代理 | 分布式数据库中间件 | 生态最全的数据库中间件 |
| 部署复杂度 | 低,单进程 | 中,依赖 ZooKeeper | 高,概念多、配置复杂 |
| 读写分离 | 原生支持,规则灵活 | 支持 | 支持 |
| 分库分表 | 不支持 | 支持 | 强项 |
| 故障自动切换 | 内置健康检查 | 需额外部署 | 需配合其他组件 |
| 配置方式 | SQL 语句配置 | XML 配置 | YAML/Java API |
| 性能损耗 | 很低(C++ 开发) | 中等 | 中等 |
| 学习曲线 | 平缓 | 中等 | 陡峭 |
| 适合场景 | 纯读写分离 | 分库分表 | 分库分表 + 企业级需求 |
我当时只需要读写分离,没有分库分表的需求。MyCAT 和 ShardingSphere 功能太重,为了一个读写分离引入一套复杂架构,成本和收益不成正比。ProxySQL 轻量、专注、性能好,够用就行。
当然,如果你已经在用 ShardingSphere 做分库分表,直接用它做读写分离也顺理成章。选型没有绝对对错,看现状。
三、ProxySQL 核心概念
动手之前,先搞清楚几个核心概念,不然配置的时候会一脸懵。
3.1 三层配置体系
ProxySQL 的配置分三层,这是最容易搞混的地方:
┌─────────────┐
│ RUNTIME │ ← 正在生效的配置(内存中,最快)
├─────────────┤
│ MEMORY │ ← 你正在编辑的配置(还没生效)
├─────────────┤
│ DISK │ ← 持久化到 SQLite 的配置(重启不丢)
└─────────────┘
操作逻辑:改 MEMORY → LOAD 到 RUNTIME → SAVE 到 DISK。
刚开始我老是忘了 SAVE,重启 ProxySQL 后配置全没了,又得重新配一遍 😭
3.2 几个关键表
ProxySQL 的配置存在表里,不是配置文件,这点和传统中间件很不一样:
| 表名 | 作用 |
|---|---|
mysql_servers |
后端 MySQL 实例(主库、从库都在这登记) |
mysql_users |
应用连接 ProxySQL 用的账号 |
mysql_query_rules |
核心:定义读写分离规则 |
mysql_replication_hostgroups |
主从组别管理(自动故障切换用) |
四、实战:从零搭起来
4.1 Docker 启动
docker run -d \
--name proxysql \
-p 6033:6033 \
-p 6032:6032 \
proxysql/proxysql:2.5
# 6033 是应用连接端口,6032 是管理端口
连接管理端口,开始配置:
mysql -u admin -padmin -h 127.0.0.1 -P 6032 --prompt='ProxySQL> '
4.2 添加后端 MySQL 实例
-- 添加主库(写节点)
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, comment)
VALUES (10, 'mysql-master', 3306, 1000, '主库-写');
-- 添加从库(读节点)
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, comment)
VALUES (20, 'mysql-slave1', 3306, 500, '从库1-读');
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, comment)
VALUES (20, 'mysql-slave2', 3306, 500, '从库2-读');
-- 加载到 RUNTIME 并持久化
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
hostgroup_id 是分组用的:10 是写组,20 是读组。权重 weight 决定流量分配比例,两个从库都是 500,流量就是对半分。
4.3 配置应用账号
-- 添加应用连接账号(应用用这个连 ProxySQL)
INSERT INTO mysql_users (username, password, default_hostgroup)
VALUES ('app_user', 'app_pass', 10);
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;
default_hostgroup=10 的意思是:默认请求都走写组(主库),除非后面的规则明确指定读组。
4.4 核心:配置读写分离规则
这是最关键的一步,规则决定了哪些 SQL 走主库、哪些走从库。
-- 规则1:SELECT 且不在事务中 → 走读组(20)
INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*', 20, 1);
-- 规则2:事务中的 SELECT → 要走主库(保证一致性)
INSERT INTO mysql_query_rules (rule_id, active, match_digest, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^SELECT.*FOR UPDATE', 10, 1);
-- 规则3:写操作(INSERT/UPDATE/DELETE)→ 走写组(10)
INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply)
VALUES (3, 1, '^(INSERT|UPDATE|DELETE)', 10, 1);
-- 规则4:默认兜底,走写组
INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply)
VALUES (4, 1, '.*', 10, 1);
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;
规则按 rule_id 从小到大匹配,匹配到就停止。所以顺序很重要:先匹配 SELECT FOR UPDATE(要读主),再匹配普通 SELECT(读从),再匹配写操作,最后兜底走主库。
五、验证效果
应用连接改成 ProxySQL 的地址:
spring:
datasource:
url: jdbc:mysql://proxysql:6033/mydb
username: app_user
password: app_pass
然后用 stats_mysql_query_digest 表查看路由情况:
SELECT digest_text, sum_time, count_star, hostgroup
FROM stats_mysql_query_digest
ORDER BY count_star DESC
LIMIT 10;
如果看到 SELECT 的 hostgroup 是 20,INSERT/UPDATE 的 hostgroup 是 10,恭喜,读写分离生效了 ✅
六、踩坑实录(血泪史)
💣 坑 1:事务里的读操作被路由到从库
现象:一个事务里先 INSERT 了一条数据,紧接着 SELECT 查出来是空的。
原因:SELECT 被规则匹配到读组,去从库查了,但从库还没同步完这条数据(主从延迟)。
解决:开启事务时,ProxySQL 会自动把所有请求路由到同一个 hostgroup(默认是事务开始的那个)。但我当时没用事务包裹,就出问题了。
正确做法:涉及"写后立即读"的逻辑,要么放事务里,要么在 SQL 前加注释强制走主库:
/* hostgroup=10 */ SELECT * FROM orders WHERE user_id = 123;
💣 坑 2:从库挂了,流量没自动切走
现象:一个从库宕机后,ProxySQL 还在往上面发请求,导致部分查询报错。
原因:没配健康检查,或者检查间隔太长。
解决:用 mysql_replication_hostgroups 表让 ProxySQL 自动管理主从状态:
INSERT INTO mysql_replication_hostgroups (writer_hostgroup, reader_hostgroup, comment)
VALUES (10, 20, '主从自动管理');
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
配上之后,从库挂了会自动摘流量,主库挂了会触发切换(配合 MGR 或 Orchestrator 效果更好)。
💣 坑 3:规则写得太宽泛,漏匹配
现象:有些查询没被规则匹配到,全走到了默认的主库,从库闲置。
原因:match_digest 用的是正则,^SELECT.* 看起来能匹配所有 SELECT,但如果 SQL 里有换行或者注释,就可能匹配不上。
解决:用 SELECT ... FOR UPDATE 这种明确的模式做精确匹配,普通 SELECT 放最后兜底。规则宁可写细一点,别贪多。
💣 坑 4:忘了 SAVE 到 DISK
现象:重启 ProxySQL 后,所有配置都没了。
解决:每次改完配置,记得 SAVE MYSQL ... TO DISK;。后来我写了个脚本,改完自动 LOAD + SAVE,再也没丢过配置。
💣 坑 5:监控没跟上,出问题了才知道
现象:ProxySQL 本身挂了,应用全连不上,半小时后才被发现。
解决:监控 ProxySQL 的关键指标:
ProxySQL_Threadpool_TrxNum:当前事务数mysql_server_ping_errors:后端节点健康状态stats_mysql_connection_pool:连接池使用情况
配合 Prometheus + Grafana,ProxySQL 出问题能秒级告警。
七、总结
选型这件事,说难也难,说简单也简单。
我目前只需要读写分离,没有分库分表的需求。ProxySQL 做不了分库分表,但恰好够用,这就是最合适的选择。
手写代码做读写分离,短期轻松长期痛苦。中间件看似多引入一层,省掉的是后面无限叠加的维护成本。
规则顺序和事务一致性,是读写分离最容易翻车的两个地方。配规则的时候多测几遍,别等上线了再翻车。
👋 我是数据库小学妹,一个用设计师思维学数据库的转行人。你们在读写分离选型上踩过什么坑?
本文基于 ProxySQL 2.5 + MySQL 8.0 环境。不同版本配置略有差异,建议参考官方文档确认参数。