监听 Canal

在 OpenResty 中监听 Canal(数据库 binlog 同步工具),核心是 对接 Canal 的输出协议(TCP 原生协议或 HTTP 协议),接收数据库变更事件(增删改),再通过 Lua 脚本实现后续业务逻辑(如缓存更新、消息推送、数据校验等)。

Canal 支持两种核心输出模式,OpenResty 需针对性适配:

  1. TCP 长连接模式(Canal Server 主动推送 binlog 事件):适合高吞吐、低延迟场景,OpenResty 作为 TCP 客户端连接 Canal Server;
  2. HTTP 回调模式(Canal 主动 POST 事件到 OpenResty 接口):适合简单场景,无需维护长连接,配置更简单。

一、前置准备

1. Canal 环境配置

先确保 Canal Server 已启动,且能正常解析数据库 binlog(以 MySQL 为例):

  • 参考 Canal 官方文档 完成安装配置(开启 binlog、创建 Canal 账号、配置 instance);

  • 关键配置(canal/conf/canal.properties):

    properties

    复制代码
    # 启用 TCP 协议(默认开启,端口 11111)
    canal.port = 11111
    # 启用 HTTP 协议(可选,用于 HTTP 回调模式,端口 8089)
    canal.serverMode = tcp,http
    canal.http.port = 8089
  • 启动 Canal Server:sh bin/startup.sh

2. OpenResty 依赖安装

需安装 Lua 非阻塞网络库(对接 TCP 协议)和 JSON 解析库(解析 Canal 事件):

bash

运行

复制代码
# 安装 LuaRocks(OpenResty 自带,若未安装则手动安装)
sudo /usr/local/openresty/luajit/bin/luarocks install lua-resty-tcp  # 非阻塞 TCP 客户端
sudo /usr/local/openresty/luajit/bin/luarocks install lua-cjson     # JSON 解析库

二、方案 1:TCP 长连接监听(推荐高吞吐场景)

OpenResty 作为 TCP 客户端,与 Canal Server 建立长连接,持续接收 binlog 事件(Canal 主动推送)。核心是用 lua-resty-tcp 实现非阻塞连接,避免影响 OpenResty 主进程。

1. OpenResty 配置与 Lua 脚本

创建配置文件 nginx.conf,新增独立的 Lua 模块处理 Canal 连接:

nginx

复制代码
worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    lua_jit on;

    # 配置 Canal 连接参数(可通过 env 或配置文件注入)
    lua_shared_dict canal_config 1m;  # 共享内存存储配置
    init_by_lua_block {
        local config = ngx.shared.canal_config
        config:set("host", "127.0.0.1")  # Canal Server 地址
        config:set("port", 11111)        # Canal TCP 端口
        config:set("username", "")       # Canal 用户名(默认空)
        config:set("password", "")       # Canal 密码(默认空)
        config:set("destination", "example")  # Canal instance 名称(默认 example)
    }

    # 启动时初始化 Canal 长连接(用 ngx.timer 异步执行,避免阻塞启动)
    init_worker_by_lua_block {
        local canal_listener = require("canal_listener")
        ngx.timer.at(0, function()
            canal_listener.start()  # 启动 Canal 监听
        end)
    }

    server {
        listen       80;
        server_name  localhost;

        # 健康检查接口(可选)
        location /canal/health {
            content_by_lua_block {
                local config = ngx.shared.canal_config
                local status = config:get("connection_status") or "disconnected"
                ngx.say(cjson.encode({ status = status }))
            }
        }
    }
}
2. 编写 Canal 监听模块(canal_listener.lua

创建文件 /usr/local/openresty/lualib/canal_listener.lua,实现 TCP 连接、认证、事件接收与解析:

lua

复制代码
local tcp = require("resty.tcp")
local cjson = require("cjson")
local config = ngx.shared.canal_config

-- Canal 协议常量(参考 Canal 官方协议文档)
local CANAL_MAGIC_NUMBER = 0x12345678
local CANAL_VERSION = 1
local CANAL_COMMAND_CONNECT = 1
local CANAL_COMMAND_CONNECT_ACK = 2
local CANAL_COMMAND_ROWS = 3

-- 连接 Canal Server
local function connect()
    local sock = tcp:new()
    sock:set_timeout(3000)  -- 连接超时 3 秒

    local host = config:get("host")
    local port = config:get("port")
    local ok, err = sock:connect(host, port)
    if not ok then
        ngx.log(ngx.ERR, "Canal 连接失败: ", err)
        return nil, err
    end
    ngx.log(ngx.INFO, "Canal 连接成功: ", host, ":", port)
    config:set("connection_status", "connected")
    return sock
end

-- Canal 认证(发送连接请求)
local function auth(sock)
    local auth_data = {
        magicNumber = CANAL_MAGIC_NUMBER,
        version = CANAL_VERSION,
        destination = config:get("destination"),
        username = config:get("username"),
        password = config:get("password"),
        clientId = "openresty-canal-listener",  -- 客户端 ID,自定义
        filter = ""  -- 过滤规则(如 "db1.table1,db2.table2",空表示所有表)
    }

    -- 序列化认证数据(Canal 协议要求:4 字节长度 + JSON 字符串)
    local json_str = cjson.encode(auth_data)
    local len = string.pack(">I4", #json_str)  -- 大端序 4 字节长度
    local ok, err = sock:send(len .. json_str)
    if not ok then
        ngx.log(ngx.ERR, "Canal 认证发送失败: ", err)
        return false, err
    end

    -- 接收认证响应
    local ack_len, err = sock:receive(4)  -- 先读 4 字节长度
    if not ack_len then
        ngx.log(ngx.ERR, "Canal 认证响应长度读取失败: ", err)
        return false, err
    end
    local ack_body_len = string.unpack(">I4", ack_len)
    local ack_body, err = sock:receive(ack_body_len)
    if not ack_body then
        ngx.log(ngx.ERR, "Canal 认证响应读取失败: ", err)
        return false, err
    end

    local ack = cjson.decode(ack_body)
    if ack.command ~= CANAL_COMMAND_CONNECT_ACK or ack.status ~= 0 then
        ngx.log(ngx.ERR, "Canal 认证失败: ", cjson.encode(ack))
        return false, "auth failed"
    end
    ngx.log(ngx.INFO, "Canal 认证成功")
    return true
end

-- 解析 Canal binlog 事件
local function parse_event(data)
    local event = cjson.decode(data)
    if event.command ~= CANAL_COMMAND_ROWS then
        return nil  -- 忽略非数据变更事件
    end

    -- 提取核心信息(数据库、表名、操作类型、变更数据)
    local result = {
        db = event.dbName,
        table = event.tableName,
        op = event.eventType,  -- INSERT/UPDATE/DELETE
        ts = event.executeTime,  -- 执行时间戳
        data = event.rows  -- 变更数据(INSERT/UPDATE 为新数据,DELETE 为旧数据)
    }
    return result
end

-- 处理 binlog 事件(自定义业务逻辑)
local function handle_event(event)
    ngx.log(ngx.INFO, "收到 binlog 事件: ", cjson.encode(event))
    
    -- 示例 1:更新 Redis 缓存(如用户表更新后刷新缓存)
    if event.db == "user_db" and event.table == "user_info" then
        local redis = require("resty.redis")
        local red = redis:new()
        red:set_timeout(1000)
        red:connect("127.0.0.1", 6379)
        for _, row in ipairs(event.data) do
            local user_id = row.id
            red:set("user:" .. user_id, cjson.encode(row))  -- 缓存用户数据
            red:expire("user:" .. user_id, 3600)  -- 1 小时过期
        end
        red:close()
    end

    -- 示例 2:推送消息到 MQ(如订单创建后触发后续流程)
    -- if event.db == "order_db" and event.table == "order" and event.op == "INSERT" then
    --     local http = require("resty.http")
    --     local httpc = http.new()
    --     httpc:request_uri("http://mq-server/push", {
    --         method = "POST",
    --         body = cjson.encode(event),
    --         headers = { ["Content-Type"] = "application/json" }
    --     })
    -- end
end

-- 循环接收 Canal 事件(非阻塞)
local function receive_events(sock)
    while true do
        -- 读取事件长度(4 字节大端序)
        local len, err = sock:receive(4)
        if not len then
            ngx.log(ngx.ERR, "Canal 事件长度读取失败: ", err)
            break
        end

        local body_len = string.unpack(">I4", len)
        if body_len <= 0 then
            ngx.log(ngx.WARN, "Canal 空事件,跳过")
            goto continue
        end

        -- 读取事件体
        local body, err = sock:receive(body_len)
        if not body then
            ngx.log(ngx.ERR, "Canal 事件体读取失败: ", err)
            break
        end

        -- 解析并处理事件
        local event = parse_event(body)
        if event then
            handle_event(event)
        end

        ::continue::
    end

    -- 连接断开,触发重连
    sock:close()
    config:set("connection_status", "disconnected")
    ngx.log(ngx.WARN, "Canal 连接断开,5 秒后重连")
    ngx.timer.at(5, function() start() end)  -- 5 秒后重连
end

-- 启动监听(入口函数)
function _M.start()
    local sock, err = connect()
    if not sock then
        ngx.timer.at(5, function() start() end)  -- 连接失败,5 秒后重连
        return
    end

    local ok, err = auth(sock)
    if not ok then
        sock:close()
        ngx.timer.at(5, function() start() end)  -- 认证失败,5 秒后重连
        return
    end

    -- 非阻塞接收事件(用 coroutine 避免阻塞)
    ngx.thread.spawn(receive_events, sock)
end

return _M
3. 启动与测试

bash

运行

复制代码
# 启动 OpenResty
sudo /usr/local/openresty/nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf

# 查看日志(确认连接与事件接收)
tail -f /usr/local/openresty/nginx/logs/error.log

测试步骤:

  1. 操作数据库(如 INSERT INTO user_db.user_info (id, name) VALUES (1, "test"));
  2. 查看 OpenResty 日志,应打印 收到 binlog 事件: {"db":"user_db",...}
  3. 验证 Redis 缓存:redis-cli get user:1,应返回插入的用户数据。

三、方案 2:HTTP 回调模式(简单场景)

Canal 主动将 binlog 事件通过 HTTP POST 推送到 OpenResty 的接口,无需维护长连接,配置更简单(适合低吞吐、对延迟不敏感的场景)。

1. Canal 配置 HTTP 回调

修改 Canal instance 配置(canal/conf/example/instance.properties),添加 HTTP 回调地址:

properties

复制代码
# 启用 HTTP 回调(Canal 会 POST 事件到该地址)
canal.instance.eventMode = http
canal.instance.http.callback.url = http://127.0.0.1/canal/callback  # OpenResty 接口地址
canal.instance.http.callback.contentType = application/json  # 事件格式为 JSON

重启 Canal Server:sh bin/restart.sh

2. OpenResty 配置回调接口

修改 nginx.conf,添加接收 Canal 事件的 HTTP 接口:

nginx

复制代码
http {
    lua_jit on;
    server {
        listen       80;
        server_name  localhost;

        # Canal HTTP 回调接口
        location /canal/callback {
            content_by_lua_block {
                local cjson = require("cjson")
                local ngx_req = require("ngx.req")

                -- 读取 Canal 推送的事件(JSON 格式)
                ngx_req.read_body()
                local body = ngx_req.get_body_data()
                if not body then
                    ngx.log(ngx.ERR, "Canal 回调无数据")
                    ngx.exit(400)
                end

                -- 解析事件(Canal HTTP 回调格式与 TCP 一致)
                local events = cjson.decode(body)
                for _, event in ipairs(events) do
                    -- 提取核心信息(同 TCP 模式)
                    local result = {
                        db = event.dbName,
                        table = event.tableName,
                        op = event.eventType,
                        ts = event.executeTime,
                        data = event.rows
                    }

                    -- 自定义业务逻辑(如更新缓存、推送 MQ,同 TCP 模式的 handle_event)
                    ngx.log(ngx.INFO, "HTTP 回调收到事件: ", cjson.encode(result))
                    
                    -- 示例:更新 Redis 缓存(复用 TCP 模式的逻辑)
                    if result.db == "user_db" and result.table == "user_info" then
                        local redis = require("resty.redis")
                        local red = redis:new()
                        red:connect("127.0.0.1", 6379)
                        for _, row in ipairs(result.data) do
                            red:set("user:" .. row.id, cjson.encode(row))
                        end
                        red:close()
                    end
                end

                -- 响应 Canal(必须返回 200,否则 Canal 会重试)
                ngx.say(cjson.encode({ code = 200, msg = "success" }))
            }
        }
    }
}
3. 测试
  1. 重启 OpenResty:sudo /usr/local/openresty/nginx/sbin/nginx -s reload
  2. 操作数据库(如更新用户数据);
  3. 查看 OpenResty 日志,应打印 HTTP 回调收到事件: ...,且 Redis 缓存同步更新。

四、关键注意事项

  1. 非阻塞 I/O 原则 :对接 Canal 时,必须使用 OpenResty 生态的 非阻塞库 (如 lua-resty-tcplua-resty-redislua-resty-http),避免使用阻塞 I/O(如原生 Lua socket),否则会导致 OpenResty 工作进程阻塞,严重影响并发。

  2. 重连机制 :TCP 模式下,需处理 Canal 连接断开的情况(如 Canal 重启、网络波动),通过 ngx.timer 实现自动重连,确保监听不中断。

  3. 事件过滤与限流

    • 若无需监听所有表的变更,可在 Canal 配置中设置过滤规则(如 filter = "db1.table1,db2.table2"),减少无效事件;
    • 高并发场景下,可通过 lua_shared_dict 实现限流(如限制每秒处理 1000 个事件),避免业务逻辑过载。
  4. 数据一致性:Canal 同步 binlog 是异步的,可能存在延迟(毫秒级),若需强一致性(如更新数据库后立即读取缓存),需结合业务设计(如先更新缓存再写数据库,或使用分布式锁)。

  5. 日志与监控:关键事件(连接失败、认证失败、数据处理异常)需打印日志,建议对接 Prometheus/Grafana 监控 Canal 连接状态、事件处理量、错误率。

五、两种方案对比

方案 优点 缺点 适用场景
TCP 长连接模式 高吞吐、低延迟、无重复推送 需维护长连接、配置复杂 高并发业务(如电商、支付)
HTTP 回调模式 配置简单、无需维护连接 延迟略高、可能重复推送(需幂等) 简单场景(如日志收集、低并发更新)

若你的业务对并发和延迟要求较高,优先选择 TCP 长连接模式 ;若仅需简单同步数据,可使用 HTTP 回调模式

相关推荐
软件技术NINI1 小时前
html css js网页制作成品——敖瑞鹏html+css+js 4页附源码
javascript·css·html
是罐装可乐1 小时前
前端架构知识体系:通过发布-订阅者模式解耦路由和请求
前端·架构·vue·路由
笃行客从不躺平1 小时前
认识 Java 中的锁升级机制
java·开发语言
weixin_307779131 小时前
Jenkins Branch API插件详解:多分支项目管理的核心引擎
java·运维·开发语言·架构·jenkins
程序员小寒1 小时前
Vue.js 为什么要推出 Vapor Mode?
前端·javascript·vue.js
milanyangbo1 小时前
从硬盘I/O到网络传输:Kafka与RocketMQ读写模型及零拷贝技术深度对比
java·网络·分布式·架构·kafka·rocketmq
小股虫1 小时前
消息中间件关键技术、设计原理与实现架构总纲
java·开发语言·架构
风萧萧19991 小时前
Java:PPT转图片
java·python·powerpoint
洲星河ZXH1 小时前
Java,日期时间API
java·开发语言·python