ProxySQL选型实战:从手写读写分离到中间件的踩坑全记录

📌 关键词: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 启动

bash 复制代码
docker run -d \
  --name proxysql \
  -p 6033:6033 \
  -p 6032:6032 \
  proxysql/proxysql:2.5

# 6033 是应用连接端口,6032 是管理端口

连接管理端口,开始配置:

bash 复制代码
mysql -u admin -padmin -h 127.0.0.1 -P 6032 --prompt='ProxySQL> '

4.2 添加后端 MySQL 实例

sql 复制代码
-- 添加主库(写节点)
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 配置应用账号

sql 复制代码
-- 添加应用连接账号(应用用这个连 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 走主库、哪些走从库。

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 的地址:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://proxysql:6033/mydb
    username: app_user
    password: app_pass

然后用 stats_mysql_query_digest 表查看路由情况:

sql 复制代码
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 前加注释强制走主库:

sql 复制代码
/* hostgroup=10 */ SELECT * FROM orders WHERE user_id = 123;

💣 坑 2:从库挂了,流量没自动切走

现象:一个从库宕机后,ProxySQL 还在往上面发请求,导致部分查询报错。

原因:没配健康检查,或者检查间隔太长。

解决 :用 mysql_replication_hostgroups 表让 ProxySQL 自动管理主从状态:

sql 复制代码
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 环境。不同版本配置略有差异,建议参考官方文档确认参数。

相关推荐
许彰午2 小时前
开发转兼职DBA(六):换了个数据库,问题还是那些问题
数据库·dba
handler012 小时前
【MySQL】常用约束语法总结
linux·运维·数据库·笔记·mysql
一条泥憨鱼2 小时前
详解MyBatis 动态 SQL
java·数据库·sql·mysql·mybatis·动态sql
fengxin_rou2 小时前
【后端配置模块实战】:索引、中间件与缓存架构全解析
缓存·中间件·架构
weixin_3975740912 小时前
用自然语言查数据库出图表靠谱吗?一次智能问数实践复盘
数据库
字节跳动开源14 小时前
Viking AI 搜索 CLI 正式发布:会说话,就能做搜索推荐
数据库·人工智能·开源
TechWJ15 小时前
数据库在公司内网,出差路上想查数据怎么办?
服务器·数据库·mariadb
我是一颗柠檬15 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
橙子圆12315 小时前
Redis知识9之集群
数据库·redis·缓存