MySQL 中如何归档数据的实现方法

MySQL 中如何归档数据的实现方法,第1张

MySQL 中如何归档数据的实现方法 目录
  • 什么是pt-archiver
  • 安装
  • 简单入门
  • 实现原理
  • 批量归档
  • 不同归档参数之间的速度对比
  • 其它常见用法
    • (1)删除数据
    • (2)将数据归档到文件中
  • 如何避免主从延迟
    • 常用参数
      • 总结

        归档,在 MySQL 中,是一个相对高频的操作。

        它通常涉及以下两个动作:

        • 迁移。将数据从业务实例迁移到归档实例。
        • 删除。从业务实例中删除已迁移的数据。

        在处理类似需求时,都是开发童鞋提单给 DBA,由 DBA 来处理。

        于是,很多开发童鞋就好奇,DBA 都是怎么执行归档操作的?归档条件没有索引会锁表吗?安全吗,会不会数据删了,却又没归档成功?

        针对这些疑问,下面介绍 MySQL 中的数据归档神器 - pt-archiver。

        本文主要包括以下几部分:

        • 什么是 pt-archiver
        • 安装
        • 简单入门
        • 实现原理
        • 批量归档
        • 不同归档参数之间的速度对比
        • 常见用法
        • 如何避免主从延迟
        • 常用参数

        什么是 pt-archiver

        pt-archiver 是 Percona Toolkit 中的一个工具。

        Percona Toolkit 是 Percona 公司提供的一个 MySQL 工具包。工具包里提供了很多实用的 MySQL 管理工具。

        譬如,我们常用的表结构变更工具 pt-online-schema-change ,主从数据一致性校验工具 pt-table-checksum 。

        毫不夸张地说,熟练使用 Percona Toolkit 是 MySQL DBA 必备的技能之一。

        安装

        Percona Toolkit 下载地址:https://www.percona.com/downloads/percona-toolkit/LATEST/

        官方针对多个系统提供了现成的软件包。

        我常用的是 Linux - Generic 二进制包。

        下面以 Linux - Generic 版本为例,看看它的安装方法。

        # cd /usr/local/
        # wget https://downloads.percona.com/downloads/percona-toolkit/3.3.1/binary/tarball/percona-toolkit-3.3.1_x86_64.tar.gz --no-check-certificate
        # tar xvf percona-toolkit-3.3.1_x86_64.tar.gz
        # cd percona-toolkit-3.3.1
        # yum install perl-ExtUtils-MakeMaker perl-DBD-MySQL perl-Digest-MD5
        # perl Makefile.PL
        # make
        # make install

        简单入门

        首先,我们看一个简单的归档 Demo。

        测试数据

        mysql> show create table employees.departments\G
        *************************** 1. row ***************************
               Table: departments
        Create Table: CREATE TABLE `departments` (
          `dept_no` char(4) NOT NULL,
          `dept_name` varchar(40) NOT NULL,
          PRIMARY KEY (`dept_no`),
          UNIQUE KEY `dept_name` (`dept_name`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
        1 row in set (0.00 sec)
        
        mysql> select * from employees.departments;
        +---------+--------------------+
        | dept_no | dept_name          |
        +---------+--------------------+
        | d009    | Customer Service   |
        | d005    | Development        |
        | d002    | Finance            |
        | d003    | Human Resources    |
        | d001    | Marketing          |
        | d004    | Production         |
        | d006    | Quality Management |
        | d008    | Research           |
        | d007    | Sales              |
        +---------+--------------------+
        9 rows in set (0.00 sec)

        下面,我们将 employees.departments 表的数据从 192.168.244.10 归档到 192.168.244.128。

        具体命令如下:

        pt-archiver --source h=192.168.244.10,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --dest h=192.168.244.128,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --where "1=1"

        命令行中指定了三个参数。

        • --source:源库(业务实例)的 DSN。

          DSN 在 Percona Toolkit 中比较常见,可理解为目标实例相关信息的缩写。

          支持的缩写及含义如下:

          缩写  含义
          ===  =============================================
          A    默认的字符集
          D    库名
          F    只从给定文件中读取配置信息,类似于MySQL中的--defaults-file
          P    端口
          S    用于连接的socket文件
          h    主机名
          p    密码
          t    表名
          u    用户名
        • --dest:目标库(归档实例)的 DSN。

        • --where:归档条件。"1=1"代表归档全表。

        实现原理

        下面结合 General log 的输出看看 pt-archiver 的实现原理。

        源库日志

        2022-03-06T10:58:20.612857+08:00       10 Query SELECT /*!40001 SQL_NO_CACHE */ `dept_no`,`dept_name` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) ORDER BY `dept_no` LIMIT 1
        
        2022-03-06T10:58:20.613451+08:00       10 Query DELETE FROM `employees`.`departments` WHERE (`dept_no` = 'd001')
        2022-03-06T10:58:20.620327+08:00       10 Query commit
        
        2022-03-06T10:58:20.628409+08:00       10 Query SELECT /*!40001 SQL_NO_CACHE */ `dept_no`,`dept_name` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) AND ((`dept_no` >= 'd001')) ORDER BY `dept_no` LIMIT 1
        
        2022-03-06T10:58:20.629279+08:00       10 Query DELETE FROM `employees`.`departments` WHERE (`dept_no` = 'd002')
        2022-03-06T10:58:20.636154+08:00       10 Query commit
        ...

        目标库日志

        2022-03-06T10:58:20.613144+08:00       18 Query INSERT INTO `employees`.`departments`(`dept_no`,`dept_name`) VALUES ('d001','Marketing')
        2022-03-06T10:58:20.613813+08:00       18 Query commit
        
        2022-03-06T10:58:20.628843+08:00       18 Query INSERT INTO `employees`.`departments`(`dept_no`,`dept_name`) VALUES ('d002','Finance')
        2022-03-06T10:58:20.629784+08:00       18 Query commit
        ...

        结合源库和目标库的日志,可以看到,

        • pt-archiver 首先会从源库查询一条记录,然后再将该记录插入到目标库中。

          目标库插入成功,才会从源库中删除这条记录。

          这样就能确保数据在删除之前,一定是归档成功的。

        • 仔细观察这几个操作的执行时间,其先后顺序如下。

          (1)源库查询记录。

          (2)目标库插入记录。

          (3)源库删除记录。

          (4)目标库 COMMIT。

          (5)源库 COMMIT。

          这种实现借鉴了分布式事务中的两阶段提交算法。

        • --where 参数中的 "1=1" 会传递到 SELECT 操作中。

          "1=1" 代表归档全表,也可指定其它条件,如我们常用的时间。

        • 每次查询都是使用主键索引,这样即使归档条件中没有索引,也不会产生全表扫描。

        • 每次删除都是基于主键,这样可避免归档条件没有索引导致全表被锁的风险。

        批量归档

        如果使用 Demo 中的参数进行归档,在数据量比较大的情况下,效率会非常低,毕竟 COMMIT 是一个昂贵的操作。

        所以在线上,我们通常都会进行批量操作。

        具体命令如下:

        pt-archiver --source h=192.168.244.10,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --dest h=192.168.244.128,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --where "1=1" --bulk-delete --limit 1000 --commit-each --bulk-insert

        相对于之前的归档命令,这条命令额外指定了四个参数,其中,

        • --bulk-delete:批量删除。

        • --limit:每批归档的记录数。

        • --commit-each:对于每一批记录,只会 COMMIT 一次。

        • --bulk-insert:归档数据以 LOAD DATA INFILE 的方式导入到归档库中。

        看看上述命令对应的 General log 。

        源库

        2022-03-06T12:13:56.117984+08:00       53 Query SELECT /*!40001 SQL_NO_CACHE */ `dept_no`,`dept_name` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) ORDER BY `dept_no` LIMIT 1000
        ...
        2022-03-06T12:13:56.125129+08:00       53 Query DELETE FROM `employees`.`departments` WHERE (((`dept_no` >= 'd001'))) AND (((`dept_no` <= 'd009'))) AND (1=1) LIMIT 1000
        2022-03-06T12:13:56.130055+08:00       53 Query commit

        目标库

        2022-03-06T12:13:56.124596+08:00    51 Query LOAD DATA LOCAL INFILE '/tmp/hitKctpQTipt-archiver' INTO TABLE `employees`.`departments`(`dept_no`,`dept_name`)
        2022-03-06T12:13:56.125616+08:00    51 Query commit:

        注意:

        • 如果要执行 LOAD DATA LOCAL INFILE 操作,需将目标库的 local_infile 参数设置为 ON。

        • 如果不指定 --bulk-insert 且没指定 --commit-each,则目标库的插入还是会像 Demo 中显示的那样,逐行提交。

        • 如果不指定 --commit-each,即使表中的 9 条记录是通过一条 DELETE 命令删除的,但因为涉及了 9 条记录,pt-archiver 会执行 COMMIT 操作 9 次。目标库同样如此。

        • 在使用 --bulk-insert 归档时要注意,如果导入的过程中出现问题,譬如主键冲突,pt-archiver 是不会提示任何错误的。

        不同归档参数之间的速度对比

        下表是归档 20w 数据,不同参数之间的执行时间对比。

        归档参数执行时间(s)不指定任何批量相关参数850.040--bulk-delete --limit 1000422.352--bulk-delete --limit 1000 --commit-each46.646--bulk-delete --limit 5000 --commit-each46.111--bulk-delete --limit 1000 --commit-each --bulk-insert7.650--bulk-delete --limit 5000 --commit-each --bulk-insert6.540--bulk-delete --limit 1000 --bulk-insert47.273

        通过表格中的数据,我们可以得出以下几点:

        • 第一种方式是最慢的。

          这种情况下,无论是源库还是归档库,都是逐行操作并提交的。

        • 只指定 --bulk-delete --limit 1000 依然很慢。

          这种情况下,源库是批量删除,但 COMMIT 次数并没有减少。

          归档库依然是逐行插入并提交的。

        • --bulk-delete --limit 1000 --commit-each

          相当于第二种归档方式,源库和目标库都是批量提交的。

        • --limit 1000 和 --limit 5000 归档性能相差不大。

        • --bulk-delete --limit 1000 --bulk-insert 与 --bulk-delete --limit 1000 --commit-each --bulk-insert 相比,没有设置 --commit-each。

          虽然都是批量操作,但前者会执行 COMMIT 操作 1000 次。

          由此来看,空事务并不是没有代价的。

        其它常见用法

        (1)删除数据

        删除数据是 pt-archiver 另外一个常见的使用场景。

        具体命令如下:

        pt-archiver --source h=192.168.244.10,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --where "1=1" --bulk-delete --limit 1000 --commit-each --purge --primary-key-only

        命令行中的 --purge 代表只删除,不归档。

        指定了 --primary-key-only ,这样,在执行 SELECT 操作时,就只会查询主键,不会查询所有列。

        接下来,我们看看删除命令相关的 General log 。

        为了直观地展示 pt-archiver 删除数据的实现逻辑,实际测试时将 --limit 设置为了 3。

        # 开启事务
        set autocommit=0;
        
        # 查看表结构,获取主键
        SHOW CREATE TABLE `employees`.`departments`;
        
        # 开始删除第一批数据
        # 通过 FORCE INDEX(`PRIMARY`) 强制使用主键
        # 指定了 --primary-key-only,所以只会查询主键
        # 这里其实无需获取所有满足条件的主键值,只取一个最小值和最大值即可。
        SELECT /*!40001 SQL_NO_CACHE */ `dept_no` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) ORDER BY `dept_no` LIMIT 3;
        
        # 基于主键进行删除,删除的时候同时带上了 --where 指定的删除条件,以避免误删
        DELETE FROM `employees`.`departments` WHERE (((`dept_no` >= 'd001'))) AND (((`dept_no` <= 'd003'))) AND (1=1) LIMIT 3;
        
        # 提交
        commit;
        
        # 删除第二批数据
        SELECT /*!40001 SQL_NO_CACHE */ `dept_no` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) AND ((`dept_no` >= 'd003')) ORDER BY `dept_no` LIMIT 3;
        DELETE FROM `employees`.`departments` WHERE (((`dept_no` >= 'd004'))) AND (((`dept_no` <= 'd006'))) AND (1=1); LIMIT 3
        commit;
        
        
        # 删除第三批数据
        SELECT /*!40001 SQL_NO_CACHE */ `dept_no` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) AND ((`dept_no` >= 'd006')) ORDER BY `dept_no` LIMIT 3;
        DELETE FROM `employees`.`departments` WHERE (((`dept_no` >= 'd007'))) AND (((`dept_no` <= 'd009'))) AND (1=1) LIMIT 3;
        commit;
        
        # 删除最后一批数据
        SELECT /*!40001 SQL_NO_CACHE */ `dept_no` FROM `employees`.`departments` FORCE INDEX(`PRIMARY`) WHERE (1=1) AND ((`dept_no` >= 'd009')) ORDER BY `dept_no` LIMIT 3;
        commit;

        在业务代码中,如果我们有类似的删除需求,不妨借鉴下 pt-archiver 的实现方式。

        (2)将数据归档到文件中

        数据除了能归档到数据库,也可归档到文件中。

        具体命令如下:

        pt-archiver --source h=192.168.244.10,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --where "1=1" --bulk-delete --limit 1000 --file '/tmp/%Y-%m-%d-%D.%t'

        指定的是 --file ,而不是 --dest。

        文件名使用了日期格式化符号,支持的符号及含义如下:

        %d    Day of the month, numeric (01..31)
        %H    Hour (00..23)
        %i    Minutes, numeric (00..59)
        %m    Month, numeric (01..12)
        %s    Seconds (00..59)
        %Y    Year, numeric, four digits
        %D    Database name
        %t    Table name

        生成的文件是 CSV 格式,后续可通过 LOAD DATA INFILE 命令加载到数据库中。

        如何避免主从延迟

        无论是数据归档还是删除,对于源库,都需要执行 DELETE 操作。

        很多人担心,如果删除的记录数太多,会造成主从延迟。

        事实上,pt-archiver 本身就具备了基于主从延迟来自动调节归档(删除)操作的能力。

        如果从库的延迟超过 1s(由 --max-lag 指定)或复制状态不正常,则会暂停归档(删除)操作,直到从库恢复。

        默认情况下,pt-archiver 不会检查从库的延迟情况。

        如果要检查,需通过 --check-slave-lag 显式设置从库的地址,譬如,

        pt-archiver --source h=192.168.244.10,P=3306,u=pt_user,p=pt_pass,D=employees,t=departments --where "1=1" --bulk-delete --limit 1000 --commit-each --primary-key-only --purge --check-slave-lag h=192.168.244.20,P=3306,u=pt_user,p=pt_pass

        这里只会检查 192.168.244.20 的延迟情况。

        如果有多个从库需要检查,需将 --check-slave-lag 指定多次,每次对应一个从库。

        常用参数

        --analyze

        在执行完归档操作后,执行 ANALYZE TABLE 操作。

        后面可接任意字符串,如果字符串中含有 s ,则会在源库执行 ANALYZE 操作。

        如果字符串中含有 d ,则会在目标库执行 ANALYZE 操作。

        如果同时带有 d 和 s ,则源库和目标库都会执行 ANALYZE 操作。如,

        --analyze ds

        --optimize

        在执行完归档操作后,执行 OPTIMIZE TABLE 操作。

        用法同 --analyze 类似。

        --charset

        指定连接(Connection)字符集。

        在 MySQL 8.0 之前,默认是 latin1。

        在 MySQL 8.0 中,默认是 utf8mb4 。

        注意,这里的默认值与 MySQL 服务端字符集 character_set_server 无关。

        若显式设置了该值,pt-archiver 在建立连接后,会首先执行 SET NAMES 'charset_name' 操作。

        --[no]check-charset

        检查源库(目标库)连接(Connection)字符集和表的字符集是否一致。

        如果不一致,会提示以下错误:

        Character set mismatch: --source DSN uses latin1, table uses gbk.  You can disable this check by specifying --no-check-charset.

        这个时候,切记不要按照提示指定  --no-check-charset 忽略检查,否则很容易导致乱码。

        针对上述报错,可将 --charset 指定为表的字符集。

        注意,该选项并不是比较源库和目标库的字符集是否一致。

        --[no]check-columns

        检查源表和目标表列名是否一致。

        注意,只会检查列名,不会检查列的顺序、列的数据类型是否一致。

        --columns

        归档指定列。

        在有自增列的情况下,如果源表和目标表的自增列存在交集,可不归档自增列,这个时候,就需要使用 --columns 显式指定归档列。

        --dry-run

        只打印待执行的 SQL,不实际执行。

        常用于实际操作之前,校验待执行的 SQL 是否符合自己的预期。

        --ignore

        使用 INSERT IGNORE 归档数据。

        --no-delete

        不删除源库的数据。

        --replace

        使用 REPLACE 操作归档数据。

        --[no]safe-auto-increment

        在归档有自增主键的表时,默认不会删除自增主键最大的那一行。

        这样做,主要是为了规避 MySQL 8.0 之前自增主键不能持久化的问题。

        在对全表进行归档时,这一点需要注意。

        如果需要删除,需指定 --no-safe-auto-increment 。

        --source

        给出源端实例的信息。

        除了常用的选项,其还支持如下选项:

        • a:指定连接的默认数据库。

        • b:设置 SQL_LOG_BIN=0 。

          如果是在源库指定,则 DELETE 操作不会写入到 Binlog 中。

          如果是在目标库指定,则 INSERT 操作不会写入到 Binlog 中。

        • i:设置归档操作使用的索引,默认是主键。

        --progress

        显示进度信息,单位行数。

        如 --progress 10000,则每归档(删除)10000 行,就打印一次进度信息。

        TIME                ELAPSED   COUNT
        2022-03-06T18:24:19       0       0
        2022-03-06T18:24:20       0   10000
        2022-03-06T18:24:21       1   20000

        第一列是当前时间,第二列是已经消耗的时间,第三列是已归档(删除)的行数。

        总结

        前面,我们对比了归档操作中不同参数的执行时间。

        其中,--bulk-delete --limit 1000 --commit-each --bulk-insert 是最快的。不指定任何批量操作参数是最慢的。

        但在使用 --bulk-insert 时要注意 ,如果导入的过程中出现问题,pt-archiver 是不会提示任何错误的。

        常见的错误有主键冲突,数据和目标列的数据类型不一致。

        如果不使用 --bulk-insert,而是通过默认的 INSERT 操作来归档,大部分错误是可以识别出来的。

        譬如,主键冲突,会提示以下错误。

        DBD::mysql::st execute failed: Duplicate entry 'd001' for key 'PRIMARY' [for Statement "INSERT INTO `employees`.`departments`(`dept_no`,`dept_name`) VALUES (?,?)" with ParamValues: 0='d001', 1='Marketing'] at /usr/local/bin/pt-archiver line 6772.

        导入的数据和目标列的数据类型不一致,会提示以下错误。

        DBD::mysql::st execute failed: Incorrect integer value: 'Marketing' for column 'dept_name' at row 1 [for Statement "INSERT INTO `employees`.`departments`(`dept_no`,`dept_name`) VALUES (?,?)" with ParamValues: 0='d001', 1='Marketing'] at /usr/local/bin/pt-archiver line 6772.

        当然,数据和类型不一致,能被识别出来的前提是归档实例的 SQL_MODE 为严格模式。

        如果待归档的实例中有 MySQL 5.6 ,我们其实很难将归档实例的 SQL_MODE 开启为严格模式。

        因为 MySQL 5.6 的 SQL_MODE 默认为非严格模式,所以难免会产生很多无效数据,譬如时间字段中的 0000-00-00 00:00:00 。

        这种无效数据,如果插入到开启了严格模式的归档实例中,会直接报错。

        从数据安全的角度出发,最推荐的归档方式是:

        • 先归档,但不删除源库的数据。
        • 比对源库和归档库的数据是否一致。
        • 如果比对结果一致,再删除源库的归档数据。

        其中,第一步和第三步可通过 pt-archiver 搞定,第二步可通过 pt-table-sync 搞定。

        相对于边归档边删除的这种方式,虽然麻烦不少,但相对来说,更安全。

        到此这篇关于MySQL 中如何归档数据的实现方法的文章就介绍到这了,更多相关MySQL  归档数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

        欢迎分享,转载请注明来源:内存溢出

        原文地址:https://outofmemory.cn/sjk/2997241.html

        (0)
        打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
        上一篇 2022-09-26
        下一篇2022-09-26

        随机推荐

        • 共话桑麻是什么意思

          共话桑麻的意思:“桑麻”指的桑树和麻,“共话”指的是坐在一起聊天,“共话桑麻”就是一群人坐在一起聊着农家事。“共话桑麻”出自于唐代诗人孟浩然的一首五律《过故人庄》。《过故人庄》原文:故人具鸡黍,邀我至

          2022-12-06
          000
        • 雪是由什么变化而来的,雪在什么情况下形成的

          雪和雨一样,都是空气里的水汽冷却凝结而成的,只是凝结时天气冷的程度不同,因而有了雨、雪的区别。当天空上层的温度很低、空气中的水汽凝结时,它就不是凝结成水滴,而是由水汽直接凝结成小雪片。由气体不经过液体

          2022-12-06
          000
        • 青蛙效应是什么意思

          青蛙效应是源自美国康奈尔大学做的一个实验,他们将一只青蛙放在煮沸的大锅里,青蛙触电般地立即窜了出去。后来,人们又把它放在一个装满凉水的大锅里,任其自由游动。然后用小火慢慢加热,青蛙虽然可以感觉到外界温

        • 废话文学是什么意思

          废话文学,网络流行语,用‌‌‌‌‌‌‌‌‌‌‌‌来形容网上那些有点搞笑、看似说了话又没有任何有用信息的言论和视频。废话文学的来源:该梗出自B站评论区,起因是up主“那就叫王师傅吧”和“旅途船长”的视频

        • 康熙几岁登基

          康熙八岁登基,在位六十一年的时间,是清朝历史上在位时间最长的皇帝。因为顺治帝死得早,所以玄烨八岁那年,就在众臣的拥立下匆匆登上了皇位。康熙登基虽早,可朝政大权一直把持在辅政大臣手上,直到康熙十四岁那年

          2022-12-06
          000
        • 吴承恩哪个朝代的

          吴承恩是中国明代杰出的小说家,字汝忠,号射阳山人,汉族,淮安府山阳县人,祖籍安徽。他创作的《西游记》是中国古代第一部浪漫主义章回体长篇神魔小说,与《红楼梦》、《三国演义》、《水浒传》并称为中国古典四大

          2022-12-06
          000
        • 长坂坡之战主要内容

          公元208年八月,荆州牧刘表去世,次子刘琮继位。曹操认为此时是拿下荆州的最佳时机,于是亲率大军南下,意图夺取荆州。刘琮自觉无法抵挡便率众投降。同年九月,曹军不受任何抵抗突破宛城到达新野。刘备知道

          2022-12-06
          000
        • 教师节祝福语

          1、在某校的教师节感恩大会上,一女同学慷慨激昂的发言:啊!我亲爱的老师啊,虽然您是男地,但却用您干瘪的乳汁哺育着我们!2、愿温馨喜悦与幸福,如同吐露芬芳的花朵,洋溢在您欢乐的时光。我们最爱的老师,衷心

          2022-12-06
          000
        • 慈禧的宫殿叫什么名字

          慈禧的宫殿叫颐和园;颐和园,中国清朝时期皇家园林,前身为清漪园,是保存最完整的一座皇家行宫御苑,被誉为皇家园林博物馆。咸丰十年,清漪园被英法联军焚毁,光绪十四年重建,改称颐和园,作消夏游乐地,光绪二十

          2022-12-06
          000

        发表评论

        登录后才能评论

        评论列表(0条)

          保存