PostgreSQL笔记18:物理备份体系详解——从底层API到工具生态

电商订单表实战:用PostgreSQL讲透数据建模底层逻辑 数据模型是业务需求向数据库物理存储转化的核心翻译层,其本质在于将自然语言描述的实体关系(如客户、订单、商品)精准映射为可执行的数据库对象——表、列、外键与约束。在PostgreSQL中,这一过程直面范式权衡、物理优化、时区处理与高并发一致性等工程现实。通过电商四表模型(Customer/Orders/OrderItems/Product),可系统掌握实体关系建模、CHECK与外键约束落地、TIMESTAMPTZ时区实践、索引设计原则及OLTP到OLAP演进路径。它不仅是数据库设计方法论,更是保障数据准确、查 阅读详情

概述

PostgreSQL 的物理备份(Physical Backup)是数据库高可用与灾难恢复的基石。与逻辑备份(pg_dump)不同,物理备份直接复制数据库集群的数据目录文件,保留了事务日志(WAL)的完整上下文,是实现 时间点恢复(PITR, Point-In-Time Recovery) 和搭建流复制备库的前提。

本文将从 PostgreSQL 官方提供的底层备份函数出发,深入剖析 pg_backup_start / pg_backup_stop 的工作原理、排他/非排他备份模式的历史演进与陷阱,进而介绍自动化工具 pg_basebackup 的核心参数,最后盘点 PG 生态中主流的第三方备份工具(pgBackRest、Barman、pg_probackup)并进行横向对比,为生产环境的备份选型提供参考。

纲要

  • pg_backup_start / pg_backup_stop:底层备份函数的工作原理
    • full_page_writes 与块折断预防
    • 检查点(Checkpoint)与 backup_label 文件
  • 排他备份模式(Exclusive Backup Mode)vs 非排他备份模式(Non-Exclusive Backup Mode)
    • 排他模式的致命缺陷:崩溃恢复时的“时光倒流”陷阱
    • PostgreSQL 15 的重大变更:移除排他模式与函数重命名
  • pg_basebackup:官方自动化物理备份工具
    • 核心用法与 --max-rate 限速参数
    • PostgreSQL 17 新特性:增量备份(Incremental Backup)
  • 第三方备份工具生态对比
    • pgBackRest、Barman、pg_probackup 功能矩阵
    • 各工具的限制与适用场景
  • 生产环境备份选型建议

底层备份 API:pg_backup_start 与 pg_backup_stop

PostgreSQL 提供了一组底层函数,用于将数据库实例置入“备份模式”(Backup Mode),以便外部工具(如 cp、rsync、存储快照)能够安全地复制数据目录。

核心工作流程

一个标准的低级别物理备份流程如下:

  1. 进入备份模式:执行 SELECT pg_backup_start('label', false);。
  2. 复制数据目录:使用 cp、rsync 或存储快照技术复制整个数据目录。
  3. 退出备份模式:执行 SELECT pg_backup_stop();。

在执行 pg_backup_start 时,PostgreSQL 内部会依次完成以下动作:

  • 强制开启 full_page_writes:在备份期间,每个数据页第一次被修改时会将完整页面写入 WAL。这是为了预防“块折断”(Block Splitting)问题——操作系统通常以 4KB 为单位写入,而 PostgreSQL 数据页默认为 8KB,若在写入过程中发生崩溃,可能导致数据页损坏。full_page_writes 确保了恢复时能从 WAL 中获取完整的页面副本进行修复。
  • 切换 WAL 日志段:强制切换当前 WAL 日志文件,确保备份开始后的所有变更都记录在新的 WAL 段中。
  • 执行检查点(Checkpoint):将内存中的脏数据刷写到磁盘,并将检查点位置记录到 backup_label 文件中。
  • 创建 backup_label 文件:该文件位于数据目录根目录,记录了备份的起始 LSN(Log Sequence Number)、检查点位置、备份标签等信息。

pg_backup_stop 则执行对称的清理工作:退出备份模式、写入一条备份结束的 WAL 记录、创建 backup_history 文件(记录备份的时间范围和 WAL 起止位置),并删除 backup_label 文件(非排他模式下返回其内容而非写入磁盘)。

整个备份流程的时序关系如下图所示:

Disk PostgreSQL User Disk PostgreSQL User SELECT pg_backup_start('label', false) 强制开启 full_page_writes 切换 WAL 段 执行检查点 创建 backup_label(非排他模式暂存于内存) 返回起始 LSN 复制数据目录(cp/rsync/快照) SELECT pg_backup_stop() 写入 WAL 结束记录 创建 backup_history 返回 labelfile(非排他模式) 手动保存 labelfile 为 backup_label

排他备份模式的陷阱与 PostgreSQL 15 的变革

在 PostgreSQL 15 之前,pg_start_backup 和 pg_stop_backup 支持两种模式:排他备份(Exclusive Backup) 和非排他备份(Non-Exclusive Backup)。

排他模式(Exclusive Backup)的致命缺陷

排他备份模式存在一个严重的安全隐患:如果在备份期间数据库崩溃,重启时可能导致恢复时间异常延长,甚至启动失败。

其根本原因在于 backup_label 文件。正常情况下,PostgreSQL 崩溃恢复时从控制文件 pg_control 中读取最新的检查点位置开始回放 WAL。但若数据目录中存在 backup_label 文件,PostgreSQL 会优先从中读取检查点位置。

考虑以下场景:

  1. 管理员执行 pg_start_backup,生成了一个 backup_label 文件(记录了两天前的检查点 LSN)。
  2. 由于某种原因(如备份脚本中断),pg_stop_backup 未被调用,backup_label 残留。
  3. 两天后数据库崩溃。
  4. 重启时,PostgreSQL 检测到 backup_label 文件,从两天前的检查点开始回放 WAL——而非从最近的检查点。

这意味着数据库需要重放过去两天产生的所有 WAL 日志,恢复时间被无限拉长。更糟糕的是,如果这两天的 WAL 日志已被归档或回收,数据库将直接启动失败,报错 could not locate required checkpoint record。

注意:排他模式自 PostgreSQL 9.6 引入非排他模式后便被标记为弃用(Deprecated),历经 6 个主版本的过渡期后,于 PostgreSQL 15 中正式移除。

非排他模式(Non-Exclusive Backup)的优势

非排他备份模式解决了上述问题。其核心差异在于:

  • 不会在数据目录中创建 backup_label 文件,而是将备份标签信息保存在内存中,并在 pg_stop_backup 执行时以结果集形式返回。
  • 允许同时运行多个非排他备份(排他模式同一时间只允许一个备份会话)。
  • 即使备份会话异常中断,也不会留下干扰崩溃恢复的残留文件。

因此,所有现代备份工具(pg_basebackup、pgBackRest、Barman 等)均默认使用非排他模式。

PostgreSQL 15 的函数重命名

为避免混淆并明确标识非排他模式为唯一选项,PostgreSQL 15 将相关函数进行了重命名:

PostgreSQL 14 及更早PostgreSQL 15 及之后
pg_start_backup(label, exclusive, ...)pg_backup_start(label, false)
pg_stop_backup()pg_backup_stop()
pg_is_in_backup()已移除
pg_backup_start_time()已移除

版本提示:若你的备份脚本仍在使用 pg_start_backup / pg_stop_backup,请尽快迁移至 PostgreSQL 15+ 的新 API。

pg_basebackup:官方自动化备份工具

pg_basebackup 是 PostgreSQL 官方提供的物理备份工具,底层自动调用 pg_backup_start / pg_backup_stop 并完成数据目录的复制。

基本用法

# 全量备份到指定目录
pg_basebackup -D /backup/base -U replicator -h 192.168.1.100 -p 5432 -Fp -P -v
参数说明
-D备份目标目录
-U连接用户名(需具备 REPLICATION 权限)
-h / -p数据库主机和端口
-Fp输出格式为 plain(目录结构),-Ft 为 tar 格式
-P显示进度信息
-v详细输出
-X stream备份期间流式传输 WAL(推荐)

限速参数 --max-rate:生产环境的必备利器

在生产环境中,备份操作可能占用大量 I/O 带宽,影响业务响应。pg_basebackup 提供了 --max-rate 参数来限制传输速率:

pg_basebackup -D /backup/base --max-rate=100M -U replicator -h 192.168.1.100

上述命令将备份传输速率限制为 100MB/s。该参数在以下场景尤为实用:

  • 网络带宽有限的环境
  • 对 I/O 延迟敏感的生产系统
  • 需要在业务高峰期执行备份时

PostgreSQL 17 新特性:增量备份(Incremental Backup)

从 PostgreSQL 17 开始,pg_basebackup 原生支持块级增量备份(Block-Level Incremental Backup)。

增量备份的原理是:基于上一次全量备份的 backup_manifest 清单文件,仅备份自参考备份以来发生变化的数据块(而非整个文件)。

# 1. 创建全量备份
pg_basebackup -D /backup/full -c fast -P -v

# 2. 基于全量备份创建增量备份(PostgreSQL 17+)
pg_basebackup -D /backup/inc1 --incremental=/backup/full/backup_manifest -c fast -P -v

重要限制:

  • 增量备份不能直接用于恢复,必须使用 pg_combinebackup 将增量备份与依赖的全量备份合并。
  • --incremental 参数仅适用于 PostgreSQL 17 及更高版本的服务器。
  • 使用增量备份前,需将 summarize_wal 参数设置为 on。

第三方备份工具生态对比

PostgreSQL 社区拥有丰富的第三方备份解决方案,各有侧重。以下对主流的四个工具进行横向对比。

特性pgBackRestBarmanpg_probackupWAL-G
开发者Crunchy DataEnterpriseDB (EDB)Postgres Professional社区
全量备份✅✅✅✅
增量备份✅(块级)✅(rsync 差分)✅(PTRACK 页级)✅(块级)
对象存储(S3)✅✅(需插件)✅✅
加密✅✅✅✅
数据校验✅✅✅✅
并行备份/恢复✅✅✅✅
压缩✅✅✅✅
备库备份❌(依赖 SSH 免密)✅✅✅
限制仅支持 8KB 数据块大小需配置 SSH 免密不支持 2GB 以上大文件分段配置较复杂

pgBackRest:功能最全面的首选

pgBackRest 是目前 PG 生态中功能最强大、生态最完善的备份工具。它支持:

  • 并行备份与恢复
  • 块级增量备份
  • 加密与压缩
  • S3、Azure、GCS 等云存储
  • 备份完整性校验

主要限制:

  • 仅支持 8KB 数据块大小:若数据库编译时指定了 --with-blocksize=16 或 --with-blocksize=32,pgBackRest 将无法使用,报错 page size is 32768 but must be 8192。
  • 备库备份依赖 SSH 免密:在通过跳板机访问的生产环境中,配置 SSH 免密存在安全风险。

尽管存在上述限制,pgBackRest 凭借其卓越的健壮性和功能完整性,仍是绝大多数生产环境的首选。

pg_probackup:PTRACK 页级增量的独特优势

pg_probackup 由 Postgres Professional 开发,其最突出的特点是支持 PTRACK(Page Tracking)页级增量备份——仅备份自上次备份以来实际发生变化的页面,增量效率极高。

主要限制:

  • 不支持大于 2GB 的单个文件分段(segment)
  • 社区活跃度相对较低

Barman:EDB 官方支持的成熟方案

Barman 是 EnterpriseDB 推出的备份工具,与 EDB 的商业支持体系紧密结合。适合已经深度使用 EDB 产品线的企业用户。

选型建议

  • 通用生产环境:优先选择 pgBackRest,功能全面且生态成熟。
  • 超大规模数据库(PB 级):可评估 pg_probackup 的 PTRACK 增量效率。
  • EDB 生态用户:选择 Barman 可获得最佳兼容性。
  • 轻量级/简单场景:直接使用 pg_basebackup 即可满足需求(PostgreSQL 17+ 已支持增量备份)。

API 速览

pg_backup_start(label text [, fast boolean]) → text

属性描述
所属PostgreSQL 系统函数
适用版本PostgreSQL 15+
参数label:备份标签(任意文本标识);fast:是否快速启动(强制立即检查点)
返回值备份起始的 WAL 位置(LSN)
说明将数据库置入备份模式。在 PostgreSQL 15 中,exclusive 参数已被移除
-- PostgreSQL 15+ 用法
SELECT pg_backup_start('my_backup_20260814', false);

pg_backup_stop([wait_for_archive boolean]) → record

属性描述
所属PostgreSQL 系统函数
适用版本PostgreSQL 15+
参数wait_for_archive:是否等待 WAL 归档完成(默认 true)
返回值包含 lsn、labelfile、spcmapfile 等字段的记录
说明退出备份模式,返回备份标签和表空间映射内容(非排他模式下需手动保存)
SELECT * FROM pg_backup_stop();

pg_basebackup 命令行工具

属性描述
所属PostgreSQL 客户端工具
适用版本PostgreSQL 9.1+(增量备份需 17+)
关键参数-D、-U、-h、-p、--max-rate、--incremental(17+)
权限要求REPLICATION 权限或超级用户
# 全量备份 + 限速
pg_basebackup -D /backup/full -U replicator -h 192.168.1.100 -p 5432 --max-rate=100M -Fp -P -v

# 增量备份(PostgreSQL 17+)
pg_basebackup -D /backup/inc --incremental=/backup/full/backup_manifest -U replicator -h 192.168.1.100 -P -v

Demo:基于 Node.js 的完整备份示例

以下示例使用 Node.js 和 pg 库(原生 PostgreSQL 驱动)演示完整的物理备份流程,包含底层 API 调用和文件复制。

运行说明

  1. 安装依赖:
    npm init -y
    npm install pg
    
  2. 修改数据库连接配置(host、port、user、password、database)。
  3. 确保备份目录(/tmp/pg_backup)存在且有写入权限。
  4. 执行脚本:
    node backup.js
    

完整代码

const { Client } = require('pg');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');

// ===== 配置 =====
const PG_CONFIG = {
  host: '127.0.0.1',
  port: 5432,
  user: 'postgres',
  password: 'your_password',
  database: 'postgres',
};

const BACKUP_LABEL = 'node_backup_20260814';
const BACKUP_DIR = '/tmp/pg_backup';
const DATA_DIR = '/var/lib/postgresql/18/main'; // 根据实际 PG 数据目录修改

// ===== 核心备份函数 =====
async function performPhysicalBackup() {
  const client = new Client(PG_CONFIG);
  await client.connect();

  try {
    console.log('[1/5] 进入备份模式...');
    // PostgreSQL 15+ 使用 pg_backup_start,不再有 exclusive 参数
    const startResult = await client.query(
      'SELECT pg_backup_start($1, false) as lsn',
      [BACKUP_LABEL]
    );
    const startLsn = startResult.rows[0].lsn;
    console.log(`  -> 备份起始 LSN: ${startLsn}`);

    console.log('[2/5] 复制数据目录...');
    // 使用 rsync 复制数据目录(排除不需要的文件)
    const rsyncCmd = `rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' ${DATA_DIR}/ ${BACKUP_DIR}/`;
    await execPromise(rsyncCmd);
    console.log(`  -> 数据已复制到: ${BACKUP_DIR}`);

    console.log('[3/5] 复制 WAL 日志...');
    // 复制 WAL 日志(pg_wal 目录)
    const walDir = path.join(DATA_DIR, 'pg_wal');
    const walBackupDir = path.join(BACKUP_DIR, 'pg_wal');
    if (!fs.existsSync(walBackupDir)) {
      fs.mkdirSync(walBackupDir, { recursive: true });
    }
    const walRsyncCmd = `rsync -av ${walDir}/ ${walBackupDir}/`;
    await execPromise(walRsyncCmd);
    console.log(`  -> WAL 已复制到: ${walBackupDir}`);

    console.log('[4/5] 退出备份模式...');
    const stopResult = await client.query(
      'SELECT * FROM pg_backup_stop()'
    );
    const stopData = stopResult.rows[0];
    console.log(`  -> 备份结束 LSN: ${stopData.lsn}`);
    
    // 非排他模式下,备份标签内容由 pg_backup_stop 返回,需手动保存
    if (stopData.labelfile) {
      const labelPath = path.join(BACKUP_DIR, 'backup_label');
      fs.writeFileSync(labelPath, stopData.labelfile);
      console.log(`  -> backup_label 已保存到: ${labelPath}`);
    }
    if (stopData.spcmapfile) {
      const spcPath = path.join(BACKUP_DIR, 'tablespace_map');
      fs.writeFileSync(spcPath, stopData.spcmapfile);
      console.log(`  -> tablespace_map 已保存到: ${spcPath}`);
    }

    console.log('[5/5] 备份完成!');
    console.log(`  -> 备份位置: ${BACKUP_DIR}`);
    console.log(`  -> 恢复时需结合 WAL 归档进行 PITR`);

  } catch (error) {
    console.error('备份失败:', error);
    // 尝试退出备份模式(清理残留状态)
    try {
      await client.query('SELECT pg_backup_stop()');
    } catch (_) { /* ignore */ }
    throw error;
  } finally {
    await client.end();
  }
}

// ===== 辅助函数 =====
function execPromise(cmd) {
  return new Promise((resolve, reject) => {
    exec(cmd, { maxBuffer: 1024 * 1024 * 100 }, (error, stdout, stderr) => {
      if (error) {
        reject(new Error(`命令执行失败: ${error.message}\n${stderr}`));
        return;
      }
      resolve(stdout);
    });
  });
}

// ===== 执行 =====
performPhysicalBackup()
  .then(() => {
    console.log('\n✅ 物理备份成功完成!');
    process.exit(0);
  })
  .catch((err) => {
    console.error('\n❌ 物理备份失败:', err.message);
    process.exit(1);
  });

技术点总结

技术点说明
底层 API 调用使用 pg_backup_start / pg_backup_stop 手动控制备份模式
非排他模式pg_backup_start 不再包含 exclusive 参数,默认非排他
备份标签保存非排他模式下 backup_label 由 pg_backup_stop 返回,需手动写入文件
数据复制使用 rsync 实现增量式文件复制,支持断点续传
WAL 复制单独复制 pg_wal 目录,确保恢复时有完整的 WAL 日志
错误处理备份失败时自动调用 pg_backup_stop 清理残留状态

PostgreSQL 原生指令对照:

-- 进入备份模式
SELECT pg_backup_start('node_backup_20260814', false);
 
-- 退出备份模式
SELECT * FROM pg_backup_stop();

多语言示例

Go 语言示例:基于 pgx 驱动实现物理备份

本示例使用 Go 语言和 pgx 驱动(PostgreSQL 原生驱动)调用底层备份 API,并通过 rsync 命令复制数据目录。

运行说明
  1. 初始化 Go 模块并安装依赖:
    go mod init backup_demo
    go get github.com/jackc/pgx/v5
    
  2. 修改代码中的数据库连接配置(host、port、user、password、database)以及备份目录和数据目录路径。
  3. 确保系统已安装 rsync 命令。
  4. 执行:
    go run backup.go
    
完整代码
package main

import (
    "context"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "time"

    "github.com/jackc/pgx/v5"
)

const (
    backupLabel = "go_backup_20260814"
    backupDir   = "/tmp/pg_backup_go"
    dataDir     = "/var/lib/postgresql/18/main" // 根据实际 PG 数据目录修改
)

func main() {
    ctx := context.Background()

    // 数据库连接配置
    connStr := "postgres://postgres:your_password@127.0.0.1:5432/postgres"
    conn, err := pgx.Connect(ctx, connStr)
    if err != nil {
        panic(fmt.Errorf("连接失败: %w", err))
    }
    defer conn.Close(ctx)

    fmt.Println("[1/5] 进入备份模式...")
    var startLsn string
    err = conn.QueryRow(ctx, "SELECT pg_backup_start($1, false)", backupLabel).Scan(&startLsn)
    if err != nil {
        panic(fmt.Errorf("pg_backup_start 失败: %w", err))
    }
    fmt.Printf("  -> 备份起始 LSN: %s\n", startLsn)

    fmt.Println("[2/5] 复制数据目录...")
    rsyncCmd := exec.Command("rsync", "-av", "--exclude=postmaster.pid", "--exclude=pg_wal/*",
        dataDir+"/", backupDir+"/")
    if out, err := rsyncCmd.CombinedOutput(); err != nil {
        panic(fmt.Errorf("rsync 数据目录失败: %w, 输出: %s", err, out))
    }
    fmt.Printf("  -> 数据已复制到: %s\n", backupDir)

    fmt.Println("[3/5] 复制 WAL 日志...")
    walSrc := filepath.Join(dataDir, "pg_wal")
    walDst := filepath.Join(backupDir, "pg_wal")
    if err := os.MkdirAll(walDst, 0755); err != nil {
        panic(fmt.Errorf("创建 WAL 备份目录失败: %w", err))
    }
    rsyncWalCmd := exec.Command("rsync", "-av", walSrc+"/", walDst+"/")
    if out, err := rsyncWalCmd.CombinedOutput(); err != nil {
        panic(fmt.Errorf("rsync WAL 失败: %w, 输出: %s", err, out))
    }
    fmt.Printf("  -> WAL 已复制到: %s\n", walDst)

    fmt.Println("[4/5] 退出备份模式...")
    var stopLsn, labelfile, spcmapfile string
    err = conn.QueryRow(ctx, "SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()").
        Scan(&stopLsn, &labelfile, &spcmapfile)
    if err != nil {
        panic(fmt.Errorf("pg_backup_stop 失败: %w", err))
    }
    fmt.Printf("  -> 备份结束 LSN: %s\n", stopLsn)

    if labelfile != "" {
        labelPath := filepath.Join(backupDir, "backup_label")
        if err := os.WriteFile(labelPath, []byte(labelfile), 0644); err != nil {
            panic(fmt.Errorf("写入 backup_label 失败: %w", err))
        }
        fmt.Printf("  -> backup_label 已保存到: %s\n", labelPath)
    }
    if spcmapfile != "" {
        spcPath := filepath.Join(backupDir, "tablespace_map")
        if err := os.WriteFile(spcPath, []byte(spcmapfile), 0644); err != nil {
            panic(fmt.Errorf("写入 tablespace_map 失败: %w", err))
        }
        fmt.Printf("  -> tablespace_map 已保存到: %s\n", spcPath)
    }

    fmt.Println("[5/5] 备份完成!")
    fmt.Printf("  -> 备份位置: %s\n", backupDir)
    fmt.Println("  -> 恢复时需结合 WAL 归档进行 PITR")
}
技术点总结
  • 使用 pgx 驱动的 QueryRow 执行 pg_backup_start / pg_backup_stop
  • 通过 os/exec 调用 rsync 实现数据复制
  • 正确处理非排他模式下返回的 labelfile 和 spcmapfile
  • 错误处理和资源清理(defer 关闭连接)

Python 语言示例:基于 psycopg2 实现物理备份

本示例使用 Python 和 psycopg2 驱动调用备份 API,并通过 subprocess 执行 rsync。

运行说明
  1. 安装依赖:
    pip install psycopg2-binary
    
  2. 修改代码中的数据库连接参数(host、port、user、password、database)以及备份目录和数据目录。
  3. 确保系统已安装 rsync。
  4. 执行:
    python backup.py
    
完整代码
import os
import subprocess
import psycopg2
from psycopg2 import sql

BACKUP_LABEL = "py_backup_20260814"
BACKUP_DIR = "/tmp/pg_backup_py"
DATA_DIR = "/var/lib/postgresql/18/main"  # 根据实际 PG 数据目录修改

def run_cmd(cmd):
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        raise Exception(f"命令执行失败: {cmd}\n{result.stderr}")
    return result.stdout

def main():
    # 数据库连接
    conn = psycopg2.connect(
        host="127.0.0.1",
        port=5432,
        user="postgres",
        password="your_password",
        database="postgres"
    )
    conn.autocommit = True
    cur = conn.cursor()

    try:
        print("[1/5] 进入备份模式...")
        cur.execute("SELECT pg_backup_start(%s, false)", (BACKUP_LABEL,))
        start_lsn = cur.fetchone()[0]
        print(f"  -> 备份起始 LSN: {start_lsn}")

        print("[2/5] 复制数据目录...")
        rsync_cmd = f"rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' {DATA_DIR}/ {BACKUP_DIR}/"
        run_cmd(rsync_cmd)
        print(f"  -> 数据已复制到: {BACKUP_DIR}")

        print("[3/5] 复制 WAL 日志...")
        wal_src = os.path.join(DATA_DIR, "pg_wal")
        wal_dst = os.path.join(BACKUP_DIR, "pg_wal")
        os.makedirs(wal_dst, exist_ok=True)
        wal_rsync = f"rsync -av {wal_src}/ {wal_dst}/"
        run_cmd(wal_rsync)
        print(f"  -> WAL 已复制到: {wal_dst}")

        print("[4/5] 退出备份模式...")
        cur.execute("SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()")
        stop_lsn, labelfile, spcmapfile = cur.fetchone()
        print(f"  -> 备份结束 LSN: {stop_lsn}")

        if labelfile:
            label_path = os.path.join(BACKUP_DIR, "backup_label")
            with open(label_path, "w") as f:
                f.write(labelfile)
            print(f"  -> backup_label 已保存到: {label_path}")

        if spcmapfile:
            spc_path = os.path.join(BACKUP_DIR, "tablespace_map")
            with open(spc_path, "w") as f:
                f.write(spcmapfile)
            print(f"  -> tablespace_map 已保存到: {spc_path}")

        print("[5/5] 备份完成!")
        print(f"  -> 备份位置: {BACKUP_DIR}")
        print("  -> 恢复时需结合 WAL 归档进行 PITR")

    except Exception as e:
        print(f"备份失败: {e}")
        # 尝试清理
        try:
            cur.execute("SELECT pg_backup_stop()")
        except:
            pass
        raise
    finally:
        cur.close()
        conn.close()

if __name__ == "__main__":
    main()
技术点总结
  • 使用 psycopg2 的自动提交模式(autocommit=True)以避免显式事务干扰
  • 通过 subprocess.run 执行 rsync 并捕获异常
  • 使用 os.makedirs 递归创建目录
  • 在异常处理中尝试调用 pg_backup_stop() 清理残留状态

Java 语言示例:基于 JDBC 实现物理备份

本示例使用 Java 和标准 JDBC 驱动(org.postgresql)调用备份 API,并通过 ProcessBuilder 执行 rsync。

运行说明
  1. 创建 Maven 项目并添加依赖(或直接下载 PostgreSQL JDBC 驱动):
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.3</version>
    </dependency>
    
  2. 修改代码中的数据库连接 URL、用户名、密码以及备份目录和数据目录。
  3. 确保系统已安装 rsync。
  4. 编译并运行:
    javac Backup.java
    java Backup
    
完整代码
import java.io.*;
import java.nio.file.*;
import java.sql.*;

public class Backup {
    private static final String BACKUP_LABEL = "java_backup_20260814";
    private static final String BACKUP_DIR = "/tmp/pg_backup_java";
    private static final String DATA_DIR = "/var/lib/postgresql/18/main"; // 根据实际修改
    private static final String DB_URL = "jdbc:postgresql://127.0.0.1:5432/postgres";
    private static final String DB_USER = "postgres";
    private static final String DB_PASS = "your_password";

    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) {
            conn.setAutoCommit(true);
            System.out.println("[1/5] 进入备份模式...");
            String startLsn;
            try (PreparedStatement stmt = conn.prepareStatement("SELECT pg_backup_start(?, false)")) {
                stmt.setString(1, BACKUP_LABEL);
                ResultSet rs = stmt.executeQuery();
                rs.next();
                startLsn = rs.getString(1);
            }
            System.out.println("  -> 备份起始 LSN: " + startLsn);

            System.out.println("[2/5] 复制数据目录...");
            String rsyncCmd = String.format("rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' %s/ %s/",
                    DATA_DIR, BACKUP_DIR);
            runCommand(rsyncCmd);
            System.out.println("  -> 数据已复制到: " + BACKUP_DIR);

            System.out.println("[3/5] 复制 WAL 日志...");
            String walSrc = DATA_DIR + File.separator + "pg_wal";
            String walDst = BACKUP_DIR + File.separator + "pg_wal";
            Files.createDirectories(Paths.get(walDst));
            String walRsync = String.format("rsync -av %s/ %s/", walSrc, walDst);
            runCommand(walRsync);
            System.out.println("  -> WAL 已复制到: " + walDst);

            System.out.println("[4/5] 退出备份模式...");
            String stopLsn, labelfile, spcmapfile;
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()")) {
                rs.next();
                stopLsn = rs.getString("lsn");
                labelfile = rs.getString("labelfile");
                spcmapfile = rs.getString("spcmapfile");
            }
            System.out.println("  -> 备份结束 LSN: " + stopLsn);

            if (labelfile != null && !labelfile.isEmpty()) {
                String labelPath = BACKUP_DIR + File.separator + "backup_label";
                Files.write(Paths.get(labelPath), labelfile.getBytes());
                System.out.println("  -> backup_label 已保存到: " + labelPath);
            }
            if (spcmapfile != null && !spcmapfile.isEmpty()) {
                String spcPath = BACKUP_DIR + File.separator + "tablespace_map";
                Files.write(Paths.get(spcPath), spcmapfile.getBytes());
                System.out.println("  -> tablespace_map 已保存到: " + spcPath);
            }

            System.out.println("[5/5] 备份完成!");
            System.out.println("  -> 备份位置: " + BACKUP_DIR);
            System.out.println("  -> 恢复时需结合 WAL 归档进行 PITR");

        } catch (Exception e) {
            System.err.println("备份失败: " + e.getMessage());
            // 尝试清理
            try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) {
                try (Statement stmt = conn.createStatement()) {
                    stmt.execute("SELECT pg_backup_stop()");
                }
            } catch (Exception ignored) {}
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static void runCommand(String cmd) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder("bash", "-c", cmd);
        pb.redirectErrorStream(true);
        Process p = pb.start();
        int exitCode = p.waitFor();
        if (exitCode != 0) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.err.println(line);
                }
            }
            throw new IOException("命令执行失败,退出码: " + exitCode);
        }
    }
}
技术点总结
  • 使用 JDBC 的 PreparedStatement 执行 pg_backup_start,使用 Statement 执行 pg_backup_stop
  • 通过 ProcessBuilder 调用 rsync,捕获输出和错误流
  • 使用 Files 和 Paths 进行文件和目录操作
  • 在 finally 或异常处理中尝试调用 pg_backup_stop() 释放备份模式

三种语言实现对比

语言驱动外部命令执行文件操作错误清理
Gopgx/v5os/execos.WriteFile, os.MkdirAlldefer 关闭连接,异常时调用 pg_backup_stop
Pythonpsycopg2subprocess.runopen, os.makedirstry/finally 关闭连接,异常时尝试清理
JavaJDBCProcessBuilderFiles.write, Files.createDirectoriescatch 中重试 pg_backup_stop

三种实现均遵循 PostgreSQL 15+ 的非排他备份模式,正确处理 pg_backup_stop 返回的 labelfile 和 spcmapfile,并包含完整的错误处理和资源释放逻辑。

阶段性总结

本附录提供了 Go、Python、Java 三种语言的物理备份示例,均基于 Node.js 原版示例的流程进行移植。核心步骤完全一致:调用 pg_backup_start → 复制数据目录和 WAL → 调用 pg_backup_stop → 保存备份标签文件。各语言实现均使用其生态中最主流的 PostgreSQL 驱动,并通过系统命令调用 rsync 完成文件复制,确保了备份的原子性和一致性。开发者可根据自身技术栈直接选用对应版本,并调整数据目录、备份目录及数据库连接参数即可投入生产使用。

项目难点与解决方案

核心难点

排他备份模式的历史遗留问题:在 PostgreSQL 15 之前,pg_start_backup 默认使用排他模式,许多存量备份脚本未显式指定非排他模式。升级到 PostgreSQL 15 后,这些脚本将直接报错或产生不可预期的行为。

解决方案

  • 脚本迁移:将所有 pg_start_backup(label) 调用改为 pg_backup_start(label, false),并移除对 pg_stop_backup() 返回值中 backup_label 文件的依赖——改为捕获函数返回的 labelfile 字段并手动保存。
  • 工具升级:使用 pg_basebackup 或 pgBackRest 等已适配 PostgreSQL 15 的工具替代手工脚本。

广度

涉及 PostgreSQL 内核函数、备份协议、WAL 机制、文件系统复制、第三方工具生态等多个技术层面。

深度

深入剖析了 backup_label 文件导致崩溃恢复异常的根本原因,以及 PostgreSQL 15 移除排他模式的设计考量。

复杂度

排他模式的移除涉及备份工具链的全面适配,从底层 API 到上层备份脚本均需调整。同时,PG 生态中多种备份工具各有优劣,选型需综合考虑数据规模、基础设施、运维能力等因素。

官方文档

参考链接

总结

PostgreSQL 的物理备份体系从底层的 pg_backup_start / pg_backup_stop 函数到上层的 pg_basebackup 工具,再到功能丰富的第三方备份方案(pgBackRest、Barman、pg_probackup),形成了完整的分层架构。

排他备份模式在 PostgreSQL 15 中被正式移除,统一使用非排他模式,消除了 backup_label 文件残留导致崩溃恢复异常的重大隐患。pg_basebackup 在 PostgreSQL 17 中新增了块级增量备份能力,进一步缩小了官方工具与第三方工具的功能差距。

在生产环境选型时,pgBackRest 凭借其功能全面性和生态成熟度成为通用首选,但需注意其仅支持 8KB 数据块大小的限制。

PG备份管理工具pg_probackup 文章目录一、pg_probackup概述二、安装部署2.1 源码安装2.2 rpm包安装部署三、常用命令四、工具使用4.1 初始化配置4.2 数据备份(全备+增备)4.3 数据恢复 一、pg_probackup概述 pg_probackup 是一款免费的postgres数据库集群备份工具,与其他备份工具相比,它主要有如下一些优势: 提供增量备份,增量备份一定程度上可以节省磁盘空间的使用并且减少备份时间消耗 可通过全量备份+增量备份进行增量恢复 无需通过实际的数据恢复操作验证备份文件是否有效 Verific 阅读详情

相关推荐

PostgreSQL开发与实战(5)备份管理pg_probackup

提供增量备份,增量备份一定程度上可以节省磁盘空间的使用并且减少备份时间消耗可通过全量备份+增量备份进行增量恢复无需通过实际的数据恢复操作验证备份文件是否有效可以通过设置恢复时间以及备份最大文件数来进行备份文件以及WAL归档日志的保留策略对于backup、restore、merge、delete、validate、checkdb操作都可开启并发线程执行提供压缩备份以节省磁盘空间可对远程实例进行备份恢复可从standby实例进行备份。

1658

PostgreSQL备份恢复管理器pg_probackup

PostgreSQL备份恢复管理

多米爸比的专栏 628

Ubuntu 18.04 部署 Mahara 19.04:PostgreSQL + Apache + PHP 7.2 全流程详解

ePortfolio(电子档案)系统是教育数字化转型的核心基础设施,其底层依赖强一致的数据模型与可扩展的Web服务架构。Mahara作为成熟开源ePortfolio平台,深度绑定PostgreSQL的递归CTE与JSONB能力,以支撑能力框架层级查询和动态字段存储;同时依赖Apache的.htaccess重写机制实现语义化路由与多租户隔离。PHP 7.2是其兼容性黄金版本,兼顾密码哈希算法支持与向后兼容性。该技术栈组合不仅满足LMS(学习管理系统)对高并发编辑、文件上传、LDAP集成等工程需求,更契合高校数

weixin_33924220的博客 364

PG数据的备份

pg10的数据库备份

hongjunaiyu的博客 7558

Ubuntu 18.04 部署 Zabbix 6.0:源码编译+PostgreSQL+PSK 全栈安全实践

Zabbix 是一款企业级开源监控系统,其核心能力在于统一采集、存储、告警与可视化。实现安全可靠的远程服务器监控,需深入理解其通信加密原理(如 TLS 1.3 握手、PSK 预共享密钥认证)与数据持久化机制(如 PostgreSQL 的 MVCC 并发控制与 TOAST 压缩)。技术价值体现在满足等保三级、金融审计等强合规场景下的身份鉴别、传输加密与操作留痕要求。典型应用场景包括老旧 Ubuntu 18.04 物理服务器集群的监控加固、信创环境混合架构下的 Agent 统一纳管,以及高并发时序数据写入下的稳

weixin_34389926的博客 540

Docker部署PostgreSQL生产实践:版本控制、数据持久化与安全配置

PostgreSQL作为主流关系型数据库,其本地开发与生产环境一致性长期困扰开发者。Docker通过容器化封装,将数据库从操作系统级依赖降维为应用级依赖,实现版本可锁定、配置可复现、环境可销毁。核心原理在于利用命名卷(named volume)解耦宿主机权限模型,结合语义化镜像标签(如postgres:17.4)与SHA256摘要校验保障不可变性,并通过127.0.0.1端口绑定和自定义网络构建防御纵深。该方案显著提升微服务本地联调效率、CI/CD流水线稳定性及跨团队协作可信度,广泛应用于教育SaaS、金融

weixin_34124651的博客 445

基于 ADR-0008 的 Databasus 物理备份架构:全面采用 PostgreSQL 17 原生增量备份与强制 WAL Summary

本指南剖析 Databasus 的架构决策记录 ADR-0008([adr/0008-why-pg17-native-backups-with-mandatory-wal-summary.md](https://link.gitcode.com/i/12c46c28a908e90524d7710cbc7bb147)),说明该项目为何选择 PostgreSQL 17 的原生物理备份栈(`pg_bas

gitblog_00926的博客 771

Ubuntu 18.04 上部署 TimescaleDB 的兼容性与实战方案

TimescaleDB 是基于 PostgreSQL 构建的高性能时序数据库扩展,其核心能力依赖 PostgreSQL 12+ 的分区架构与 ABI 兼容性。在已停止维护的 Ubuntu 18.04 系统上,因默认仅提供 PostgreSQL 10、缺少官方 APT 支持、系统级共享内存限制及构建工具链版本冲突,直接安装极易失败。技术价值在于将传统关系型数据库升级为支持高频写入、自动分片(hypertable)、时间窗口聚合(time_bucket)和数据保留策略的时序平台。典型应用场景包括工业物联网(II

weixin_34161029的博客 497

Databasus PostgreSQL 物理备份的故障转移自动重锚定:从时间线检测到即时替换 FULL 的设计

本文基于 Databasus 已归档的变更设计文档 [automatic-physical-reanchor-after-failover](https://link.gitcode.com/i/6345fc46a42c74043689819a56cc85e8),深入讲解当 PostgreSQL 主库发生 promotion(故障转移)后,物理备份如何检测时间线切换、自动调度一次即时替换 FULL

gitblog_00568的博客 639

Ubuntu 18.04 部署 Discourse 的 Docker 官方方案详解

Discourse 是一款基于 Ruby on Rails 构建的现代化开源论坛系统,强调实时协作、邮件集成与高可用架构。其运行依赖严格的组件协同(如 PostgreSQL、Redis、Sidekiq)和容器化隔离环境,因此官方仅支持通过 Docker Compose 编排部署,彻底弃用传统 LAMP/LEMP 手动安装方式。在 Ubuntu 18.04 这类已进入 ESM 阶段的长期支持系统上,部署成功的关键在于内核参数调优(如 cgroup_enable=memory)、Swap 空间配置、Docker

weixin_34184561的博客 398

Apache AGE:PostgreSQL原生图数据库的生产级实践

知识图谱已从概念验证迈向高可用、可审计、与现有数据栈融合的生产落地阶段。图数据库的核心价值在于高效表达实体关联、支撑路径推理与子图匹配,其技术底座需兼顾事务一致性、运维成熟度与权限体系兼容性。Apache AGE作为PostgreSQL扩展,将Cypher查询深度集成至关系引擎,复用MVCC、WAL、索引与RBAC等基础设施,从根本上解决数据孤岛与运维割裂问题。相比独立图库,它在百亿边规模下仍保持低延迟、高内存效率与强一致读取能力,特别适合已有PG技术栈的企业快速构建可交付的知识图谱系统。

weixin_30564785的博客 1219

Databasus PostgreSQL 物理备份的故障切换时间线检测与自动重新锚定实现

本篇围绕 Databasus 中 PostgreSQL 物理备份在源集群 promotion(提升为新的时间线)后如何保持可恢复性这一主题展开:讲解增量备份的根 FULL 时间线校验、流失败后的集群身份复检、调度器自动触发一次性替换 FULL、FULL 期间 WAL 孤儿清理的串行化,以及同一时间线上跨新 FULL 的 PITR 可达性。读完你可以理解这套"检测到时间线切换即自动重新锚定(re-a

gitblog_06829的博客 111

backup-manifest:用于为整个文件目录递归生成哈希清单的工具。 用于备份验证

备份清单 用于为整个文件目录递归生成哈希清单的工具。 用于备份验证。 目前提供两个工具 给定一个目录,创建一个哈希和文件大小,将清单文件引用为 JSON 给定两个目录、两个清单文件或两者的任意组合,找出差异(如果有) 安装 作为独立工具安装 将仓库克隆到所需目录 > git clone https://github.com/dwighthouse/backup-manifest.git 导航到 BackupManifest 目录 > cd backup-manifest/ 使用 ,安装依赖项 > npm install 作为依赖项安装 使用NPM下载安装到node_modules目录 > npm install backup-manifest 生成清单 使用 -m 选项和目录路径运行索引文件 > node index.js -m DIRECTORY_TO_ANALYZE 例如

Databasus 的 PostgreSQL 双向 TLS 测试环境:在备份/恢复测试中复现 Cloud SQL 客户端证书要求

本篇介绍 Databasus 仓库中为 PostgreSQL 逻辑备份/恢复测试专门构建的 mTLS(mutual TLS,双向 TLS)测试夹具:一套自签 CA、服务端/客户端证书与强制 `clientcert=verify-ca` 的 `pg_hba.conf`,用于在容器内复现"启用了 Require trusted client certificates 的 Cloud SQL 实例"。读

gitblog_00322的博客 760

农业杂草检测——棉花杂草数据集

然后将本层13×13的特征图谱经过×2上采样扩充到26×26,并与上层中26×26的特征图谱拼接,得到新的26×26的特征图谱,在该特征尺度上进行第二次预测,此时得到的特征图谱是相对输入图像16倍下采样,具有中尺度感受野,适合检测中等尺度对象;通过目标检测,能够及时清除杂草,减少病虫害的滋生和传播,保障棉花的健康生长,并有助于降低生产成本。对于地膜覆盖的棉田,由于膜内耕作层土温高、底墒好,杂草出苗早而集中,一般覆膜后5 - 7天杂草陆续出土,在墒情好的情况下,15天左右即形成出土高峰。

qq_38220914的博客 1247

数字图像处理基础实验(二):直方图均衡化

一、实验内容及原理 1、计算灰度图像的归一化直方图 具体内容:利用 OpenCV 对图像像素进行操作,计算归一化直方图.并在 窗口中以图形的方式显示出来 步骤: (1)初始化一个256大小的数组,存放图像中每级灰度值的像素点的个数 (2)遍历整张图像 (3)归一化数组 2、灰度图像直方图均衡处理 具体内容:通过计算归一化直方图,设计算法实现直方图均衡化处理。 步骤: (1)读取该像素点的灰度值 (...

lyd1995的博客 4528

Ubuntu 运维系列 | 第 18 天:数据库运维——MySQL 与 PostgreSQL 部署、优化和备份

本文系统讲解Ubuntu环境下MySQL与PostgreSQL的部署、优化与备份实战。从安装配置、访问控制、性能调优(内存、慢查询、索引)到定时备份脚本设计,涵盖生产环境核心运维技能。强调“部署规范、优化有据、备份可靠”的原则,帮助运维人员快速搭建稳定数据库体系,为后续高可用架构奠定基础。

人走茶良的博客 240

高分六号GF-6 WFV数据处理全流程:从原始数据解压到经纬度投影转换

本文详细解析了高分六号GF-6 WFV遥感数据的完整处理流程。从原始数据解压与分块结构理解入手,指导如何安装ENVI国产卫星支持工具进行自动拼接,并重点阐述了利用RPC模型进行正射校正以消除几何畸变,以及将UTM投影转换为地理经纬度坐标的关键步骤。文章旨在帮助用户高效地将原始卫星数据转化为可直接用于农业监测、生态环境分析等领域的标准地理空间图像。

weixin_29047919的博客 196

为什么我说PostgreSQL是Agent Database的最佳选择

本文将深度解析 PostgreSQL 胜出的 5 个底层逻辑,以及在实际落地中如何避开那些"看起来很美"的选型陷阱。

Database_Cool_的博客 640

保姆级教程:用PyBullet在Windows/Mac上快速搭建你的第一个机器人仿真环境(附避坑指南)

本教程详细介绍了如何使用PyBullet在Windows和Mac上快速搭建机器人仿真环境,包括Python环境配置、依赖安装避坑指南、首个机械臂仿真实现及性能优化技巧。通过分步指导和实战代码,帮助初学者轻松入门机器人仿真技术。

weixin_42526087的博客 412

AOI检测总误判?五大常见问题与实战解决方案

通过对图像质量、检测程序、外界干扰、算法优化以及设备维护和校准的全面把控,企业可以有效降低误判率,提升AOI检测的准确性和可靠性,为工业生产提供更有力的质量保障。表现描述:在AOI检测时,元器件字符因印刷颜色深浅、模糊或受到灰尘干扰等差异,再加上元件库参数未根据实际生产优化,会使系统误将合格元件判定为不合格,严重影响生产效益和产品质量。优化元件库:着重检测关键差异字符,如型号编码等关键信息,剔除不必要字符的检测项,减少干扰,专注于关键识别的准确性。检测时,根据物料信息动态更新检测参数,使系统适应物料变化。

ALeader_AOI的博客 3500

终极PostgreSQL复制槽迁移指南:使用golang-migrate/migrate实现无缝数据同步

PostgreSQL复制槽是实现高效数据同步的关键组件,而golang-migrate/migrate作为一款强大的Go语言数据迁移工具,能够帮助开发者轻松管理PostgreSQL数据库架构变更。本文将详细介绍如何将PostgreSQL复制槽迁移与golang-migrate/migrate深度集成,通过简单步骤实现安全可靠的数据同步。 ## 为什么选择golang-migrate/migrat

gitblog_00237的博客 509

YOLOv5改进 | 多尺度特征提取 | 结合多样分支块及融合的高级设计(CVPR2021)

YOLOv5改进 | 多尺度特征提取 | 结合多样分支块及融合的高级设计(CVPR2021)

qq_44224801的博客 2068

Databasus PostgreSQL SSL 测试夹具实战:自签证书、强制 TLS 的 pg_hba 与 Docker 镜像构建

本文为 Databasus(PostgreSQL 备份工具,支持 PITR 与恢复验证)中 `backend/internal/features/tests/logical/postgresql/testdata/ssl` 测试夹具的深度解析。通过这篇指南,你将理解如何用自签服务器证书、只允许 TLS 连接的 `pg_hba.conf` 和 Testcontainers 动态构建的 Docker

gitblog_00694的博客 2939

LLaMA-Factory微调教程2:命令行sft微调

本文介绍了如何使用llama-factory项目在本地运行Qwen2-7B大语言模型进行对话。首先通过llamafactory-cli env查看环境配置,然后使用git lfs下载Qwen2-7B模型。创建配置文件qwen2_7b_instruct.yaml,修改模型路径和模板参数。最后通过llamafactory-cli chat命令启动对话服务,加载模型配置文件和tokenizer,完成环境初始化后即可进行交互式对话。

Cachel Wood的博客 8815

S32DS S32K312 LPSPI + DMA配置异步SPI

配置DMA->Specific Configuration->Dma Logic Channel->Configuration->Global。SpiPhyUnitMapping:将该配置Mapping到某一路Spi,此工程配置为Spi1,遂选择LPSPI_1。配置的该路SPI的片选ID,此处ID对应配置Pin脚时lpspi_pcs后的下表(本工程此处为PCS3)配置该路SPI对应的SPI硬件单元ID,本工程配置为Lpspi1,因此配置为CSIB1。首先配置SPI的Pin脚,此处使用的是LPSPI_1。

希望我的博客,能帮上你解决学习中工作中所遇到的问题 2365
上一篇: PostgreSQL笔记17:逻辑备份工具链与性能优化实践
下一篇: PostgreSQL笔记19:进程与内存架构精解
Wang's Blog
博客等级 码龄12年 7438粉丝 2421原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值