第22章 MySQL Proxy深度实践:构建读写分离中间层

在当今高并发的互联网应用中,数据库的读写压力越来越大。主从复制技术解决了数据备份和高可用问题,但应用程序需要自行处理读写分离逻辑:写操作发给主库,读操作发给从库。这不仅增加了代码复杂度,还需要在应用层维护多个数据源。MySQL Proxy作为一款轻量级的中间件,能够透明地实现读写分离,让应用程序像访问单库一样访问集群,大大简化了架构。

本章将深入讲解MySQL Proxy的原理、安装配置,并通过商业案例展示如何利用它构建高效的读写分离架构。同时,也会探讨其局限性及替代方案,帮助读者在实际项目中做出合理选型。

22.1 MySQL Proxy核心概念与作用

22.1.1 什么是MySQL Proxy

MySQL Proxy是一个位于客户端和MySQL服务器之间的代理程序,它监听客户端的连接请求,解析、修改或转发SQL语句到后端的MySQL服务器。它支持使用Lua脚本进行定制化处理,可以实现读写分离、负载均衡、查询过滤、日志记录等功能。

从架构上看,MySQL Proxy充当了数据库网关的角色。应用程序不再直接连接具体的MySQL实例,而是连接Proxy,由Proxy根据规则将请求分发到合适的后端数据库。这层抽象带来了诸多好处:

  • 读写分离:自动将SELECT查询发送到从库,将INSERT/UPDATE/DELETE发送到主库。
  • 负载均衡:在多个从库之间分发读请求,提升系统吞吐量。
  • 高可用:当后端节点故障时,Proxy可以自动剔除故障节点,应用无感知。
  • 安全控制:可以在Proxy层进行SQL过滤、权限检查,增强安全性。
  • 监控与分析:记录所有SQL,便于性能分析和审计。

22.1.2 MySQL Proxy的工作原理

MySQL Proxy的核心是一个Lua虚拟机,它允许用户编写Lua脚本处理不同阶段的网络事件。主要事件包括:

  • 连接事件:客户端连接建立时,可以决定是否允许连接、选择后端服务器。
  • 认证事件:处理客户端认证信息。
  • 查询事件:当客户端发送SQL查询时,Proxy可以解析SQL类型,修改SQL内容,选择后端服务器,并转发结果。
  • 结果集事件:在返回结果前,可以修改结果集。

读写分离正是利用查询事件:在Lua脚本中判断SQL语句类型,如果是SELECT(且非事务内或特定标记),则选择一个从库转发;否则转发到主库。

22.1.3 适用场景与商业价值

在MySQL 5.7时代,MySQL Proxy是除应用层读写分离外的主流选择之一。尤其适合以下场景:

  • 中小型互联网应用:初期使用一主多从架构,通过Proxy透明实现读写分离,避免应用改造。
  • 快速迭代项目:希望快速上线读写分离功能,无需在代码中引入复杂的数据源路由。
  • 数据库中间件轻量化需求:相比MyCAT、ShardingSphere等重量级中间件,MySQL Proxy更轻量,部署简单。

然而,随着技术的发展,MySQL Proxy存在一些局限(如性能瓶颈、Lua编程门槛、官方停止维护等),逐渐被MySQL Router、ProxySQL等替代。但在老系统中仍有大量部署,理解其原理对维护和迁移至关重要。

22.2 安装与配置实战

22.2.1 下载与安装MySQL Proxy

MySQL Proxy的官方下载地址为:https://downloads.mysql.com/archives/proxy/。MySQL 5.7对应推荐的Proxy版本为0.8.5。注意:MySQL Proxy已停止更新,但0.8.5版本在MySQL 5.7上运行稳定。

22.2.1.1 Windows平台安装

  1. 下载Windows版本的ZIP包(如mysql-proxy-0.8.5-win32.zip)。
  2. 解压到目标目录,例如C:\mysql-proxy。
  3. 进入bin目录,可以看到mysql-proxy.exe。

22.2.1.2 Linux平台安装

以CentOS 7为例,可以使用预编译的二进制包:

wget https://downloads.mysql.com/archives/get/p/21/file/mysql-proxy-0.8.5-linux-glibc2.3-x86-64bit.tar.gz
tar -zxvf mysql-proxy-0.8.5-linux-glibc2.3-x86-64bit.tar.gz -C /usr/local/
cd /usr/local
ln -s mysql-proxy-0.8.5-linux-glibc2.3-x86-64bit mysql-proxy

设置环境变量(可选):

export PATH=$PATH:/usr/local/mysql-proxy/bin

22.2.2 配置MySQL Proxy参数

MySQL Proxy通过命令行参数或配置文件指定运行选项。核心参数包括:

  • –proxy-address:Proxy监听的IP和端口,默认:4040。
  • –proxy-backend-addresses:指定后端主库地址,可多个(但通常主库只有一个)。
  • –proxy-read-only-backend-addresses:指定只读从库地址,可多个。
  • –proxy-lua-script:指定Lua脚本路径,用于自定义转发逻辑。
  • –daemon:以守护进程运行。
  • –log-file:日志文件路径。
  • –log-level:日志级别(critical、error、warning、message、debug)。

22.2.2.1 基本配置示例

假设后端架构:

  • 主库:192.168.1.10:3306
  • 从库1:192.168.1.11:3306
  • 从库2:192.168.1.12:3306

启动命令:

mysql-proxy \
--proxy-address=0.0.0.0:4040 \
--proxy-backend-addresses=192.168.1.10:3306 \
--proxy-read-only-backend-addresses=192.168.1.11:3306 \
--proxy-read-only-backend-addresses=192.168.1.12:3306 \
--proxy-lua-script=/usr/local/mysql-proxy/share/doc/mysql-proxy/rw-splitting.lua \
--daemon \
--log-file=/var/log/mysql-proxy.log \
--log-level=message

注意:rw-splitting.lua是MySQL Proxy自带的读写分离脚本,位于share/doc/mysql-proxy/目录下。也可以自定义Lua脚本。

22.2.3 配置Path变量(Windows)

为了方便在任意路径下执行mysql-proxy,可以将bin目录添加到系统Path环境变量中。操作步骤:

  • 右键“此电脑” -> 属性 -> 高级系统设置 -> 环境变量。
  • 在系统变量中找到Path,编辑,添加C:\mysql-proxy\bin。
  • 确定保存。

之后即可在CMD中直接运行mysql-proxy命令。

22.3 使用MySQL Proxy实现读写分离

22.3.1 读写分离Lua脚本原理

自带的rw-splitting.lua脚本实现了一个简单的读写分离逻辑:

  • 如果SQL语句以SELECT开头,且不在事务中(没有BEGIN/COMMIT标记),则选择从库;否则选择主库。
  • 从库之间使用轮询(round-robin)负载均衡。
  • 支持连接池复用。

但其功能相对简单,不支持更复杂的规则,如根据SQL正则、用户、表名等路由。在实际生产环境中,通常需要定制Lua脚本。

22.3.2 部署一主两从环境

在开始之前,确保已经配置好MySQL主从复制(参照第18章)。假设主库为192.168.1.10,从库1为192.168.1.11,从库2为192.168.1.12,复制正常运行。

22.3.3 启动MySQL Proxy

在服务器上执行上述启动命令。检查进程是否启动:

ps aux | grep mysql-proxy

查看日志文件/var/log/mysql-proxy.log,确认无错误。

22.3.4 测试读写分离

22.3.4.1 连接测试

使用MySQL客户端连接Proxy:

mysql -h 192.168.1.100 -P 4040 -u app_user -p

其中192.168.1.100是部署Proxy的服务器IP。输入密码后,应能正常登录。

22.3.4.2 执行写操作

USE ecommerce;
INSERT INTO orders (user_id, amount) VALUES (1001, 299);

执行后,检查主库和从库的数据一致性。由于主从复制,从库也应很快出现该记录。

22.3.4.3 执行读操作

SELECT * FROM orders WHERE user_id = 1001;

多次执行,观察从库的负载情况。可以在从库上执行SHOW PROCESSLIST查看连接来源,确认请求被分发到不同的从库。

22.3.4.4 事务测试

START TRANSACTION;
INSERT INTO orders (user_id, amount) VALUES (1002, 399);
SELECT * FROM orders WHERE user_id = 1002;
COMMIT;

在事务中的SELECT应路由到主库,以保证事务内一致性。rw-splitting.lua通过检测事务标记实现了这一点。

22.3.5 自定义Lua脚本实现更灵活的路由

实际业务中,可能需要更精细的控制,例如:

  • 根据表名路由:查询订单表走从库,查询库存表走主库(因为库存实时性要求高)。
  • 根据用户标记路由:某些VIP用户的查询直接走主库。
  • 读写分离权重分配:不同从库性能不同,可分配不同权重。

下面是一个简单的自定义脚本示例,实现根据表名路由:

-- custom_rw.lua
local backend_ndx = 0

function read_query(packet)
    -- 获取SQL语句
    local cmd = packet:byte()
    if cmd == proxy.COM_QUERY then
        local query = packet:sub(2)
        -- 转换为小写方便匹配
        local lower_query = string.lower(query)
        
        -- 判断是否为写操作
        if lower_query:match("^insert") or lower_query:match("^update") or lower_query:match("^delete") then
            -- 写操作使用主库
            proxy.connection.backend_ndx = 1
        else
            -- 读操作进一步判断
            if lower_query:match("inventory") then
                -- 库存表查询走主库(实时性要求高)
                proxy.connection.backend_ndx = 1
            else
                -- 其他读操作从从库轮询
                local num_backends = #proxy.global.backends
                if num_backends > 0 then
                    backend_ndx = (backend_ndx % (num_backends - 1)) + 2 -- 从库索引从2开始(1是主库)
                    proxy.connection.backend_ndx = backend_ndx
                end
            end
        end
    end
end

启动时使用自定义脚本:

mysql-proxy ... --proxy-lua-script=/path/to/custom_rw.lua

22.4 商业实战案例:电商系统读写分离架构

22.4.1 业务背景

某电商平台经过快速发展,数据库压力日益增大。核心订单表日增10万行,商品表频繁读取。当前架构为一主一从,主库承担所有读写,从库仅做冷备。随着大促临近,需要将读流量分流到从库,提升系统承载能力。

22.4.2 架构设计

目标架构:

  • 一主两从:主库(写)、从库1(读)、从库2(读)。
  • 部署MySQL Proxy作为中间层,应用连接Proxy,无感知实现读写分离。
  • 对实时性要求极高的库存查询、用户余额查询,仍走主库(通过自定义Lua标记)。

22.4.3 实施步骤

22.4.3.1 环境准备

  • 主库:192.168.1.10
  • 从库1:192.168.1.11
  • 从库2:192.168.1.12
  • Proxy服务器:192.168.1.100(也可部署在应用服务器上)

所有数据库已配置好主从复制,且应用账号app_user具有相应权限。

22.4.3.2 安装MySQL Proxy

参照22.2节,在192.168.1.100上安装MySQL Proxy。

22.4.3.3 编写Lua脚本

考虑到业务需求,我们定制脚本ecommerce_rw.lua

-- ecommerce_rw.lua
local backends = proxy.global.backends
local current_backend = 1  -- 默认主库

function read_query(packet)
    local cmd = packet:byte()
    if cmd ~= proxy.COM_QUERY then return end
    local query = packet:sub(2)
    local lower_query = string.lower(query)
    
    -- 写操作一律主库
    if lower_query:match("^insert") or lower_query:match("^update") or lower_query:match("^delete") then
        proxy.connection.backend_ndx = 1
        return
    end
    
    -- 读操作,判断是否为特殊表
    if lower_query:match("inventory") or lower_query:match("balance") then
        proxy.connection.backend_ndx = 1  -- 实时数据走主库
        return
    end
    
    -- 其他读操作从从库中轮询
    local slave_count = #backends - 1  -- 假设主库是第一个
    if slave_count > 0 then
        -- 简单轮询,可改为更复杂的策略
        current_backend = ((current_backend - 1) % slave_count) + 2
        proxy.connection.backend_ndx = current_backend
    else
        proxy.connection.backend_ndx = 1
    end
end

注意:脚本中的proxy.global.backends是在启动时通过--proxy-backend-addresses--proxy-read-only-backend-addresses指定的顺序,主库在前(索引1),从库依次。

22.4.3.4 启动Proxy

mysql-proxy \
--proxy-address=0.0.0.0:3306 \
--proxy-backend-addresses=192.168.1.10:3306 \
--proxy-read-only-backend-addresses=192.168.1.11:3306 \
--proxy-read-only-backend-addresses=192.168.1.12:3306 \
--proxy-lua-script=/etc/mysql-proxy/ecommerce_rw.lua \
--daemon \
--log-file=/var/log/mysql-proxy.log \
--log-level=message

注意:这里将Proxy监听端口设为3306,与应用原本连接数据库的端口一致,这样应用只需修改IP指向Proxy即可,无需改端口。

22.4.3.5 应用切换

修改应用配置中的数据库连接IP为192.168.1.100,端口3306,用户名密码不变。重启应用,观察日志和监控。

22.4.4 效果验证与监控

  • 通过SHOW PROCESSLIST观察主库和从库的连接数,确认读请求分布到从库。
  • 使用压力测试工具模拟读写混合场景,对比系统QPS和响应时间。
  • 监控Proxy日志,检查是否有错误或异常。

22.4.5 高可用考虑

MySQL Proxy本身是单点,存在故障风险。可以通过Keepalived实现VIP漂移,部署双Proxy(主备)来提升可用性。但Proxy无状态,可以挂载多台Proxy,前端用负载均衡器(如LVS、HAProxy)分发。

22.5 专家解惑

22.5.1 MySQL Proxy的性能如何?

MySQL Proxy使用单线程事件驱动模型,性能主要取决于Lua脚本的复杂度和网络IO。在普通硬件上,可以支撑数千QPS。但如果Lua脚本过于复杂,会成为瓶颈。对于高并发场景,建议使用C++编写的ProxySQL或MySQL Router。

22.5.2 如何处理事务中的读写分离?

在事务中,所有SQL(包括SELECT)都应路由到主库,以保证事务内数据一致性。自带脚本已经处理了这一点,通过检测BEGIN/COMMIT状态。自定义脚本时也需注意。

22.5.3 连接池与连接复用

MySQL Proxy本身不支持连接池,但可以复用客户端连接。每个客户端连接在Proxy上保持,Proxy与后端服务器也建立连接。当短连接频繁时,Proxy会消耗较多文件描述符。建议应用层使用连接池,减少连接创建开销。

22.5.4 主从延迟导致脏读问题

当从库延迟较大时,刚写入的数据可能读不到。解决方案:

  • 在应用层标记关键读请求走主库(如用户刚下的订单查询)。
  • 设置--proxy-skip-profiling等参数无帮助,需在Lua脚本中实现更精细的控制,如检测写操作后的SELECT强制走主库。

22.5.5 MySQL Proxy的替代方案

由于MySQL Proxy已停止维护,新项目推荐使用:

  • MySQL Router:官方推出的轻量级中间件,支持读写分离、故障转移。
  • ProxySQL:功能强大的高性能代理,支持复杂路由、查询缓存、健康检查等。
  • ShardingSphere:功能全面的数据库中间件,适用于分库分表场景。

22.5.6 如何调试Lua脚本?

可以在脚本中添加print语句,输出到Proxy日志。例如:

print("Query: " .. query)

需设置日志级别为message或debug。另外,可以使用proxy.debug()函数。

22.5.7 如何监控MySQL Proxy的健康状态?

MySQL Proxy提供管理接口(默认端口4041),可以通过mysql客户端连接该端口执行SELECT * FROM proxy.*查询内部状态。例如:

mysql -h 127.0.0.1 -P 4041

进入后可以查看后端的连接数、状态等。

22.5.8 如何实现从库的负载均衡权重?

修改Lua脚本,为每个从库分配权重。例如,用数组存储权重,轮询时按权重比例选择。

22.5.9 遇到MySQL server has gone away错误?

可能原因:

  • Proxy与后端服务器连接超时,调整后端MySQL的wait_timeoutinteractive_timeout
  • Proxy本身空闲连接超时,可设置--proxy-connect-timeout等参数。
  • 网络问题,检查防火墙。

22.5.10 是否支持SSL连接?

MySQL Proxy 0.8.5支持SSL,需要在编译时启用。需配置--proxy-ssl-key--proxy-ssl-cert等参数。

22.6 经典习题

  1. 简述MySQL Proxy在读写分离架构中的作用。
  2. 如何在一主两从环境下使用MySQL Proxy实现读写分离?写出启动命令。
  3. 如果业务要求某些读操作(如库存查询)必须走主库,如何修改Lua脚本实现?
  4. 什么是MySQL Proxy的Lua脚本?写出一个简单的Lua函数,记录所有SELECT查询。
  5. 如何监控MySQL Proxy的运行状态?
  6. MySQL Proxy存在哪些局限性?有哪些替代方案?
  7. 假设应用需要连接池,但MySQL Proxy不支持,你有什么办法?
  8. 在事务中执行SELECT语句,应该路由到主库还是从库?为什么?
  9. 如何解决因主从延迟导致的脏读问题?
  10. 使用MySQL Proxy后,如果某个从库宕机,Proxy会自动处理吗?如何处理?

本章全面介绍了MySQL Proxy的安装、配置、读写分离实现以及商业案例。虽然MySQL Proxy已不再是主流选择,但通过学习它,你可以深入理解数据库中间件的基本原理,为使用更先进的代理产品打下基础。在实际工作中,根据业务需求和团队技术栈,合理选择代理工具,才能构建出高效稳定的数据库架构。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐