��������

Binlog配置

启动配置

binlog_row_event_max_size

当启动ROW模式记录binlog时,该配置用于限制单事件的大小,byte为单位。尽可能地,储存在binlog的数据行会被划分为不同组中,且组大小不超过设定值。

log_bin_basename

log_bin_basename用于指定binlog文件的文件前缀,其包含了储存路径和文件名的一个固定前缀,这个固定前缀默认为hostname_bin

一个完整的binlog文件名由log_bin_basename和递增的数字序列后缀组成,比如C:ProgramData\MySQL\MySQL Server 8.0\Data\LEVI_bin.000001

log_bin_index

binlog的索引文件,内容包含了所有binlog的名称,用于快速定位到指定binlog中。默认地,binlog索引文件的名称为log_bin_basename和.index后缀组成, 比如C:\ProgramData\MySQL\MySQL Server 8.0\Data\LEVI_bin.index


以下配置将影响哪些语句写入二进制日志,从而由复制源服务器发送到其副本。对于副本,还有一些选项可以控制从源接收到的哪些语句应该执行或忽略。

binlog_do_db

该选项的实际效果取决于是以ROW模式还是Statement模式记录binlog.

binlog_do_db可以指定多个数据库名, 只是需要重复多次使用该选项,每次指定一个数据库名, 而不是一次提供一个由逗号分隔的数据库名列表。

基于Statement模式

概括地说, 该选项会选择一个数据库名,只有在默认数据库为指定数据库的情况(使用USE db_name)下,后续的修改动作才会被记录到binlog中.

如果配置了binlog_do_db=sales但默认数据库为prices, 即使执行了UPDATE sales.january SET amount=amount+1000;这条语句也不会被记录到binlog中.

如果配置了binlog_do_db=sales且默认数据库为sales, 即使执行的是UPDATE prices.discounts SET percentage = percentage + 10;,这条语句也会被记录到binlog中.

基于Row模式

概括地说,该选项会选择一个数据库名,只有属于指定数据库的表内容的修改才会被记录到binlog中,默认数据库不再视为一个影响因素。

如果配置了binlog_do_db=sales但默认数据库为prices, 执行语句为UPDATE sales.january SET amount=amount+1000;这条语句会被记录到binlog中.

如果配置了binlog_do_db=sales且默认数据库为sales, 但执行的是UPDATE prices.discounts SET percentage = percentage + 10;,这条语句则不会被记录到binlog中.

binlog_ignore_db

同样的,该选项的实际效果取决于是以ROW模式还是Stratement模式记录binlog

基于Statement模式

如果默认数据库是该选项指定的数据库,所有的修改动作都不会被记录到binlog中

MySQL 5.7.2以前, 如果没有指定默认数据库, 该配置会导致任何带有完整表名的语句都不会被记录

基于Row模式

如果修改的表位于该选项指定的数据库中,该表上的所有修改动作都不会被记录到binlog中。


binlog_checksum

MySQL支持binlog校验和的读写,默认为CRC32算法。无法在一个事务中更改该配置。

如果要控制副本从relay log中的校验和的读取,可以使用slave_sql_verify_checksum

系统配置

系统配置可以在服务器启动前设置,也可以在运行时更改。

binlog_cache_size

在一次事务中,能够支持所有更改记录的内存大小。默认为4096,单位为Bytes。如果某次事务的修改数据大小超过了预设值,超量数据会被记录到临时文件中。在事务提交后,内存缓冲区和临时文件中的数据才会被刷到bin log中。

binlog_cache_size只为事务缓存设置大小, 语句的缓存大小由binlog_stmt_cache_size控制.

binlog_checksum

binlog_checksum目前支持两个值: NONE,CRC32。默认值为CRC32. 如果设置为NONE,服务器则通过写入和检查每个事件的事件长度(而不是校验和)来验证它是否只向二进制日志写入了完整的事件。

在MySQL 8.0.20及以前版本,组复制不支持checksum,如果需要兼容旧版本的副本,数据源(或者说主机)需要明确设置该配置为NONE。

如果在运行时修改该配置,会导致已有的bin log翻转重设,因为校验和必须写入整个二进制日志文件,而不能只写入其中的一部分

binlog_error_action

当服务器无法正常写入、刷新或是同步bin log时, binlog_error_action用于配置后续的处理动作,这些意外可能导致数据源的bin log不一致,副本断开同步。binlog_error_action支持两个值: ABORT_SERVER和IGNORE_ERROR, 默认为ABORT_SERVER。

ABORT_SERVER会使得服务器随时在遇到bin log异常时停止记录并退出, 再重新启动时, 恢复过程等同于服务器意外停止时的处理过程.

IGNORE_ERROR会让服务器忽略发生的意外并继续正在执行的事务, 随后记录发生的异常并停止记录, 但数据更新依然继续。如果要恢复bin log则需要重启服务器。

binlog_expire_logs_seconds

设置bin log过期时间, 单位为秒,默认时长为30天。过期时间结束后,bin log文件会被自动删除。在MySQL 8.0.29及以后,过期自动删除bin log的行为可以被binlog_expire_logs_auto_purge控制, 这一变量的优先级高于binlog_expire_logs_seconds的任何设置。当在旧版本中,只能通过设置binlog_expire_logs_seconds = 0来阻止过期自动删除bin log的行为。

binlog_format

设置bin log的记录形式, binlog_format目前支持: MIXED,STATEMENT和ROW, 默认为ROW。

binlog_format将在MySQL 8.0.34后弃用, 意味着未来版本将只保留ROW形式。因此,任何新的 MySQL 复制设置都只能使用基于行的日志记录

在MySQL 5.7.7之前, binlog_format默认值为STATEMENT, MySQL 5.7.7及以后, binlog_format默认值为ROW

根据不同形式,服务器会以不同方式将事件记录到bin log中。

STATEMENT:数据源会以SQL语句为粒度记录bin log。副本在同步数据时会依次执行记录好的SQL语句。

ROW:数据源会以数据行为粒度记录到bin log,记录内容可以指示每张表的每一行是如何变换的。副本在同步数据时直接复制数据行的变换结果。

MIXED:MIXED状态下,通常使用STATEMENT形式记录,在特殊情况下,依据SQL语句和储存引擎,会切换到ROW模式记录

通常情况下,MIXED模式做到提供数据完整性和性能的最佳组合。STATEMENT模式和ROW模式的侧重不同,可用于服务特定场景。了解这两种模式的优劣,可以帮助选择出更为适合的模式。

ADVANTAGES-STATEMENT:

  1. 技术成熟稳定;
  2. 因为是以SQL语句为粒度,实际记录到bin log的数据量更少,数据恢复和复制的耗时也会更短;

DISADVANTAGES-STATEMENT:

  1. 并不是所有修改操作都能被STATEMENT模式恢复或复制的, 一些结果不确定的行为,如UUID()/RAND()会使得副本在同步数据时得到不一样的结果;
  2. 批量插入如INSERT…SELECT会请求比起ROW模式更多的行锁
  3. 批量更新如果使用了全表查询, 也会比起ROW模式请求更多的行锁
  4. 使用了AUTO_INCREMENT的INSERT操作会阻塞其他的INSERT操作
  5. 对于复杂语句, 如果实际修改的行数并不多, 实际效率可能还不如ROW模式, 因为STATEMENT模式下副本必须重复评估和执行一次

ADVANTAGES-ROW:

  1. 能够恢复所有更改, 最能保证数据地完整性
  2. 数据源需要更少的行锁, 进而能提高并发
  3. 副本在执行修改语句时也只需要更少的行数, 因为是以数据行为粒度的, 只需要锁住目标行即可.

DISADVANTAGES-ROW:

  1. bin log的实际记录量普遍地明显高于STATEMENT模式, 数据恢复和复制的耗时也会更长
  2. 如果修改了大BLOB或大TEXT数据, 记录到bin log则会占用大量的空间
  3. 副本在恢复和复制数据的时候, 无法知道数据源的行为轨迹. 只能通过mysqlbinlog --base64-output=DECODE-ROWS工具来了解前后的数据变化

binlog_group_commit_sync_delay

控制bin log提交与同步到磁盘的延迟等待时间, 单位为微秒。默认情况下,binlog_group_commit_sync_delay 设置为 0,即没有延迟。将 binlog_group_commit_sync_delay 设置为微秒级延迟,可以使更多事务同一批同步到磁盘,从而减少提交一组事务的总体时间

binlog_row_event_max_size

当选用ROW模式时, 该配置可以限制单次bin log记录事件的大小。尽可能地,服务器会将bin log中储存的数据行划分为事件,且事件大小不超过设定值。除非事件本身不可分割,比如一次事务。

binlog_row_image

当选用ROW模式时, 该配置可以决定向bin log写入哪些数据行. binlog_row_image当前可选值: FULL、MINIMAL和NOBLOB,默认为FULL

在ROW模式中,每行数据变动事件都包含两个快照。“before”快照,在搜索要更新的行时,要与该快照的列进行匹配;“after”快照则包含了具体更改。

  1. DELETE操作只会记录“before”快照;INSERT操作只会记录“after”快照;UPDATE操作会记录两个快照

  2. “before“快照只会留有能唯一定位数据行的最小列集,如果数据表有主键,就只需要保留主键列即可。

FULL:记录所有列的”before“快照和”after“快照

MINIMAL:只记录”before“快照中能唯一定位数据行的最小列集,也只记录”after“快照中被SQL语句指定的字段或自增字段

NOBLOB:记录所有列的”before“快照和”after“快照,但那些不能用于定位数据行或没被修改的BLOB列或TEXT列不会被记录

binlog_row_metadata

当选用ROW模式时, 该配置可以决定写入bin log的表元数据数量. binlog_row_metadata当前可选值: MINIMAL、FULL,默认为MINIMAL。

MINIMAL:只记录SIGNED标志、列字符集和几何类型的元数据;

FULL:记录完整的元数据,额外包括:列名、ENUM或SET类型值、主键信息等。

该配置选用为FULL主要用于以下场景:

  1. 副本需要利用完整的元数据做字段转换,以适用于表结构与数据源表结构不一致的情况
  2. 外置程序需要通过完整的元数据来还原事件,再将数据储存到其他数据库中

binlog_row_value_options

当选用ROW模式或是MIXED模式时,该配置会以一种节省空间的bin log格式记录。binlog_row_value_options当前可选值: PARTIAL_JSON和””, 默认为空字符串.

PARTIAL_JSON针对于UPDATE操作, 且该操作利用了JSON_SET()、JSON_REPLACE()、JSON_REMOVE()函数来修改JSON列。这时,服务器只会在”after“快照中记录修改的部分,而非完整的JOSN文本。

binlog_row_value_options = PARTIAL_JSON搭配着binlog_row_image = MINIMAL或binlog_row_image = NOBLOB能最大地节省bin log空间开销,因为JSON文本主要储存在NOBLOB列或TEXT列。但搭配着binlog_row_image = FULL的效果就没那么好, 因为”before”快照依然会保留完整的JSON文本。

expire_logs_days

在MySQL 5.7及以前, 该配置用于指示服务器自动删除bin log文件的间隔天数, 默认值为0, 最大值为99, 单位为天。当expire_logs_days = 0时, 服务器不会自动删除bin log。通过PURGE BINARY LOGS可以手动清除bin log文件。

在MySQL 8.0,binlog_expire_logs_seconds替代了该配置。

log_slave_updates

用于决定副本在接受数据源的修改记录时, 是否要同步将修改记录到自己的bin log中。log_slave_update是个boolean值, 默认为OFF.

当数据库既是副本又要作为数据源的时候, 可以开启该配置。比如在链式的主从复制架构中:A->B->C, B作为A的副本的同时B又是C的数据源, 这时B就要开启log_bin又要开启log_slave_updates

max_binlog_cache_size

用于限制一次事务能使用的最大内存容量, 该配置的最小值为4096, 单位为Bytes. 官方建议最大值设为4GB左右, 因为这也是MySQL目前能处理的bin log最大容量。

和binlog_cache_size一样, max_binglog_cache_size可以在运行时更改, 但只会在新会话中生效。

max_binlog_size

bin log文件的理论最大容量, 该配置最小值为4096, 单位为Bytes. 默认值且最大值为1GB.

如果某次事务的写入导致bin log容量超阈值, 服务器并不会强行拆分事务到两个不同的bin log中, 而是直接写入当前bin log. 所以1GB只是理论值, 如果经常执行大型事务, 还是会有不少超阈值的bin log文件.

sql_log_bin

该配置可以控制当前会话的修改是否记录到bin log中。 sql_log_bin的类型为boolean, 默认值为ON.

如果向数据源的修改并不想同步到副本, 可以临时性的设置sql_log_bin = OFF来禁止bin log记录。

动态设置会话级的系统配置,应使用SET SESSION语句。比起在运行时修改全局级的系统变量,SET SESSION一般无需SUPER权限。但对于特殊的会话级系统配置如binlog_format和sql_log_bin, 它们也会影响到其他会话, 因此也需要SUPER权限.

sql_log_bin不能在事务中或是子查询中设置。

sync_binlog

控制MySQL服务器向磁盘同步bin log记录的频率. sync_binlog的最小值为0, 默认值为1, 单位为bin log收集到提交组的次数.

sync_binlog = 0: 禁止服务器主动刷盘, 这种情况下只能依赖于操作系统本身时不时地刷盘. 这种配置能得到最佳性能, 但也带来极高的bin log丢失的隐患, 比如因系统断电或崩溃, 服务器提交了尚未记录到bin log的事务.

sync_binlog = 1: 在每次事务提交前就先将记录刷到bin log中. 这种配置能最大程度地保证记录的完整性, 但带来的大量磁盘写会影响整体性能.

sync_binlog = N: 在N次提交组被服务器收集后, 才会将记录刷到bin log中. N的取值需要在性能和记录完整性上做权衡.

对于InnoDB引擎的事务, 官方建议以下配置, 能尽可能保证耐久性和一致性:

从库配置

server-id

无论主从库, server-id都必须保证唯一性。该配置可从MySQL配置文件或show variables等方式查看。

启动配置

大多启动命令可以在执行CHANGE MASTER TO语句的时候一同设置,但其他如--replicate-*等配置必须在副本启动后才能设置。

max-relay-log-size

relay log的理论最大容量, 超阈值后relaybinlog_size log则自动翻页。max-relay-log-size默认为0,单位为Bytes。如果max-relay-log-size = 0,则参考max_binlog_size

replicate-do-db

该配置用于建立一个复制过滤器, 在副本复制数据源bin log的时候, 只复制指定数据库名的更改。过滤器的细致的效果取决于副本采用STATEMENT模式还是ROW模式

基于Statement模式

配置会告知负责复制的SQL线程,将复制限制在那些默认数据库名为配置值的SQL语句上。

如果副本以replicate_do_db = sales启动,但在数据源上执行以下命令,那么这条UPDATE语句不会被副本复制:

1
2
USE prices;
UPDATE sales.january SET amount=amount+1000;

基于Row模式

配置会告知负责复制的SQL线程,将复制限制在那些发生指定数据库上的SQL语句。默认数据库则不作为影响因素

如果副本以replicate_do_db = sales启动,即使在数据源上执行以下命令,这条UPDATE语句也会被副本复制:

1
2
USE prices;
UPDATE sales.february SET amount=amount+100;

除了以上两点差异,当单语句出现多数据库更新时,这两种模式的效果也大不相同:

假设副本以replicate_do_db = db1启动, 在数据源上执行以下命令:

1
2
USE db1;
UPDATE db1.table1, db2.table2 SET db1.table1.col1 = 10, db2.table2.col2 = 20;

如果副本采用STATEMENT模式,db1和db2的更改都会被副本复制。如果副本采用ROW模式,只有db1的更改会被副本复制

以上述情况为例,如果ROW模式下想要db1,db2都被副本复制,除了副本执行两次replicate_do_db外, 还可以通过--replicate-wild-do-table=db.%来实现模糊匹配,这可以用于一些会自动扩容的数据表。

replicate_ignore_db

该配置用于建立一个复制过滤器, 在副本复制数据源bin log的时候, 忽略掉指定数据库名的更改。过滤器的细致的效果取决于副本采用STATEMENT模式还是ROW模式

基于Statement模式

配置会告知负责复制的SQL线程,忽略掉任何发生在默认数据库名为指定数据库上的更改。

基于Row模式

配置会告知负责复制的SQL线程,忽略掉那些发生在指定数据库上的SQL语句。默认数据库不再作为影响因素。

replicate_do_table

该配置会告知负责复制的SQL线程, 将复制限定在指定数据表上。如果要指定多张表,只能多次执行该配置,每次指定一张表。

对于在多数据库上或默认数据库上的更新,该配置的效果都一样,不受STATEMENT模式或ROW模式的影响。

replicate_ignore_table

该配置会告知负责复制的SQL线程,不去复制在指定数据表上的更改,即使在同一SQL语句中有其他数据表也被更改了。如果要指定多张表,只能多次执行该配置,每次指定一张表。

replicate_rewrite_db

--replicate-rewrite-db = "from_name->to_name"。该配置会告知负责复制的SQL线程, 从数据源指定数据库from_name里复制的更改, 要更名到副本的指定数据库to_name中。

skip-slave-start

告知副本服务器是否要在服务启动时同时启动复制线程, skip_slave_start默认为OFF, 这时需要START SLAVE来手动启动复制线程

slave-skip-errors

通常地, 副本服务器在遇到复制错误时会停止复制。而该配置可以允许副本服务器跳过指定错误码的错误,但这可能会带来主从数据的不一致,所以在使用该配置时必须十分清楚错误发生原因。

slave-skip-errors目前可选值有:OFF、[list of error codes]、all、ddl_exist_errors,默认值为OFF。

对于可选用的错误码,应先参考副本错误日志中的错误信息和SHOW SLAVE STATUS输出,再考虑是否要将错误码加入到配置项中。官网文档 Error Messages and Common Problems中列出了详细的服务器错误码。上述的 ddl_exist_errors 就等同于列表: 1007,1008,1050,1051,1054,1060,1061,1068,1094,1146

系统配置

init_slave

该配置指定的语句会在每次复制线程启动时, 由副本服务器执行一次。上诉的START SLAVE就会触发init_slave。

复制线程会在执行 init_slave 之前向客户端发送确认。因此,不能保证 START SLAVE 返回时,init_slave会被执行完。

rpl_stop_slave_timeout

该配置可以控制STOP SLAVE命令在超时前需要等待的时间, rpl_stop_slave_timeout最小值为2, 单位为秒. 最大值和默认值为1年。

如何配置主主

以一体机的MySQL 5.5.54-log为例,配置一组主主同步。假设主机A(192.168.1.106)和主机B(192.168.1.107)要互为主备。

添加主从同步账户

在A主机上执行以下命令,用于创建和B通信的账户

1
2
grant replication slave on *.* to 'replication'@'192.168.1.107' identified by '000000';
flush privileges;

在B主机上执行 以下命令,用于创建和A通信的账户

1
2
grant replication slave on *.* to 'replication'@'192.168.1.106' identified by '000000';
flush privileges;

修改主机A、B配置

手动修改主机A配置文件,名为my.ini或my.cnf。要求只同步test数据库下的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]  
server-id = 1
binlog-format=ROW
log-bin="A-bin"
binlog-do-db = test
binlog-ignore-db=mysql,information_schema
#主主需加入的部分
log-slave-updates
sync_binlog=1
auto_increment_offset=1
auto_increment_increment=2
replicate-do-db = test
replicate-ignore-db = mysql,information_schema

手动修改主机B配置文件。要求只同步test数据库下的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]  
server-id = 2
binlog-format=ROW
log-bin = "B-bin"
replicate-do-db = test
replicate-ignore-db = mysql,information_schema
#主主需要加入部分
binlog-do-db = test
binlog-ignore-db=mysql
log-slave-updates
sync_binlog=1
auto_increment_offset=2
auto_increment_increment=2

随后重启A、B主机,Linux下可以执行mysql restart,Windows下则可以在服务管理器中找到A、B服务然后手动重启。

忽略特定数据表的同步

如果主机有一些特定数据单独储存在某些数据表中, 但不希望被同步到副本中。可以考虑在上述基础上加上replicate-ignore-table或replicate-do-table

以A主机为例, 其不希望将test.configure下的数据同步到B中, 因为表中储存的是A主机特有的配置. 那么B主机的配置文件应如下所示,以阻止B对test.configure的复制行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[mysqld]  
server-id = 2
binlog-format=ROW
log-bin = "B-bin"
replicate-do-db = test
replicate-ignore-db = mysql,information_schema
binlog-do-db = test
binlog-ignore-db=mysql
log-slave-updates
sync_binlog=1
auto_increment_offset=2
auto_increment_increment=2
#不从A主机那里复制test.configure中的数据
replicate-ignore-table = test.configure

另一边, A主机也添上replicate-ignore-table = test.configure,就能阻止B主机的配置数据复制到A主机中。

注意的是, replicate-ignore-table = test.configure只是阻止副本的复制行为。对于数据源来说它依然会把test.configure的数据记录到bin log中,只是副本的SQL线程在复制数据源bin log的时候主动忽略了该表。因此,如果换了新的副本,而他又没有配置replicate-ignore-table = test.configure,那么新副本还是会复制的。

构建主主关系

先分别在A、B中查看主服务器状态,重点关注当前bin log的记录位置,以A服务器为例:

1
2
3
4
5
6
7
8
mysql> flush tables with read lock;
mysql> show master status\G
*************************** 1. row ***************************
File: A-bin.000003
Position: 107
Binlog_Do_DB:
Binlog_Ignore_DB:
1 row in set (0.00 sec)

结果说明目前A服务器的最新记录在A-bin.000003,偏移量为107的位置。这里锁表的目的是为了不让写入数据,能让从服务器精确定位同步的开始位置。初次同步完成后,需要解锁。

回到B服务器,需要选定A服务器为数据源:

1
mysql>change master to master_host='192.168.1.106', master_user='replication', master_password='000000', master_log_file='A-bin.000003', master_log_pos=107;

同样的,A服务器也要选定B服务器作为数据源(具体master_log_file和master_log_pos值要查看B服务器的状态决定):

1
mysql>change master to master_host='192.168.1.107', master_user='replication', master_password='000000', master_log_file='B-bin.000003', master_log_pos=107;

至此,A、B双方都互认为主备,后续只要开启主备同步即可。注意的是,MySQL 5.x往后不再支持在配置文件中指定主服务器,因此这一步只能在命令行中执行。

开启主备同步

在A、B服务器上分别执行START SLAVE,随后执行show slave status\G,如果显示如下则正常:

1
2
3
4
mysql>show slave status\G;

Slave_IO_Running: Yes
Slave_SQL_Running: Yes

同步完成后分别执行unlock tables完成解锁。

其他

配置文件添加的binlog-do-dbbinlog-ignore-db是属于主机配置,能在show master status的结果中看到;

配置文件添加的replicate-do-dbreplicate-ignore-db,乃至replicate-wild-do-dbreplicate-wild-ignore-db都属于备机配置,能在show slave status的结果中看到。

主主测试

顺利的话,A和B已经形成了主主同步。现进入A主机,新建表test.tp

1
2
3
4
5
USE test;
CREATE TABLE tp(
id int primary key auto_increment,
name varchar(20)
);

可以发现B主机也会出现新表test.tp。向A主机的test.tp插入几条数据:

1
2
3
INSERT INTO tp(name) VALUES('tp');
INSERT INTO tp(name) VALUES('tp1');
INSERT INTO tp(name) VALUES('tp2');

A主机的test.tp内容如下:

1
2
3
4
5
6
7
8
mysql >select * from tp;
+----+------+
| id | name |
+----+------+
| 1 | tp |
| 3 | tp1 |
| 5 | tp2 |
+----+------+

切换到B主机的test.tp,查询的内容一致。这说明A的修改同步到了B中,A->B形成了主备关系。

向B主机的test.tp插入几条数据:

1
2
INSERT INTO tp(name) VALUES('tp4');
INSERT INTO tp(name) VALUES('tp5');

B主机的test.tp内容如下:

1
2
3
4
5
6
7
8
9
10
mysql >select * from tp;
+----+------+
| id | name |
+----+------+
| 1 | tp |
| 3 | tp1 |
| 5 | tp2 |
| 6 | tp4 |
| 8 | tp5 |
+----+------+

切换到A主机的test.tp,查询的内容一致。说明B的修改也同步到了A中,B->A形成了主备关系。至此A、B形成了主主关系。

如果我们在主主配置的时候在A、B配置文件中都添加了replicate-ignore-table = test.configure,说明test.configure不会相互同步。

在A主机上新增一张表test.configure,切换到B主机能发现没有发现test.configure,这说明配置已生效。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!