ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
PostgreSQL本身提供了逻辑导出工具pg_dumpall和pg_dump,其中pg_dumpall导出所有的数据库,pg_dump导出单个数据库,两个工具的用法和参数不再详细介绍,本文从代码层面上对此过程进行分析。 概括地说,逻辑导出要干的事情就是连接对应数据库,读出各个数据库对象的定义和数据,此外还包括comment、服务器配置和权限控制等等,这些数据库对象定义的SQL语句会被写入到对应的dump文件中。其中可以设置只导出模式或者只导出数据,默认是导出模式和数据,这样就可以支持分步导出和恢复。而数据表数据可以选择COPY方式或者INSERT语句的方式写入备份文件中。 这个过程主要涉及几个文件,包括pg_dumpall.c,pg_dump.c,pg_backup_db.c。其中pg_dumpall.c导出所有的数据库,pg_dump.c导出单个数据库,会被pg_dumpall.c不断调用,从而导出所有的数据库,这里重点分析下pg_dump.c的工作。 ## pg_dump过程分析 pg_dump.c文件的main函数,主要完成如下工作: (1) 解析各类参数,包括对应变量赋值和参数间是否相互兼容,如果不兼容,报错退出。 (2) 调用CreateArchive函数,打开输出文件,输出流为g_fout,g_fout是Archive类型,这里比较巧妙的地方就是根据不同的文件格式,会产生不同的g_fout,对应也就使用不同的.c文件独立封装了不同导出的文件格式下的处理函数,这样可以很容易地增加新的导出文件格式,提高了可维护性和扩展性,具体的实现方法我们会在下面进行分析。目前支持四种导出文件格式和分别是: | custom | pg_backup_custom.c | 导出对象存储到二进制格式的文件中 | | file | pg_backup_files.c | 导出对象存储到指定的文件中 | | plain | pg_backup_null.c | 导出文件到标准输出 | | tar | pg_backup_tar.c | 以压缩文件的格式导出文件 | (3)调用ConnectDatabase函数,连接目的数据库,并在这个数据库上执行一些SQL语句,如设定C/S之间的编码、设定数据库对于日期类型的使用格式、针对不同版本的服务器设置一些与版本相关的信息。 (4) 在(3)中的数据库连接上开启一个事务,保证导出的所有数据的一致性,同时为了保证能够导出浮点数,设置正确的浮点数输出格式: ~~~ do_sql_command(g_conn, "BEGIN"); do_sql_command(g_conn, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); do_sql_command(g_conn, "SET extra_float_digits TO 2"); ~~~ (5) 为了保持pg_dump工具向低版本兼容,根据服务器的版本号决定一些变量的取值。 (6) 查询并存储需要导出的模式和表的信息。 (7) 调用getSchemaData函数,决定导出哪些数据库对象,并调用了如下函数保存具体的数据库对象: ~~~ proclanginfo = getProcLangs(&numProcLangs); agginfo = getAggregates(&numAggregates); oprinfo = getOperators(&numOperators); oprinfoindex = buildIndexArray(oprinfo, numOperators, sizeof(OprInfo)); opcinfo = getOpclasses(&numOpclasses); prsinfo = getTSParsers(&numTSParsers); tmplinfo = getTSTemplates(&numTSTemplates); dictinfo = getTSDictionaries(&numTSDicts); cfginfo = getTSConfigurations(&numTSConfigs); fdwinfo = getForeignDataWrappers(&numForeignDataWrappers); srvinfo = getForeignServers(&numForeignServers); daclinfo = getDefaultACLs(&numDefaultACLs); opfinfo = getOpfamilies(&numOpfamilies); convinfo = getConversions(&numConversions); tblinfo = getTables(&numTables); tblinfoindex = buildIndexArray(tblinfo, numTables, sizeof(TableInfo)); inhinfo = getInherits(&numInherits); ruleinfo = getRules(&numRules); castinfo = getCasts(&numCasts); flagInhTables(tblinfo, numTables, inhinfo, numInherits); getTableAttrs(tblinfo, numTables); flagInhAttrs(tblinfo, numTables); getIndexes(tblinfo, numTables); getConstraints(tblinfo, numTables); getTriggers(tblinfo, numTables); ~~~ 值得注意的是,在此不完全决定了对象的导出次序,原则是被依赖的对象先导出。在这些函数中,注意类似如下的调用序列: ~~~ tblinfo = getTables(numTables); tblinfoindex = buildIndexArray(tblinfo, numTables, sizeof(TableInfo)); ~~~ 这表明先导出表,再导出依附于表的索引信息。 ~~~ flagInhTables(tblinfo, numTables, inhinfo, numInherits); ~~~ 上句表明要父表先于子表导出。 (8) 每一个getXXXs函数,都将执行如下过程: * 根据服务器版本号,查询系统表,读出对象的元数据信息。 * 循环遍历每个数据库对象元数据信息,对每个数据库对象通过pg_depend系统表计算其依赖对象,并记录所有对象的元数据信息和它依赖对象的元数据信息。 (9) 调用getTableData函数,“导出”表中的数据。注意,这里导出加引号并不是真正的导出数据,而是用一个链表去存储每一个需要导出的数据库对象的基本信息,到真正需要导出的时候再遍历这个链表依次做出对应的处理。这里使用了占位的思想,不会占用大量的内存空间。 (10) 如果需要导出大对象,调用getBlobs,同上也是建立一个链表,并没有真正去做导出。 (11) 根据步骤(8)得到每个对象的依赖关系,调用getDependencies函数,重新整理对象间的依赖关系,调用sortDumpableObjects来决定各个数据库对象导出的顺序(不同类型的对象的导出优先级取决于newObjectTypePriority数组;相同类型的对象,按名称排序)。 (12) 存储编码等信息以及本连接对应的目的数据库的信息。 (13) 遍历所有对象,逐个“导出”对象(调用了dumpDumpableObject函数,本函数调用一堆诸如dumpNamespace、dumpTable等对象)。如果是“导出”表,则根据“导出”对象的信息,查询系统表,查阅到每个表对应的列信息,生成表对象对应的SQL语句,输出SQL语句到g_fou;如果是“导出”表数据,则调用dumpTableData,有两种方式选择,一是生成Insert语句,默认的是生成PostgreSQL自身的copy语句。这里不再具体去介绍。 (14) 在“导出”每一个对象时,通常都会调用ArchiveEntry,做真正的SQL语句生成工作。另外,还会调用dumpComment、dumpSecLabel、dumpACL等函数,“导出”本对象对应的一些诸如注释、权限等相关信息。 (15) 调用RestoreArchive函数,真正的导出数据,注意这里是根据不同的导出文件格式来选择不同的RestoreArchive函数。 (16) 关闭句柄释放资源等。 接下来,我们简单分析下目前支持的四种导出格式以及如何实现不同导出格式对应不同处理函数。目前PostgreSQL提供四种导出文件格式,具体如下: | custom | pg_backup_custom.c | 导出到二进制格式的备份文件,包括文件头和文件体。文件体是一个链表,保存每个备份对象,每一个可备份对象都有一套统一的结构标识,支持压缩(压缩功能依赖于系统编译选项和pg_config.h文件中的宏定义开关)。 | | plain | pg_backup_null.c | 把SQL脚本内容输出到标准输出,默认方式。 | | file | pg_backup_files.c | 导出包括备份一个主文件和一些辅助文件;主文件方式类似于custom的文件格式,辅助文件是数据文件,每一个辅助文件对应备份对象中的一个表。 | | tar | pg_backup_tar.c | 文件备份基本类似“file”方式,但是,最后备份的所有文件都要归档到一个tar文件中。文件最大大小为8GB(受限于tar file format)。 | PostgreSQL通过函数指针来实现这四种导出文件格式对应不同的处理函数。在pg_backup_archiver.h文件中,定义有大量的函数指针,如: ~~~ typedef void (*ClosePtr) (struct _archiveHandle * AH); typedef void (*ReopenPtr) (struct _archiveHandle * AH); typedef void (*ArchiveEntryPtr) (struct _archiveHandle * AH, struct _tocEntry * te); 这些函数指针,被用到了如下文件中(文件->被调用的函数): pg_backup_custom.c->InitArchiveFmt_Custom(ArchiveHandle *AH) pg_backup_null.c->InitArchiveFmt_Null(ArchiveHandle *AH) pg_backup_files.c->InitArchiveFmt_Files(ArchiveHandle *AH) pg_backup_tar.c->InitArchiveFmt_Tar(ArchiveHandle *AH) 在数据结构ArchiveHandle中,使用了大量的函数指针,使得在初始化不同导出文件格式的Archive结构时能够为处理函数赋值为各自不同的处理函数。 这样在pg_dump.c中,只要根据用户指定的文件格式的参数,就可以调用相应的处理函数,代码如下: /* open the output file */ if (pg_strcasecmp(format, "a") == 0 || pg_strcasecmp(format, "append") == 0) { /* This is used by pg_dumpall, and is not documented */ plainText = 1; g_fout = CreateArchive(filename, archNull, 0, archModeAppend); } else if (pg_strcasecmp(format, "c") == 0 || pg_strcasecmp(format, "custom") == 0) g_fout = CreateArchive(filename, archCustom, compressLevel, archModeWrite); else if (pg_strcasecmp(format, "f") == 0 || pg_strcasecmp(format, "file") == 0) { /* * Dump files into the current directory; for demonstration only, not * documented. */ g_fout = CreateArchive(filename, archFiles, compressLevel, archModeWrite); } else if (pg_strcasecmp(format, "p") == 0 || pg_strcasecmp(format, "plain") == 0) { plainText = 1; g_fout = CreateArchive(filename, archNull, 0, archModeWrite); } else if (pg_strcasecmp(format, "t") == 0 || pg_strcasecmp(format, "tar") == 0) g_fout = CreateArchive(filename, archTar, compressLevel, archModeWrite); else { write_msg(NULL, "invalid output format \"%s\" specified\n", format); exit(1); } ~~~ 概括得说,pg_dump导出的内容可以分为数据库对象的定义和对象数据。数据库对象的定义导出,是通过查询系统表把对应的元信息读取出来后,把该对象的各类信息置于一个链表上,包括其依赖的对象oid。而具体的数据,也就是每个数据表的数据,也被抽象为了一个数据库对象(这种对象我们可以称为数据对象),保存在此链表中(链表上的所有对象都有自己的类型,TocEntry结构上有个成员“teSection section”,是标识本节点的类型)。通过调节导出顺序,会先把数据库对象的定义导出,然后导出其数据对象,只要通过链表中对应数据对象节点的信息,执行相应的SQL语句,从表中读出数据,然后把数据写出去。所以,在内存中只是链表上的对象的定义,数据是在边读边写出的,完全可以实现流式导出,如下: 导出数据,通过管道和psql工具,导入到目的库 ~~~ pg_dump -h host1 dbname | psql -h host2 dbname ~~~ 导出数据到标准输出 ~~~ pg_dump dbname > outfile ~~~ 导出大于操作系统所支持的最大文件的大数据量 ~~~ pg_dump dbname | gzip > filename.gz ~~~ 恢复大数据量的数据到目的库 ~~~ gunzip -c filename.gz | psql dbname ~~~ or: ~~~ cat filename.gz | gunzip | psql dbname ~~~ 当然,除了上面的分析外,还有很多其它详细的内容需要具体分析,比如不同版本的数据库操作方式、版本兼容性的问题、对象权限如何处理、约束关系如何处理等。 这些问题都是值得下一步具体分析的。