• ☆MySQL备份与恢复



    解读binlog

    通过配置参数 log-bin[=name] 可以启动二进制日志 。如果不指定name ,则默认二进制日志文件名为主机名,后缀名为二进制日志的序列号,所在路径为数据库数据所在目录 (datadir)。

    注意!!修改配置文件后,要先停止服务,再启动服务!!

    • ①# mysqladmin -uroot -p shutdown -h127.0.0.1 -P3307
    • ②# mysqld_safe –defaults-file=/etc/mysqla.cnf &
    • ③# mysql -uroot -p -P3307 -h127.0.0.1

    (1)binlog产生的过程:
    用户连上数据库执行sql(这sql就是事务),在用户空间里开辟一块空间来存放事务的第一条、第二条…,用户事务提交的时候,事务才会写到binlog里面去。binlog小的话,超了后就会写到tmp里(有个状态值可以看)。
    (2)binlog记录的格式:记录的是SQL语句。binlog存放在数据目录下,binlog会自动切换。
    binlog切换的条件

    • ①大小达到阈值 max_binlog_size
    • ②数据库重启会出现自动切换(很多小的binlog)
    • ③flush logs 强行切换

    (3)binlog的查看

    #mysqlbinlog -vv mysqlserver.000035 | tail -1000        
    #详细显示000035binlog的最后一千行

    (4)恢复某段日志
    法①
    将binlog里的相中的某段复制到一个a.a,然后
    cat a.a|mysql
    法②

    #mysqlbinlog --start-position=4777222 --stop-position=477487 -vv mysqlserver.000035 >a.a | mysql

    多个binlog的时候:

    #mysqlbinlog --start-position=4777222 --stop-position=477487 -vv mysqlserver.000001[3-9]

    (法②由于-vv,会在选中的日志头尾各加上一些东西)


    逻辑备份和物理备份、冷备、热备

    按照备份的方法可以分为:

    • Hot Backup (热备)(ibbackup、xtrabackup)
    • Cold Backup (冷备)
    • Warm Backup (温备)

    Hot Backup是指在数据库运行中直接备份,对正在运行的数据库没有任何影响 (用工具ibbackup、xtrabackup)。这种方式在MySQL 官方手册中称为 Online Backup(在线备份)。

    Cold Backup是指在数据库停止的情况下进行备份(直接拷贝相关文件),这种备份最为简单,只需要拷贝相关的数据库物理文件即可。这种方式在MySQL官方手册中称为 Offline Backup (离线备份)。

    Warm Backup备份同样是在数据库运行时进行 ,但是会对当前数据库的操作有所影响,例如加一个全局读锁以保证备份数据的一致性。

    按照备份后文件的内容 ,又可以分为:

    • 逻辑备份(mysqldump、select * into outfile)
    • 裸文件(物理)备份

    ①逻辑备份:备份后的文件是内容是可读的,一般是文本文件,内容一般是sql语句,或表内的实际数据。
    (将数据行抽取出来,写到一个文件中去)
    方法:
    mysqldump、select * into outfile
    方法的好处:可以看到导出文件的内容,一般用于数据库的升级、迁移等,但是恢复的时间往往比较长。

    ②裸文件(物理)备份:是指拷贝数据库的物理文件,数据库既可以处于运行状态(ibbackup、xtrabackup),也可以处于停止状态。恢复时间比逻辑备份短。

    (物理备份:shutdown(冷备)将数据库相关的文件cp备份出来、open(热备)使用相关的工具将数据备份出来)
    (物理备份速度块、大小小)

    按照备份的内容来分:

    • 完全备份
    • 增量备份
    • 日志备份

    (都能用xtrabackup)

    ①完全备份:对数据库的完整备份。
    ②增量备份:是指在上次的完全备份基础上,对更新的数据进行备份。
    ③日志备份:主要针对binlog的备份。通过对一个完全备份进行binlog的重做来完成数据库的point-in-time的恢复工作。MySQL复制(Replication)的原理就是异步实时进行binlog 重做。


    逻辑备份的工具之 mysqldump(mysql自带)

    备份数据库

    #mysqldump 数据库名 >数据库备份名
    #mysqldump -A -u用户名 -p密码 数据库名>数据库备份名
    #mysqldump -d -A --add-drop-table -uroot -p >xxx.sql

    导出结构不导出数据:

    mysqldump --opt -d 数据库名 -u root -p > xxx.sql

    导出数据不导出结构:

    mysqldump -t 数据库名 -uroot -p > xxx.sql

    导出数据和表结构:

    mysqldump 数据库名 -uroot -p > xxx.sql

    导出特定表的结构:

    mysqldump -uroot -p -B 数据库名 --table 表名 > xxx.sql

    导入数据:

    #mysql 数据库名 < 文件名
    #source /tmp/xxx.sql  

    常用的options :

    • ①-u:用户
    • ②-p:密码
    • ③-F:flush logs
    • ④–single-transaction:
      保证备份的一致性,实际上它的工作原理是设定本次会话的隔离级别为:REPEATABLE READ,以确保本次会话(dump)时,不会看到其他会话已经提交了的数据。
    • ⑤-l:lock tables。
      在备份前,例如9点,把所有表都加上lock。9点40备份完了,但是都是九点的。恢复=备份好的+40分钟内的binlog
    • ⑥-T:
      导出一行行的数据,默认以tab做分隔符。-T指定哪个文件夹,这条命令就得到哪个文件夹下面执行。
     -- fields-terminated-by=',!#'

    列和列之间的分隔符设置为 ,或!或#

     --fields-terminated-by=name:

    Fields in the output file are terminated(终止) by the given string(字符串).
    (输出文件中的字段由给定字符串终止)

    --fields-enclosed-by=name:

    Fields in the output file are enclosed(封闭) by the given character(字符).
    (输出文件中的字段由给定的字符封闭)

    --fields-optionally-enclosed-by=name:

    Fields in the output file are optionally(可选择的) enclosed by the given character.
    (输出文件中的字段是由给定的字符可选的封闭)

    --fields-escaped-by=name:

    Fields in the output file are escaped(逃避) by the given character.
    (输出文件中的字段是由给定的字符避开)

    dump的选项很多,具体可以用–help看。

    mysqldump备份命令

    ①备份指定的数据库或者此数据库中的某些表

    #mysqldump [options] db_name [tables]

    ②备份指定的一个或多个数据库

    #mysqldump [options] --database DB1 [DB2 DB3...]

    ③备份所有数据库

    #mysqldump [options] --all-database

    备份的例子:
    ①备份数据库test下的表emp:

    #mysqldump -uroot -p -l test emp >emp.sql

    ②备份数据库test:

    #mysqldump -uroot -p -l test >test.sql

    ③备份所有数据库:

    #mysqldump -uroot -p -l --all-database >all.sql

    ④备份数据库test下的表emp和dept:

    #mysqldump -uroot -p -l test emp dept >emp_demp.sql

    ⑤备份数据库test下的所有表为逗号分隔的文本,备份到/tmp

    #mysqldump -uroot -T /tmp -l test emp --fields-terminated-by ','
    #more emp.txt

    这里写图片描述

    备份一张表:

    #mysqldump -uroot -p -l -F --single-transaction -S /usr/local/mysqldata/mysql.sock test t1 >aa.sql

    恢复这张表:

    #mysqldump -uroot -p -S /usr/local/mysqldata/mysql.sock <aa.sql

    关注的点:
    备份出来的数据对应的binlog的日志起点 备份时候要把表给锁住,对myisam来说,备份期间锁住的表是不能用的;对innodb来说,不锁表,在备份期间结合MVCC和undo把开始备份时刻和备份完成时刻之间的数据恢复。

    跑binlog
    法①
    将binlog里的相中的某段复制到一个a.a,然后cat a.a|mysql
    法②

    #mysqlbinlog --start-position=4777222 --stop-position=477487 -vv mysqlserver.000035 >a.a | mysql

    多个binlog的时候:

    #mysqlbinlog --start-position=4777222 --stop-position=477487 -vv mysqlserver.000001[3-9]

    (法②由于-vv,会在选中的日志头尾各加上一些东西)


    物理备份工具之 mysqlhotcopy、ibbackup

    mysqlhotcopy -> myisam
    ibbackup -> innodb (花钱)

    注意:只要对myisam表 备份,都会锁住表!
    而对innodb来说,就不会锁表!


    物理备份工具之 xtrabackup(第三方的备份工具)的安装

    对 myisam、innodb均可备份,模拟的ibbackup
    使用xtrabackup备份主库
    主备库同时安装xtrabackup软件

    yum install cmake gcc gcc-c++ libaio libaio-devel automake autoconf bison libtool ncurses-devel libgcrypt-devel libev-devel libcurl-devel vim-common

    rpm -ivh libev4-4.15-7.1.x86_64.rpm

    rpm -ivh libev-devel-4.15-21.1.x86_64.rpm

    (上面两个软件需要单独安装)
    ④解压 percona-xtrabackup-2.3.4.tar.gz

    #cmake -DBUILD_CONFIG=xtrabackup_release -DWITH_MAN_PAGES=OFF && make -j4
    #make install

    [root@mysqlserver ~]# echo $PATH
    /usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/mysql/bin:/usr/local/xtrabackup/bin
    [root@mysqlserver ~]# innobackupex --help

    (修改PATH变量eg:)

    [root@root mysql]# PATH=$PATH:/usr/local/mysql/bin
    [root@root mysql]# vim /root/.bash_profile
    

    (rpm版本的安装详见:
    http://blog.csdn.net/qq_18312025/article/details/78897023

    ⑥建立备份目录 /backup/
    ⑦授权备份目录

    #chown -R mysql:mysql /backup
    # innobackupex --user=root --password=rootroot --no-timestamp /backup
    # innobackupex --apply-log /backup/

    Xtrabackup的介绍

    是Percona公司开发的 基于InnoDB的在线热备工具。开源、免费、占磁盘小。
    包含了两个主要的工具:xtrabackupinnobackupex
    xtrabackup只能备份innodb和xtraDB两种表,不能备份myisam表;
    innobackupex是一个封装了xtrabackup的perl脚本。支持myisam和innodb。但对myisam备份时需要加锁。

    innobackupex的全备 原理过程

    ①备份开始时先会开启一个后台检测进程,实时检测mysql redo的变化,一旦发现redo有新的日志写入,立即将日志记入后台日志文件 xtrabackup_log 中;
    ②之后复制InnoDB的数据文件和系统表空间文件ibdata1;(大部分的时间耗在这里)
    ③复制结束后,执行 flush tables with read lock 操作,复制 .frm(表结构定义的文件)、.MYI、.MYD等文件。执行flush tables with read lock 是防止数据表发生DDL操作,并且在这一时刻获得binlog的位置;
    ④最后unlock tables,把表设为可读写状态,最终停止 xtrabackup_log 。
    这里写图片描述

    (其实,从flush tables 到unlock tables 是对myisam表的备份过程,所以有加锁;innodb是不用加锁的,不影响使用)
    注意!在备份期间,产生的所有的redo log都会拷贝出来,放在/backup下的xtrabackup_logfile中

    这里写图片描述

    备份目录下的文件

    ①backup-my.cnf
    ②ibdata1、mysql、performance_schema、test、tpcc1000
    ③xtrabackup_logfile
    ④xtrabackup_info
    ⑤xtrabackup_binlog_info #从哪开始恢复
    ⑥xtrabackup_checkpoints

    ①backup-my.cnf:备份命令用到的配置选项信息

    这里写图片描述

    ②ibdata1、mysql、performance_schema、test、tpcc1000:
    这些是备份的数据文件
    ③xtrabackup_logfile:备份期间产生的redo log存放在此。
    ④xtrabackup_info:备份的详细信息

    [root@LPeng backuptwo]# cat xtrabackup_info
    uuid = d14b2e72-7d7a-11e6-a809-000c29f7a0ac   #通用唯一识别码 (Universally Unique Identifier)
    name = 
    tool_name = innobackupex    #备份工具的名字
    tool_command = --defaults-file=/etc/mysqla.cnf --user=root --password=... --port=3307 --no-timestamp --socket=/usr/local/mysqldata/mysql.sock /backuptwo    #备份时执行的命令
    tool_version = 2.3.4        #版本号
    ibbackup_version = 2.3.4    #对应的ibbackup的
    server_version = 5.6.15-log #mysql的版本
    start_time = 2016-09-18 16:33:14    #备份的开始时间
    end_time = 2016-09-18 16:35:13  #备份的结束时间
    lock_time = 0   #锁了多少时间
    binlog_pos = filename 'mybin.000010', position '120'    #binlog的位置。恢复的时候就从这开始恢复。
    innodb_from_lsn = 0 #innodb的lsn开始。日志序号 (LSN:Log sequence number) 。
    innodb_to_lsn = 1400172578  #innodb的lsn结束。最新的数据页。增量备份时从此处往后开始备份。
    partial = N #局部的
    incremental = N #增量=N,没有增量  
    format = file   #备份的形式  
    compact = N 
    compressed = N  #没有压缩   
    encrypted = N       #没有加密

    ⑤xtrabackup_binlog_info:mysql服务器当前正在使用的二进制日志文件及至备份这一刻为止binlog的位置。 -> 恢复时开始的位置
    这里写图片描述
    ⑥xtrabackup_checkpoints:备份类型(如完全或增量)、备份状态(如是否已经为prepared状态)和LSN(日志序列号)范围信息。
    这里写图片描述
    from_lsn:从哪个lsn开始备份
    to_lsn:备份的所有的lsn的一个范围
    last_lsn:备份的最后一个lsn
    (注:⑤⑥的信息都在xtrabackup_info里面有!)


    用innobackupex 全备的操作步骤

    1、首先新建一个备份目录/backup,然后将属主和属组均设置为mysql:

    #chown -R mysql:mysql /backup

    2、备份数据库:将相关的数据文件拷贝出来

    #innobackupex --user=root --password="" --no-timestamp /backup (不建立带时间戳的目录)

    如果sock文件不是默认或者端口不是3306时:

    #innobackupex --defaults-file=/etc/mysqla.cnf --user=root --password=123123 --port=3307 --no-timestamp --socket=/usr/local/mysqldata/mysql.sock  /backup

    备份成功的话,在最后会有一个complete!
    然后模拟环境:插入数据/删除一张表/更新数据等(体验apply-log的功能)

    3、apply-log

    [root@localhost backup]# innobackupex --apply-log /backup/ 
    [root@localhost backup]# cat xtrabackup_checkpoints
     backup_type = full-prepared 

    不仅是全备而且apply log了

    4、关闭数据库,删除数据库(注意一定不要删除binlog)
    将binlog从里面剪切出来,注意不要放到backup下面,否则一会恢复的时候binlog页都给恢复了。

    5、恢复数据库:就是把backup下面的文件再拷贝回去

    [root@localhost backup]# innobackupex --defaults-file=/etc/mysla.cnf --copy-back /backup/ 
    #注意:最好指定配置文件,有的会报如下错误

    这里写图片描述

    6、文件拷贝完成记得一定要修改datadir下面文件的属主和属组,重启数据库

    # chown -R mysql:mysql /mysqldata/
    # mysqld_safe --defaults-file=/etc/my.cnf --user=root &

    7、跑binlog:

    # cd /backup/
    # cat xtrabackup_binlog_info
    # mysqlbinlog --strat-position=686 /mysqlbin/mybin.000002 | mysql -uroot -p123123 -h127.0.0.1

    备份与恢复的方案/思路

    ①备份对生产的影响、备份和数据传输
    ②备份的恢复时间:
    重点是

    • 1.[copyback]备份拷贝回来的时间=500G(数据库大小)/100M(带宽,1000b/s)==5000s==1.5h
    • 2.binlog的恢复时间非常不可控,需实际测试![有可能某个sql特别慢]
      专门建立一个从库,从库就是用来放备份的。这样可以经常做全备。

    • 1.备份期间IO很大,库基本是不可用的;

    • 2.备份完成后取走时,会产生很大的带宽、IO(备份进来一次,取走一次)
    • 3.周一全备,,周二周三增量,每天备份binlog(恢复的时候时间非常长,20天常见的)

    增量备份

    主要是对innodb而言的,对于myisam和其他的表还是一个全拷贝。
    每个数据页都会有一个lsn号,每产生一次变化,lsn都会发生改变。
    增量备份:备份的是发生变化的页(这些数据页指的是LSN大于xtrabackup_checkpoints中给定的 LSN),增备是基于全备的,第一次的增备的数据必须基于上一次的全备。

    增量备份的过程:
    这里写图片描述

    • 缺点:要把所有的页都扫描一遍才能知道哪些页发生变化,所以备份的时候对库的压力并没有变小,只是备份出来的数据量有可能变少了。
    • 好处:恢复的时候是可控的,可以把增量apply到全备里面;备份完成之后就可以apply到全备里面去,不是非要等到恢复的时候再apply。

    完全备份+增量备份+apply-log

    增量备份在恢复的时候是可控的。
    【注意!下面的备份恢复过程中,rpm安装的mysql,不用加–socket,–defaults-file,–port】
    1.完全备份

    #innobackupex --defaults-file=/etc/mysqla.cnf --user=root --password=123123 --port=3307 --socket=/usr/local/mysqldata/mysql.sock  /quanbei

    (注意!不加 no-timestamp 就会自动生成带时间戳的目录,增备常带时间!)

    ①并行:

    innobackupex [--parallel =NUMBER-OF-FORKS]  BACKUP-DIR

    当IO没有达到最大吞吐量时,就可以采用并行。能够充分利用IO的性能。
    并行:加大吞吐量,快点备份完。
    ②限流 –throttle =100
    备份的时候可以限流,限制流量为100,即:控制IO使用量,延长备份时间,降低对备份库的使用影响。)
    (注:innobackupex 备份myisam引擎的表(performance_schema和myisam两个库表都为myisam引擎)时注意会短时间加锁(flush table with read lock)!从而影响生产!;
    备份时,建议加 –safe-slave-backup 选项,会停止从库的更新进行备份。会大大提升备份速度。)

    模拟:随便添加点数据

    2.新建增量备份的目录 /backup_incre ,然后授权

    3.第一次增量备份 (–incremental-basedir 是上次全备的目录)(增量备份希望有时间戳)

    #innobackupex --defaults-file=/etc/mysqla.cnf --user=root --password=123123 --socket=/usr/local/mysqldata/mysql.sock --incremental /backup_incre --incremental-basedir=/quanbei/

    log scanned up to (日志lsn号)如果出现这些,说明产生了新页)
    在备份的过程中会有大量的log scan up to:不断的记日志,数据页不断的更新;说明我读到的这个数据页在不断的更新。
    !!注意:备份期间innodb表也会短时间的加锁,最好在从库上备份,并且暂停主库对从库的更新。

    4.第二次增量备份(basedir应指定为上次增量备份的目录)
    跟第一次一样,只不过要修改为上一次增备的目录 –incremental-basedir=/backup_incre/上次的时间戳目录/(有xtrabackup_checkpoints)

    5.把增量prepare(即apply-log)到全备里面去,只要后面有增量就要一直redo-only!

    #innobackupex --defaults-file=/etc/mysqla.cnf --user=root --password=123123 --port=3307 --socket=/usr/local/mysqldata/mysql.sock  --apply-log-(-only) --redo-only /quanbei/  --incremental-dir=/backup_incre/第一个时间戳目录/

    (mysql5.7 –apply-log要加-only)

    6.增量备份的prepare
    将第二次的增备添加到全备中(由于是添加的最新的增备,后面没有增量了,所以不用再加–redo-only)

    #innobackupex --user=root --password=123123 --apply-log(-only)  /quanbei/ --incremental-dir=/backup_incre/新的时间戳目录/

    7.破坏数据库,但是一定不能破坏binlog!!
    杀死mysqld进程(ps -ef|grep mysqld ; kill -9 进程号)、把/mysqldata/下的所有binlog备份出来、删除/mysqldata/下的内容。

    8.进行恢复

    #innobackupex --copy-back(--move-back) /quanbei/

    copy-back和move-back的区别:
    如果备份和数据库所在的是一个文件系统,move-back的时候其实就是给了一个名字,速度非常的快。

    9、恢复之后,注意看/mysqldata/下的文件的权限。

    # chown -R mysql:mysql /mysqldata/

    10、启动mysql

    # mysqld_safe --defaults-file=/etc/my.cnf --user=root &

    11、登录mysql,看看之前删除的数据有没有回来

    12、恢复binlog(此时恢复的是备份期间产生的redo)
    ①查看备份恢复到具体位置:

    # cd /quanbei/
    # cat xtrabackup_binlog_info

    这里写图片描述
    ②恢复

    #mysqlbinlog --strat-position=686 /mysqlbin/mybin.000002 | mysql -uroot -p123123 -h127.0.0.1

    innobackupex 常用参数详解

    • –apply-log :全备完毕后接着进行apply 备份。
    • –compress
    • –compress-threads=NUMBER-OF-THREADS:备份的时候压缩非常消耗CPU,压的时候可以加大cpu的力度
    • –copy-back :恢复
    • –encrypt=ENCRYPTION-ALGORITHM]:备份有加密的需求
    • –no-timestamp:备份完之后不生成时间戳
    • –compact:优化,所有表上的二级索引不备份。只备份表数据。
    • –parallel=NUMBER-OF-FORKS:只支持全备,备份的时候可以用并发(并行)的方式去备份(在带宽足够的条件下,可以大量的减少备份的时间)
    • –throttle=# 阈值, 限流:限制流量,控制他的IO使用量,降低对主库使用的影响,不要对生产产生影响
    • –safe-slave-backup:停止对从库的更新,然后进行备份,这样备份非常快。
    • –incremental :接增量备份的目录
    • –incremental-basedir :第一次接全备的目录,后来接上次增备的目录
    • –log-copy-interval=# :每隔多长时间记录一下日志
    • –kill-long-queries-timeout=#:如果系统里有一个长事务一直未提交,加锁加不上,那么备份的时候就会hang住,等多长时间事务不提交加不上锁的话就把他杀死。
    • –ftwrl-wait-timeout=# :
    • –no-lock:在备份期间不加锁。使用这个参数要保证在备份期间没有DDL(会导致数据不一致) ,没有对myisam表的更新。
    • –redo-only :This is necessary if the backup will have incremental changes applied to it later. 只要后面还有增量就一定要加上redo-only。

    MySQL启动流程分析:

    1、cat /etc/my.cnf 文件是否存在,文件内容是否正确,主要看datadir
    2、看datadir的权限
    3、进入datadir,看里面的内容是否齐全:
    ibdata1,ib_logfile,mysql,performation_scheme,information_scheme
    4、确认一下errorlog,pid文件,sock文件
    5、mysql_safe –defaults-file=/etc/my.cnf &
    6、ps -ef | grep mysqld
    7、tail -100f errorlog
    8、登陆mysql -uroot -p -h127.0.0.1 或者mysql -uroot -p -S /…/…sock


    数据的导入和导出(如何处理纯文本文件里的行数据)

    一般都是基于表的。
    导出数据:两种方式
    1、select … into outfile
    这里写图片描述

    options:

    select * from t3 into outfile "/data/1.txt" fields terminated by ',' ;    

    列和列之间用逗号做分隔符

    select * from t3 into outfile "/data/1.txt" fields terminated by ',' lines terminated by '
    ' ; 

    行和行之间用回车作分隔符

    select * from t3 into outfile "/data/1.txt" fields terminated by ',' enclosed by '"' ;

    所有的列都加单引号

    select * from t3 into outfile "/data/1.txt" fields terminated by ',' optionally enclosed by '"' ;  

    只有varchar和char列加单引号

    insert into t4 values(1,'	dasd');      #插入一行数据以tab分隔
    select * from t4 into outfile "/data/1.txt" fields terminated by ',' escaped by '\';' ; 

    只在个别的字符类型上加引用符

    2、mysqldump -T

    mysqldump -u username- T target_ dir dbname tablename [option]

    导入数据

    1、load data

    mysql>LOAD DATA [LOCAL] INFILE 'filename' INTO TABLE tablename [option] 

    load比insert into速度要快好多,load属于批量导入。

    2、mysqllimport

    Shell> mysqlimport -u root -p*** [--LOCAL] dbname order_tab.txt [option]

    这里写图片描述


    Create By LPeng

  • 相关阅读:
    HttpServletRequest这篇文章就够了
    java工程师linux命令,这篇文章就够了
    http简介看这篇就够了
    微服务资源springboot、springcloud、docker、dubbo项目实战等倾心分享
    jdbc就是这么简单
    Ambari WebUI-集群管理操作使用说明
    【Java并发编程实战-阅读笔记】02-对象的共享
    【Java并发编程实战-阅读笔记】01-线程安全性
    kafka+SparkStreaming以及Kafka+Kerberos+Sentry+SparkStreaming代码调试问题与分析
    【译】回调地狱 Callback Hell
  • 原文地址:https://www.cnblogs.com/lpeng94/p/12546447.html
Copyright © 2020-2023  润新知