SQL Server 变更数据捕获(CDC)
标签:SQL SERVER/MSSQL SERVER/数据库/DBA/字段/对象更改
概述
变更数据捕获用于捕获应用到 SQL Server 表中的插入、更新和删除活动,并以易于使用的关系格式提供这些变更的详细信息。变更数据捕获所使用的更改表中包含镜像所跟踪源表列结构的列,同时还包含了解所发生的变更所需的元数据。
对表开启了变更捕获之后,对该表的所有DML和DDL操作都会被记录,有助于跟踪表的变化。
测试环境: SQL SERVER 2008 R2,案例库AdventureWorks2008R2
目录
配置变更数据捕获
启动数据库捕获
--开启cdc USE [AdventureWorks2008R2] EXECUTE sys.sp_cdc_enable_db GO ---如果开启数据库捕获报错误:15517,使用下面的方法处理 ALTER AUTHORIZATION ON DATABASE::[AdventureWorks2008R2] TO [sa] ---查看是否设置成功,1代表开启cdc SELECT is_cdc_enabled FROM SYS.databases WHERE name='AdventureWorks2008R2'
设置跟踪表
为当前数据库中指定的源表启用变更数据捕获。对表启用变更数据捕获时,应用于此表的每个数据操纵语言 (DML) 操作的记录都将写入事务日志中。变更数据捕获进程将从日志中检索此信息,并将其写入可通过使用一组函数访问的更改表中。
格式:
sys.sp_cdc_enable_table [ @source_schema = ] 'source_schema', ---表所属的架构名 [ @source_name = ] 'source_name' ,----表名 [ @role_name = ] 'role_name'---是用于控制更改数据访问的数据库角色的名称。 [,[ @capture_instance = ] 'capture_instance' ]--是用于命名变更数据捕获对象的捕获实例的名称,这个名称在后面的存储过程和函数中需要经常用到。 [,[ @supports_net_changes = ] supports_net_changes ]---指示是否对此捕获实例启用净更改查询支持如果此表有主键,或者有已使用 @index_name 参数进行标识的唯一索引,则此参数的默认值为 1。否则,此参数默认为 0。 [,[ @index_name = ] 'index_name' ]--用于唯一标识源表中的行的唯一索引的名称。index_name 为 sysname,并且可以为 NULL。如果指定,则 index_name 必须是源表的唯一有效索引。如果指定 index_name,则标识的索引列优先于任何定义的主键列,就像表的唯一行标识符一样。 [,[ @captured_column_list = ] 'captured_column_list' ]--需要对哪些列进行捕获。captured_column_list 的数据类型为 nvarchar(max),并且可以为 NULL。如果为 NULL,则所有列都将包括在更改表中。 [,[ @filegroup_name = ] 'filegroup_name' ]--是要用于为捕获实例创建的更改表的文件组。 [,[ @partition_switch = ] 'partition_switch' ]--指示是否可以对启用了变更数据捕获的表执行 ALTER TABLE 的 SWITCH PARTITION 命令。allow_partition_switch 为 bit,默认值为 1。
注意:
对表启用变更数据捕获时,将生成一个更改表以及一个或两个查询函数。更改表充当捕获进程从事务日志中提取的源表更改的存储库。查询函数则用于从更改表中提取数据。这些函数的名称按以下方式派生自 capture_instance 参数:
所有更改函数: cdc.fn_cdc_get_all_changes_ < capture_instance >
净更改函数: cdc.fn_cdc_get_net_changes_ < capture_instance >
1.如果源表是数据库中第一个要启用变更数据捕获的表,并且数据库不存在事务发布,则 sys.sp_cdc_enable_table 还将为数据库创建捕获和清理作业。
它将 sys.tables 目录视图中的 is_tracked_by_cdc 列设置为 1。
2.对表启用变更数据捕获时,SQL Server 代理不必正在运行。但是,只有当 SQL Server 代理正在运行时,捕获进程才会处理事务日志并将条目写入更改表。
USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_enable_table @source_schema = N'HumanResources' , @source_name = N'Department' , @role_name = N'cdc_admin'--增加的角色 , @capture_instance = N'HR_Department'--实例名 , @supports_net_changes = 1 , @index_name = N'AK_Department_Name' , @captured_column_list = N'Name, GroupName'--跟踪的字段 , @filegroup_name = N'PRIMARY'; GO ---查询表是否启动跟踪,值为1标示已启动 SELECT is_tracked_by_cdc FROM SYS.tables WHERE name='Department'
数据库的变化
- 增加的系统表
- 增加的系统视图
- 增加的系统存储过程
- 增加的函数
- 增加的用户与角色
---所有cdc有关的对象 SELECT * FROM SYS.all_objects WHERE name LIKE '%CDC%'OR SCHEMA_ID IN(SELECT SCHEMA_ID FROM SYS.schemas WHERE name='CDC') ORDER BY TYPE SELECT * FROM msdb.SYS.objects WHERE name='cdc_jobs'
对象分析
插入测试数据
---查询插入数据之前表中的内容
USE [AdventureWorks2008R2] SELECT * FROM [HumanResources].[Department]
--1.插入一条记录 INSERT INTO [AdventureWorks2008R2].[HumanResources].[Department]([Name],[GroupName],[ModifiedDate]) VALUES('TEST','TEST',GETDATE()) GO --更新一条记录 UPDATE [HumanResources].[Department] SET GroupName='TEST1' WHERE GroupName='TEST' --增加一个字段 ALTER TABLE [HumanResources].[Department] ADD Dcolumn INT
分析系统表
SELECT * FROM cdc.ddl_history---与DDL有关的操作记录
SELECT * FROM cdc.HR_Department_CT /* __$operation字段的值: 1 = 删除 2 = 插入 3 = 更新(旧值) 列数据中具有执行更新语句之前的行值。 4 = 更新(新值) 列数据中具有执行更新语句之后的行值。 */
为每个在更改表中存在行的事务返回一行。该表用于在日志序列号 (LSN) 提交值和提交事务的时间之间建立映射。没有对应的更改表项的项也可以记录下来,以便表在变更活动少或者无变更活动期间将 LSN 处理的完成过程记录下来。
SELECT * FROM cdc.lsn_time_mapping
分析存储过程
---查询当前作业配置 SELECT * FROM MSDB.dbo.cdc_jobs --或者使用 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_help_jobs; GO 1.sys.sp_cdc_add_job 在当前数据库中创建变更数据捕获清理或捕获作业 1.创建捕获作业 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_add_job @job_type = N'capture'; GO 2.创建清理作业 ---创建清理作业,作业连续运行,更改数据行将在更改表中保留2880分钟,清除时使用一条语句最多删除4000条记录 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_add_job @job_type = N'cleanup' ,@start_job=1 ,@retention=2880 ,@threshold =4000 2.sys.sp_cdc_change_job 修改当前数据库中变更数据捕获清除或捕获作业的配置 --仅在使用 sp_cdc_stop_job 停止作业并使用 sp_cdc_start_job 重新启动该作业后,对该作业所做的更改才会生效 1.更改捕获作业 --将每个循环扫描最多处理的事务数更改为200,为了从日志中提取所有行而要执行的最大扫描循环50次 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_change_job @job_type = N'capture', @maxtrans = 200, @maxscans = 50; GO 2.更改清除作业,将记录保留时间更改为3440分钟 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_change_job @job_type = N'cleanup', @retention = 3440; GO 3.sys.sp_cdc_cleanup_change_table 根据指定的 low_water_mark 值从当前数据库的更改表中删除行,重置更改表中的最小 __$start_lsn,并删除小于该值的数据. 将同时清除cdc.HR_Department_CT,cdc.lsn_time_mapping表的记录 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_cleanup_change_table @capture_instance =N'HR_Department', @low_water_mark=0x0000037D000000D30008, @threshold=2000; SELECT sys.fn_cdc_increment_lsn(sys.fn_cdc_get_max_lsn()) 4.sys.sp_cdc_drop_job 从 msdb 中删除当前数据库的变更数据捕获清除或捕获作业。 --下例删除 AdventureWorks2008R2 数据库的清除作业和捕获作业 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_drop_job @job_type = N'cleanup'; USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_drop_job @job_type = N'capture'; 5.sys.sp_cdc_disable_db 对当前数据库禁用变更数据捕获 禁用当前对数据库中的所有表启用的变更数据捕获。与变更数据捕获相关的所有系统对象(如更改表、作业、存储过程和函数)都将被删除。sys.databases 目录视图中的数据库条目的 is_cdc_enabled 列设置为 0。 如果在禁用变更数据捕获时为数据库定义了很多捕获实例,则长时间运行事务可能导致 sys.sp_cdc_disable_db 的执行失败。通过在运行 sys.sp_cdc_disable_db 之前使用 sys.sp_cdc_disable_table 禁用单个捕获实例,可以避免此问题。 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_disable_db; GO 6.sys.sp_cdc_disable_table 对当前数据库中指定的源表和捕获实例禁用变更数据捕获 删除与指定的源表和捕获实例相关联的变更数据捕获更改表和系统函数。它会删除任何与来自变更数据捕获系统表的指定捕获实例相关联的行,并将 sys.tables 目录视图中的表项的 is_tracked_by_cdc 列设置为 0。 ---下例对 HumanResources.Department 表禁用了变更数据捕获 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_disable_table @source_schema = N'HumanResources' , @source_name = N'Department' , @capture_instance = N'HR_Department' ---这里是定义的实例名称,在一开始创建捕获的时候创建的,这里也可以制定ALL(禁用表HumanResources.Department的所有捕获), 7.sys.sp_cdc_enable_db 对当前数据库启用变更数据捕获。必须先对数据库执行此过程,然后才能对该数据库中的任何表启用变更数据捕获。变更数据捕获可记录应用到所启用的表中的插入、更新和删除活动,同时采用易于使用的关系格式提供变更详细信息。此操作将为已修改的行捕获反映了所跟踪源表列结构的列信息,同时还捕获将更改应用到目标环境所需的元数据。 将创建以全数据库为作用域的变更数据捕获对象,包括元数据表和 DDL 触发器。它还会创建 cdc 架构和 cdc 数据库用户,并将 sys.databases 目录视图中的数据库条目的 is_cdc_enabled 列设置为 1。 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_enable_db; GO 8.sys.sp_cdc_enable_table --对需要进行跟踪的表启动CDC,sys.sp_cdc_enable_table /* 为当前数据库中指定的源表启用变更数据捕获。对表启用变更数据捕获时,应用于此表的每个数据操纵语言 (DML) 操作的记录都将写入事务日志中。 变更数据捕获进程将从日志中检索此信息,并将其写入可通过使用一组函数访问的更改表中。 */ sys.sp_cdc_enable_table [ @source_schema = ] 'source_schema', ---表所属的架构名 [ @source_name = ] 'source_name' ,----表名 [ @role_name = ] 'role_name'---是用于控制更改数据访问的数据库角色的名称。 [,[ @capture_instance = ] 'capture_instance' ]--是用于命名特定于实例的变更数据捕获对象的捕获实例的名称 [,[ @supports_net_changes = ] supports_net_changes ]---指示是否对此捕获实例启用净更改查询支持 [,[ @index_name = ] 'index_name' ]--用于唯一标识源表中的行的唯一索引的名称。index_name 为 sysname,并且可以为 NULL。如果指定,则 index_name 必须是源表的唯一有效索引。如果指定 index_name,则标识的索引列优先于任何定义的主键列,就像表的唯一行标识符一样。 [,[ @captured_column_list = ] 'captured_column_list' ]--需要对哪些列进行捕获。captured_column_list 的数据类型为 nvarchar(max),并且可以为 NULL。如果为 NULL,则所有列都将包括在更改表中。 [,[ @filegroup_name = ] 'filegroup_name' ]--是要用于为捕获实例创建的更改表的文件组。 [,[ @partition_switch = ] 'partition_switch' ]--指示是否可以对启用了变更数据捕获的表执行 ALTER TABLE 的 SWITCH PARTITION 命令。allow_partition_switch 为 bit,默认值为 1。 9.sp_cdc_generate_wrapper_function 生成用于为 SQL Server 中可用的变更数据捕获查询函数创建包装函数的脚本 EXEC sys.sp_cdc_generate_wrapper_function 10. sys.sp_cdc_help_change_data_capture 返回当前数据库中为变更数据捕获启用的每个表的变更数据捕获配置。最多可为每个源表返回两行,为每个捕获实例返回一行。 ---返回制定表的捕获信息 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_help_change_data_capture @source_schema = N'HumanResources', --架构名 @source_name = N'Department';--表名 GO --返回所有表的捕获信息 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_help_change_data_capture 11.sys.sp_cdc_get_captured_columns 返回指定捕获实例所跟踪的捕获源列的变更数据捕获元数据信息。 USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_get_captured_columns @capture_instance = N'HR_Department'; GO 12.sys.sp_cdc_get_ddl_history 返回自对指定的捕获实例启用变更数据捕获后与该捕获实例关联的数据定义语言 (DDL) 更改历史记录。 与查询表是一样的结果 SELECT * FROM cdc.ddl_history USE AdventureWorks2008R2; GO EXECUTE sys.sp_cdc_get_ddl_history @capture_instance = N'HR_Department'; 13.sp_cdc_help_jobs 报告关于当前数据库中所有变更数据捕获清除或捕获作业的信息。因为一个数据库只会在第一个表创建捕获的时候创建作业 所以这里只需要在当前库执行就可以。 sys.sp_cdc_help_jobs USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_help_jobs; GO 14.sp_cdc_scan 执行变更数据捕获日志扫描操作,需要进行捕获的时候使用,默认情况下5会自动进行捕获。 如果变更数据捕获正在使用 SQL Server 代理捕获作业,则 sys.sp_MScdc_capture_job 将内部调用 sys.sp_cdc_scan。如果变更数据捕获日志扫描操作已经处于活动状态,或数据库启用了事务复制,则无法显式执行此过程。此存储过程应当由需要自定义自动配置的捕获作业的行为的管理员使用。 USE AdventureWorks2008R2; GO EXEC sp_cdc_scan 15.sys.sp_cdc_start_job, 启动和停止当前数据库的变更数据捕获清除或捕获作业。 ---启动清除作业 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_start_job @job_type = N'cleanup'; ---停止捕获作业 USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_stop_job @job_type = N'capture'; GO USE AdventureWorks2008R2; GO EXEC sys.sp_cdc_stop_job @job_type = N'cleanup'; GO
分析函数
1.fn_cdc_get_all_changes_capture_instance 针对在指定日志序列号 (LSN) 范围内应用到源表的每项更改均返回一行 USE AdventureWorks2008R2; GO DECLARE @from_lsn binary(10), @to_lsn binary(10) SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department') SET @to_lsn = sys.fn_cdc_get_max_lsn() SELECT * FROM cdc.fn_cdc_get_all_changes_HR_Department (@from_lsn, @to_lsn, N'all'); GO 2.fn_cdc_get_net_changes_capture_instance 针对指定 LSN 范围内每个已更改的源行返回一个净更改行。也就是说,如果在 LSN 范围内源行具有多项更改, 则该函数将返回反映该行最终内容的单一行。例如,如果事务在源表中插入一行,并且 LSN 范围内的后续事务更新了该行中的一个或多个列, 则该函数将只返回一行,其中包含多个更新的列值。 如果值最后是删除操作,则不返回该LSN的值 USE AdventureWorks2008R2; GO DECLARE @begin_time datetime, @end_time datetime, @from_lsn binary(10), @to_lsn binary(10); -- Obtain the beginning of the time interval. SET @begin_time = GETDATE() -1; -- DML statements to produce changes in the HumanResources.Department table. INSERT INTO HumanResources.Department (Name, GroupName) VALUES (N'MyDept', N'MyNewGroup'); UPDATE HumanResources.Department SET GroupName = N'Resource Control' WHERE GroupName = N'Inventory Management'; DELETE FROM HumanResources.Department WHERE Name = N'MyDept'; -- Obtain the end of the time interval. SET @end_time = GETDATE(); -- Map the time interval to a change data capture query range. SET @from_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', @begin_time); SET @to_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time); -- Return the net changes occurring within the query window. SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@from_lsn, @to_lsn, 'all'); 3.sys.fn_cdc_decrement_lsn 根据指定的 LSN 返回序列中的上一个日志序列号 (LSN) Use AdventureWorks2008R2; GO SELECT sys.fn_cdc_decrement_lsn(sys.fn_cdc_get_max_lsn()) 下例在一个返回 LSN 值小于最大 LSN 值的更改数据行的查询中,使用 sys.fn_cdc_decrement_lsn 来设置 LSN 上限。 Use AdventureWorks2008R2; GO DECLARE @from_lsn binary(10), @to_lsn binary(10); SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department'); SET @to_lsn = sys.fn_cdc_decrement_lsn(sys.fn_cdc_get_max_lsn()); SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department( @from_lsn, @to_lsn, 'all'); GO 4.sys.fn_cdc_increment_lsn 根据指定的 LSN 返回序列中的下一个日志序列号 (LSN)。 此函数返回的 LSN 值始终大于指定的值,并且不存在介于这两个值之间的 LSN 值。 若要系统地查询随时间变化的更改数据流,可以定期重复调用该查询函数,每次调用时指定一个新的查询间隔来限定查询中返回的更改的范围。为帮助确保不丢失数据,通常使用前一个查询的上限来生成后一个查询的下限。由于查询间隔是一个闭区间,因此新的下限必须大于前一个上限,但要足够小,以确保不存在 LSN 值介于此值与旧上限之间的更改。sys.fn_cdc_increment_lsn 函数就是用来获取此值的。 Use AdventureWorks2008R2; GO SELECT sys.fn_cdc_increment_lsn(sys.fn_cdc_get_max_lsn()) 5.sys.fn_cdc_get_column_ordinal 返回实例制定列的列序号。 Use AdventureWorks2008R2; GO SELECT sys.fn_cdc_get_column_ordinal ( 'HR_Department','NAME'); 6.sys.fn_cdc_get_max_lsn 返回 cdc.lsn_time_mapping 系统表的 start_lsn 列中的最大日志序列号 (LSN)。您可以使用此函数为任何捕获实例返回变更数据捕获时间线的高端点 USE AdventureWorks2008R2; GO SELECT sys.fn_cdc_get_max_lsn()AS max_lsn; SELECT sys.fn_cdc_get_min_lsn(N'HR_Department'); USE AdventureWorks2008R2; GO DECLARE @from_lsn binary(10), @to_lsn binary(10); SET @from_lsn = sys.fn_cdc_get_min_lsn(N'HR_Department'); SET @to_lsn = sys.fn_cdc_get_max_lsn(); SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@from_lsn, @to_lsn, 'all'); GO 7.sys.fn_cdc_get_min_lsn USE AdventureWorks2008R2; GO SELECT sys.fn_cdc_get_min_lsn ('HR_Department')AS min_lsn;---查询制定的实例名的最小LSN 8.sys.fn_cdc_has_column_changed ( 'capture_instance','column_name' , update_mask ) 标识指定的更新掩码是否指示已更新关联的更改行中的指定列。 USE AdventureWorks2008R2; GO SELECT sys.fn_cdc_has_column_changed ('HR_Department','name' , 2) 9.sys.fn_cdc_is_bit_set 指示捕获的列是否已更新,采用的方法是检查是否在提供的位掩码内设置了其序号位置。 USE AdventureWorks2008R2; GO DECLARE @from_lsn binary(10), @to_lsn binary(10), @GroupNm_ordinal int; SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department'); SET @to_lsn = sys.fn_cdc_get_max_lsn(); SET @GroupNm_ordinal = sys.fn_cdc_get_column_ordinal('HR_Department','GroupName'); SELECT sys.fn_cdc_is_bit_set(@GroupNm_ordinal,__$update_mask) as 'IsGroupNmUpdated', * FROM cdc.fn_cdc_get_all_changes_HR_Department( @from_lsn, @to_lsn, 'all') WHERE __$operation = 4; GO 10.sys.fn_cdc_map_lsn_to_time 为指定的日志序列号 (LSN) 返回 cdc.lsn_time_mapping 系统表的 tran_end_time 列中的日期和时间值。 您可以使用此函数系统地将 LSN 范围映射到更改表中的日期范围 SELECT sys.fn_cdc_map_lsn_to_time(sys.fn_cdc_get_max_lsn()); 11.sys.fn_cdc_map_time_lsn 下面的示例使用 sys.fn_cdc_map_time_lsn 函数来确定在 cdc.lsn_time_mapping 表中是否有 tran_end_time 值大于或等于指定时间的行。例如,可以用此查询来确定捕获进程是否已处理完截至前指定时间提交的更改 DECLARE @extraction_time datetime, @lsn binary(10); SET @extraction_time = GETDATE(); SELECT @lsn = sys.fn_cdc_map_time_to_lsn ('smallest greater than or equal', @extraction_time); IF @lsn IS NOT NULL BEGIN print '...' END DECLARE @begin_time datetime, @end_time datetime, @begin_lsn binary(10), @end_lsn binary(10); SET @begin_time = '2015-04-07 18:00:00.000'; SET @end_time = '2015-04-08 18:00:00.000'; SELECT @begin_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than', @begin_time); SELECT @end_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time); SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@begin_lsn, @end_lsn, 'all ');
分析系统视图
1.sys.dm_cdc_log_scan_sessions 针对当前数据库中的每个日志扫描会话返回一行。返回的最后一行表示当前会话。您可以使用此视图返回有关当前日志扫描会话的状态信息, 或有关自 SQL Server 实例上次启动以来所有会话的聚合信息。 USE AdventureWorks2008R2; SELECT * FROM sys.dm_cdc_log_scan_sessions --可以观察empty_scan_count字段的值可以发现它的变化,5秒增加一次,和前面配置的日志扫描作业的频率是一样的 USE AdventureWorks2008R2; GO print getdate() SELECT empty_scan_count FROM sys.dm_cdc_log_scan_sessions WHERE session_id = (SELECT MAX(b.session_id) from sys.dm_cdc_log_scan_sessions AS b) waitfor DELAY '00:01' print getdate() SELECT empty_scan_count FROM sys.dm_cdc_log_scan_sessions WHERE session_id = (SELECT MAX(b.session_id) from sys.dm_cdc_log_scan_sessions AS b) 2.sys.dm_cdc_errors 为变更数据捕获日志扫描会话中遇到的每个错误返回一行。 USE AdventureWorks2008R2; GO SELECT * FROM sys.dm_cdc_errors
总结
捕获方法可以跟踪一个表对象的更改操作,但是开启了变更捕获对性能存在一定的影响,特别在日志读写这一块,首先它比正常的操作需要进行更多的日志写操作,而且日志的读操作也是很频繁的,有时候可能会引起日志等待类型,所以要慎重使用。
如果文章对大家有帮助,希望大家能给个推荐,谢谢!!!
备注: 作者:pursuer.chen 博客:http://www.cnblogs.com/chenmh 本站点所有随笔都是原创,欢迎大家转载;但转载时必须注明文章来源,且在文章开头明显处给明链接,否则保留追究责任的权利。 《欢迎交流讨论》 |