0.背景

XtraBackup 优势

  1. 在线热备
    :支持在不停止数据库的情况下进行 InnoDB 和 XtraDB 的热备份,适合高可用环境。
  2. 增量备份
    :支持增量备份,能够显著减少备份时间和存储空间需求。
  3. 流压缩
    :可以在备份过程中进行流压缩,减少传输数据量,提高传输效率。
  4. 主从同步
    :XtraBackup 可以更方便地创建和维护主从同步关系,简化数据库扩展。
  5. 低负载备份
    :在备份过程中对主数据库的负载相对较小,不会显著影响正常业务。

备份工具选择

  • xtrabackup
    :专门用于 InnoDB 和 XtraDB 表的备份。
  • innobackupex
    :一个脚本封装,能够同时处理 InnoDB 和 MyISAM 表,但在处理 MyISAM 时会加锁。

其他备份策略

  • 分库分表
    :对于超大数据量,考虑使用分库分表策略以优化管理和备份效率。

  • LVM 快照
    :利用 LVM 快照来快速获取数据库的瞬时备份,这样可以减少备份时间并降低对原库的压力。

一、备份方式 xtrabackup

1.安装

Software Downloads - Percona

mysql版本:mysql8.0.24

xtrabackup版本:8.0.25

版本对应关系

暂时没用

在线安装

yum install -y https://repo.percona.com/yum/percona-release-latest.noarch.rpm
yum install -y percona-xtrabackup-80
yum list | grep percona-xtrabackup

离线包下载地址

image-20240925110412143

服务器更新yum源

sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
sudo yum clean all
sudo yum makecache

依赖包安装

sudo yum install epel-release
sudo yum install -y zstd
zstd --version

离线安装,需要网络解决依赖

yum -y localinstall percona-xtrabackup-80-8.0.25-17.1.el7.x86_64.rpm

2.备份

2.1 工具说明

Xtrabackup 工具简介

  • xtrabackup
    :用于热备份 InnoDB 和 XtraDB 表的数据工具,不能备份其他类型的表或数据表结构。
  • innobackupex
    :将
    xtrabackup
    封装的 Perl 脚本,提供了备份 MyISAM 表的能力。

常用选项

  • --host
    :指定主机。

  • --user
    :指定用户名。

  • --password
    :指定密码。

  • --port
    :指定端口。

  • --databases
    :指定数据库。

  • --incremental
    :创建增量备份。

  • --incremental-basedir
    :指定包含完整备份的目录。

  • --incremental-dir
    :指定包含增量备份的目录。

  • --apply-log:对备份进行预处理操作。
    

    • 说明
      :在备份完成后,数据尚不能用于恢复,因为备份中可能包含尚未提交的事务或已经提交但尚未同步至数据文件的事务。
      --apply-log
      的主要作用是回滚未提交的事务,并同步已提交的事务,使数据文件处于一致性状态。
  • --redo-only
    :不回滚未提交事务。

  • --copy-back
    :恢复备份目录。

2.2 全量备份
xtrabackup --backup \
    --target-dir=/var/backups/xtrabackup \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --parallel=4  \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --compress-threads=4 \
    --compress-chunk-size=65536

gzip 本地压缩备份

使用流式备份,配合管道使用 gzip 命令对备份在本地进行压缩

--stream=xbstream \
	| gzip - > /data/backup/backup1.gz

恢复时需要先使用 gunzip 解压,再使用 xbstream 解压,才能进行 Prepare 阶段。

# gzip 解压
gunzip backup1.gz

# xbstream 解压
xbstream -x --parallel=10 -C /data/backup/backup_full < ./backup1

说明

  • --target-dir
    :备份目标目录需要事先创建,确保该目录存在。
  • --password
    :建议使用环境变量或其他安全方式传递密码以保护敏感信息。
  • --compress=quicklz
    指定使用
    quicklz
    作为压缩算法。
  • --compress-threads=4
    指定使用 4 个线程进行压缩。
  • --compress-chunk-size=65536
    指定压缩线程的工作缓冲区大小。

报错,没有权限
failed to execute query ‘LOCK INSTANCE FOR BACKUP’ : 1227 (42000) Access denied

grant BACKUP_ADMIN on *.* to 'root'@'%';
flush privileges;

LOCK INSTANCE FOR BACKUP 是MySQL 8.0引入的一种新的备份相关SQL语句,主要用于在进行数据库备份时,以一种更为细粒度和高效的方式控制对数据库实例的访问,以保证备份的一致性。这个命令的工作原理及特点如下:

目的:在执行备份操作时,此命令用于获取一个实例级别的锁,该锁允许在备份过程中继续执行DML(数据操作语言,如INSERT、UPDATE、DELETE)操作,同时防止那些可能导致数据快照不一致的DDL(数据定义语言,如CREATE、ALTER、DROP)操作和某些管理操作。这样可以在不影响数据库服务的情况下进行备份,特别适用于需要最小化服务中断的在线备份场景。

权限需求:执行LOCK INSTANCE FOR BACKUP语句需要用户具备BACKUP_ADMIN权限。这是一个专门为了备份相关的高级操作而设计的权限级别。

兼容性:此特性是在MySQL 8.0及以上版本中引入的,早于8.0的MySQL版本并不支持这一语句,因此在使用旧版本时,可能需要依赖其他机制(如FLUSH TABLES WITH READ LOCK)来确保备份的一致性。

解锁:执行备份后,需要使用UNLOCK INSTANCE语句来释放之前由LOCK INSTANCE FOR BACKUP获得的锁,从而恢复正常操作。

与传统备份命令的对比:相比于传统的备份方法,如使用FLUSH TABLES WITH READ LOCK,LOCK INSTANCE FOR BACKUP提供了更小的性能影响,因为它不会完全阻止写操作,只是限制了可能引起数据不一致的活动,更适合于高可用性和高性能要求的生产环境。

2.3 增量备份

xtrabackup 支持增量备份。在做增量备份之前,需要先做一个全量备份。xtrabackup 会基于 innodb page 的 lsn 号来判断是否需要备份一个 page。如果 page lsn 大于上次备份的 lsn 号,就需要备份该 page。
在这里插入图片描述

先进行一次全量备份。

xtrabackup --backup \
    --target-dir=/var/backups/full \
    --extra-lsndir=/var/backups/full \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --parallel=4  \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --compress-threads=4 \
    --compress-chunk-size=65536 \
    --stream=xbstream \
     2>/var/backups/full/backup_full.log | gzip -  > /var/backups/full/full.gz 

备份命令加上 了–extra-lsndir 选项,将 xtrabackup_checkpoints 单独输出到文件。增量备份时需要根据 xtrabackup_checkpoints中的 lsn,以下是相关文件。

[root@node83 full]# ll
total 4684
-rw-r--r-- 1 root root   43918 Sep 25 15:40 backup_full.log
-rw-r--r-- 1 root root 4741603 Sep 25 15:40 full.gz
-rw-r--r-- 1 root root     102 Sep 25 15:40 xtrabackup_checkpoints
-rw-r--r-- 1 root root     794 Sep 25 15:40 xtrabackup_info

现在,发起增量备份。

xtrabackup --backup \
    --target-dir=/var/backups/inc1 \
    --extra-lsndir=/var/backups/inc1 \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --incremental-basedir=/var/backups/full  \
    --stream=xbstream \
    2>/var/backups/full/backup_full.log | gzip -  > /var/backups/inc1/backup_inc1.gz 
  • –incremental-basedir:全量备份或上一次增量备份 xtrabackup_checkpoints 文件所在目录

增量备份也可以在上一次增量备份的基础上进行

xtrabackup --backup \
    --target-dir=/var/backups/inc2 \
    --extra-lsndir=/var/backups/inc2 \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --incremental-basedir=/var/backups/inc1 \
    --stream=xbstream \
    | gzip -  > /var/backups/inc2/backup_inc2.gz

结构如下

[root@node83 backups]# tree
.
├── full
│   ├── backup_full.log
│   ├── full.gz
│   ├── xtrabackup_checkpoints
│   └── xtrabackup_info
├── inc1
│   ├── backup_inc1.gz
│   ├── xtrabackup_checkpoints
│   └── xtrabackup_info
└── inc2
    ├── backup_inc2.gz
    ├── xtrabackup_checkpoints
    └── xtrabackup_info

3 directories, 10 files

恢复增量备份时,需要先对基础全量备份进行恢复,然后再依次按增量备份的时间进行恢复。
在这里插入图片描述

恢复全量备份

cd /var/backups/full

gunzip full.gz

# 需要先删除这两个文件,否则 xbstream 提取文件时有冲突
rm xtrabackup_checkpoints xtrabackup_info

xbstream -x -v < full

# 由于使用compress压缩,所有还有一层压缩
xtrabackup --decompress --target-dir=/var/backups/full
# 准备阶段
xtrabackup --prepare --apply-log-only --target-dir=. > prepare_full.log 2>&1

恢复增量备份时,切换到全量备份的目录执行

cd /var/backups/inc1
rm xtrabackup_checkpoints xtrabackup_info
gunzip full.gz
xbstream -x -v < full
# 由于使用compress压缩,所有还有一层压缩
xtrabackup --decompress --target-dir=/var/backups/inc1

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

最后

2.4 流式备份

XtraBackup支持流式备份,将备份以指定的tar或xbstream格式发送到STDOUT,而不是直接将文件复制到备份目录。

在 xtrabackup 2.4 版中支持 tar 和 xbstream 流格式,但 tar 格式不支持并行备份。

在 xtrabackup 8.0 中,仅支持 xbstream 流格式,不再支持 tar 格式

xtrabackup --backup \
--datadir=/data/dstore/1.0.0.0/mysql/data \
--user=root \
--password=Hbis@123 \
--socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
--host=localhost \
--parallel=4  \
--compress \
--compress-threads=4 \
--compress-chunk-size=65536 \
--stream=xbstream | ssh -C root@192.168.2.41 "cat > /var/backups/backup.xbstream.gz" 

登录远程主机解压

xbstream -x --parallel=10 -C /opt/backup  < /opt/backups/backup.xbstream.gz

xbstream 中的 -x 表示解压,–parallel 表示并行度,-C 指定解压的目录,最后一级目录必须存在。

远程备份限速

直接备份到远程服务器,如果担心备份会占用较大的网络带宽,可以使用 pv 命令限速。

--stream=xbstream | pv -q -L10m | ssh -C root@192.168.2.41 "cat > /var/backups/backup.xbstream.gz" 

pv 命令中,-q 是指 quiet 不输出进度信息,-L 是指传输速率 10m 指 10MB。

2.5 备份指定库

注意备份的单个库恢复到别的机器时,需要提前手动创建好数据库和表结构

我们可以备份数据库架构,并使用与上述相同的过程进行恢复。

使用
--databases
选项备份数据库

对于多个数据库,请将数据库指定为列表,例如
--databases=“db1 db2 db3”。
数据库也可以在文本文件中指定,并与选项
--databases-file
一起使用。要从备份中排除数据库,请使用选项
--databases-exclude

使用
--export
选项准备备份。

xtrabackup \
  --defaults-file=/srv/dstore/1.0.0.0/mysql/conf/my.cnf \
  --backup \
  --target-dir=/var/backups/test2 \
  --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
  --user=root \
  --password=Hbis@123 \
  --databases="test2" \
  --stream=xbstream | gzip > test2.tar.gz

说明:

  • --defaults-file
    :指定 MySQL 配置文件。
  • --backup
    :表示执行备份操作。
  • --target-dir
    :指定备份文件的目标目录。
  • --socket
    :指定 MySQL socket 文件路径。
  • --user

    --password
    :指定连接数据库的用户和密码。
  • --databases
    :指定要备份的数据库(在这里是
    test2
    )。
  • --stream=xbstream
    :将备份数据以 xbstream 格式输出。
  • | gzip > test2.tar.gz
    :将输出通过管道压缩为
    test2.tar.gz
    文件。

备份完成后scp到远端机器,如/var/backups ,执行导入命令

gunzip test2.tar.gz
xbstream -x --parallel=10  <test2
# 执行准备
xtrabackup --prepare --apply-log-only --export --target-dir=.

现在使用 ALTER TABLE

DISCARD TABLESPACE 删除数据库中所有 InnoDB 表的表空间。

ALTER TABLE person  DISCARD TABLESPACE;

将所有表文件从备份目录 (/var/backups/test2/test/*) 复制到 mysql 数据目录 (/opt/mysql/data)。

注意:
在复制文件之前,请禁用 selinux。复制文件后,如果备份用户不同,请将复制文件的所有权更改为 mysql 用户。

最后,使用 ALTER TABLE

IMPORT TABLESPACE; 恢复表。

ALTER TABLE person IMPORT TABLESPACE; 

这会将表还原到备份时。对于
时间点恢复
,二进制日志可以进一步应用于数据库,但应注意仅应用那些影响正在还原的表的事务。

使用此方法的优点是不需要停止数据库服务器。一个小缺点是每个表都需要单独恢复,尽管它可以在脚本的帮助下克服。

脚本如下:

#!/bin/bash

# 检查输入参数
if [ "$#" -lt 2 ]; then
  echo "用法: \$0 <数据库名> <删除|导入>"
  exit 1
fi

DB_NAME=\$1
ACTION=\$2
MYSQL_USER="root"
MYSQL_PASSWORD="Hbis@123"
MYSQL_SOCKET="/srv/dstore/1.0.0.0/mysql/mysql.sock"

# 获取所有表名
TABLES=$(mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "SHOW TABLES;" | awk '{ print \$1 }' | grep -v '^Tables_in_')

echo "数据库 '$DB_NAME' 中的表: $TABLES"

# 根据操作参数执行删除或导入
for TABLE in $TABLES; do
  if [ "$ACTION" == "删除" ]; then
    echo "删除表空间: $TABLE"
    mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "ALTER TABLE $TABLE DISCARD TABLESPACE;"
  elif [ "$ACTION" == "导入" ]; then
    echo "导入表空间: $TABLE"
    mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "ALTER TABLE $TABLE IMPORT TABLESPACE;"
  else
    echo "无效的操作: $ACTION"
    exit 1
  fi
done

echo "操作完成."

3. 恢复备份

解压缩

压缩算法默认是
qpress
,所以解压缩需要有 qpress 命令

yum -y install qpress

使用 --decompress压缩的备份集在准备备份之前需要解压,解压工具是qpress。解压后的原文件不会被删除,可以使用--remove-original选项清除,--parallel可与--decompress选项一起使用以同时解压缩多个文件。

xtrabackup --defaults-file=/opt/mysql/conf/my.cnf --decompress --target-dir=/var/backups/xtrabackup

准备备份命令

首先要进行 Prepare 阶段,在该阶段 Xtrabackup 会启动一个嵌入的 InnoDB 实例来进行 Crash Recovery。该实例的缓冲池的大小由 --use-memory 参数指定,默认为 100MB。如果有充足的内存,通过设置较大的 memory 可以减少 Prepare 阶段花费的时间。

 --use-memory=2G 
xtrabackup --defaults-file=/opt/mysql/conf/my.cnf \
    --prepare \
    --target-dir=/var/backups/xtrabackup \
    --user=root \
    --password=123456 \
    --socket=/opt/mysql/mysql.sock \
    --host=localhost \
    --apply-log-only

Prepare 阶段完成后,下面进入恢复阶段,可以手动拷贝文件到数据目录,也可以使用 xtrabackup 工具进行拷贝。

恢复备份命令

xtrabackup 	--defaults-file=/opt/mysql/conf/my.cnf \
    --copy-back \
    --parallel=10 \
    --target-dir=/var/backups/xtrabackup \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost 

恢复全量备份时,需要加上 apply-log-only 参数。如果不加上 apply-log-only 参数,执行 prepare 的最后阶段,会回滚未提交的事务,但是这些事务可能在下一次增量备份时已经提交了。

  • 恢复备份前需要保证数据目录为空
  • 数据库必须处于停止状态

执行成功后,必须授权

 chown -R mysql:mysql /opt/mysql/data

启动数据库

mysqld_safe --defaults-file=/etc/my.cnf &

二、测试数据完整性

方式一

  • 模拟4个数据库,执行定时数据写入,模拟随机插入。
  • 备份
    • 首先全量备份
    • (记录偏移量)增量备份
  • scp数据,恢复数据到从机
  • 配置主从,停止主节点脚本,对比数据完整性
编写脚本

模拟数据

#!/bin/bash

# MySQL 配置
MYSQL_USER="root"
MYSQL_PASSWORD="Hbis@123"
MYSQL_SOCKET="/srv/dstore/1.0.0.0/mysql/mysql.sock"

# 创建数据库和插入数据的函数
create_databases_and_insert_data() {
    for i in {1..4}; do
        DB_NAME="test_db_$i"
        TABLE_NAME="test_table"
        
        # 创建数据库
        mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;"
        
        # 创建表
        mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "
        CREATE TABLE IF NOT EXISTS $TABLE_NAME (
            id INT AUTO_INCREMENT PRIMARY KEY,
            data_value VARCHAR(255) NOT NULL
        );"

        # 插入约10000条数据
        for ((j=1; j<=10000; j++)); do
            random_string=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 10)
            mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "
            INSERT INTO $TABLE_NAME (data_value) VALUES ('$random_string');"
        done

        echo "数据库 $DB_NAME 创建完成,已插入 10000 条数据."
    done
}

# 执行创建数据库和插入数据
create_databases_and_insert_data

单个库数据量:474K

模拟定时插入脚本

#!/bin/bash

# MySQL 配置
MYSQL_USER="root"
MYSQL_PASSWORD="Hbis@123"
MYSQL_SOCKET="/srv/dstore/1.0.0.0/mysql/mysql.sock"

# 数据库和表的名称
DB_COUNT=4
TABLE_NAME="test_table"
INSERT_COUNT=10  # 设置要插入的总数

# 模拟定时插入数据
insert_data() {
    for ((n=1; n<=INSERT_COUNT; n++)); do
        for i in $(seq 1 $DB_COUNT); do
            DB_NAME="test_db_$i"
            random_string=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 10)
            mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" -D "$DB_NAME" -e "
            INSERT INTO $TABLE_NAME (data_value) VALUES ('$random_string');"
            echo "向 $DB_NAME 插入数据: $random_string"
        done
        sleep 2  # 每次循环后暂停1秒
    done
}

# 执行数据插入
insert_data

模拟数据生成后,开启定时写入脚本,接下来开始执行备份

nohup sh insert_data.sh > insert_data.log 2>&1 & 
备份测试

首先备份全量数据

xtrabackup --backup \
    --target-dir=/var/backups/xtrabackup \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --parallel=4  \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --compress-threads=4 \
    --compress-chunk-size=65536

再执行增量备份

查看偏移点

mysql> show master status;
+---------------+----------+--------------+------------------+----------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                            |
+---------------+----------+--------------+------------------+----------------------------------------------+
| binlog.000029 |    26321 |              |                  | 3949cf93-71b5-11ef-925f-fa163e75258c:1-40306 |
+---------------+----------+--------------+------------------+----------------------------------------------+
1 row in set (0.03 sec)

开始增量备份

xtrabackup --backup \
    --target-dir=/var/backups/inc1 \
    --extra-lsndir=/var/backups/inc1 \
    --datadir=/data/dstore/1.0.0.0/mysql/data \
    --user=root \
    --password=Hbis@123 \
    --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
    --host=localhost \
    --compress \
    --incremental-basedir=/var/backups/xtrabackup

执行完后,开始执行恢复操作,需要将xtrabackup目录和inc1 传输到从节点

scp -r /var/backups/   192.168.2.41:`pwd`

执行恢复操作【参考上述3.0恢复备份】

数据迁移完成,启动数据库后执行主从配置

stop slave;

CHANGE MASTER TO 
  MASTER_HOST='192.168.2.83', 
  MASTER_USER='asyncuser', 
  MASTER_PASSWORD='Hbis@123', 
  MASTER_PORT=3306,
  MASTER_LOG_FILE='binlog.000029',
  MASTER_LOG_POS=26321;
  
start slave;
show slave status\G;

执行成功后,检查数据一致性

数据库
test_table1 10033 10033
test_table2 10033 10033
test_table3 10033 10033

可以看到数据是一致的

方式二

  • 首先安装从库,并按照主库创建数据库、表结构信息

  • 记录偏移量

    binlog.000030 |   617532 
    
  • 按照顺序备份 test_table1 、2、3、4

  • 恢复数据

  • 配置主从,查看数据一致性

在进行备份之前,您需要启用 innodb_file_per_table

开启数据循环插入脚本

备份 test_table1 、test_table2、test_table3、test_table4,命令示例:

xtrabackup \
  --defaults-file=/srv/dstore/1.0.0.0/mysql/conf/my.cnf \
  --backup \
  --target-dir=/var/backups/db1 \
  --socket=/srv/dstore/1.0.0.0/mysql/mysql.sock \
  --user=root \
  --password=Hbis@123 \
  --databases="test_db_1"

数据备份完成并传输到从机,执行恢复操作

xtrabackup \
    --prepare \
	--export \
	--apply-log-only  \
	--target-dir=/var/backups/db1

发现报错

[ERROR] [MY-012179] [InnoDB] Could not find any file associated with the tablespace ID: 10

暂时没有排查出来,测试发现当定时写入脚本运行时,导出的数据报错

依次恢复每个数据的表

检查发现数据存在无法同步,不一致

$.参考

https://blog.csdn.net/m0_66011019/article/details/136206192

https://blog.csdn.net/weixin_4156186

MySQL 社区开源备份工具 Xtrabackup 详解-CSDN博客

Percona XtraBackup:备份和恢复单个表或数据库

备份和恢复单个数据库 - MySQL & MariaDB / Percona XtraBackup - Percona社区论坛

前言

Volo.Abp.VirtualFileSystem
是ABP(ASP.NET Boilerplate)框架中的一个重要组件,它提供了一种抽象文件系统的方式,使得应用程序可以轻松地访问和管理文件资源,无论这些资源是来自于物理文件系统、嵌入资源,还是远程存储。

通过
Volo.Abp.VirtualFileSystem
,开发者可以使用统一的接口来处理文件和目录,而不用关心这些文件和目录的实际存储位置。这使得应用程序更加灵活,可以轻松地切换不同的文件存储方式,而不用修改大量的代码。

新建mvc项目

引用以下nuget包

新建BookAppWebModule.cs

using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
using Volo.Abp.Autofac;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Hosting.Internal;
using BookApp.Localization;
using BookCategory;

namespace BookApp
{
    [DependsOn(
        typeof(AbpAutofacModule),
        typeof(AbpLocalizationModule),
        typeof(AbpVirtualFileSystemModule),
        typeof(AbpAspNetCoreMvcModule),
        typeof(BookCategoryModule)
    )]
    public class BookAppWebModule: AbpModule
    {
        public override void PreConfigureServices(ServiceConfigurationContext context)
        {
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            var configuration = context.Services.GetConfiguration();

            context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
            {
                options.AddAssemblyResource(
                    typeof(BookStoreResource)
                );
            });
        }
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var hostingEnvironment = context.Services.GetHostingEnvironment();

            ConfigureVirtualFileSystem(hostingEnvironment);

            Configure<AbpLocalizationOptions>(options =>
            {
                options.Languages.Add(new LanguageInfo("ar", "ar", "العربية"));
                options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština"));
                options.Languages.Add(new LanguageInfo("en", "en", "English"));
                options.Languages.Add(new LanguageInfo("en-GB", "en-GB", "English (UK)"));
                options.Languages.Add(new LanguageInfo("hu", "hu", "Magyar"));
                options.Languages.Add(new LanguageInfo("fi", "fi", "Finnish"));
                options.Languages.Add(new LanguageInfo("fr", "fr", "Français"));
                options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi"));
                options.Languages.Add(new LanguageInfo("it", "it", "Italiano"));
                options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português"));
                options.Languages.Add(new LanguageInfo("ru", "ru", "Русский"));
                options.Languages.Add(new LanguageInfo("sk", "sk", "Slovak"));
                options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
                options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文"));
                options.Languages.Add(new LanguageInfo("zh-Hant", "zh-Hant", "繁體中文"));
                options.Languages.Add(new LanguageInfo("de-DE", "de-DE", "Deutsch"));
                options.Languages.Add(new LanguageInfo("es", "es", "Español"));

                options.Resources
                    .Add<BookStoreResource>("en")
                    .AddVirtualJson("/Localization/BookStore");

                options.DefaultResourceType = typeof(BookStoreResource);
            });
        }

        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var app = context.GetApplicationBuilder();
            var env = context.GetEnvironment();

            app.UseAbpRequestLocalization();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
        }

        private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment)
        {
            Configure<AbpVirtualFileSystemOptions>(options =>
            {
                options.FileSets.AddEmbedded<BookAppWebModule>();

                if (hostingEnvironment.IsDevelopment())
                {
                    options.FileSets.ReplaceEmbeddedByPhysical<BookAppWebModule>(hostingEnvironment.ContentRootPath);
                    options.FileSets.ReplaceEmbeddedByPhysical<BookCategoryModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}BookCategory", Path.DirectorySeparatorChar)));
                }
            });
        }
    }
}

修改Program.cs

using BookApp;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Host
    .AddAppSettingsSecretsJson()
    .UseAutofac();

await builder.AddApplicationAsync<BookAppWebModule>();

var app = builder.Build();

await app.InitializeApplicationAsync();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

await app.RunAsync();

新建资源文件

新建BookCategory类库项目

新建BookCategoryModule.cs

using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
using Volo.Abp;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc;
using BookApp.Localization;

namespace BookCategory
{
    [DependsOn(
        typeof(AbpVirtualFileSystemModule),
        typeof(AbpAspNetCoreMvcModule)
    )]
    public class BookCategoryModule: AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            var configuration = context.Services.GetConfiguration();

            Configure<AbpVirtualFileSystemOptions>(options =>
            {
                options.FileSets.AddEmbedded<BookCategoryModule>();//添加程序集到虚拟文件系统

                if (hostingEnvironment.IsDevelopment())
                {

                }
            });

            Configure<AbpLocalizationOptions>(options =>
            {
                options.Resources
                    .Add<BookCategoryResource>("en")
                    .AddVirtualJson("/Localization/BookCategory"); //这里必需添加,不然本地化时找不到相应的json文件
            });
        }

        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {

        }
    }
}

新建BookCategoryResource.cs


using Volo.Abp.Localization;

namespace BookApp.Localization;

[LocalizationResourceName("BookCategory")]
public class BookCategoryResource
{

}

新建资源文件

修改mvc项目的HomeController.cs中的Privacy方法

        public IActionResult Privacy()
        {
            var resourcePath = "/Localization/BookCategory/en.json";
            var fileInfo = _fileProvider.GetFileInfo(resourcePath);

            if (fileInfo.Exists)
            {
                using (var stream = fileInfo.CreateReadStream())
                using (var reader = new StreamReader(stream))
                {
                    var content = reader.ReadToEnd();
                    return Content(content);
                }
            }

            return Content("Resource not found");
        }

此时我们就可以访问BookCategory类库中的资源文件

修改mvc项目中的视图Index.cshtml

@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using BookApp.Controllers
@using BookApp.Localization

@inject IHtmlLocalizer<BookStoreResource> HtmlLocalizer
@inject IStringLocalizer<BookCategoryResource> StringLocalizer

@{
    ViewData["Title"] = "Home Page";
}

<div>string: @StringLocalizer["AppName"]</div>

<div>html: @HtmlLocalizer["AppName"]</div>

IStringLocalizer
StringLocalizer 这里就可以读取BookCategory类库中的资源文件

作者

吴晓阳(手机:13736969112微信同号)

AI之旅
开篇
之后的第一篇本准备写一篇简单的技术分享,却遇到一个「意外」,这个意外惊喜让园子的「AI之旅」起步即迈出重要一步。

经过3个多月的接触,园子最近终于和
Chat2DB
达成了战略合作,以下内容是 Chat2DB 创始人发布的战略合作公告博文:

在AI技术的浪潮中,我们时常思考,如何让工具更加智能,如何让开发者的工作更加高效。我们与博客园的战略合作,正是一次回应这样的思考。Chat2DB,一款专注于数据库管理和数据分析的AI驱动工具,与博客园联手,致力于为广大开发者提供一系列智能、贴心的开发工具。通过开源免费加增值服务的模式,我们希望让更多开发者的技术生涯更加顺畅、充满活力。

从EasyExcel到Chat2DB:技术人的初心与坚守

回首过往,作为EasyExcel的作者,我曾在技术的长路上与许多同道中人并肩前行。EasyExcel诞生于解决复杂报表需求的初衷,但成长于开源社区的热爱与贡献之中。它不仅仅是一款简单的Excel处理工具,更代表了我对开发者社区的承诺——用技术为大家提供最简洁高效的工具,减少繁琐工作,提升生产力。多年来,和开源社区的伙伴们交流、协作,让我深刻体会到技术人那种为了理想共同拼搏的情谊。

然而,技术的发展从未停止脚步。我们在去年见证了人工智能技术的快速迭代,这让我意识到AI在数据库领域的潜力。于是,Chat2DB应运而生(
https://chat2db-ai.com/zh-CN
)。这个项目并非只是单一地关注数据库管理方面,而是借助AI的能力,让数据管理、数据分析变得更智能、更高效。我们希望开发者在面临复杂的数据库场景时,能够少些繁琐,多些智能和人性化体验。

与博客园的邂逅:认同与合作的源起

在技术人的圈子里,提到博客园,几乎无人不知。

作为国内最有影响力的技术社区之一,博客园不仅是许多开发者学习、交流的聚集地,更是一个充满温暖与活力的家。多年来,博客园用无数的技术文章、交流讨论、分享会等形式,陪伴着一代又一代的技术人走过成长路程。我自己也曾在博客园中写下随笔、阅读他人的经验分享。对于博客园,我有着深深的认同与敬意。它不仅是一个技术平台,更是一片孕育梦想的土壤。

在今年7月,一个偶然的机会,我看到博客园关于“救园”的文章,深受触动。那种为了守护社区的坚定与不懈努力让我再次体会到“社区”二字的深刻含义。于是,我主动与博客园联系,探讨合作的可能性。因为我们有太多共同的地方:同样都喜欢写代码,同样都怀揣着技术情怀,同样希望能为技术人带来更多价值。于是,在杭州的一个夏夜,我们第一次见面,便产生了许多共鸣。经过几轮深入探讨,双方决定共同推动一场围绕“开源+社区+AI”的合作,共同为开发者提供更智能化的工具支持,提升生产力,增添创作乐趣。

战略合作的意义:从共鸣到共创

我们这次合作的初衷,是希望通过战略性协作,将我们各自的优势汇聚,为开发者带来更为便捷、智能的工具体验。Chat2DB凭借AI驱动的数据库管理与数据分析能力,与博客园丰富的社区资源和技术积累,可以共同推动“AI+社区”的融合,真正助力开发者的成长。这不仅是一次商业合作,更是一次基于认同和信念的携手共进。

博客园希望在未来通过产品化路径,为社区带来更多的服务和价值。而我希望通过与博客园的合作,推广AI数据库管理工具在开发者群体中的普及率,让更多开发者在日常工作中享受到AI带来的便利和效率提升。合作的背后,是我们对技术的热爱与信仰,是我们对技术人长久以来的支持与陪伴。通过这样的战略合作,我们相信可以实现“开源免费+增值服务”模式的成功落地,既让工具更高效地服务开发者,又能让社区更具活力。

前路漫漫,合作未来可期

在这条前路上,我们深知困难重重。AI技术虽然日新月异,但如何让它真正服务于人、服务于开发者是一个需要长期努力的问题。我们和博客园也经历了多次合作方案的调整,每一次的碰面交流,每一个思考的细节,都是为了让合作真正落地。10月31日,我们终于敲定了具体方案,这段时间的努力没有白费,这份承诺也将持续下去。

未来,我们将围绕开发者的需求,共同探索并与更多AI开发工具厂商展开合作,为社区引入更多优质的AI技术工具和资源,丰富博客园的工具生态,共同开拓「AI+社区」驱动的开发者工具市场,持续为开发者们提供更多样化、更高效的开发支持和服务。此外,博客园将继续围绕社区开展多种互动活动,进一步增强社区氛围,让更多开发者从中获益。

结语:共创AI时代的开发者新篇章

在技术变革的路上,我们不会孤单。我们与博客园的合作,只是一个开始。未来,更多的技术人将加入这场旅程,共同书写AI时代的开发者新篇章。我们希望,这条路上的每一个人,都能够在智能工具与温暖社区的双重支持下,走得更远、飞得更高。一起携手,迎接更智能、更开放、更美好的技术未来。

Chat2DB 官方公众号同步发布
https://mp.weixin.qq.com/s/d4nfBLcJEfUJvxp_S2unbA

前言

在实际工作中,我们需要经常跟第三方平台打交道,可能会对接第三方平台Controller接口,或者提供Controller接口给第三方平台调用。

那么问题来了,如果设计一个优雅的Controller接口,能够满足:安全性、可重复调用、稳定性、好定位问题等多方面需求?

今天跟大家一起聊聊设计Controller接口时,需要注意的一些地方,希望对你会有所帮助。

1. 签名

为了防止Controller接口中的数据被篡改,很多时候我们需要对Controller接口做
签名

接口请求方将
请求参数
+
时间戳
+
密钥
拼接成一个字符串,然后通过
md5
等hash算法,生成一个前面sign。

然后在请求参数或者请求头中,增加sign参数,传递给API接口。

API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。

如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。

如果两个sign不相等,则API接口的网关服务会直接返回签名错误。

问题来了:签名中为什么要加时间戳?

答:为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。

这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。

目前生成签名中的密钥有两种形式:

一种是双方约定一个固定值privateKey。

另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。

2. 加密

有些时候,我们的Controller接口直接传递的非常重要的数据,比如:用户的登录密码、银行卡号、转账金额、用户身份证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。

由此,我们需要对数据进行非对称加密。

目前使用比较多的是用
RSA

RSA包含了一对:
公钥

私钥

我们以用户登录密码为例。

在用户输入密码之后,在前端需要对密码使用公钥做加密处理。

公钥是保留在前端代码中的,即使泄露给别人了,也没关系。

因为使用公钥加密后的密码,只能使用后端服务中对应的私钥才能解密。

而我们私钥保存在后端服务的配置中,别人无法获取到。

因此,使用RSA加密和解密是安全的。

我们可以使用在线工具生成密钥对:https://tools.ytdevops.com/rsa-key-pair-generator

3. ip白名单

为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。

需求限制请求
ip
,增加
ip白名单

只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。

ip白名单也可以加在API网关服务上。

但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。

这时候就需要增加web防火墙了,比如:ModSecurity等。

4. 限流

如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。

第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。

由此,必须要对API接口做
限流

限流方法有三种:

  1. 对请求ip做限流:比如同一个ip,在一分钟内,对
    API接口总的请求次数
    ,不能超过10000次。
  2. 对请求接口做限流:比如同一个ip,在一分钟内,对
    指定的API接口
    ,请求次数不能超过2000次。
  3. 对请求用户做限流:比如同一个
    AK/SK用户
    ,在一分钟内,对API接口总的请求次数,不能超过10000次。

我们在实际工作中,可以通过
nginx

redis
或者
gateway
实现限流的功能。

5. 参数校验

我们需要对API接口做
参数校验
,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。

这样做可以拦截一些无效的请求。

比如在新增数据时,字段长度超过了数据字段的最大长度,数据库会直接报错。

但这种异常的请求,我们完全可以在API接口的前期进行识别,没有必要走到数据库保存数据那一步,浪费系统资源。

有些金额字段,本来是正数,但如果用户传入了负数,万一接口没做校验,可能会导致一些没必要的损失。

还有些状态字段,如果不做校验,用户如果传入了系统中不存在的枚举值,就会导致保存的数据异常。

由此可见,做参数校验是非常有必要的。

在Java中校验数据使用最多的是
hiberate

Validator
框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。

用它们校验数据非常方便。

当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。

6. 统一返回值

我之前调用过别人的API接口,正常返回数据是一种json格式,比如:

{
    "code":0,
    "message":null,
    "data":[{"id":123,"name":"abc"}]
},

签名错误返回的json格式:

{
    "code":1001,
    "message":"签名错误",
    "data":null
}

没有数据权限返回的json格式:

{
    "rt":10,
    "errorMgt":"没有权限",
    "result":null
}

这种是比较坑的做法,返回值中有多种不同格式的返回数据,这样会导致对接方很难理解。

出现这种情况,可能是API网关定义了一直返回值结构,业务系统定义了另外一种返回值结构。如果是网关异常,则返回网关定义的返回值结构,如果是业务系统异常,则返回业务系统的返回值结构。

但这样会导致API接口出现不同的异常时,返回不同的返回值结构,非常不利于接口的维护。

其实这个问题我们可以在设计
API网关
时解决。

业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。

所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。

7. 统一封装异常

我们的API接口需要对
异常
进行统一处理。

不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。

返回值中包含了
异常堆栈信息

数据库信息

错误代码和行数
等信息。

如果直接把这些内容暴露给第三方平台,是很危险的事情。

有些不法分子,利用接口返回值中的这些信息,有可能会进行sql注入或者直接脱库,而对我们系统造成一定的损失。

因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:

{
    "code":500,
    "message":"服务器内部错误",
    "data":null
}

返回码
code

500
,返回信息
message

服务器内部异常

这样第三方平台就知道是API接口出现了内部问题,但不知道具体原因,他们可以找我们排查问题。

我们可以在内部的日志文件中,把堆栈信息、数据库信息、错误代码行数等信息,打印出来。

我们可以在
gateway
中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。

8. 请求日志

在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。

我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。

最好有
traceId
,可以通过它串联整个请求的日志,过滤多余的日志。

当然有些时候,请求日志不光是你们公司开发人员需要查看,第三方平台的用户也需要能查看接口的请求日志。

这时就需要把日志落地到数据库,比如:
mongodb
或者
elastic search
,然后做一个UI页面,给第三方平台的用户开通查看权限。这样他们就能在外网查看请求日志了,他们自己也能定位一部分问题。

9. 幂等设计

第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做
幂等设计

也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。

这样做的目的是不会产生错误数据。

我们在日常工作中,可以通过在
数据库
中增加
唯一索引
,或者在
redis
保存
requestId
和请求参来保证接口幂等性。

对接口幂等性感兴趣的小伙伴,可以看看我的另一篇文章《
高并发下如何保证接口的幂等性?
》,里面有非常详细的介绍。

10. 限制记录条数

对于对我提供的批量接口,一定要
限制请求的记录条数

如果请求的数据太多,很容易造成API
接口超时
等问题,让API接口变得不稳定。

通常情况下,建议一次请求中的参数,最多支持传入500条记录。

如果用户传入多余500条记录,则接口直接给出提示。

建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。

11. 压测

上线前我们务必要对API接口做一下
压力测试
,知道各个接口的
qps
情况。

以便于我们能够更好的预估,需要部署多少服务器节点,对于API接口的稳定性至关重要。

之前虽说对API接口做了限流,但是实际上API接口是否能够达到限制的阀值,这是一个问号,如果不做压力测试,是有很大风险的。

比如:你API接口限流1秒只允许50次请求,但实际API接口只能处理30次请求,这样你的API接口也会处理不过来。

我们在工作中可以用
jmeter
或者
apache benc
对API接口做压力测试。

12. 异步处理

一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。

但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。

这种情况下,为了提升API接口的性能,我们可以改成
异步处理

在API接口中可以发送一条
mq消息
,然后直接返回成功。之后,有个专门的
mq消费者
去异步消费该消息,做业务逻辑处理。

直接异步处理的接口,第三方平台有两种方式获取到。

第一种方式是:我们
回调
第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。

第二种方式是:第三方平台通过
轮询
调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。

13. 数据脱敏

有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银行卡号等等。

这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。

这就需要对部分数据做
数据脱敏
了。

我们可以在返回的数据中,部分内容用
星号
代替。

已用户手机号为例:
182****887

这样即使数据被泄露了,也只泄露了一部分,不法分子拿到这份数据也没啥用。

14. 完整的接口文档

说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。

接口文档中需要包含如下信息:

  1. 接口地址
  2. 请求方式,比如:post或get
  3. 请求参数和字段介绍
  4. 返回值和字段介绍
  5. 返回码和错误信息
  6. 加密或签名示例
  7. 完整的请求demo
  8. 额外的说明,比如:开通ip白名单。

接口文档中最好能够统一接口和字段名称的命名风格,比如都用
驼峰标识
命名。

接口地址中可以加一个版本号v1,比如:v1/query/getCategory,这样以后接口有很大的变动,可以非常方便升级版本。

统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。

统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。

接口文档中写明AK/SK和域名,找某某单独提供等。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

本篇介绍Manim中创建三维立体的两个常用对象:
Cube

Prism

Cube
在制作动画时,可以用于展示立体几何中的立方体概念,或者通过旋转、缩放等动画效果来帮助理解三维空间中的几何变换。

Prism
是长方体,继承自
Cube
,与
Cube
相比,它可以更进一步设置不同的边长。

1. 主要参数

Cube
的主要参数有:

参数名称 类型 说明
side_length float 立方体的边长
fill_opacity float 立方体的透明度
fill_color Color 立方体的颜色
stroke_width float 设置立方体边框的宽度

Prism

Cube
的区别在于它没有
side_length
参数,取而代之是
dimensions
参数。

参数名称 类型 说明
dimensions tuple[float, float, float] 长方体的长,宽和高

Prism

dimensions
参数
长宽高
定义为同样的值时,就是
Cube

2. 主要方法

Cube

Prism
的没有特有的方法,对于通用的方法都支持,

比如
shift
(平移),
rotate
(旋转)和
scale
(缩放)等。

3. 使用示例

这个对象的使用比较简单,下面的示例演示其基本的使用和操作。

3.1. 默认显示

这个示例展示了如何在
Manim
中创建并显示一个立方体(
Cube
)和一个长方体(
Prism
)。

# 创建一个立方体
cube = Cube()
# 创建一个棱柱
prism = Prism()

3.2. 变换颜色

在这个示例中,首先创建了一个红色的立方体和一个蓝色的长方体。

接着,立方体逐渐变化为绿色,而长方体逐渐变化为黄色。

实际动画中,通过颜色的变换可以更好地演示形状属性的动态变化。

# 创建一个立方体
cube = Cube()
cube2 = Cube(fill_color=RED)

# 创建一个棱柱
prism = Prism()
prism2 = Prism(fill_color=GREEN)

3.3. 移动和旋转

此示例首先展示了一个立方体和一个长方体。

接下来,让立方体向右移动,而长方体向左移动,然后立方体顺时针旋转45度,而长方体则逆时针旋转45度。

这种移动和旋转的效果可以生动地展示三维空间中的几何变换。

# 创建一个立方体
cube = Cube(fill_color=RED)
# 创建一个棱柱
prism = Prism(fill_color=GREEN)

# 移动
self.play(
    cube.animate.shift(RIGHT),
    prism.animate.shift(LEFT),
)
# 旋转
self.play(
    cube.animate.rotate(PI / 4),
    prism.animate.rotate(-PI / 4),
)

3.4. 组合使用

在这个示例中,同样先创建一个立方体和一个长方体。

然后将这两个形状组合成一个整体,再整体向上移动,并旋转一个小角度。

这种组合使用的方式可以展示如何在
Manim
中处理和组织多个形状,以及如何对它们进行整体的动画效果。

# 创建一个立方体
cube = Cube(fill_color=RED)
# 创建一个棱柱
prism = Prism(fill_color=GREEN)
# 将立方体放在棱柱的右边
cube.next_to(prism, RIGHT)

# 组合使用
vg = VGroup(cube, prism)

# 整体移动组合
self.play(vg.animate.shift(UP))
# 整体旋转组合
self.play(vg.animate.rotate(PI / 2, axis=UP))

4. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
cube_prism.py
),

下载地址:
完整代码
(访问密码: 6872)