Skip to content

PostgreSQL ​

通过 libpq 提供企业级关系型数据库访问,适合复杂查询、强一致性约束、JSON/Array 等富类型场景。

database.postgresql 是 Shield 官方提供的 shield.database.v1 实现之一,基于 libpq(PostgreSQL 官方 C 客户端库)。libpq 是 PostgreSQL 协议的参考实现,支持经典协议的所有特性,包括 SSL、SCRAM 认证、异步通知等。

包信息 ​

  • 包 ID: database.postgresql
  • 接口: shield.database.v1、shield.pool.stats.v1
  • Capabilities: sql, transactions
  • 版本: 1.0.0
  • CMake 选项: SHIELD_BUILD_DB_PLUGIN_POSTGRESQL
  • 源码: plugins/postgresql/
  • 依赖: libpq (vcpkg 端口 libpq)

构建启用 ​

在 CMake 配置阶段打开 SHIELD_BUILD_DB_PLUGIN_POSTGRESQL:

bash
cmake -B build -DSHIELD_BUILD_DB_PLUGIN_POSTGRESQL=ON
cmake --build build

该选项触发:

  1. plugins/postgresql/ 下的 shared library 被构建到 plugins/database.postgresql/bin/。
  2. vcpkg manifest feature database-postgresql 被启用,自动安装 libpq 端口(含 libpq-fe.h 头文件和导入库)。

libpq 是一个相对轻量的依赖(远小于 mysql-connector-cpp),构建体验较友好。

配置 Schema ​

manifest.yaml 的 config_schema 字段如下。

字段类型必填默认值说明
hoststring否127.0.0.1PostgreSQL 服务器主机名或 IP。
portinteger否5432PostgreSQL 端口。范围 1-65535。
databasestring是—数据库名。
usernamestring是—登录用户名。
passwordstring否—登录密码。标记为 secret,日志和 dashboard 会脱敏。
connect_timeout_msinteger否5000连接超时(秒级精度,libpq 的 connect_timeout 单位是秒),范围 100-60000。池耗尽时等待归还的时长也复用该值。
query_timeout_msinteger否5000单条 SQL 超时,范围 100-300000。
pool_sizeinteger否4每实例连接池容量,范围 1-256。
asyncboolean否truequery / query_one / execute / transaction 走实例 worker 池异步执行(连接 acquire 与 BEGIN 也在 worker 上):协程内调用挂起等待完成,同 service 其他消息继续处理;事务 body 内的语句挂在被持有的那条连接上,跑在事务专用串行 lane。设为 false 退回同步入口(调用即阻塞到 SQL 结束)。见 DB 异步入口。
call_timeout_msinteger否0异步入口的调用方挂起预算。0 表示 query_timeout_ms + 500。超时返回 {code="timeout", retryable=true};迟到的真实结果会被丢弃(不投递),不会覆盖超时结果。范围 0-300000。

完整 app.yaml 示例 ​

一个游戏服场景,主业务库 + 只读副本:

yaml
plugins:
  directory: "./plugins"
  instances:
    - id: db.main
      package: database.postgresql
      required: true
      config:
        host: "pg-primary.internal"
        port: 5432
        database: "game"
        username: "shield_app"
        password: "${DB_MAIN_PASSWORD}"
        connect_timeout_ms: 5000
        query_timeout_ms: 30000
    - id: db.replica
      package: database.postgresql
      required: false
      config:
        host: "pg-replica.internal"
        port: 5432
        database: "game"
        username: "shield_reader"
        password: "${DB_REPLICA_PASSWORD}"
  bindings:
    database.default: db.main
    database.readonly: db.replica

db.replica 设为 required: false,副本故障不阻塞主业务;调用方需要降级处理。

接口契约 ​

本插件实现 include/shield/plugin/database.h 的 shield_database_v1。

连接生命周期 ​

c
struct shield_db_conn* (*connect)(const struct shield_db_connect_args* args,
                                  char* err_buf, int err_buf_size);
void (*disconnect)(struct shield_db_conn* conn);
int  (*ping)(struct shield_db_conn* conn);
方法语义
connect把 shield_db_connect_args 字段拼成 libpq connection string:host=... port=... user=... password=... dbname=... connect_timeout=...,调用 PQconnectdb。失败返回 NULL 并把 PQerrorMessage 写入 err_buf。
disconnect调用 PQfinish。NULL 安全。
ping执行 SELECT 1,成功返回 1。

注意 libpq 的 connect_timeout 单位是秒(不是毫秒,且必须是整数——小数会被 libpq 直接判为解析错误),插件内部把 connect_timeout_ms 向上取整为整秒转换。query_timeout_ms 不直接对应 libpq 选项——业务侧需要时可以通过 SET statement_timeout 在会话级设置。

query vs execute ​

c
int (*query)(struct shield_db_conn* conn, const char* sql,
             const char* const* params, int n_params,
             struct shield_db_result* out_result);
int (*execute)(struct shield_db_conn* conn, const char* sql,
               const char* const* params, int n_params,
               struct shield_db_result* out_result);

两者底层共用 run_pg_query,差别在 is_select 标志,影响 PGRES_TUPLES_OK 的处理方式:

方法行为
queryis_select=true:把 result set 物化到 out_result->cells。
executeis_select=false:PGRES_TUPLES_OK 时也按 command 处理,不读 cells。

参数绑定使用 PQexecParams(带参数的 prepared execution)。n_params == 0 时退回 PQexec(普通查询)。

占位符重写 ​

重要:libpq 要求 SQL 占位符是 $1, $2, ... 形式,而 Shield 其他插件(MySQL/SQLite)用 ?。为了业务代码跨数据库可移植,本插件在执行前做一次朴素的占位符重写:扫描 SQL,把每个 ? 替换成 $1, $2, ...。

情况处理
普通 SQL WHERE id = ?正常重写为 WHERE id = $1
SQL 字符串字面量中的 ? 'what?'会错误重写。当前重写器不区分字符串字面量,会破坏 SQL。
注释中的 ?同上,也会被错误重写

Shield 的 shield::data SQL helper 不会生成这种 SQL,所以业务侧使用 mapper/entity helper 时安全。直接拼 SQL 时需要避开在字符串字面量或注释中放 ?。

事务 ​

c
int (*begin)(struct shield_db_conn* conn, struct shield_db_result* out_result);
int (*commit)(struct shield_db_conn* conn, struct shield_db_result* out_result);
int (*rollback)(struct shield_db_conn* conn, struct shield_db_result* out_result);

分别执行 BEGIN / COMMIT / ROLLBACK。不暴露 SAVEPOINT。PostgreSQL 在事务中发生错误后会进入 "aborted transaction" 状态,后续 SQL 全部失败,业务侧必须显式 ROLLBACK 后再重试。

shield_db_result 的内存所有权 ​

同其他数据库插件:

  • cells 数组及其中每个字符串都由插件 malloc 分配,长度 row_count * col_count。
  • NULL 指针表示 SQL NULL(通过 PQgetisnull 判断)。
  • host 必须在使用完后调用 free_result;该函数会释放 cells、error_msg、error_code,但不释放 result 结构体本身。

使用示例 ​

C++ 侧(通过 binding 访问) ​

cpp
#include "shield/plugin/database.h"
#include "shield/plugin/plugin_host.hpp"

auto* db = shield::plugin::global_host()
              .get_by_binding<shield_database_v1>("database.default");
if (!db) return;

shield_db_connect_args args{};
args.host = "pg-primary.internal";
args.port = 5432;
args.user = "shield_app";
args.password = std::getenv("DB_MAIN_PASSWORD");
args.database = "game";
args.connect_timeout_ms = 5000;
args.query_timeout_ms = 30000;

char err_buf[256] = {};
shield_db_conn* conn = db->connect(&args, err_buf, sizeof(err_buf));
if (!conn) {
    shield::log::error(std::format("pg connect failed: {}", err_buf));
    return;
}

// 用 ? 占位符(插件自动重写为 $1, $2 ...)
shield_db_result r{};
const char* params[] = { player_id.c_str() };
if (db->query(conn,
              "SELECT player_id, nickname, level "
              "FROM players WHERE player_id = ?",
              params, 1, &r) == 0 && r.success) {
    for (int i = 0; i < r.row_count; ++i) {
        const char* pid   = r.cells[i * r.col_count + 0];
        const char* name  = r.cells[i * r.col_count + 1];
        const char* level = r.cells[i * r.col_count + 2];
        // 业务处理 ...
    }
}
db->free_result(&r);
db->disconnect(conn);

Lua 侧 ​

PostgreSQL 插件通过 register_lua 暴露 shield.database.postgresql callable namespace:

lua
local db = shield.database.postgresql("database.default")
local ok, row = db:query_one(
    "SELECT player_id, nickname FROM players WHERE player_id = ?",
    { player_id })

local ok, err = db:transaction(function(tx)
    local ok2, r = tx:execute(
        "UPDATE wallet SET gold = gold - ? WHERE player_id = ?",
        { amount, pid })
    if not ok2 then return false, r end  -- 首返回值 false → ROLLBACK
    tx:execute("INSERT INTO logs(uid, action) VALUES(?, ?)",
               { pid, "debit" })
    return true, r.affected              -- → COMMIT,值透传给外层
end)

具体 API 契约见 Lua API 的 Database(SQL)一节。协程派发内这些调用默认异步挂起(async 开关),返回形状与同步路径一致;异步事务挂起期间独占一条池连接(/ops/metrics 的 shield_plugin_db_holding),body 内只允许 tx: 的 SQL 方法(池级调用会被运行时 raise)。

平台特性 ​

libpq 连接字符串 ​

本插件在内部把配置字段拼成 libpq connection string。如果业务需要更复杂的连接选项(如 SSL mode、target_session_attrs、service name),可以等后续扩展 shield_db_connect_args.extra_json 来传递——当前 v1 ABI 不暴露该字段。

LISTEN / NOTIFY(未支持) ​

PostgreSQL 提供 LISTEN/NOTIFY 实现进程间通知,但本插件的执行模型是 PQexec 阻塞调用(异步入口只是把它挪到 worker 线程上),没有实现事件循环式的通知监听。如果业务需要 NOTIFY,可以:

  • 单独建一个长连接,用 PQexec 发 LISTEN channel,然后用业务自己的循环调用 PQconsumeInput + PQnotifies 轮询。
  • 等待后续 v1.x 在 shield.queue.v1 之上提供 PostgreSQL NOTIFY 适配。

连接池与 worker 池 ​

Lua 路径(shield.database.postgresql(binding) 的 proxy 方法)的连接池在插件实例内部自治:free-list + pool_size(容量)+ 池耗尽时按 connect_timeout_ms 等待归还;取出的连接经 lazy health check(PQstatus),坏连接 PQfinish 丢弃、下次 acquire 补新。async 开启时另有一组 worker 线程(数量 = min(pool_size, 8),首用懒建)承接 acquire 与语句执行;异步事务的语句/提交/回滚跑在单条串行 tx lane 上。C vtable 路径仍是 per-call connect/disconnect,不走池也不走 worker。

池与异步入口的实时指标经 shield.pool.stats.v1 上报,/ops/metrics 导出 shield_plugin_pool_* 与 shield_plugin_db_pending_async / shield_plugin_db_holding。

SSL ​

libpq 原生支持 SSL,但当前 v1 ABI 不暴露 SSL 选项字段。默认行为是 libpq 的 sslmode=prefer,即服务器支持 SSL 就用,否则明文。如果业务需要强制 SSL,可以通过以下方式之一:

  • 在 PostgreSQL 服务端 pg_hba.conf 强制 hostssl。
  • 设置环境变量 PGSSLMODE=verify-full(libpq 会读取)。
  • 等待 extra_json 字段扩展后通过插件配置。

错误状态机 ​

PostgreSQL 的事务错误状态机比 MySQL/SQLite 严格:事务中任意一条 SQL 失败,整个事务进入 aborted 状态,后续所有 SQL 直接返回错误,必须 ROLLBACK 才能恢复。业务侧使用插件 proxy 的事务 helper 时,应在 callback 返回失败时 rollback;显式事务场景需要业务方自己注意。

错误处理 ​

shield_db_result.success 为 0 时,error_code 通过 PostgreSQL SQLSTATE 前缀映射。

error_codeSQLSTATE 前缀类别
connection_lost08* (connection exception), 57* (operator intervention)连接异常、服务器重启
pool_exhausted53* (insufficient resources)内存/磁盘不足
transaction_aborted40* (transaction rollback)死锁、序列化失败、事务中断
constraint_violation23* (integrity constraint violation)主键冲突、外键、CHECK、NOT NULL
syntax_error42* (syntax error or access rule violation)SQL 语法错误、表/列不存在
auth_failed28* (invalid authorization)用户名/密码错误
db_query_failed其他所有兜底

业务侧重试策略:

错误码重试
connection_lost, pool_exhausted可重试,但 pool_exhausted 通常表示数据库压力过大,应配合退避
transaction_aborted可重试(死锁),重试时需要重开事务
constraint_violation, syntax_error, auth_failed不可重试,属于程序 bug 或配置错误

具体 SQLSTATE 字符串不通过 v1 ABI 暴露——业务侧只能拿到上面的稳定 error_code。如果需要精确区分(例如区分 unique_violation 和 foreign_key_violation),可以通过 error_msg 字符串匹配 SQLSTATE "23505" 等模式。

部署 ​

二进制位置 ​

plugins/database.postgresql/
├── manifest.yaml
└── bin/
    ├── libshield_db_pgsql.dll        # Windows
    ├── libshield_db_pgsql.so         # Linux
    └── libshield_db_pgsql.dylib      # macOS

运行时依赖 ​

平台驱动形态
全平台vcpkg 端口 libpq 以静态库链接进插件产物(Linux 为 libpq.a,ldd libshield_db_pgsql.so 只剩系统库),运行时不需要随包驱动 DLL/SO

启用 database-postgresql feature 时 vcpkg manifest mode 自动处理构建期依赖与链接,部署目录里只有插件本身。

跨平台注意事项 ​

  • 驱动统一走 vcpkg 构建(静态链接),不与系统包/Postgres.app 的动态 libpq 混用,避免构建期头库不一致。
  • 容器化部署时,确保 PostgreSQL 客户端证书(如使用 SSL 客户端证书认证)挂载到正确路径,libpq 默认读取 ~/.postgresql/。

相关链接 ​

Apache License 2.0