聊到数据同步和实时数仓,SQL Server CDC是个绕不开的话题。CDC全称Change Data Capture,在SQL Server里其实是2008就开始提供的企业级功能,不过直到最近几年Flink CDC这套实时同步方案火起来,才被更多做数据集成的人当成主力手段来用。这篇文章就用一张实际业务表作为例子,把从数据库级到表级开启CDC的完整操作过程一步步过一遍,包括前置检查、权限、验证、读取变更数据、关闭流程,以及我自己实际踩过的坑。无论你是管传统数仓,还是做实时管道,只要手里有SQL Server 2008以上版本,都可以直接照着操作。
1. 为什么要开CDC:场景与原理
1.1 CDC到底解决什么问题
平时我们同步SQL Server数据,最常见的办法有两种:一种是在业务表上加时间戳字段,每次同步取最近几分钟的新增和修改;另一种是定期全表比对,把差异刷到目标端。这两种做法在数据量小、业务不复杂的时候够用,但一旦表达到千万级,或者业务方要求分钟级甚至秒级延迟,问题就出来了:时间戳字段只能记录“最后修改时间”,删除操作根本抓不到;全量比对则对源库压力极大,跑一次全表扫描,IO和CPU直接拉满,DBA看到都想拉黑你。
CDC的定位就是专门解决这个问题的。它通过读取SQL Server事务日志,把对表的INSERT、UPDATE、DELETE操作以增量形式记录到专门的捕获表中,业务系统本身的查询和写入完全不受影响,也不需要改动表结构、不需要加触发器、不需要动应用代码。对于下游来说,CDC提供了一张“操作流水账”,你可以随时拿到某个时间点之后发生了哪些变更,等于给数据同步装了一个只读的旁路监控。
1.2 核心原理:日志读取、捕获实例与作业
SQL Server CDC的实现机制,通俗点说就是三步:先由捕获作业把事务日志里跟某张表相关的操作解析出来;然后把这些操作按顺序写入CDC专用的系统表(也就是cdc架构下的表);最后由下游通过系统函数读取这些变更记录。整个过程是异步的,源表的DML操作不会因为CDC慢而阻塞,这一点和同步触发的数据库触发器有本质区别。
这里有几个关键角色:cdc架构、捕获实例(Capture Instance)、捕获作业和清理作业。数据库启用CDC之后,系统会自动创建cdc架构以及一系列元数据表;每张业务表启用CDC时,会生成一个唯一的捕获实例,名字默认是架构名_表名,比如schema_Orders;捕获作业负责把日志里的变更写进cdc.dbo_Orders_CT这样的变更表,清理作业则按配置的保留时间定期删除过期数据。理解了这个模型,后面排查问题就顺手很多。
1.3 为什么不用触发器和临时表
很多刚接触CDC的同学会问:我写几个触发器不是也能记录变更吗?确实能,但触发器有几个天然短板:第一,它在业务事务内同步执行,等于给每一个DML都增加额外开销,高并发下性能影响非常明显;第二,触发器代码分散在各张表上,维护成本极高,加字段、改逻辑都得动一遍;第三,触发器无法拿到事务级别的上下文,比如你要判断某个批量的UPDATE具体影响了哪些行、按什么顺序执行,处理起来很痛苦。
CDC把这些事全部收敛到数据库内部机制里,有统一的数据字典、统一的清理策略、统一的时间线。你只需要开启功能、读取数据,不用自己去设计变更日志表的结构,也不用担心业务高峰期偶发的并发更新把日志表锁死。所以结论很明确:只要是SQL Server 2008以上、企业版或开发版环境,做数据同步首选CDC,没有太大悬念。
2. 开启前的准备:版本、权限与配置检查
2.1 版本支持范围
CDC虽然是SQL Server自带功能,但不是所有版本都能用。这里必须要先讲清楚,很多人在标准版上折腾半天,最后发现功能根本不存在。官方支持情况是:企业版(Enterprise)、开发版(Developer)和标准版(Standard)从SQL Server 2016 SP1开始也支持CDC,Web版和Express版不支持。
也就是说,如果你用的是SQL Server 2012标准版,那确实开不了CDC;如果是2016 SP1之后的标准版,可以用了,但要注意部分高级选项可能受版本限制。我平时在2019和2022上操作最多,这两个版本在CDC的行为上基本一致,下面的脚本在两个版本上都能跑。另外还要提醒一点:AlwaysOn可用性组环境下开启CDC,需要先在主副本上启用,辅副本会自动同步相关元数据,但读取CDC数据时建议走只读副本,避免给主库增加额外压力。
2.2 检查项清单:Agent、权限、主键与恢复模式
动手开启之前,我建议先走一遍快速检查,避免操作到一半报错。SQL Server CDC依赖SQL Server代理(Agent)服务,因为捕获作业和清理作业实际上都是Agent作业,所以Agent必须处于运行状态,且服务账号需要有sysadmin权限来创建和运行作业。
权限方面,开启数据库级CDC的人必须是sysadmin固定服务器角色或db_owner固定数据库角色的成员;启用表级CDC时,要求同样不低,实际操作中通常也要求db_owner。如果你用的是只读账号,哪怕业务连接字符串权限再大,也无法完成CDC配置。
再一个关键点是主键。CDC要求启用变更捕获的源表必须有主键,这是硬条件。为什么呢?因为CDC需要靠主键来唯一标识一行记录,尤其是在生成净变更(Net Changes)时,没有主键就无法把同一行的多条变更合并成一条最新状态。没有主键的表,页面数据又不是UPDATETEXT这种大对象更新的,就很难做到准确追踪。
恢复模式方面,CDC本身不强制要求完整恢复模式,但为了保证事务日志能支撑CDC捕获和时点恢复,生产环境建议设置为完整恢复模式。简单恢复模式下,日志截断频繁,存在CDC数据丢失的隐患,所以官方文档也推荐完整恢复模式。
3. 完整实操:从数据库到表的CDC开启
3.1 数据库级启用:一条SQL加上背后的动作
现在进入正题。假设我有一个业务库叫BizDB,里面有一张订单表dbo.Orders,需要把这个表的增删改同步到数仓。开启CDC的第一步是启用数据库级别的CDC,这一步操作会创建cdc架构、系统表以及默认的捕获和清理作业。
-- 先确认当前数据库的CDC状态 SELECT name, is_cdc_enabled FROM sys.databases WHERE name = 'BizDB'; -- 启用数据库级CDC USE BizDB; GO EXEC sys.sp_cdc_enable_db; GO执行完成后,再查一次is_cdc_enabled,结果应该变成1。这一步执行速度很快,通常一两秒就能完成。不过要提醒的是,它背后其实做了不少事情:创建cdc架构,创建cdc.change_tables、cdc.captured_columns、cdc.ddl_history等系统表,同时还会创建两个Agent作业:捕获作业(名字一般是cdc.BizDB_capture)和清理作业(cdc.BizDB_cleanup)。如果Agent没有启动,这一步虽然可能不报错,但后续不会有任何数据被捕获。
3.2 表级启用:捕获实例、净变更与捕获列
数据库级开启成功后,接下来对目标表启用捕获。sys.sp_cdc_enable_table存储过程的核心参数有四个:@source_schema(源表架构)、@source_name(源表名)、@role_name(访问角色,可空)、@capture_instance(捕获实例名,默认是架构_表名)。还有两个比较常用的可选参数:@supports_net_changes表示是否支持净变更查询,@captured_column_list表示只捕获指定列,默认捕获所有列。
对dbo.Orders表启用捕获,最简单的写法:
USE BizDB; GO EXEC sys.sp_cdc_enable_table @source_schema = 'dbo', @source_name = 'Orders', @role_name = NULL, @supports_net_changes = 1, @capture_instance = 'dbo_Orders'; GO执行完可以用下面的语句确认:
SELECT capture_instance, source_schema, source_table, supports_net_changes, is_tracked_by_cdc FROM cdc.change_tables;is_tracked_by_cdc为1就代表这张表已经处于被追踪状态。这时系统会自动创建一张变更表,名字格式是cdc.dbo_Orders_CT,里面除了源表的所有列之外,还附加了5个元数据列:__$start_lsn、__$end_lsn、__$seqval、__$operation、__$update_mask。每次对Orders表的INSERT、UPDATE、DELETE操作,都会以独立行的形式记录在这张变更表中。
关于@supports_net_changes参数,我多说一句。所谓净变更,就是把同一行的多次变更合并成“最终状态”,比如同一行被UPDATE了三次,你只需要拿到最后一次的结果。这个参数在实际同步中非常有用,能显著减少下游需要处理的中间变更量。前提是源表必须有主键,如果没有主键,只能把该参数设为0,退化为只支持查询所有变更(All Changes)。
如果你的表有大量列,但下游只需要其中几列,可以在@captured_column_list中只列出需要的列:
EXEC sys.sp_cdc_enable_table @source_schema = 'dbo', @source_name = 'Orders', @role_name = NULL, @supports_net_changes = 1, @captured_column_list = 'OrderID, CustomerID, OrderDate, TotalAmount';这样做的好处是变更表体积更小,日志解析和存储开销更低。但我一般建议除非列真的很多且明确不需要,否则直接用默认全列捕获,避免后面需求变化又要重建捕获实例。
3.3 验证CDC是否真的在干活
开启完成不等于万事大吉,我见过不少案例,配置步骤全对,但同步管道却始终拿不到数据,最后发现是Agent作业没有正常启动。所以验证环节必须做。
第一步检查作业状态。在SSMS里展开SQL Server代理、作业,找到cdc.BizDB_capture和cdc.BizDB_cleanup,确认它们的状态是“已启用”。两个作业默认都配置了调度计划,捕获作业间隔一般是每30秒跑一次,清理作业每天跑一次。
第二步做一次DML测试。往源表插一条数据,等几十秒,再查变更表:
-- 插入测试数据 INSERT INTO dbo.Orders (OrderID, CustomerID, OrderDate, TotalAmount) VALUES (10001, 'C001', '2025-01-10', 199.00); -- 等30秒左右,查询变更表 SELECT TOP 10 __$operation, OrderID, CustomerID, OrderDate, TotalAmount FROM cdc.dbo_Orders_CT ORDER BY __$start_lsn;如果能看到一行__$operation = 2的记录,说明捕获正常。__$operation字段的含义是:1表示删除,2表示插入,3表示更新前镜像(旧值),4表示更新后镜像(新值)。这组数字你最好记下来,写下游消费逻辑时要经常用到。
3.4 读取变更数据:查询函数与LSN水位
变更表虽然可以直接查,但正规做法是使用CDC提供的系统函数。常用的有两组:cdc.fn_cdc_get_all_changes_<捕获实例>返回所有变更,cdc.fn_cdc_get_net_changes_<捕获实例>返回净变更。如果捕获实例名是dbo_Orders,对应的函数就是cdc.fn_cdc_get_all_changes_dbo_Orders和cdc.fn_cdc_get_net_changes_dbo_Orders。
函数需要传入起始LSN(Log Sequence Number,日志序列号)和结束LSN。LSN就是日志里的一个单调递增的位置标记,CDC数据是按LSN顺序写入的。我们可以通过sys.fn_cdc_get_min_lsn和sys.fn_cdc_get_max_lsn拿到捕获数据的边界:
DECLARE @from_lsn binary(10), @to_lsn binary(10); SET @from_lsn = sys.fn_cdc_get_min_lsn('dbo_Orders'); SET @to_lsn = sys.fn_cdc_get_max_lsn(); SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_Orders(@from_lsn, @to_lsn, N'all');第三个参数如果传'all',会返回所有变更行;如果传'all update old',则更新操作会同时返回旧值行和新值行。实际做增量同步时,常见做法是把上次消费到的__$start_lsn保存为一个断点,下次同步时用sys.fn_cdc_increment_lsn往前推一位,再作为起始LSN调用函数,这样就不会丢数据也不会重复。
4. 日常维护:查询状态、调整捕获窗口与关闭CDC
4.1 捕获表的生命周期和数据清理
CDC捕获表不是无限增长的,它受清理作业控制。清理作业会根据数据库里配置的保留期(默认是3天)删除过期的变更记录。保留期的配置存放在cdc.change_tables表中,可以通过存储过程sys.sp_cdc_change_job调整:
-- 查看当前清理作业配置 EXEC sys.sp_cdc_help_jobs; -- 把保留期改为7天 EXEC sys.sp_cdc_change_job @job_type = 'cleanup', @retention = 4320; -- 单位是分钟,4320分钟=72小时=3天,改为10080则是7天注意@retention的单位是分钟,不是天数。很多人在这里填成7,结果清理作业几分钟就把CDC数据删光了,排查半天才反应过来。另外,清理作业的调度也可以调整,如果业务流量大,变更表增长快,可以把清理周期缩短,避免磁盘被占满。
4.2 关闭CDC时的顺序问题
如果某张表不再需要同步,应该先关闭表级CDC,再关闭数据库级CDC,顺序不能反。因为如果你先关闭数据库级CDC,系统会把cdc架构和所有相关对象一并删除,这时候表级关闭操作会直接失败。
关闭语句很简单:
-- 关闭表级CDC EXEC sys.sp_cdc_disable_table @source_schema = 'dbo', @source_name = 'Orders', @capture_instance = 'dbo_Orders'; GO -- 关闭数据库级CDC EXEC sys.sp_cdc_disable_db; GO执行完sp_cdc_disable_table后,对应捕获实例和变更表会被删除,但已经读取过的CDC数据不会再保留。如果只是暂时不想同步,不一定要关闭CDC,可以直接停用捕获作业,等需要时再启用,这种方式对业务无感,也省去重新初始化CDC的时间。
4.3 日常监控要点
CDC运行起来之后,我建议至少监控三个指标:捕获作业是否按计划执行、变更表大小和磁盘剩余空间、以及延迟情况。延迟指的是业务表发生DML到它出现在变更表之间的时间差,正常情况下应该在秒级到分钟级,取决于Agent作业调度间隔和系统负载。
查询捕获作业历史可以用系统视图msdb.dbo.sysjobs和msdb.dbo.sysjobhistory,或者直接用SSMS看作业历史。如果发现捕获作业执行失败,最常见的原因是日志文件满了、数据库变成只读,或者事务日志损坏。捕获作业一旦失败,会导致变更数据堆积在日志中,源库的事务日志快速增长,甚至造成业务库不可用,所以监控Agent作业状态是CDC运维中最重要的一环。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
执行sp_cdc_enable_db报“SQL Server Agent必须作为服务运行” | Agent服务未启动 | 打开SQL Server配置管理器,启动SQL Server代理服务,并设为自动启动 |
sp_cdc_enable_table提示表必须有主键 | 源表没有主键 | 为源表添加主键,或新建一个包含主键的复制表做采集 |
| 启用成功但变更表一直没数据 | 捕获作业未运行或调度异常 | 检查Agent作业cdc.*_capture状态;手动执行一次作业测试 |
| CDC变更表数据增长过快,磁盘告警 | 保留期设置太长,清理作业不频繁 | 调低sp_cdc_change_job的保留期,缩短清理作业调度周期 |
| 读取函数报“指定的LSN无效” | LSN断点落后于清理作业删除范围 | 检查清理作业是否把还没消费的数据删了;调整保留期并持久化消费断点 |
| 数据库恢复后CDC作业丢失 | 数据库在另一实例上还原,Agent作业未一并还原 | 重新手动创建或通过脚本重建捕获和清理作业 |
| 无法删除数据库,提示CDC对象存在 | 数据库级CDC没关干净 | 执行sys.sp_cdc_disable_db后再删库 |
这些基本都是我实际工作中被问得最多的问题。大部分问题都指向两类:要么是没有满足版本、权限、主键、Agent运行状态这四大前提;要么是清理作业和消费端断点没有协调好。所以如果你在某个环节卡住了,优先倒回去检查前置条件,而不是反复执行开关命令。
5.2 几个值得写进笔记的实操心得
第一点,开启CDC最好放在业务低峰期。数据库级启用不需要长时间锁表,表级启用也只要获取架构锁,但SQL Server解析初始日志时仍会产生额外IO,我在生产环境遇到过启用瞬间日志写入放大,导致磁盘延迟上升的情况。低峰期操作,配合提前检查磁盘空间和日志备份策略,能省去很多麻烦。
第二点,消费端一定要记录LSN断点。CDC数据不是留一辈子的,清理作业默认3天就把记录清了,如果下游管道挂了超过3天,断点就会失效,只能重新拉一次全量。所以成熟的做法是把LSN水位存到专门的断点表或者消息队列里,宁可重复消费也不能丢水位。
第三点,不要在生产环境直接手动修改cdc架构下的系统表。虽然原理上它们就是普通表,但一旦改坏,CDC捕获作业可能直接挂掉,而且官方明文不支持手动修改。如果你发现__$operation之类的元数据列有异常,应该先重建捕获实例而不是突发奇想去修表。
第四点,SQL Server 2016 SP1之前,非企业版开不了CDC,这个限制让很多中小团队只能眼巴巴看着。如果你手头是标准版且版本较低,又确实需要变更捕获,可以考虑用轮询时间戳加触发器方案过渡,等升级到支持版本之后再切到CDC。但别为了省成本去搞破解版,数据库这种基础设施一旦出问题,代价远高于许可证费用。
5.3 与Flink CDC配合时的一点提示
现在很多人开SQL Server CDC,最终目的是给Flink CDC提供数据源。Flink CDC 3.x配合SQL Server时,本质上是作为CDC的消费端,读取cdc架构下对应的变更表或直接调用查询函数。所以你在SQL Server这边把CDC开好,剩下的工作就是在Flink里配置连接信息和断点续传。
这里有个容易被忽略的点:Flink CDC自动会识别SQL Server的捕获实例,如果你在开启CDC时指定了自定义捕获实例名,Flink的配置里也要对应填写,不然它找不到变更表。另外,Flink CDC任务频繁重启时,要注意SQL Server端清理作业的保留期,如果任务从上次断点恢复的时间间隔超过保留期,会出现数据断档,只能重置位点重新消费。我个人的习惯是把保留期调到7天,给下游留足缓冲时间。
结尾
SQL Server CDC这个东西,功能本身很成熟,真正考验人的是怎么在业务场景里把它用稳。我记得第一次在生产库开CDC,直接在业务高峰期执行了sp_cdc_enable_db,结果日志文件瞬间涨了几个G,被DBA追着问了一下午。后来学乖了,每次操作前先看Agent、查版本、确认主键、检查磁盘,四步走完才动手,基本没再出过岔子。如果你刚开始用,我建议先在一个测试库完整走一遍启用、验证、消费、清理的流程,再上生产,这样既能把原理吃透,也能避开不少隐藏坑。希望这篇文章能帮你少走几步弯路。