一、先说一个让人头疼的场景

你是不是也遇到过这种事:公司里跑着一个老系统,数据库就一台,所有的业务模块全都连着同一个库。平时没事还好,一到月底对账、或者做个大促活动,数据库压力一大,备份的时候业务就卡顿,甚至偶尔报错。更要命的是,有一次半夜误操作,把线上表给删了,结果运维翻备份一看——昨天晚上的备份文件是坏的,根本恢复不了。那一刻,估计想死的心都有了。

这其实就是单体架构下共享数据库最常见的痛点:数据全在一个篮子里,备份如果做得不靠谱,那整个公司的数据安全就悬在一条钢丝绳上。所以今天咱们不聊那些高大上的分布式架构,就踏踏实实聊聊,在单体架构里,到底该怎么设计一套靠谱的备份策略,保障那个共享数据库里的数据不出事。

二、先搞清楚共享数据库的“共享”到底意味着什么

2.1 多个业务模块共用一套库

单体应用里,用户模块、订单模块、商品模块,甚至日志模块,都写在同一个项目里,连的也是同一个数据库。这种架构的好处是开发简单、部署方便,但坏处也很明显:一旦数据库挂了,所有业务全挂。备份也一样,因为大家共用一个库,所以备份的范围必须覆盖全库,不能只备份某几张表。

2.2 备份的三大目标:恢复时间、丢失量、完整性

咱们做备份,最终是为了在出事儿的时候能恢复。所以有三个指标必须心里有数:

  • RTO(恢复时间目标):从出故障到系统恢复,允许花多久?比如目标是1小时,那备份和恢复方案就得能在一小时内把库拉起来。
  • RPO(恢复点目标):最多能容忍丢多少数据?比如目标是5分钟,那就意味着备份频率至少要5分钟一次。
  • 完整性:备份文件本身是不是好的?能不能在关键时刻还原成功?

在单体架构里,因为只有一个库,这三个指标反而好做。但很多人恰恰忽略了完整性,导致备份文件坏了都不知道,这比不备份还可怕。

三、单体架构备份的几种主流策略

3.1 物理备份:简单粗暴的“文件复制”

物理备份就是直接把数据库底下的数据文件、日志文件复制一份。最快,也最完整。适合数据库比较大的场景。但缺点是只能恢复到某个时间点,做增量备份的时候需要点技术。

拿 MySQL 来说,最常用的物理备份工具是 xtrabackup,它能在不锁表的情况下完成备份。

# 技术栈:Shell + Xtrabackup

# 全量备份,把整个MySQL数据目录备份到/backup/full/目录
xtrabackup --backup \
           --user=backup_user \
           --password=backup_pass \
           --host=127.0.0.1 \
           --target-dir=/backup/full/

# 执行完成后,会在/backup/full/下生成一堆文件
# 其中xtrabackup_checkpoints文件记录了备份的LSN号,这是增量备份的关键

# 做完全量备份后,第二天做一个增量备份
xtrabackup --backup \
           --user=backup_user \
           --password=backup_pass \
           --host=127.0.0.1 \
           --target-dir=/backup/inc1/ \
           --incremental-basedir=/backup/full/

# 恢复的时候,先把全量备份恢复,再叠加增量备份
xtrabackup --prepare \
           --apply-log-only \
           --target-dir=/backup/full/

xtrabackup --prepare \
           --apply-log-only \
           --target-dir=/backup/full/ \
           --incremental-dir=/backup/inc1/

注意这段脚本里的注释,全量加增量,恢复时顺序不能乱。物理备份的好处是恢复速度快,坏处是跨平台能力差,换一台不同版本的机器可能就恢复不了。

3.2 逻辑备份:更安全,但恢复比较慢

逻辑备份是用 SQL 语句把数据导出来,比如 MySQL 的 mysqldump。这种备份文件是可读的文本,跨版本、跨平台都能恢复。缺点是恢复速度慢,特别是几百万行的表,导出来大,再导进去更慢。

# 技术栈:Shell + mysqldump

# 全量逻辑备份,导出某个共享数据库的全部数据
# 注意:--single-transaction 适合InnoDB引擎,保证一致性视图,不锁表
mysqldump -h 127.0.0.1 -u backup_user -p'密码' \
          --single-transaction \
          --set-gtid-purged=OFF \
          --routines \
          --triggers \
          --databases shared_db > /backup/shared_db_$(date +%F).sql

# 压缩备份文件,减少磁盘占用
gzip /backup/shared_db_$(date +%F).sql

# 恢复的时候先解压,再用mysql命令导入
gunzip /backup/shared_db_$(date +%F).sql.gz
mysql -h 127.0.0.1 -u root -p'密码' < /backup/shared_db_2025-01-01.sql

逻辑备份比较适合数据量不大的场景,比如几百 GB 以内。如果数据库已经几个 TB,那用逻辑备份恢复一次可能要半天,RTO 根本没法达标。

3.3 Binlog 增量备份:补上时间缝隙

不管是物理还是逻辑备份,全量备份的频率不可能太高,一般一天一次。那从全量结束到下一次全量开始之间的数据怎么办?这就需要靠数据库的 binlog(二进制日志)来补。

binlog 会记录所有增删改的操作,只要保留 binlog,就能恢复到任意一个时间点。具体做法是:每天凌晨做全量备份,然后实时或定时把 binlog 归档到另一台机器上。

# 技术栈:Shell + mysqlbinlog

# 先把binlog文件从数据库服务器拉到备份服务器
# 假设线上数据库的binlog路径是/var/lib/mysql/mysql-bin.000231
mkdir -p /backup/binlogs/
cp /var/lib/mysql/mysql-bin.000231 /backup/binlogs/

# 用mysqlbinlog把指定时间段的binlog导出成SQL语句
mysqlbinlog --start-datetime="2025-01-01 00:00:00" \
            --stop-datetime="2025-01-01 03:00:00" \
            /backup/binlogs/mysql-bin.000231 > /backup/recover_20250101.sql

# 把导出的SQL语句恢复到数据库(注意:恢复前最好先恢复全量备份)
mysql -h 127.0.0.1 -u root -p'密码' < /backup/recover_20250101.sql

这样,如果今天上午 10 点数据库崩了,咱们可以把昨晚的全量备份恢复,再把今天 0 点到 10 点之间的 binlog 导出来重新执行一遍,数据就能恢复到崩溃前一秒的状态。

四、备份策略的核心:场景决定方案

4.1 场景一:追求最小数据丢失(金融、订单类系统)

这类业务不允许丢数据,哪怕丢一条订单也不行。那策略得这么做:

  • 每天凌晨 1 点做全量物理备份。
  • 开启 binlog,并且把 binlog 实时同步到另一台机器(比如用 MySQL 主从复制中的延迟从库,或者直接用 binlog 归档工具)。
  • 每 5 分钟做一次增量备份(如果硬件条件允许,也可以直接用 binlog 做时间点恢复)。

这里有个细节:为了保险,建议做“延迟从库”,也就是从库故意延迟同步 1 小时。万一主库被误删,延迟从库上还有一小时前的数据,可以立刻从从库找回。

4.2 场景二:追求恢复速度快(RTO 很短)

比如后台管理系统,不能停机超过半小时。那方案就偏向物理备份加冷备机:

  • 每天做一次全量物理备份,同时定期把备份文件复制到一台空闲的备用服务器上。
  • 备用服务器上解压好数据文件,一旦主库挂了,直接把备用服务器的数据目录切换过去,几分钟就能恢复。
  • 如果不想浪费一台服务器,也可以使用数据库自带的克隆插件,比如 MySQL 的 clone 插件,快速重置一份数据到备用实例。
-- 技术栈:MySQL Clone 插件

-- 在备用实例上执行克隆操作,从主实例拉取数据
-- 注意:需要在备用机上先安装clone插件
INSTALL PLUGIN clone SONAME 'mysql_clone.so';

-- 设置克隆工作目录,必须有写权限
SET GLOBAL clone_valid_donor_list = '主库IP:3306';

-- 开始克隆,会直接把主库的数据文件复制到备用机
-- 这一步是全量物理备份+自动恢复的一体化操作
CLONE INSTANCE FROM 'backup_user'@'主库IP'
    IDENTIFIED BY '备份用户密码'
    DATA DIRECTORY = '/data/clone_dir';

-- 克隆完成后,备用机就有了和主库一模一样的数据
-- 之后可以手动把服务切过去,或者继续做增量同步

克隆插件很适合做“物理冷备机”,因为它不会影响主库性能,而且比传统搬文件更快。

4.3 场景三:预算有限,合租服务器

很多小公司就一台云主机,数据库和代码都在上面,没那么多钱搞备用机。那也得有备份意识,至少做到:

  • 每天凌晨用 mysqldump 全量备份,然后压缩,传到对象存储(比如阿里云 OSS、腾讯云 COS)上。
  • 每天备份的对象存储保留 7 天,每周的保留一个月。
  • 开启 binlog,并且每天把 binlog 也传到对象存储留存。
# 技术栈:Shell + Cron + 云存储工具

#!/bin/bash
# 这是Cron任务,每天凌晨2点执行
# 先做全量备份
mysqldump -h 127.0.0.1 -u backup_user -p'密码' \
          --single-transaction \
          --all-databases > /tmp/all_db_$(date +%F).sql

# 压缩
gzip /tmp/all_db_$(date +%F).sql

# 上传到对象存储
# 使用腾讯云COS的cli工具,也可以用阿里云OSS的ossutil
coscmd upload /tmp/all_db_$(date +%F).sql.gz /backup/db/

# 删除本地7天前的备份,防止磁盘占满
find /tmp/all_db_*.sql.gz -mtime +7 -delete

# 同时把当天的binlog也上传
# 注意:binlog一般比较多,可以只保留当天的
for log_file in /var/lib/mysql/mysql-bin.*; do
    coscmd upload "$log_file" /backup/binlog/$(date +%F)/
done

# 上传完成后删除本地的binlog旧文件,保留一天即可
find /var/lib/mysql/mysql-bin.* -mtime +1 -delete

这种方案成本最低,但恢复时间会长一些,可能需要下载备份文件再导入,适合非核心业务。

五、备份验证:千万别让备份成为“心理安慰”

很多团队做了备份,但从来没验证过。直到出事故才发现备份文件损坏,或者恢复流程根本不顺。所以备份策略里必须包含“验证”这个环节。

5.1 定期做恢复演练

每个月至少做一次恢复演练。找一台临时的数据库实例,把最新备份导进去,然后启动业务,跑几个关键查询,看看数据是否完整。

# 技术栈:Python + MySQLdb
# 这是一个简单的恢复演练脚本,核心是自动化的验证流程

import subprocess
import MySQLdb
import sys

# 第一步:下载最新备份文件(假设备份在本地路径)
backup_file = "/backup/shared_db_latest.sql.gz"

# 第二步:解压备份文件
print("正在解压备份文件...")
subprocess.run(["gzip", "-d", "-k", backup_file], check=True)

# 第三步:用mysql命令把备份导入到临时恢复实例
# 假设我们在本机启动了一个临时MySQL实例,端口3307
print("正在导入备份到临时实例...")
import_cmd = "mysql -h 127.0.0.1 -P 3307 -u root -p'临时密码' < {}.sql".format(backup_file)
subprocess.run(import_cmd, shell=True, check=True)

# 第四步:连接临时实例,做几个关键查询
try:
    conn = MySQLdb.connect(host="127.0.0.1", port=3307,
                           user="root", passwd="临时密码",
                           db="shared_db")
    cursor = conn.cursor()

    # 检查订单表有没有数据
    cursor.execute("SELECT COUNT(*) FROM orders")
    order_count = cursor.fetchone()[0]
    print("订单表数据条数:", order_count)

    # 检查用户表
    cursor.execute("SELECT COUNT(*) FROM users")
    user_count = cursor.fetchone()[0]
    print("用户表数据条数:", user_count)

    # 如果两个表都有数据,说明备份基本可用
    if order_count > 0 and user_count > 0:
        print("恢复演练成功!备份文件可用。")
    else:
        print("警告:数据表为空,备份可能有问题!")
        sys.exit(1)

except MySQLdb.Error as e:
    print("连接临时数据库失败:", e)
    sys.exit(1)
finally:
    conn.close()

这个脚本只是示例,生产环境里可以加上更复杂的校验逻辑,比如比对关键业务表的行数,或者检查最大ID是否合理等。

5.2 监控备份任务的状态

备份任务本身也可能失败,所以要给备份加监控。可以用 shell 脚本在备份完成后判断 exit code,也可以把备份日志推送到监控系统(比如 Zabbix、Prometheus + Alertmanager)。

# 技术栈:Shell + 监控告警

#!/bin/bash
# 备份失败时,发送告警到企业微信机器人

backup_cmd="mysqldump --single-transaction ..."
if $backup_cmd; then
    echo "备份成功" >> /var/log/backup.log
    # 也可以把备份成功的消息发送到企业微信群里
    curl -X POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key' \
         -H 'Content-Type: application/json' \
         -d '{"msgtype": "text", "text": {"content": "数据库备份成功"}}'
else
    echo "备份失败" >> /var/log/backup.log
    curl -X POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key' \
         -H 'Content-Type: application/json' \
         -d '{"msgtype": "text", "text": {"content": "数据库备份失败,请检查!"}}'
fi

当然,更省事的方式是直接用 cors 定时任务,在任务执行后自动发送邮件或 webhook 通知。

六、共享数据库备份的几个特别注意事项

6.1 注意备份期间对线上业务的影响

物理备份虽然不锁表,但会占用大量磁盘 IO。共享数据库压力本来就大,再叠加备份任务,很容易拖垮业务。所以建议:

  • 备份时间段选在业务低峰期,比如凌晨 2-4 点。
  • 备份时设置 IO 限流,比如 xtrabackup 的 --throttle 参数。
  • 避免备份任务和应用热更新同时进行。
# 技术栈:Xtrabackup

# 限速备份,限制每秒读写512KB,减轻磁盘IO压力
xtrabackup --backup \
           --throttle=512 \
           --user=backup_user \
           --password=备份密码 \
           --target-dir=/backup/full/

6.2 注意备份文件的加密

共享数据库里很可能有用户手机号、地址等敏感信息。备份文件一旦泄露,就是重大安全事故。所以备份文件必须加密存储。

# 技术栈:Shell + OpenSSL

# 对备份文件进行AES256加密
openssl enc -aes-256-cbc -salt -pbkdf2 \
            -in /backup/shared_db.sql \
            -out /backup/shared_db.sql.enc \
            -k '你的加密密码'

# 解密的时候
openssl enc -d -aes-256-cbc -pbkdf2 \
            -in /backup/shared_db.sql.enc \
            -out /backup/shared_db_decrypted.sql \
            -k '你的加密密码'

当然,也可以直接用云厂商的对象存储加密功能,或者用 gpg 加密。

6.3 注意备份保留周期

备份不是越久越好。保留太多会把磁盘塞满,恢复时文件太多也麻烦。我的建议是:

  • 每日全量备份保留 7 天。
  • 每周全量备份保留 1 个月。
  • 每月全量备份保留 1 年。
  • binlog 至少保留 7 天,这样既能保证数据安全,又不会占用太多空间。

6.4 注意“共享数据库中多个业务表之间的数据一致性”

因为多个模块共用一个库,备份时一定要保证所有模块的表是同一个时间点的快照,不能出现用户表是今天的,订单表却还是昨天的。所以备份时用 --single-transaction 或 xtrabackup 的一致性快照功能很关键,这样才能保证全局一致性。

万一某些表用了非事务引擎(比如 MyISAM),那备份时就得锁表,会导致短暂的写阻塞。建议这种情况下尽早把表结构改成 InnoDB。

七、完整的备份策略示例:一套可以落地的方案

我把上面所有知识点汇总成一套具体方案,假设我们是一家使用 MySQL 的单体架构公司,共享数据库叫做 shop_db,数据量约 200GB,每天订单量约 1 万。

7.1 备份工具选择

  • 全量备份:使用 xtrabackup 做物理备份,速度快,对业务影响小。
  • 增量备份:每天晚上做一次增量,基于前一天的全量或增量。
  • binlog 归档:实时同步 binlog 到备份服务器。
  • 恢复验证:每周在测试环境自动恢复一次。

7.2 备份计划表

时间点        任务
------------------------------------------------
02:00         全量备份(周日)/增量备份(其他天)
03:00         上传备份到云存储
04:00         自动验证昨天备份的完整性
每分钟        同步binlog到备份服务器

7.3 对应脚本示例

# 技术栈:Shell + Xtrabackup + 云存储

#!/bin/bash
# 这是增量备份脚本,每周日做全量,其他天做增量

DAY_OF_WEEK=$(date +%u)  # 周日=7
BACKUP_BASE="/backup/db"
BINLOG_DIR="/backup/binlog"

# 如果今天是周日,做全量备份
if [ "$DAY_OF_WEEK" -eq 7 ]; then
    FULL_DIR="$BACKUP_BASE/full_$(date +%F)"
    xtrabackup --backup \
               --user=backup_user \
               --password=backup_pass \
               --target-dir="$FULL_DIR" \
               --throttle=512
    
    # 全量备份完成后,删除7天前的全量备份
    find "$BACKUP_BASE" -maxdepth 1 -type d -name "full_*" -mtime +7 -exec rm -rf {} \;
else
    # 非周日,做增量备份
    # 先找到最近一次全量备份目录
    LAST_FULL=$(ls -d "$BACKUP_BASE"/full_* | tail -n1)
    INC_DIR="$BACKUP_BASE/inc_$(date +%F_%H%M)"
    
    xtrabackup --backup \
               --user=backup_user \
               --password=backup_pass \
               --target-dir="$INC_DIR" \
               --incremental-basedir="$LAST_FULL" \
               --throttle=512
    
    # 清理3天前的增量备份
    find "$BACKUP_BASE" -maxdepth 1 -type d -name "inc_*" -mtime +3 -exec rm -rf {} \;
fi

# 上传今天的备份文件到云存储
coscmd upload "$BACKUP_BASE" /backup/remote/ --recursive

# 把binlog也上传,保留7天
coscmd upload "$BINLOG_DIR" /backup/remote/binlog/ --recursive

恢复的时候,按以下流程操作:

  1. 准备全量备份。
  2. 叠加所有增量备份(按时间顺序)。
  3. 复制回数据目录。
  4. 启动 MySQL。
  5. 再通过 binlog 恢复到指定时间点。
# 技术栈:Shell + Xtrabackup 恢复流程

# 第一步:准备全量备份,把日志重放
xtrabackup --prepare --apply-log-only --target-dir=/backup/full_2025-01-01

# 第二步:依次叠加增量备份
xtrabackup --prepare --apply-log-only \
           --target-dir=/backup/full_2025-01-01 \
           --incremental-dir=/backup/inc_2025-01-02

xtrabackup --prepare --apply-log-only \
           --target-dir=/backup/full_2025-01-01 \
           --incremental-dir=/backup/inc_2025-01-03

# 第三步:最后一步不再加apply-log-only,保证数据一致性
xtrabackup --prepare --target-dir=/backup/full_2025-01-01

# 第四步:把数据目录复制到MySQL的数据目录
rsync -avP /backup/full_2025-01-01/ /var/lib/mysql/

# 第五步:设置权限并启动MySQL
chown -R mysql:mysql /var/lib/mysql
systemctl start mysql

# 第六步:如果需要恢复到更精确的时间点,用binlog补数据
mysqlbinlog --start-datetime="2025-01-03 00:00:00" \
            --stop-datetime="2025-01-03 11:32:16" \
            /backup/binlog/mysql-bin.000245 | mysql -u root -p

八、备份策略的优缺点对比

方案 优点 缺点
物理备份 恢复快、对业务影响小 跨版本恢复困难、文件体积大
逻辑备份 文件可读、跨平台好恢复 恢复慢、备份慢
binlog 增量 能做到秒级恢复 需要额外管理、时间长了文件多
云存储归档 成本低、安全、异地容灾 需要额外下载、网络带宽要求
备机热备 RTO 极短 成本高、需要额外维护

没有完美的方案,只有适合你的方案。建议中小公司先做逻辑备份 + binlog,数据量大了再升级到物理备份 + 备机。

九、给不同基础的开发者一些建议

9.1 如果你是刚入行的开发者

先学会用 mysqldump 做全量备份,再学会用 binlog 恢复数据。不要一开始就搞复杂的物理备份。多看备份日志,了解备份耗时和数据量之间的关系。

9.2 如果你是运维或高级开发

掌握 xtrabackup 的使用,特别是增量备份和恢复的参数。把备份流程写成交互式脚本,加上监控和告警。定期做恢复演练,别把备份当成摆设。

9.3 如果你是架构师

考虑引入“备份中心化”思路:所有备份文件和 binlog 都统一传输到独立备份服务器,避免单点故障。甚至可以在备份服务器上再每天做一次快照,双重保险。

十、文章总结

单体架构下共享数据库的数据安全,看似简单,实际处处是坑。备份不是跑一次脚本就完了,而是要形成一套完整的策略:全量 + 增量 + binlog 三层配合,再加上定期验证和加密存储,才能真正做到“事发可恢复”。实际落地时,根据自己的业务量、预算、团队能力,选择合适的组合方案。记住,最贵的不是备份工具,而是数据丢失后的代价。哪怕再麻烦,也请务必把备份这件事做到位,毕竟数据没了,故事也就没了。