SQL Server 变更数据捕获(CDC) - pursuer.chen - 博客园

mikel阅读(1067)

来源: SQL Server 变更数据捕获(CDC) – pursuer.chen – 博客园

变更数据捕获用于捕获应用到 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
复制代码

 

总结

捕获方法可以跟踪一个表对象的更改操作,但是开启了变更捕获对性能存在一定的影响,特别在日志读写这一块,首先它比正常的操作需要进行更多的日志写操作,而且日志的读操作也是很频繁的,有时候可能会引起日志等待类型,所以要慎重使用。

SQL Server 2008中新增的变更数据捕获(CDC)和更改跟踪 - 邀月 - 博客园

mikel阅读(1050)

来源: SQL Server 2008中新增的变更数据捕获(CDC)和更改跟踪 – 邀月 – 博客园

SQL Server 2008中SQL应用系列–目录索引

本文主要介绍SQL Server中记录数据变更的四个方法:触发器、Output子句、变更数据捕获(Change Data Capture 即CDC)功能、同步更改跟踪。其中后两个为SQL Server 2008所新增。

一、触发器

SQL Server的早期版本中,如果要记录某个表或视图的Insert/Update/Delete操作,我们可以借助触发器(Trigger)(http://msdn.microsoft.com/zh-cn/library/ms189799.aspx),这在数据量较小的情况下往往是有效的方式之一,其中后触发器(After Trigger)只能跟踪表的三个操作中的任意组合,而前触发器(Instead Of trigger)可以处理表和视图的更新(即使普通的Update View语句在某些列不明确的情况下报错)。我们看两个例子:

准备基础数据:

复制代码
USE testDb2
GO
--创建两个测试表
IF NOT OBJECT_ID('DepartDemo') IS NULL
DROP TABLE [DepartDemo]
GO
IF NOT OBJECT_ID('DepartChangeLogs') IS NULL
DROP TABLE [DepartChangeLogs]
GO
--测试表
CREATE TABLE [dbo].[DepartDemo](
[DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL
)
GO
--记录日志表
CREATE TABLE [DepartChangeLogs]
([LogID] [bigint] IDENTITY(1001,1) NOT NULL PRIMARY KEY,
[DID] [int] NOT NULL,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL,
[LogTime] DateTime Default(Getdate()) Not Null,
[InsOrUpd] char not null
)
GO
复制代码

创建触发器:

复制代码
/*******   创建一个After DML触发器  ******/
/*********  3w@live.cn  邀月***************/
CREATE TRIGGER dbo.tri_LogDepartDemo
ON [dbo].[DepartDemo]
AFTER INSERT, Delete  /************此处使用update与“Insert,Delete”效果是一样的,邀月注  **********/
AS
SET NOCOUNT ON   --屏蔽触发器发送“受影响的行数”给应用程序

-- Inserted rows
INSERT [DepartChangeLogs]
(DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 LogTime, InsOrUPD)
SELECT DISTINCT DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 GETDATE(), 'I'
FROM inserted i

-- Deleted rows
INSERT [DepartChangeLogs]
(DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 LogTime, InsOrUPD)
SELECT DISTINCT DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 GETDATE(), 'D'
FROM deleted d
GO

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'国家统计局房产审计一科', N'0', N'胡不归', 0, N'DeomUser',
 CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
 1, N'专业评估全国房价,为老百姓谋福祉', 0, N'')
GO

----该Update不会被触发器记录,但Update会生效
UPDATE departDemo SET [Manager]='任我行' WHERE DID=101
GO

DELETE FROM departDemo where DID=101
GO

SELECT * FROM [DepartChangeLogs]
复制代码

统计效果:
邀月工作室
如果你觉得触发器过于浪费,你可以试着根据某些字段以缩小触发器的范围

复制代码
/********* 使用DML触发器记录特定列的修改 ***/
/*********  3w@live.cn  邀月***************/
CREATE TRIGGER dbo.[tri_LogDepartDemo2]
ON [dbo].[DepartDemo]
AFTER Update
AS
IF Update([Manager])
    Begin
        print '该部门主管实行终身任免制,不得中途更改!'
        Rollback ----回滚Update操作
    End

GO
UPDATE departDemo SET [Manager]='任我行' WHERE DID=101
GO
复制代码

执行结果:

邀月工作室
但触发器的缺陷也是显而易见的,使用触发器请注意以下几点:

1、触发器通常很隐蔽,换句话说,易忘记,特别在检查性能和逻辑问题时。

2、长时间运行的触发器会严重减慢数据操作,特别是在数据频繁修改的数据库中。

3、不记录日志的更新不会引起DML触发器的触发(如WRITETEXT、Trunacte table及批量插入操作)。

4、约束通常比触发器运行更快。

5、处理某些逻辑时,存储过程通常比触发器要更易维护和管理。

6、不允许在触发器中使用Select返回结果集。

关于触发器的更多内容,请看MSDN(http://msdn.microsoft.com/zh-cn/library/ms189799.aspx

 

二、使用Output子句

  官方解释:OutPut子句(http://technet.microsoft.com/zh-cn/library/ms177564.aspx返回受 INSERT、UPDATE、DELETE 或 MERGE 语句影响的各行中的信息,或返回基于受这些语句影响的各行的表达式。 这些结果可以返回到处理应用程序,以供在确认消息、存档以及其他类似的应用程序要求中使用。 也可以将这些结果插入表或表变量。 另外,您可以捕获嵌入的 INSERT、UPDATE、DELETE 或 MERGE 语句中 OUTPUT 子句的结果,然后将这些结果插入目标表或视图。

举例:

复制代码
 /********* 使用Output记录表记录的修改 *****/
/*********  3w@live.cn  邀月***************/

----删除前面的触发器
Drop TRIGGER dbo.[tri_LogDepartDemo]
DROP TRIGGER dbo.[tri_LogDepartDemo2]

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
OUTPUT Inserted.*,getdate(),'I' ---注意这行是新增的
INTO DepartChangeLogs           ---注意这行是新增的
VALUES (N'发改委', N'0', N'向问天', 0, N'DeomUser',
 CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
 1, N'油价,我说了算', 0, N'')
GO

SELECT * FROM [DepartChangeLogs]
复制代码

邀月工作室

注意:

1、从OUTPUT 中返回的列反映 INSERT、UPDATE 或 DELETE 语句完成之后但在触发器执行之前的数据。

2、SQL Server 并不保证由使用 OUTPUT 子句的 DML 语句处理和返回行的顺序。

3、与触发器相比,OutPut子句可以直接处理Merge语句。

以上两种方法各有千秋,在合适的情况下采取合适的方法才是明智的选择,令人惊喜的是,SQL Server 2008起,为我们提供了更为强大的内建的方法-变更数据捕获(CDC,http://msdn.microsoft.com/zh-cn/library/bb500244%28v=sql.100%29.aspx)和更改跟踪,下面我们隆重介绍它们。

三、使用“变更数据捕获”(CDC)功能

SQL Server 2008提供了内建的方法变更数据捕获(Change Data Capture 即CDC)以实现异步跟踪用户表的数据修改,而且这一功能拥有最小的性能开销。可以用于其他数据源的持续更新,例如将OLTP数据库中的数据变更迁移到数据仓库数据库。

要使用CDC功能,首先我们得在数据库中启用该功能。在此我们沿用上例中使用的数据库Testdb2

复制代码
/**************异步跟踪数据更新演示*************/
/************* 3w@live.cn 邀月***************/
use master
GO

IF EXISTS (SELECT [name] FROM sys.databases WHERE name = 'TestDb2')
drop DATABASE TestDb2
Go
CREATE DATABASE TestDb2
GO

--查看是否启用CDC
SELECT is_cdc_enabled FROM sys.databases WHERE name = 'TestDb2'

USE TestDb2
GO
----启用当前数据库的CDC功能
EXEC sys.sp_cdc_enable_db
GO
复制代码

如果报15517错误,请换用其他owner,邀月注

复制代码
SELECT is_cdc_enabled FROM sys.databases WHERE name = 'TestDb2'

/*
is_cdc_enabled
1
*/
USE testDb2
GO

CREATE TABLE [dbo].[DepartDemo](
[DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL
)
GO

/**********************************
需要启用SQL Server Agent服务,否则会报错,邀月注
SQLServerAgent is not currently running so it cannot be notified of this action.
***********************************/

/****** 捕获所有的行变更,只返回行的净变更,其他默认 *******/
EXEC sys.sp_cdc_enable_table
@source_schema = 'dbo',
@source_name = 'DepartDemo',
@role_name = NULL,
@capture_instance = NULL,
@supports_net_changes = 1,
@index_name = NULL,
@captured_column_list = NULL,
@filegroup_name = default
复制代码

注意此时,SQL Server 自启动了两个job,一个捕获,一个清除,注意清除是默认凌晨2点,清除72小时以上的数据。如果同一数据库的表中CDC已经启用,不会重建job。

复制代码
/*
Job 'cdc.TestDb2_capture' started successfully.
Job 'cdc.TestDb2_cleanup' started successfully.
*/

--确认表已经被跟踪
SELECT is_tracked_by_cdc FROM sys.tables
WHERE name = 'DepartDemo' and schema_id = SCHEMA_ID('dbo')
/*
is_tracked_by_cdc
1
*/

--确认
EXEC sys.sp_cdc_help_change_data_capture 'dbo', 'DepartDemo'
复制代码

邀月工作室

可以看到,SQL Server 增加了一个表[cdc].[dbo_DepartDemo_CT]
相比源表多了个字段:
[__$start_lsn]
,[__$end_lsn]
,[__$seqval]
,[__$operation]
,[__$update_mask]

邀月工作室

不建议直接查询该表,而应该使用下面的技巧

复制代码
USE TestDb2
GO

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'银监会', N'0', N'云中鹤', 0, N'DemoUser1',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'监管汇率', 0, N'')

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'统计局', N'0', N'神算子', 0, N'DemoUser2',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'统计数据', 0, N'')
GO

UPDATE [dbo].[DepartDemo]
SET Manager='段正淳'
WHERE DID =101

DELETE [dbo].[DepartDemo]
WHERE DID = 102
复制代码

要查询变更,我们需要借助大名鼎鼎的日志序列号(Log Sequence Numbers)即LSN(http://msdn.microsoft.com/zh-cn/library/ms190411%28v=sql.100%29.aspx)来实现LSN级别的跟踪数据变更。 下面示例中sys.fn_cdc_map_time_to_lsn(http://msdn.microsoft.com/zh-cn/library/bb500137%28v=sql.100%29.aspx)用于LSN转换为时间。

复制代码
/******* 使用LSN 查看CDC记录 *********/

--http://msdn.microsoft.com/zh-cn/library/bb500137%28v=sql.100%29.aspx
SELECT sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30') as BeginLSN

/*
BeginLSN
0x0000002C000000AA0003
*/

SELECT sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59') as EndLSN

/*
EndLSN
0x0000002C000001C20005
*/

/**************查看所有CDC记录*************/
/************* 3w@live.cn 邀月***************/

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
__$operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_all_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all')

/************查看所有更新*************************

__$operation __$update_mask DID DName Manager
2 0x1FFF 105 银监会 云中鹤
2 0x1FFF 106 统计局 神算子
1 0x1FFF 101 银监会 段正淳
1 0x1FFF 103 银监会 云中鹤
1 0x1FFF 104 统计局 神算子
1 0x1FFF 105 银监会 云中鹤
1 0x1FFF 106 统计局 神算子
2 0x1FFF 107 银监会 云中鹤
2 0x1FFF 108 统计局 神算子
4 0x0008 107 银监会 段正淳
1 0x1FFF 108 统计局 神算子
*/

/**************查看所有CDC记录*************/
/************* 3w@live.cn 邀月***************/
DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

--解释一下Operation的具体含义
SELECT
CASE __$operation
WHEN 1 THEN 'DELETE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'Before UPDATE'
WHEN 4 THEN 'After UPDATE'
END Operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_all_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all update old')
复制代码

邀月工作室

复制代码
/**************查看净更改(Net changes)CDC记录*************/
/************* 3w@live.cn 邀月 ***************/

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'药监局', N'0', N'蝶谷医仙', 0, N'DemoUser3',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'制定药价', 0, N'')
GO

UPDATE [dbo].[DepartDemo]
SET Manager='胡青牛'
WHERE DID =109

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
CASE __$operation
WHEN 1 THEN 'DELETE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'Before UPDATE'
WHEN 4 THEN 'After UPDATE'
WHEN 5 THEN 'MERGE'
END Operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_net_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all with mask')
复制代码

邀月工作室

我们还可以通过转换CDC更新掩码获得更为直观的结果,这里需要借助于另外两个函数sys.fn_cdc_is_bit_set(http://msdn.microsoft.com/zh-cn/library/bb500241%28v=SQL.110%29.aspx)和sys.fn_cdc_get_column_ordinal(http://msdn.microsoft.com/zh-cn/library/bb522549%28v=SQL.100%29.aspx

 

复制代码
/************** 转换CDC更新掩码 *************/
/************* 3w@live.cn 邀月 **************/

UPDATE dbo.[DepartDemo]
SET [Manager] = '东方不败'
WHERE DID =107

UPDATE dbo.[DepartDemo]
SET ParentID = 109
WHERE DID =107

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
sys.fn_cdc_is_bit_set (
sys.fn_cdc_get_column_ordinal (
'dbo_DepartDemo' , 'Manager' ),
__$update_mask) Manager_Updated,
sys.fn_cdc_is_bit_set (
sys.fn_cdc_get_column_ordinal (
'dbo_DepartDemo' , 'ParentID' ),
__$update_mask) ParentID_Updated,
DID,
Manager,
ParentID
FROM cdc.fn_cdc_get_all_changes_dbo_DepartDemo
(@FromLSN, @ToLSN, 'all')
WHERE __$operation = 4
复制代码

 

邀月工作室

除了前面介绍的指定LSN边界的方法,SQL Server还提供了一系列的获取边界的方法:

sys.fn_cdc_get_max_lsn(http://msdn.microsoft.com/zh-cn/library/bb500304%28v=sql.100%29.aspx

sys.fn_cdc_get_min_lsn(http://msdn.microsoft.com/zh-cn/library/bb510621%28v=sql.100%29.aspx

sys.fn_cdc_increment_lsn(http://msdn.microsoft.com/zh-cn/library/bb510745%28v=sql.100%29.aspx

sys.fn_cdc_decrement_lsn(http://msdn.microsoft.com/zh-cn/library/bb500246%28v=sql.100%29.aspx

示例如下:

复制代码
/************** 获取LSN边界的其他方法 *************/
/************* 3w@live.cn 邀月 **************/

--获取最小边界
SELECT sys.fn_cdc_get_min_lsn ('dbo_DepartDemo') Min_LSN
--获取可用的最大边界
SELECT sys.fn_cdc_get_max_lsn () Max_LSN
--获取最大边界的下一个序号
SELECT sys.fn_cdc_increment_lsn (sys.fn_cdc_get_max_lsn()) New_Lower_Bound_LSN
--获取最大边界的前一个序号
SELECT sys.fn_cdc_decrement_lsn (sys.fn_cdc_get_max_lsn())
New_Lower_Bound_Minus_one_LSN
复制代码

邀月工作室

通过以下存储过程在数据库和表级禁用CDC

sys.sp_cdc_disable_table (http://msdn.microsoft.com/zh-cn/library/bb510702(v=sql.100).aspx

sys.sp_cdc_disable_db(http://msdn.microsoft.com/zh-cn/library/bb522508(v=sql.100).aspx)注意,该命令同时也删除了CDC架构和相关的SQL代理作业。

复制代码
/************** 在数据库和表级禁用CDC *************/
/************* 3w@live.cn 邀月 **************/

EXEC sys.sp_cdc_disable_table 'dbo', 'DepartDemo', 'all'

SELECT is_tracked_by_cdc FROM sys.tables
WHERE name = 'DepartDemo' and schema_id = SCHEMA_ID('dbo')

--当前数据库上禁用CDC
EXEC sys.sp_cdc_disable_db
复制代码

 

四、使用“更改跟踪”以最小的磁盘开销跟踪净数据更改

CDC可以用来对数据库和数据仓库的持续数据变更进行异步数据跟踪,而SQL Server 2008中新增的“更改跟踪”却是一个同步进程,是DML操作本身(I/D/U)事务的一部分,它的最大优势是以最小的磁盘开销来侦测净行变更,它允许修改的数据以事务一致的形式表现,并提供了检测数据冲突的能力。它甚至可以根据外部传入的应用程序上下文,来完成更细颗粒度的更改处理,参看WITH CHANGE_TRACKING_CONTEXT (http://msdn.microsoft.com/zh-cn/library/bb895330%28v=sql.100%29.aspx

复制代码
 /***使用“更改跟踪”以最小的磁盘开销跟踪净数据更改****/
/************* 3w@live.cn 邀月 **************/

IF EXISTS (SELECT [name] FROM sys.databases WHERE name = 'TestDb4')
drop DATABASE TestDb4
Go
CREATE DATABASE TestDb4
GO

--启用更新跟踪,36小时清理一次
ALTER DATABASE TestDb4
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 36 HOURS,
AUTO_CLEANUP = ON)
复制代码

注意,下一步是允许快照隔离,这是微软推祟的“最佳实践”,尽管这样行版本的生成会增加额外的空间使用,从而会增加总的I/O数量,但不使用快照会引发事务不一致的变更信息。

复制代码
ALTER DATABASE TestDb4
SET ALLOW_SNAPSHOT_ISOLATION ON
GO

SELECT DB_NAME(database_id) 数据库名称,is_auto_cleanup_on,
retention_period,retention_period_units_desc
FROM sys.change_tracking_databases
/*
数据库名称 is_auto_cleanup_on retention_period retention_period_units_desc
TestDb4 1 36 HOURS
*/

USE TestDb4
GO
--创建测试表
CREATE TABLE dbo.DepartDemo
([DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[CurState] [smallint] NOT NULL DEFAULT ((0)),
)
GO

----TRUNCATE table dbo.DepartDemo
----GO

--启用表的列更新跟踪
ALTER TABLE dbo.DepartDemo
ENABLE CHANGE_TRACKING
WITH (TRACK_COLUMNS_UPDATED = ON)

--确认是否更新跟踪开启
SELECT OBJECT_NAME(object_id) ObjNM,is_track_columns_updated_on
FROM sys.change_tracking_tables

/*
ObjNM is_track_columns_updated_on
DepartDemo 1
*/

--增加测试数据
INSERT dbo.DepartDemo
(DName,ParentID)
VALUES
('明教', 0),
('五行集', 101),
('少林派',0)

SELECT * FROM dbo.DepartDemo

--当前版本
SELECT CHANGE_TRACKING_CURRENT_VERSION ()
as 当前版本
/*
当前版本
1
*/
SELECT CHANGE_TRACKING_MIN_VALID_VERSION
( OBJECT_ID('dbo.DepartDemo') )as 最小可用版本

/*
最小可用版本
0
*/
复制代码

 

函数ChangeTable有两种用法来检测更改:
一、使用Changes关键字
二、使用Version关键字

复制代码
/*
一、使用Changes关键字
*/

SELECT DID,SYS_CHANGE_OPERATION,
SYS_CHANGE_VERSION
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 0) AS CT
复制代码

邀月工作室

复制代码
UPDATE dbo.DepartDemo
SET Manager='张无忌'
WHERE DID = 101

UPDATE dbo.DepartDemo
SET [DName] = '五行旗'
WHERE DID = 102

DELETE dbo.DepartDemo
WHERE DID = 103

SELECT CHANGE_TRACKING_CURRENT_VERSION () as 当前版本
/*
当前版本
4
*/

--版本1之后的更改
SELECT DID,
SYS_CHANGE_VERSION,
SYS_CHANGE_OPERATION,
SYS_CHANGE_COLUMNS
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 1) AS CT
复制代码

邀月工作室

复制代码
--返回哪些列被修改,1为真,0为假
SELECT DID,
CHANGE_TRACKING_IS_COLUMN_IN_MASK(
COLUMNPROPERTY(
OBJECT_ID('dbo.DepartDemo'),'DName', 'ColumnId') ,
SYS_CHANGE_COLUMNS) 是否改变DName,
CHANGE_TRACKING_IS_COLUMN_IN_MASK(
COLUMNPROPERTY(
OBJECT_ID('dbo.DepartDemo'), 'Manager', 'ColumnId') ,
SYS_CHANGE_COLUMNS) 是否改变Manager
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 1) AS CT
WHERE SYS_CHANGE_OPERATION = 'U'
/*
DID 是否改变DName 是否改变Manager
101 0 1
102 1 0
*/
复制代码

 

复制代码
/*
二、使用Version关键字
*/

SELECT d.DID, d.DName, d.Manager,
ct.SYS_CHANGE_VERSION
FROM dbo.DepartDemo d
CROSS APPLY CHANGETABLE
(VERSION dbo.DepartDemo , (DID), (d.DID)) as ct
复制代码

邀月工作室

复制代码
UPDATE dbo.DepartDemo
SET DName = '中原明教',
CurState = 0
WHERE DID = 101

SELECT d.DID, d.DName, d.Manager,
ct.SYS_CHANGE_VERSION
FROM dbo.DepartDemo d
CROSS APPLY CHANGETABLE
(VERSION dbo.DepartDemo , (DID), (d.DID)) as ct
复制代码

邀月工作室

复制代码
SELECT CHANGE_TRACKING_CURRENT_VERSION () as 当前版本
/*
当前版本
5
*/

--跟踪外部程序哪一部分引起的更改,这样好找出源头
DECLARE @context varbinary(128) = CAST('明教内讧引起分裂' as varbinary(128));
WITH CHANGE_TRACKING_CONTEXT (@context)
INSERT dbo.DepartDemo
(DName, Manager)
VALUES
('天鹰教', '殷天正')

--查询Context更改
SELECT DID,
SYS_CHANGE_OPERATION,
SYS_CHANGE_VERSION,
CAST(SYS_CHANGE_CONTEXT as varchar) ApplicationContext
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 5) AS CT
/*
DID SYS_CHANGE_OPERATION SYS_CHANGE_VERSION ApplicationContext
104 I 6 明教内讧引起分裂
*/
复制代码

 

邀月工作室

小结:

本文总结了SQL Server中记录数据变更的四个方法:触发器、Output子句、SQL Server 2008中新增的变更数据捕获(CDC)功能、同步更改跟踪。其中后两个是SQL Server 2008中新增的功能,在SQL Server 2012中更是与Always ON紧密集成。

1、不建议前两个。

2、CDC用以实现异步跟踪用户表的数据修改,而且这一功能拥有最小的性能开销,可以用于其他数据源的持续更新,例如将OLTP数据库中的数据变更迁移到数据仓库数据库。

3、”更改跟踪”的最大优势是以最小的磁盘开销来侦测净行变更,它允许修改的数据以事务一致的形式表现,并提供了检测数据冲突的能力。

 

其他推荐文章:

1、在VS中如何将数据同步配置为使用 SQL Server 更改跟踪

http://msdn.microsoft.com/zh-cn/library/cc714038.aspx

2、SQL Server 2012中复制、更改跟踪、更改数据捕获和 AlwaysOn 可用性组 (SQL Server)

http://msdn.microsoft.com/zh-cn/library/hh403414%28v=sql.110%29.aspx

3、在SSIS 2012中使用CDC(数据变更捕获)

http://www.cnblogs.com/downmoon/p/3700045.html

4、在SQL Server 2012中实现CDC for Oracle

http://www.cnblogs.com/downmoon/p/3702145.html

 

邀月注:本文版权由邀月和博客园共同所有,转载请注明出处。
助人等于自助!  3w@live.cn

200行代码,7个对象——让你了解ASP.NET Core框架的本质 - Artech - 博客园

mikel阅读(773)

来源: 200行代码,7个对象——让你了解ASP.NET Core框架的本质 – Artech – 博客园

2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为《ASP.NET Core框架揭秘》的分享。在此次分享中,我按照ASP.NET Core自身的运行原理和设计思想创建了一个 “迷你版” 的ASP.NET Core框架,并且利用这个 “极简” 的模拟框架阐述了ASP.NET Core框架最核心、最本质的东西。整个框架涉及到的核心代码不会超过200行,涉及到7个核心的对象。

PPT下载
源代码下载

目录
1. 从Hello World谈起
2. ASP.NET Core Mini
3. Hello World 2
4. 第一个对象:HttpContext
5. 第二个对象:RequetDelegate
6. 第三个对象:Middleware
7. 第四个对象:ApplicationBuilder
8. 第五个对象:Server
9. HttpContext和Server之间的适配
10. HttpListenerServer
11. 第六个对象:WebHost
12. 第七个对象:WebHostBuilder
13. 回顾一下Hello World 2
14. 打个广告:《ASP.NET Core框架揭秘》

1、从Hello World谈起

当我们最开始学习一门技术的时候都喜欢从Hello World来时,貌似和我们本篇的主题不太搭。但事实却非如此,在我们看来如下这个Hello World是对ASP.NET Core框架本质最好的体现。

public class Program
{
    public static void Main()
    => new WebHostBuilder()
        .UseKestrel()
        .Configure(app => app.Run(context => context.Response.WriteAsync("Hello World!")))
        .Build()
        .Run();
}

如上这个Hello World程序虽然人为地划分为若干行,但是整个应用程序其实只有一个语句。这个语句涉及到了ASP.NET Core程序两个核心对象WebHostWebHostBuilder。我们可以将WebHost理解为寄宿或者承载Web应用的宿主,应用的启动可以通过启动作为宿主的WebHost来实现。至于WebHostBuilder,顾名思义,就是WebHost的构建者。

在调用WebHostBuilder的Build方法创建出WebHost之前,我们调用了它的两个方法,其中UseKestrel旨在注册一个名为Kestrel的服务器,而Configure方法的调用则是为了注册一个用来处理请求的中间件,后者在响应的主体内容中写入一个“Hello World”文本。

当我们调用Run方法启动作为应用宿主的WebHost的时候,后者会利用WebHostBuilder提供的服务器和中间件构建一个请求处理管道。这个由一个服务器和若干中间件构成的管道就是ASP.NET Core框架的核心,我们接下来的核心任务就是让大家搞清楚这个管道是如何被构建起来的,以及该管道采用怎样的请求处理流程。

clip_image002[6]

2、ASP.NET Core Mini

在过去这些年中,我不断地被问到同一个问题:如何深入地去一个开发框架。我知道每个人都具有适合自己的学习方式,而且我觉得我个人的学习方法也算不上高效,所以我很少会正面回应这个问题。不过有一个方法我倒很乐意与大家分享,那就是当你在学习一个开发框架的时候不要只关注编程层面的东西,而应该将更多的精力集中到对架构设计层面的学习。

针对某个框架来说,它提供的编程模式纷繁复杂,而底层的设计原理倒显得简单明了。那么如何检验我们对框架的设计原理是否透彻呢,我觉得最好的方式就是根据你的理解对框架进行“再造”。当你按照你的方式对框架进行“重建”的过程中,你会发现很多遗漏的东西。如果被你重建的框架能够支撑一个可以运行的Hello World应用,那么可以基本上证明你已经基本理解了这个框架最本质的东西。

虽然ASP.NET Core目前是一个开源的项目,我们可以完全通过源码来学习它,但是我相信这对于绝大部分人来说是有难度的。为此我们将ASP.NET Core最本质、最核心的部分提取出来,重新构建了一个迷你版的ASP.NET Core框架。

clip_image004[6]

ASP.NET Core Mini具有如上所示的三大特点。第一、它是对真实ASP.NET Core框架的真实模拟,所以在部分API的定义上我们做了最大限度的简化,但是两者的本质是完全一致的。如果你能理解ASP.NET Core Mini,意味着你也就是理解了真实ASP.NET Core框架。第二、这个框架是可执行的,我们提供的并不是伪代码。第三、为了让大家能够在最短的时间内理解ASP.NET Core框架的精髓,ASP.NET Core Mini必需足够简单,所以我们整个实现的核心代码不会超过200行。

3、Hello World 2

既然我们的ASP.NET Core Mini是可执行的,意味着我们可以在上面构建我们自己的应用,如下所示的就是在ASP.NET Core Mini上面开发的Hello World,可以看出它采用了与真实ASP.NET Core框架一致的编程模式。

public class Program
{
    public static async Task Main()
    {
        await new WebHostBuilder()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    public static RequestDelegate FooMiddleware(RequestDelegate next)
    => async context => {
        await context.Response.WriteAsync("Foo=>");
        await next(context);
    };

    public static RequestDelegate BarMiddleware(RequestDelegate next)
    => async context => {
            await context.Response.WriteAsync("Bar=>");

            await next(context);
        };

    public static RequestDelegate BazMiddleware(RequestDelegate next)
    => context => context.Response.WriteAsync("Baz");
}

我们有必要对上面这个Hello World程序作一个简答的介绍:在创建出WebHostBuilder之后,我们调用了它的扩展方法UseHttpListener注册了一个自定义的基于HttpListener的服务器,我们会在后续内容中介绍该服务器的实现。在随后针对Configure方法的调用中,我们注册了三个中间件。由于中间件最终是通过Delegate对象来体现的,所以我们可以将中间件定义成与Delegate类型具有相同签名的方法。

我们目前可以先不用考虑表示中间件的三个方法为什么需要成如上的形式,只需要知道三个中间件在针对请求的处理流程中都作了些什么。上面的代码很清楚,三个中间件分别会在响应的内容中写入一段文字,所以程序运行后,如果我们利用浏览器访问该应用,会得到如下所示的输出结果。

clip_image006[6]

4、第一个对象:HttpContext

正如本篇文章表示所说,我们的ASP.NET Core Mini由7个核心对象构建而成。第一个就是大家非常熟悉的HttpContext对象,它可以说是ASP.NET Core应用开发中使用频率最高的对象。要说明HttpContext的本质,还得从请求处理管道的层面来讲。对于由一个服务器和多个中间件构建的管道来说,面向传输层的服务器负责请求的监听、接收和最终的响应,当它接收到客户端发送的请求后,需要将它分发给后续中间件进行处理。对于某个中间件来说,当我们完成了自身的请求处理任务之后,在大部分情况下也需要将请求分发给后续的中间件。请求在服务器与中间件之间,以及在中间件之间的分发是通过共享上下文的方式实现的。

clip_image008[6]

如上图所示,当服务器接收到请求之后,会创建一个通过HttpContext表示的上下文对象,所有中间件都是在这个上下文中处理请求的,那么一个HttpContext对象究竟携带怎样的上下文信息呢?我们知道一个HTTP事务(Transaction)具有非常清晰的界定,即接收请求、发送响应,所以请求响应是两个基本的要素,也是HttpContext承载的最核心的上下文信息。

我们可以将请求理解为输入、响应理解为输出,所以应用程序可以利用HttpContext得到当前请求所有的输入信息,也可以利用它完成我们所需的所有输出工作。为此我们为ASP.NET Core Mini定义了如下这个极简版本的HttpContext。

public class HttpContext
{           
    public  HttpRequest Request { get; }
    public  HttpResponse Response { get; }
}
public class HttpRequest
{
    public  Uri Url { get; }
    public  NameValueCollection Headers { get; }
    public  Stream Body { get; }
}
public class HttpResponse
{
    public  NameValueCollection Headers { get; }
    public  Stream Body { get; }
    public int StatusCode { get; set;}
}

如上面的代码片段所示,HttpContext通过它的两个属性Request和Response来表示请求和响应,它们对应的类型分别为HttpRequest和HttpResponse。通过前者,我们可以得到请求的地址、手部集合和主体内容,利用后者,我们可以设置响应状态码,也可以设置首部和主体内容。

5、第二个对象:RequestDelegate

RequestDelegate是我们介绍的第二个核心对象。我们从命名可以看出这是一个委托(Delegate)对象,和上面介绍的HttpContext一样,我们也只有从管道的角度才能充分理解这个委托对象的本质。

在从事软件行业10多年来,我对软件的架构设计越来越具有这样的认识:好的设计一定是“简单”的设计。所以每当我在设计某个开发框架的时候,一直会不断告诉我自己:“还能再简单点吗?”。我们上面介绍的ASP.NET Core管道的设计就具有“简单”的特质:Pipeline = Server + Middlewares。但是“还能再简单点吗?”,其实是可以的:我们可以将多个Middleware构建成一个单一的“HttpHandler”,那么整个ASP.NET Core框架将具有更加简单的表达:Pipeline =Server + HttpHandler

clip_image010[6]

那么我们如来表达HttpHandler呢?我们可以这样想:既然针对当前请求的所有输入和输出都通过HttpContext来表示,那么HttpHandler就可以表示成一个Action<HttpContext>对象。那么HttpHandler在ASP.NET Core中是通过Action<HttpContext>来表示的吗?其实不是的,原因很简单:Action<HttpContext>只能表示针对请求的 “同步” 处理操作,但是针对HTTP请求既可以是同步的,也可以是异步的,更多地其实是异步的。

那么在.NET Core的世界中如何来表示一个同步或者异步操作呢?你应该想得到,那就是Task对象,那么HttpHandler自然就可以表示为一个Func<HttpContext,Task>对象。由于这个委托对象实在太重要了,所以我们将它定义成一个独立的类型。

clip_image012[6]

6、第三个对象:Middleware

在对RequestDelegate这个委托对象具有充分认识之后,我们来聊聊中间件又如何表达,这也是我们介绍的第三个核心对象。中间件在ASP.NET Core被表示成一个Func<RequestDelegate, RequestDelegate>对象,也就是说它的输入和输出都是一个RequestDelegate

clip_image014[6]

对于为什么会采用一个Func<RequestDelegate, RequestDelegate>对象来表示中间件,很多初学者会很难理解。我们可以这样的考虑:对于管道的中的某一个中间件来说,由后续中间件组成的管道体现为一个RequestDelegate对象,由于当前中间件在完成了自身的请求处理任务之后,往往需要将请求分发给后续中间件进行处理,所有它它需要将由后续中间件构成的RequestDelegate作为输入

当代表中间件的委托对象执行之后,我们希望的是将当前中间件“纳入”这个管道,那么新的管道体现的RequestDelegate自然成为了输出结果。所以中间件自然就表示成输入和输出均为RequestDelegate的Func<RequestDelegate, RequestDelegate>对象。

7、第四个对象:ApplicationBuilder

ApplicationBuilder是我们认识的第四个核心对象。从命名来看,这是我们接触到的第二个Builder,既然它被命名为ApplicationBuilder,意味着由它构建的就是一个Application。那么在ASP.NET Core框架的语义下应用(Application)又具有怎样的表达呢?

对于这个问题,我们可以这样来理解:既然Pipeline = Server + HttpHandler,那么用来处理请求的HttpHandler不就承载了当前应用的所有职责吗?那么HttpHandler就等于Application,由于HttpHandler通过RequestDelegate表示,那么由ApplicationBuilder构建的Application就是一个RequestDelegate对象。

clip_image016[6]

由于表示HttpHandler的RequestDelegate是由注册的中间件来构建的,所以ApplicationBuilder还具有注册中间件的功能。基于ApplicationBuilder具有的这两个基本职责,我们可以将对应的接口定义成如下的形式。Use方法用来注册提供的中间件,Build方法则将注册的中间件构建成一个RequestDelegate对象。

public interface  IApplicationBuilder
{
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
    RequestDelegate Build();
}

如下所示的是针对该接口的具体实现。我们利用一个列表来保存注册的中间件,所以Use方法只需要将提供的中间件添加到这个列表中即可。当Build方法被调用之后,我们只需按照与注册相反的顺序依次执行表示中间件的Func<RequestDelegate, RequestDelegate>对象就能最终构建出代表HttpHandler的RequestDelegate对象。

public class ApplicationBuilder : IApplicationBuilder
{
    private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
    public RequestDelegate Build()
    {
        _middlewares.Reverse();
        return httpContext =>
        {
            RequestDelegate next = _ => { _.Response.StatusCode = 404; return Task.CompletedTask; };
            foreach (var middleware in _middlewares)
            {
                next = middleware(next);
            }
            return next(httpContext);
        };
    }

    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _middlewares.Add(middleware);
        return this;
    }
}

在调用第一个中间件(最后注册)的时候,我们创建了一个RequestDelegate作为输入,后者会将响应状态码设置为404。所以如果ASP.NET Core应用在没有注册任何中间的情况下总是会返回一个404的响应。如果所有的中间件在完成了自身的请求处理任务之后都选择将请求向后分发,同样会返回一个404响应。

8、第五个对象:Server

服务器在管道中的职责非常明确,当我们自动作应用宿主的WebHost的时候,服务它被自动启动。启动后的服务器会绑定到指定的端口进行请求监听,一旦有请求抵达,服务器会根据该请求创建出代表上下文的HttpContext对象,并将该上下文作为输入调用由所有注册中间件构建而成的RequestDelegate对象。

clip_image018[6]

简单起见,我们使用如下这个简写的IServer接口来表示服务器。我们通过定义在IServer接口的唯一方法StartAsync启动服务器,作为参数的handler正是由所有注册中间件共同构建而成的RequestDelegate对象

public interface IServer
{ 
    Task StartAsync(RequestDelegate handler);
}

9、HttpContext和Server之间的适配

面向应用层的HttpContext对象是对请求和响应的封装,但是请求最初来源于服务器,针对HttpContext的任何响应操作也必需作用于当前的服务器才能真正起作用。现在问题来了,所有的ASP.NET Core应用使用的都是同一个HttpContext类型,但是却可以注册不同类型的服务器,我们必需解决两者之间的适配问题。

clip_image020[6]

计算机领域有一句非常经典的话:“任何问题都可以通过添加一个抽象层的方式来解决,如果解决不了,那就再加一层”。同一个HttpContext类型与不同服务器类型之间的适配问题也可可以通过添加一个抽象层来解决,我们定义在该层的对象称为Feature。如上图所示,我们可以定义一系列的Feature接口来为HttpContext提供上下文信息,其中最重要的就是提供请求的IRequestFeature和完成响应的IResponseFeature接口。那么具体的服务器只需要实现这些Feature接口就可以了。

clip_image022[6]

我们接着从代码层面来看看具体的实现。如下面的代码片段所示,我们定义了一个IFeatureCollection接口来表示存放Feature对象的集合。从定义可以看出这是一个以Type和Object作为Key和Value的字典,Key代表注册Feature所采用的类型,而Value自然就代表Feature对象本身,话句话说我们提供的Feature对象最终是以对应Feature类型(一般为接口类型)进行注册的。为了编程上便利,我们定义了两个扩展方法Set<T>和Get<T>来设置和获取Feature对象。

public interface IFeatureCollection : IDictionary<Type, object> { }
public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }   
public static partial class Extensions
{
    public static T Get<T>(this IFeatureCollection features) => features.TryGetValue(typeof(T), out var value) ? (T)value : default(T);
    public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
    { 
        features[typeof(T)] = feature;
        return features;
    }
}

如下所示的用来提供请求和响应IHttpRequestFeature和IHttpResponseFeature接口的定义,可以看出它们具有与HttpRequest和HttpResponse完全一致的成员定义。

public interface IHttpRequestFeature
{
    Uri                     Url { get; }
    NameValueCollection     Headers { get; }
    Stream                  Body { get; }
}    
public interface IHttpResponseFeature
{
    int                       StatusCode { get; set; }
    NameValueCollection     Headers { get; }
    Stream                  Body { get; }
}

接下来我们来看看HttpContext的具体实现。ASP.NET Core Mini的HttpContext只包含Request和Response两个属性成员,对应的类型分别为HttpRequest和HttpResponse,如下所示的就是这两个类型的具体实现。我们可以看出HttpRequest和HttpResponse都是通过一个IFeatureCollection对象构建而成的,它们对应的属性成员均有分别由包含在这个Feature集合中的IHttpRequestFeature和IHttpResponseFeature对象来提供的。

public class HttpRequest
{
    private readonly IHttpRequestFeature _feature;    
      
    public  Uri Url => _feature.Url;
    public  NameValueCollection Headers => _feature.Headers;
    public  Stream Body => _feature.Body;

    public HttpRequest(IFeatureCollection features) => _feature = features.Get<IHttpRequestFeature>();
}

public class HttpResponse
{
    private readonly IHttpResponseFeature _feature;

    public  NameValueCollection Headers => _feature.Headers;
    public  Stream Body => _feature.Body;
    public int StatusCode { get => _feature.StatusCode; set => _feature.StatusCode = value; }

    public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature>();

}

HttpContext的实现就更加简单了。如下面的代码片段所示,我们在创建一个HttpContext对象是同样会提供一个IFeatureCollection对象,我们利用该对象创建对应的HttpRequest和HttpResponse对象,并作为对应的属性值。

public class HttpContext
{           
    public  HttpRequest Request { get; }
    public  HttpResponse Response { get; }

    public HttpContext(IFeatureCollection features)
    {
        Request = new HttpRequest(features);
        Response = new HttpResponse(features);
    }
}

10、HttpListenerServer

在对服务器和它与HttpContext的适配原理具有清晰的认识之后,我们来尝试着自己定义一个服务器。在前面的Hello World实例中,我们利用WebHostBuilder的扩展方法UseHttpListener注册了一个HttpListenerServer,我们现在就来看看这个采用HttpListener作为监听器的服务器类型是如何实现的。

由于所有的服务器都需要自动自己的Feature实现来为HttpContext提供对应的上下文信息,所以我们得先来为HttpListenerServer定义相应的接口。对HttpListener稍微了解的朋友应该知道它在接收到请求之后同行会创建一个自己的上下文对象,对应的类型为HttpListenerContext。如果采用HttpListenerServer作为应用的服务器,意味着HttpContext承载的上下文信息最初来源于这个HttpListenerContext,所以Feature的目的旨在解决这两个上下文之间的适配问题。

clip_image024[6]

如下所示的HttpListenerFeature就是我们为HttpListenerServer定义的Feature。HttpListenerFeature同时实现了IHttpRequestFeature和IHttpResponseFeature,实现的6个属性成员最初都来源于创建该Feature对象提供的HttpListenerContext对象。

public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext _context;
    public HttpListenerFeature(HttpListenerContext context) => _context = context;

    Uri IHttpRequestFeature.Url => _context.Request.Url;
    NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;
    NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;
    Stream IHttpRequestFeature.Body => _context.Request.InputStream;
    Stream IHttpResponseFeature.Body => _context.Response.OutputStream;
    int IHttpResponseFeature.StatusCode { get => _context.Response.StatusCode; set => _context.Response.StatusCode = value; }
}

如下所示的是HttpListenerServer的最终定义。我们在构造一个HttpListenerServer对象的时候可以提供一组监听地址,如果没有提供,会采用“localhost:5000”作为默认的监听地址。在实现的StartAsync方法中,我们启动了在构造函数中创建的HttpListenerServer对象,并在一个循环中通过调用其GetContextAsync方法实现了针对请求的监听和接收。

public class HttpListenerServer : IServer
{
    private readonly HttpListener     _httpListener;
    private readonly string[]             _urls;

    public HttpListenerServer(params string[] urls)
    {
        _httpListener = new HttpListener();
        _urls = urls.Any()?urls: new string[] { "http://localhost:5000/"};
    }

    public async Task StartAsync(RequestDelegate handler)
    {
        Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));    
        _httpListener.Start();
        while (true)
        {
            var listenerContext = await _httpListener.GetContextAsync(); 
            var feature = new HttpListenerFeature(listenerContext);
            var features = new FeatureCollection()
                .Set<IHttpRequestFeature>(feature)
                .Set<IHttpResponseFeature>(feature);
            var httpContext = new HttpContext(features);
            await handler(httpContext);
            listenerContext.Response.Close();
        }
    }
}

当HttpListener监听到抵达的请求后,我们会得到一个HttpListenerContext对象,此时我们只需要据此创建一个HttpListenerFeature对象并它分别以IHttpRequestFeature和IHttpResponseFeature接口类型注册到创建FeatureCollection集合上。我们最终利用这个FeatureCollection对象创建出代表上下文的HttpContext,然后将它作为参数调用由所有中间件共同构建的RequestDelegate对象即可。

11、第六个对象:WebHost

到目前为止我们已经知道了由一个服务器和多个中间件构成的管道是如何完整针对请求的监听、接收、处理和最终响应的,接下来来讨论这样的管道是如何被构建出来的。管道是在作为应用宿主的WebHost对象启动的时候被构建出来的,在ASP.NET Core Mini中,我们将表示应用宿主的IWebHost接口简写成如下的形式:只包含一个StartAsync方法用来启动应用程序。

public interface IWebHost
{
    Task StartAsync();
}

由于由WebHost构建的管道由Server和HttpHandler构成,我们在默认实现的WebHost类型中,我们直接提供者两个对象。在实现的StartAsync方法中,我么只需要将后者作为参数调用前者的StartAsync方法将服务器启动就可以了。

public class WebHost : IWebHost
{
    private readonly IServer _server;
    private readonly RequestDelegate _handler; 
    public WebHost(IServer server, RequestDelegate handler)
    {
        _server = server;
        _handler = handler;
    } 
    public Task StartAsync() => _server.StartAsync(_handler);
}

12、第七个对象:WebHostBuilder

作为最后一个着重介绍的核心对象,WebHostBuilder的使命非常明确:就是创建作为应用宿主的WebHost。由于在创建WebHost的时候需要提供注册的服务器和由所有注册中间件构建而成的RequestDelegate,所以在对应接口IWebHostBuilder中,我们为它定义了三个核心方法。

public interface IWebHostBuilder
{
    IWebHostBuilder UseServer(IServer server);
    IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
    IWebHost Build();
}

除了用来创建WebHost的Build方法之外,我们提供了用来注册服务器的UseServer方法和用来注册中间件的Configure方法。Configure方法提供了一个类型为Action<IApplicationBuilder>的参数,意味着我们针对中间件的注册是利用上面介绍的IApplicationBuilder对象来完成的。

如下所示的WebHostBuilder是针对IWebHostBuilder接口的默认实现,它具有两个字段分别用来保存注册的中间件和调用Configure方法提供的Action<IApplicationBuilder>对象。当Build方法被调用之后,我们创建一个ApplicationBuilder对象,并将它作为参数调用这些Action<IApplicationBuilder>委托,进而将所有中间件全部注册到这个ApplicationBuilder对象上。我们最终调用它的Build方法得到由所有中间件共同构建的RequestDelegate对象,并利用它和注册的服务器构建作为应用宿主的WebHost对象。

public class WebHostBuilder : IWebHostBuilder
{
    private IServer _server;
    private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();   

    public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
    {
        _configures.Add(configure);
        return this;
    }
    public IWebHostBuilder UseServer(IServer server)
    {
        _server = server;
        return this;
    }   

    public IWebHost Build()
    {
        var builder = new ApplicationBuilder();
        foreach (var configure in _configures)
        {
            configure(builder);
        }
        return new WebHost(_server, builder.Build());
    }
}

13、回顾一下Hello World 2

到目前为止,我们已经将ASP.NET Core Mini涉及的七个核心对象介绍完了,然后我们再来回顾一下建立在这个模拟框架上的Hello World程序。

public class Program
{
    public static async Task Main()
    {
        await new WebHostBuilder()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    public static RequestDelegate FooMiddleware(RequestDelegate next)
    => async context => {
        await context.Response.WriteAsync("Foo=>");
        await next(context);
    };

    public static RequestDelegate BarMiddleware(RequestDelegate next)
    => async context => {
            await context.Response.WriteAsync("Bar=>");

            await next(context);
        };

    public static RequestDelegate BazMiddleware(RequestDelegate next)
    => context => context.Response.WriteAsync("Baz");
}

首选我们调用WebHostBuilder的扩展方法UseHttpListener采用如下的方式完成了针对HttpListenerServer的注册。由于中间件体现为一个Func<RequestDelegate, RequestDelegate>对象,我们自然可以采用与之具有相同声明的方法(FooMiddleware、BarMiddleware和BazMiddleware)来定义对应的中间件。中间件调用HttpResponse的WriteAsync以如下的方式将指定的字符串写入响应主体的输出流。

public static partial class Extensions
{
   public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
    => builder.UseServer(new HttpListenerServer(urls));

    public static Task WriteAsync(this HttpResponse response, string contents)
    {
        var buffer = Encoding.UTF8.GetBytes(contents);
        return response.Body.WriteAsync(buffer, 0, buffer.Length);
     }
}

14、打个广告:《ASP.NET Core框架揭秘》

ASP.NET Core Mini模拟了真实ASP.NET Core框架最核心的部分,即由服务器和中间件构成的请求处理管道。真正的ASP.NET Core框架自然要复杂得多得多,那么我们究竟遗漏了什么呢?

clip_image026[6]

如上所示的5个部分是ASP.NET Core Mini没有涉及的,其中包括依赖注入、以Startup和StartupFilter的中间件注册方式、针对多种数据源的配置系统、诊断日志系统和一系列预定义的中间件,上述的每个方面都涉及到一个庞大的主题,我们将ASP.NET Core涉及到的方方面都写在我将要出版的《ASP.NET Core框架揭秘》中,如果你想全方面了解一个真实的ASP.NET Core框架,敬请期待新书出版。

image

sqlserver无法作为数据库主体执行,因为主体 "dbo" 不存在... 解决方案_魔帥ぁ拽拽_新浪博客

mikel阅读(870)

SQLServer无法作为数据库主体执行,因为主体 “dbo” 不存在… 解决方案_魔帥ぁ拽拽_新浪博客,魔帥ぁ拽拽,

来源: sqlserver无法作为数据库主体执行,因为主体 “dbo” 不存在… 解决方案_魔帥ぁ拽拽_新浪博客

USE 出问题的库名; 
EXEC sp_changedbowner ‘sa’;

为用户添加数据库角色时提示已经存在此帐号请查看数据库下是否已经存在该帐号并删除重新授权

与VS集成的若干种代码生成解决方案[博文汇总(共8篇)] - Artech - 博客园

mikel阅读(1044)

来源: 与VS集成的若干种代码生成解决方案[博文汇总(共8篇)] – Artech – 博客园

[第1篇] 通过CodeDOM定义生成代码的结构

我不知道大家对CodeDOM的代码生成机制是否熟悉,但是有一点可以确定:如果你使用过Visual Studio,你就应该体验过它带给我们在编程上的便利。随便列举三种典型的代码生成的场景:在创建强类型DataSet的时候,VS会自动根据Schema生成相应的C#或者VB.NET代码;当我们编辑Resource文件的时候,相应的的后台代码也会自动生成;当我们通过添加Web Reference调用Web Service或者WCF Service的时候,VS会自动生成服务代理的代码和相应的配置。总的来说,通过和VS集成的动态代码生成工具使我们可以“强类型”的方式进行编程,进而提供我们的效率并减低错误的几率。

CodeDOM 提供了表示许多常见的源代码元素类型的类型。您可以设计一个生成源代码模型的程序,使用CodeDOM 元素构成一个对象图。而这个对象图包含C#或者VB.NET代码包含的基本元素:命名空间、类型、类型成员(方法、属性、构造函数、事件等),并且包括方法实现的具体语句(Statement)。也就是说它的结构就是对一个具体.vb或者.cs文件代码的反映。在这里我不会具体介绍CodeDOM体系结构……[阅读全文]

[第2篇] 通过Visual Studio的Custom Tool定义代码生成器

在《前篇》中我们已经通过CodeDOM的形式定义了将要生成的代码结构,而这个CodeDOM对象就是CodeCompileUnitCodeCompileUnit本身是与编程语言无关的,我们可以利用基于某种编程语言(VB.NET或者C#)的CodeDomProviderCodeCompileUnit对象转换成具体的代码文本。为了让我们的VS自动地为我们生成代码,我们需要以Custom Tool的形式编写相应的代码生成器。我们编写的代码生成器最终通过COM组件的形式对外提供服务,所以其中涉及到COM组件的注册。为了实现同VS的集成,还涉及到相应注册表设置……[阅读全文]

[第3篇] 不同于CodeDOM的代码生成机制——T4

前面两篇介绍了如何通过CodeDOM+Custom Tool的代码生成方式实现了代码的自动生成。实际上,我们最常用的代码生成当时不是CodeDOM,而是T4,这是一个更为强大,并且适用范围更广的代码生成技术。T4是对“Text Template Transformation Toolkit”(4个T)的简称。T4直接包含在VS2008和VS2010中,是一个基于文本文件转换的工具包。T4的核心是一个基于“文本模板”的转换引擎,我们可以通过它生成一切类型的文本型文件,比如我们常用的代码文件类型包括:C#、VB.NET、T-SQL、XML甚至是配置文件等。

对于需要通过T4来进行代码生成工作的我们来说,需要做的仅仅是根据转换源(Transformation Source),比如数据表、XML等(由于例子简单,HelloWord模板没有输入源)和目标文本(比如最终需要的C#或者T-SQL代码等)定义相应的模板。T4模板作用就相当于进行XML转化过程中使用的XSLT。

T4模板的定义非常简单,整个模板的内容包括两种形式:静态形式和动态动态。前者就是直接写在模板中作为原样输出的文本,后者是基于某种语言编写代码,T4引擎会动态执行它们。这和我们通过内联的方式编写的ASP.NET页面很相似:HTML是静态的,以C#或者VB.NET代码便写的动态执行的代码通过相应的标签内嵌其中。[阅读全文]

[第4篇] 通过T4模板实现单文件的代码生成

在《前一篇》中,我对T4模板的组成结构、语法,以及T4引擎的工作原理进行了大体的介绍,并且编写了一个T4模板实现了如何将一个XML转变成C#代码。为了让由此需求的读者对T4有更深的了解,我们通过T4来做一些更加实际的事情——SQL Generator。在这里,我们可以通过SQL Generator为某个数据表自动生成进行插入、修改和删除的存储过程……[阅读全文]

[第5篇] 通过T4模板实现多文件的代码生成

在《前一篇》中我们通过T4模板为我们指定的数据表成功生成了我们需要的用于添加、修改和删除操作的存储过程。但是这是一种基于单个文件的解决方案,即我们必须为每一个生成的存储过程建立一个模板。如果我们提供一种基于多文件的代码生成方式,将会为编程人员带来极大的便利。借助于T4 ToolBox这个开源工具箱,多文件的SQL Generator的实现变得异常简单。[阅读全文]

[第6篇] 解决T4模板的程序集引用的五种方案

如果你的T4模板需要调用一个自定义的类型,并且该类型定义在某个非系统程序集中,你就需要通过T4<#@ assembly…#>指令引用该程序集。在VS 2008环境下,你只需要为当前项目添加相应的程序集引用,并且通过<@ assembly…#>指令的name属性指定程序集(Dll)的文件名即可。但是这种方式对于VS 2010则行不通,因为T4引擎在解析和编译模板的时候并不会从项目引用列表中去定位目标程序集。本篇文章为你介绍在VS2010下5种不同的程序集引用的方式……[ 阅读全文]

[第7篇] 编写T4模板进行代码生成无法避免的两个话题:”Assembly Locking”&”Debug”

出于提高性能考虑,T4引擎在进行基于代码生成的模板转换(Template Transformation)的时候,会始终重用同一个AppDomain。由于该AppDomain不会自动卸载,这就会导致该AppDomain始终锁定所有被它加载的程序集。如果我们需要释放程序集,我们不得不重启VS。但是,对于T4模板的开发调试阶段,这种通过重新启动VS的方式去释放程序集以确保我们的项目能够成功编译是不能接受的。那么,是否有一种解决方案既能够确保T4引擎能够进行正常的模板转换,又能避免它强行锁定引用程序集呢?

VS和一些T4编辑器虽然给了基本的智能感知支持,但是在绝大部分我们相当于在编写纯文本的脚本,所以对于一些比较复杂的模板转换逻辑,我们需要通过Debug的方式去发现一些无法避免的问题……[阅读全文]

[第8篇] 通过自定义BuildProvider为ASP.NET提供代码生成

对于ASP.NET应用的开发者来说,你可能不知道什么是BuildProvider,但是你几乎无时无刻不在使用它所带来的代码生成机制。当你创建一个.aspx文件的时候,为什么会自动创建对应源代码?当你在该.aspx页面中以XML的方式添加一个按钮,源代码中为什么会自动添加一个同名的属性。实际上,ASP.NET就是通过一个特殊的BuildProvider实现了将.aspx文件内容转换成相应的源代码,这个特殊的.aspx文件就是:PageBuildProvider。基于不同的文件类型,ASP.NET会采用不同的BuildProvider进行源代码的生成。比如UserControlBuildProvider和MasterPageBuildProvider分别实现了基于用户控件文件(.ascx)和母板页(.master)的源代码生成……[阅读全文]

一个完整的用于追踪数据改变的解决方案 - Artech - 博客园

mikel阅读(750)

来源: 一个完整的用于追踪数据改变的解决方案 – Artech – 博客园

在之前一篇介绍CDC的文章中,我说Audit Trail(或者Audit Log)是大部分企业级应用不可以或缺的功能。本篇给你一个完整的Audit Trail解决方案,不仅可以记录每一笔业务操作的信息(比如操作时间、操作者等),并且可以追踪每一笔业务引起的说有数据的改变(如果需要)。

目录
一、数据表的设计
二、数据变化的表示
三、AuditLog基本信息的写入
四、通过SQLCDC追踪源表数据变化
五、删除操作的TransactionId如何被记录?
六、通过SQL Job转储AuditLog详细信息
七、代码生成的应用

一、数据表的设计

clip_image001在数据库中,我们通过如右图所示的具有主子关系的两个表存储AuditLog相关信息。我们将“事务”作为我们进行追踪的单位,不过这里的讲的“事务”更多地指业务处理事务的概念。每一个被追踪的事务在AuditLog表具有一条匹配的记录,该记录表示该事务的基本信息:UserName(操作者)、AuditTime(操作时间)、Activity(可以看成是对事物的命名)和Description(事务补充性的描述)。主键TransactionId唯一标识一个事务。

子表AuditLogData记录事务详细的信息,即事务所引起的数据变化。一个完整的业务逻辑往往涉及到对多个数据表、多条记录的操作。而AuditLogData每一条记录表示某个事务针对某个单一数据表所带来的数据变化,而SourceTable字段表示源表的名称。而DataChange字段以XML的形式表示数据的改变,它具有如下的格式。

二、数据变化的表示

数据操作类型无外乎添加、更新和删除,我们通过不同的XML结构表示不同操作引起的数据改变。具体来说,对于添加操作,我们需要记录下插入的记录;对于删除操作,需要记录下原来的记录;而对于数据更新,则需要同时记录下更新先后的记录。

举个例子,假设我们具有一个Users表,它具有三个基本字段:Id、Name和Birthday。下面的XML分别表示添加、删除和更新操作后我们需要记录下的数据变化。

添加:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="insert">
3:   <current>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </current>
8: </cdc>

删除:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="delete">
3:   <original>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </original>
8: </cdc>

更新:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="update">
3:   <original>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </original>
8:   <current>
9:     <Id type="VARCHAR(50)">001</Id>
10:     <Name type="NVARCHAR(50)">Bar</Name>
11:     <BirthDay type="DATE">1982-07-10</BirthDay>
12:   </current>
13: </cdc>

当然,你也可以根据需要自定义XML的结构。

三、AuditLog基本信息的写入

我们现在我们的目标就是如何将追踪到的基于一个事务相关的信息写入到上面我们创建的两个表中。主表AuditLog的信息是很容易被写入的,比如你可以定义像下面一样的一个AuditLogger类。

   1: public class AuditLogger
   2: {
   3:     public void Write(string activity)
   4:     { }
   5:
   6:     public void Write(string activity, string description)
   7:     { }
   8: }

AuditLogger的Write方法进行传入了Activity和Description,而没有TransactionId、UserName和AuditTime。其中AuditTime自然是当前时间,而UserName应该是登录系统的用户。而对于TransactionId,我们应该采用上下文的方式来获取,具体原因会在下面谈到。如果你直接使用System.Transactions事务实现我们进行追踪的“事务”,你可以直接使用当前事务(Transaction.Current)的DistributedIdentifier或者LocalIdentifier。

   1: var transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier;
   2: //Or
   3: var transactionId = Transaction.Current.TransactionInformation.LocalIdentifier;

基于AuditLog表的事务基本信息的日志好解决,那么我们如何将事务引起的事务变化记录到AuditLogData表中呢?这样的工作我们完全实现在SQL Server中

四、通过SQLCDC追踪源表数据变化

追踪记录每笔业务操作数据改变的利器——SQLCDC》介绍了一种有效记录基于某个数据表数据变化的方式:SQLCDC,在这里我们直接利用它来记录AuditLog的详细信息。当我们为某个表(比如Users)开启了CDC特性之后,SQL Server会为之创建一个相应的CT表(Users_CT),在默认的情况下Users_CT包含与Users表的所有字段。如果你不希望CDC追踪所有的字段,你可以显式地设定具体的字段。

AuditLogData表中有一个字段TransactionId表示记录属于哪个具体的事务,为了让CDC可以记录下正确TransactionId,需要在每一个被追踪的表中添加这么一个额外的字段。这个应该不是什么问题,比如我们的每个表中都具有6个系统字段:TransactionId、VersionNo、CreatedBy、CreatedTime、LatestUpdatedBy和LatestUpdatedTime。

由于每个数据表都具有了一个TransactionId字段,那么在进行数据提交的时候,需要将当前事务的ID为之赋值,这就是为什么我推荐采用上下文的方式来获取当前TransactionId的原因。但是,还有一个问题没有解决——数据删除操作的TransactionId如何被记录下来呢?

五、删除操作的TransactionId如何被记录?

由于代表当前事务的TransactionId最终会通过Insert或者Update SQL语句写入数据表,但是对于删除操作呢?由于我们直接调用Delete语句将相应的数据操作,表示当前删除操作所在的事务是无法被写入的,最终CDC记录下来的数据是无法反映出删除的记录隶属于哪个事务。

由于最终对数据库操作都是通过SQL提交的,或者是存储过程,或者是SQL文本。为了解决这个问题,我们只需要改变我们的SQL脚本,在Delete执行之前执行Update语句写入新的TransactionId

也就是说,对于一个删除操作,实际上是先做Update,最后做Delete。在这种情况下,CDC会为你记录下三条记录,前两条是为Update记录的,最后一条是为Delete记录的。为了区分CDC追踪的记录是正常的Update还是为了Delete而进行的Update,我们可以做一些标记。比如你可以在TransactionId的值之前添加一个前缀,表示Update操作是为Delete而作的。

六、通过SQL Job转储AuditLog详细信息

clip_image002CDC仅仅会将基于某个表的数据改变记录到基于该表的CT表中,最终我们需要将这些CT表中的数据转存到我们指定的AuditLogData表中,这个工作可以通过SQLJob来实现。你自行创建一个SQL Job实现从若干CT表到AuditLogData的数据转存,并根据你的需要(主要是实时性的需要)配制Job执行的时间或者间隔。右图揭示了AuditLog详细信息是如何一步步地被记录的。

七、代码生成的应用

在这个解决方案中,我们需要一个不可或缺的东西:代码生成器。它用于自动生成如下的SQL脚本:为某个表开启CDC特性并指定追踪字段的T-SQL脚本,和进行AuditLog详细信息转存(丛CT表到AuditLogData表)的SQL Job脚本。关于代码生成,可以参考《与VS集成的若干种代码生成解决方案

追踪记录每笔业务操作数据改变的利器——SQLCDC - Artech - 博客园

mikel阅读(804)

来源: 追踪记录每笔业务操作数据改变的利器——SQLCDC – Artech – 博客园

对于大部分企业应用来用,有一个基本的功能必不可少,那就是Audit Trail或者Audit Log,中文翻译为追踪检查、审核检查或者审核记录。我们采用Audit Trail记录每一笔业务操作的基本信息,比如操作的基本描述、操作时间、操作者等。对于一些安全级别比较高的应用,或者操作一些比较敏感的数据,我们甚至需要记录该笔业务操作引起的数据的改变。具体来说,这里的“数据改变”指的是每一条影响的记录在操作执行前后的变化。对于添加的记录,需要记录下新插入的记录;对于删除的记录,需要记录下原来的记录;对于更新的记录,则需要同时记录下更新前后的记录。

说到这里,很多人都会想到采用触发器的方式来实现对数据改变的捕捉。但是这种实现方案具有一个最大的局限:由于触发器是在数据操作所在事务范围内执行的,所有会带来性能的问题,严重的话还会因为触发器的执行导致事务超市。所以在这里,我们介绍一种更好的解决方案:SQLCDC

目录
一、SQLCDC简介
二、在数据库级别开启CDC
三、为某个数据表开启CDC
四、记录添加记录的数据改变
五、记录删除数据的数据改变
六、记录更新记录的数据改变

一、SQLCDC简介

CDC的全名为Change Data Capture,顾名思义,就是用于追踪和捕捉数据改变。CDC是在SQL Server 2008中才出现的新特性,而这个特性则在很早之前就出现在了Oracle中。对于SQL Server之前版本来说,在没有CDC的情况下,如果需要记录基于某个数据表的数据改变,我们只能采用触发器,具体来说就是通过手工创建After Insert、After Update和After Delete触发器去记录变化的数据。而CDC给了我们一种更为方便、易用和省心的方式去记录某个数据表的历史操作。

二、在数据库级别开启CDC

在默认的情况下,数据库的CDC特性是被关闭的,你可以通过系统表sys.databasesis_cdc_enabled字段确定某个数据库的CDC是否开启。如果在默认的情况下,我执行如下的SQL语句查看数据库TestDb的CDC是否开启,你将会看到该字段的值为0。image

你可以通过执行系统存储过程sys.sp_cdc_enable_db为当前数据库开启CDC特性。下面的T-SQL代码片断中,我们通过执行该存储过程为TestDb打开了CDC特性。

Use TestDb
Go
Exec sys.sp_cdc_enable_db
Go

 

三、为某个数据表开启CDC

由于CDC用于记录基于某个数据表的数据改变,所以在当前数据库CDC开启的情况下,你还需要显式地为某个数据表开启CDC特性。作为演示,我们通过如下T-SQL在TestDb下创建了一个简单的Users表,它仅仅具有三个字段:Id、Name和Birthday。

CREATE TABLE [dbo].[Users](
    [Id] [varchar](50) PRIMARY KEY,
    [Name] [nvarchar](50) NOT NULL,
    [Birthday] [date] NOT NULL)

数据表的CDC特性的开启通过执行sys.sp_cdc_enable_table存储过程实现。调用该存储过程的最简的方式就是指定数据表的Schema、名称和用于提取改变数据必须具有的权限(角色)。我通过执行下面的T-SQL将我们创建的Users表的CDC特性打开,其中@role_name参数被设置成NULL,表明我不对读取改变数据操作进行授权。sys.sp_cdc_enable_table具有很多参数,至于相应参数所影响的CDC行为,可以参考SQL Server 2008在线文档。

Use TestDb
Go
Exec sys.sp_cdc_enable_table 'dbo', 'Users', @role_name = NULL
Go

需要注意的是,CDC实际上建立在SQL Server Agent之上的,所以在执行上述T-SQL之前需要启动SQL Server Agent。当某个数据表的CDC特性被开启之后,系统会为创建一个用于保存数据变化的追踪表(Tracking Table)。该表的Schema为cdc,命名方式为被追踪表的表名后加“CT”后缀。执行上面一段T-SQL之后,会有如下一个系统表被创建出来,我们发现Users表的三个字段也在该表中。此外。该表还具有5个额外字段:__$start_lsn、__$end_lsn、__$seqval、__$operation 和__$update_mask,表示日志系列号(Log Sequence Number)、操作(删除、插入、修改前和修改后)信息。

image

四、记录添加记录的数据改变

现在我们就可以来试验CDC针对某个数据表的数据改变的捕捉功能了,我们先来试试记录的添加操作。为此,我们执行如下一段T-SQL,插入两笔User记录。

Insert Into Users(Id, Name, Birthday)
Values ('001','Foo','1981-08-24')

Insert Into Users(Id, Name, Birthday)
Values ('002','Bar','1981-08-24')

然后通过如下的T-SQL查看cdc.dbo_Users_CT表的数据是否将添加操作涉及到的数据改变保存起来。从查询结果我们清晰地看到,上面添加的两笔记录已经被记录下来,而__$operation字段为2表示的是“插入”操作。

image

五、记录更新数据的数据改变

接下来我们来CDC对更新操作的追踪记录,为此我们通过下面的T-SQL改变了用户Foo的Birthday。

Update Users
Set Birthday = '1982-7-10'
Where Name = 'Foo'

再次执行对于cdc.dbo_Users_CT的全表查询,你会看到这次多了两笔记录。其中第3条记录的是修改之前的数据,而第四条则是修改之后的数据,它们的__$operation字段德值分别为34

image

在这里值得一提的是__$update_mask字段的值,它表示的记录更新操作改变的字段。这是一个以16进制表示的数字,在进行对修改字段进行判断的时候需要将其转换成2进制。上述的更新操作对应的__$update_mask值为0x04,转化成2进制就是100,这三位分别代表3个字段。不过这里的顺序是从右到左,所以100这三位表示的字段为Birthday、Name和Id。1表示改变,0则表示保持不变。由于在上面的T-SQL中,我们只改动了Birthday,这个和100这个值是吻合的。

六、记录删除记录的数据改变

我们最后来演示当我们对记录实施删除操作的时候,CDC会为我们记录下怎样的数据。现在我们执行如下的T-SQL将Users表中所有的记录均删除。

Delete From Users

查看cdc.dbo_Users_CT的记录,多出的两笔记录正式我们删除的User记录,__$operation字段的值为1表示“删除”操作。

image

本篇文章仅仅是简单介绍SQLCDC的基本原理和大体上的使用方式,这篇文章《Introduction to Change Data Capture (CDC) in SQL Server 2008[转]》会给你更加详尽的介绍。如果你想深入研究SQLCDC,还是参考SQL Server 2008在线文档。

一个完整的用于追踪数据改变的解决方案

SqlServer中查询出多条记录变成一个记录(行变列) - 音乐啤酒 - 博客园

mikel阅读(805)

来源: SqlServer中查询出多条记录变成一个记录(行变列) – 音乐啤酒 – 博客园

碰到一个SQL问题,不知道怎么搞,问了群里面的一个SQL高手 ,夕颜大哥,他使用短短一句SQL就实现我的要求,太牛了。原来sql还可以这么神奇,唉,还是怪自己sql不精通。

一个表A,两个字段ID,name,一个ID可能对应多name,要求根据一个ID查询出对应的name,但是不是以多行的形式显示出来,而是以一个行的形式出来,多个name以,分割。
比如 数据
i name
1 a
1 b
1 c
应该查询来的结果是; [a,b,c 1]
sql是这样写的

DECLARE @names varchar(1000)
SET @names = ”
SELECT @names=@names+ISNULL(name, ”)+’,’
FROM A
WHERE id = 1
select @names

用的是sql的所谓自拼接,这是夕颜的说法,大家研究一下,感觉很有意思的。

零基础ASP.NET Core MVC插件式开发 - sylla - 博客园

mikel阅读(884)

来源: 零基础ASP.NET Core MVC插件式开发 – sylla – 博客园

零基础ASP.NET Core MVC插件式开发

一个项目随着业务模块的不断增加,系统会越来越庞大。如果参与开发的人员越多,管理起来难度也很大。面对这样的情况,首先想到的是模块化插件式开发,根据业务模块,拆分成各个独立的插件,然后分配不同开发人员开发,互相之间没有依赖完全独立。

这里介绍一种使用ASP.NET Core MVC 技术开发Web后台系统,使用插件式的方案。这个方案在项目中已经使用效果觉得还不错,这里把主要思想提取出来,做个简单的demo分享下

 

一、创建主项目

这里使用的开发工具是vs2019,ASP.NET Core 2.1。

创建一个ASP.NET Core MVC项目,项目结构如下图1,完全是脚手架自动创建好的标准项目

图1

 

这里是一个简单的默认程序,在实际的项目中,特别是传统信息管理web后台系统,登录,以及登录之后的主框架,还有一些公共的模块,可以在主项目里面做,不会随业务而变动的。然后各个具体业务模块分成独立的插件开发。

这个主项目建立好之后,运行正常,如图2

图2

 

下面开始做代码部分添加,为了方便阅读代码以截图提供,最后会附加完整的demo程序提供下载。

1、在Startup.cs类里面增加如下代码图3,具体作用看注释,这里就不再赘述。

图3

 

这里是把插件程序注册到主程序里面,核心就是使用了ApplicationParts

 

2、Startup.cs类里面还有一个地方要修改,增加一个Areas区域的路由映射,图4

图4

增加这个的具体原因等会看插件项目的说明

 

3、还有为了演示能否访问到插件,这里增加两个插件的超链接,具体到业务中,菜单肯定是数据库动态维护的。

修改Views/Shared/_Layout.cshtml代码如下 图5

图5

 

 

二、创建插件项目

插件项目也是一个标准的ASP.NET Core MVC 项目。我这里为了演示,创建了两个独立的插件项目。如下图6,图7

图6

 

图7

 

插件项目说明,插件是根据业务模块划分的,还有为了防止注册到主项目之后路由命名的冲突,插件项目就使用MVC自带的区块Areas功能来存放。这里就是前面主项目要增加Areas路由映射的原因。然后删除脚手架默认创建的外层控制器和视图文件,因为主项目也有这样同名的控制器路由,这些不删除,到时候注册到主项目,会出现重复路由错误。

插件项目增加各自的Areas,新建控制器如图8,新建对应试图如图9。这里就添加几个模拟数据。

图8

 

图9

 

到这里,把插件项目发布出来的xxx.dll和xxx.Views.dll,放到主项目里面去就可以运行了。

 

但是作为插件开发者,每次需要把文件发布放到主项目去才能运行,对开发调试都是不方便,因为插件开发者可能就没有主项目的权限,主项目是统一管理的。分配一个插件还每次都附带一个主项目也比较繁琐。

想到这里就可以反过来把主程序dll注册到插件项目里面,这样就可以在插件项目里面vs直接运行调试起来。

 

以插件1为例,Startup.cs类里面增加如下代码,图10

图10

 

这样把主程序的dll提供,比如这个演示demo主程序就需要这两个(Agile.WebPlatform.Main.dll与Agile.WebPlatform.Main.Views.dll)把他们放到插件项目的运行目录。vs直接运行插件项目,就可以正常启动了。各种开发调试完全不受其他影响。

插件里面直接启动调试图11

图11

 

运行效果如下 图12

图12

 

 

点击插件1 显示如下图13

图13

注意!这里虽然可以看到插件2,但点击插件2是会提示找不到路由的,只能调试各自插件的内容。

 

开发调试其他插件类似,提供主项目的dll,各个插件项目完全可以独立开发自己的业务模块,然后运行调试。

 

三、发布

最后,只要把插件项目的发布文件放到主项目的发布文件里面,使用主项目来运行,整个系统所有的插件都能正常使用了。这样就做到了开发调试独立,最后交付插件自己的dll,放到主项目的发布里面就可以了。

如下图14,是最后发布的程序

图14

 

使用命令运行主项目,或者宿主到iis也一样,如图15,命令行运行

图15

 

 

 

浏览器打开访问,如图16

图16

 

 

点击插件1,显示如图17

图17

 

点击插件2,显示如图18

 

图18

 

 

 

希望你看了之后有点收获,代码程序下面附件提供

demo程序

C# 超高速高性能写日志 代码开源 - Emrys5 - 博客园

mikel阅读(770)

来源: C# 超高速高性能写日志 代码开源 – Emrys5 – 博客园

1、需求

需求很简单,就是在C#开发中高速写日志。比如在高并发,高流量的地方需要写日志。我们知道程序在操作磁盘时是比较耗时的,所以我们把日志写到磁盘上会有一定的时间耗在上面,这些并不是我们想看到的。

 

2、解决方案

2.1、简单原理说明

使用列队先缓存到内存,然后我们一直有个线程再从列队中写到磁盘上,这样就可以高速高性能的写日志了。因为速度慢的地方我们分离出来了,也就是说程序在把日志扔给列队后,程序的日志部分就算完成了,后面操作磁盘耗时的部分程序是不需要关心的,由另一个线程操作。

俗话说,鱼和熊掌不可兼得,这样会有一个问题,就是如果日志已经到列队了这个时候程序崩溃或者电脑断电都会导致日志部分丢失,但是有些地方为了高性能的写日志,是否可以忽略一些情况,请各位根据情况而定。

 

2.2、示例图

 

3、关键代码部分

这里写日志的部分LZ选用了比较常用的log4net,当然也可以选择其他的日志组件,比如nlog等等。

3.1、日志至列队部分

第一步我们首先需要把日志放到列队中,然后才能从列队中写到磁盘上。

复制代码
        public void EnqueueMessage(string message, FlashLogLevel level, Exception ex = null)
        {
            if ((level == FlashLogLevel.Debug && _log.IsDebugEnabled)
             || (level == FlashLogLevel.Error && _log.IsErrorEnabled)
             || (level == FlashLogLevel.Fatal && _log.IsFatalEnabled)
             || (level == FlashLogLevel.Info && _log.IsInfoEnabled)
             || (level == FlashLogLevel.Warn && _log.IsWarnEnabled))
            {
                _que.Enqueue(new FlashLogMessage
                {
                    Message = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff") + "]\r\n" + message,
                    Level = level,
                    Exception = ex
                });

                // 通知线程往磁盘中写日志
                _mre.Set();
            }
        }
复制代码

_log是log4net日志组件的ILog,其中包含了写日志,判断日志等级等功能,代码开始部分的if判断就是判断等级和现在的日志等级做对比,看是否需要写入列队,这样可以有效的提高日志的性能。

其中的_que是ConcurrentQueue列队。_mre是ManualResetEvent信号,ManualResetEvent是用来通知线程列队中有新的日志,可以从列队中写入磁盘了。当从列队中写完日志后,重新设置信号,在等待下次有新的日志到来。

 

3.2、列队到磁盘

从列队到磁盘我们需要有一个线程从列队写入磁盘,也就是说我们在程序启动时就要加载这个线程,比如ASP.NET中就要在global中的Application_Start中加载。

 

复制代码
       /// <summary>
        /// 另一个线程记录日志,只在程序初始化时调用一次
        /// </summary>
        public void Register()
        {
            Thread t = new Thread(new ThreadStart(WriteLog));
            t.IsBackground = false;
            t.Start();
        }

        /// <summary>
        /// 从队列中写日志至磁盘
        /// </summary>
        private void WriteLog()
        {
            while (true)
            {
                // 等待信号通知
                _mre.WaitOne();

                FlashLogMessage msg;
                // 判断是否有内容需要如磁盘 从列队中获取内容,并删除列队中的内容
                while (_que.Count > 0 && _que.TryDequeue(out msg))
                {
                    // 判断日志等级,然后写日志
                    switch (msg.Level)
                    {
                        case FlashLogLevel.Debug:
                            _log.Debug(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Info:
                            _log.Info(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Error:
                            _log.Error(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Warn:
                            _log.Warn(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Fatal:
                            _log.Fatal(msg.Message, msg.Exception);
                            break;
                    }
                }

                // 重新设置信号
                _mre.Reset();
          Thread.Sleep(1);
            }
        }
复制代码

 

3.3、完整代码

 View Code

 

 

4、性能对比和应用

4.1、性能对比

经过测试发现

使用原始的log4net写入日志100000条数据需要:19104毫秒。

同样数据使用列队方式只需要251毫秒。

 

 

 

4.2、应用

4.2.1、需要在程序启动时注册,如ASP.NET 程序中在Global.asax中的Application_Start注册。

复制代码
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            FlashLogger.Instance().Register();
        }
    }
复制代码

 

4.2.2、在需要写入日志的地方直接调用FlashLogger的静态方法即可。

            FlashLogger.Debug("Debug");
            FlashLogger.Debug("Debug", new Exception("testexception"));
            FlashLogger.Info("Info");
            FlashLogger.Fatal("Fatal");
            FlashLogger.Error("Error");
            FlashLogger.Warn("Warn", new Exception("testexception"));

 

5、代码开源

https://github.com/Emrys5/Emrys.FlashLog

 

最后望对各位有所帮助,本文原创,欢迎拍砖和推荐