
聊到数据同步和实时数仓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开始也支持CDCWeb版和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。函数需要传入起始LSNLog 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, Nall);第三个参数如果传all会返回所有变更行如果传all update old则更新操作会同时返回旧值行和新值行。实际做增量同步时常见做法是把上次消费到的__$start_lsn保存为一个断点下次同步时用sys.fn_cdc_increment_lsn往前推一位再作为起始LSN调用函数这样就不会丢数据也不会重复。4. 日常维护查询状态、调整捕获窗口与关闭CDC4.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、查版本、确认主键、检查磁盘四步走完才动手基本没再出过岔子。如果你刚开始用我建议先在一个测试库完整走一遍启用、验证、消费、清理的流程再上生产这样既能把原理吃透也能避开不少隐藏坑。希望这篇文章能帮你少走几步弯路。