Cơ sở dữ liệu

Change Tracking và CDC: 302 lần sửa một sản phẩm thành 1 dòng hay 604 dòng

Change Tracking cho biết khóa nào đã đổi, CDC giữ mọi lần đổi kèm giá trị cũ và mới. Đo trên SQL Server 2019: số dòng, chi phí ghi, độ trễ, rủi ro vận hành.

Mục lục
  1. 1. Hai câu hỏi về cùng một dòng
  2. 2. Change Tracking: khóa nào đã đổi từ mốc X
  3. 3. CDC: đọc lại từng lần đổi từ transaction log
  4. 4. 302 lần sửa SP-00777: mỗi cách trả mấy dòng
  5. 5. Chi phí khi ghi, độ trễ, và khi phía đọc dừng
  6. 6. Consumer: lưu mốc cùng giao dịch với dữ liệu đích
  7. 7. Chọn cách nào
  8. Những chỗ hay hiểu sai
  9. Đọc tiếp
  10. Nguồn

20:00 thứ Sáu 2026-09-18, BanHang hạ giá SP-00777 từ 8.990.000 xuống 4.990.000 cho đợt flash sale 300 máy, rồi trả giá cũ lúc 20:40. Trong 40 phút đó, dòng SanPhamId = 777 của dbo.SanPham bị sửa 302 lần: hai lần đổi giá và 300 lần trừ kho. Trang sản phẩm và bộ tìm kiếm chỉ cần giá và tồn hiện tại của sản phẩm vừa đổi, nhưng đang quét lại cả 5.000 sản phẩm mỗi phút. Kho dữ liệu kế toán cần cả 40 phút giá 4.990.000, vì 300 đơn bán ở giá đó, trong khi ảnh chụp cuối ngày chỉ thấy 8.990.000. SQL Server có hai tính năng cho hai câu hỏi này: Change Tracking (CT) và Change Data Capture (CDC). Bài này đo cả hai trên cùng một máy: mỗi cách trả mấy dòng, tốn thêm bao nhiêu khi ghi, trễ bao lâu, hỏng thế nào khi phía đọc dừng.

Đọc nhanh

  • CT ghi khóa của dòng đã đổi ngay trong giao dịch DML. 302 lần sửa SP-00777 ra đúng 1 dòng; giá trị đọc từ bảng nguồn. Có trên mọi edition, kể cả Express.
  • CDC dùng capture job của SQL Server Agent đọc lại transaction log, giữ ảnh trước và sau: 302 lần sửa thành 604 dòng. Cần Standard hoặc Enterprise. Với pollinginterval mặc định 5 giây, thay đổi hiện ra sau trung vị 2,4 giây.
  • 10.000 đơn: CT thêm 2% log ghi xuống đĩa, CDC thêm 39% vì capture job chép lại từng thay đổi. Thông lượng gần như không đổi vì commit chờ đĩa log.
  • Capture dừng thì log không cắt được: 30.000 đơn giữ thêm 126 MiB qua hai lần log backup, log_reuse_wait_desc = REPLICATION. Consumer CT ngừng quá retention thì phải tải lại toàn bộ. Consumer nào cũng lưu mốc cùng giao dịch với dữ liệu đích.

1. Hai câu hỏi về cùng một dòng

Trang sản phẩm, bộ tìm kiếm Kho kế toán, đối soát
Cần biết Sản phẩm nào đã đổi, giá và tồn hiện tại Mọi lần đổi giá, mọi trạng thái đơn đi qua, giá trị trước và sau
Bỏ bước giữa được không Được, chỉ giá trị cuối có nghĩa Không, 40 phút giá 4.990.000 phải còn
Cơ chế hợp Change Tracking CDC

Hai cơ chế khác nhau ở thời điểm ghi nhận. CT ghi trong chính giao dịch UPDATE: commit xong là CHANGETABLE thấy ngay, giao dịch rollback không để lại gì. CDC không đụng vào giao dịch ghi: capture job đọc transaction log về sau, theo từng vòng quét, bằng cùng bộ đọc log (sp_replcmds) với transactional replication, rồi chép dòng đổi vào change table riêng.

UPDATE dbo.SanPham … SanPhamId = 777 Change Tracking đồng bộ, ghi trong chính giao dịch Change Data Capture bất đồng bộ, đọc lại từ log một giao dịch, commit cùng lúc Dòng 777 trên page của bảng change_tracking_<object_id> khóa 777 và version, mỗi lần đổi một dòng CHANGETABLE(CHANGES …, @moc) một dòng mỗi khóa, không giữ giá trị cũ JOIN dbo.SanPham lấy giá và tồn kho hiện tại Transaction log (.ldf) bản ghi sửa dòng 777, có từ lúc commit sau đó, mỗi vòng quét Capture job của SQL Server Agent quét log, nghỉ 5 giây giữa các vòng cdc.dbo_SanPham_CT mỗi UPDATE hai dòng: ảnh trước, ảnh sau fn_cdc_get_all_changes_… hoặc net_changes, theo khoảng LSN
CT trả lời trong giao dịch và chỉ giữ khóa. CDC trả lời sau một vòng quét và giữ cả giá trị.

2. Change Tracking: khóa nào đã đổi từ mốc X

Bật ở mức database rồi từng bảng. Từ đó, mỗi dòng bị INSERT, UPDATE, DELETE thêm một dòng vào bảng nội bộ change_tracking_<object_id> của bảng đó: khóa chính cộng một số phiên bản, và với TRACK_COLUMNS_UPDATED = ON thêm 4 byte cho mỗi cột bị sửa. Mỗi giao dịch commit thêm một dòng vào sys.syscommittab. Không có giá trị cột nào được chép. Microsoft so chi phí này với việc giữ thêm một chỉ mục trên bảng, đúng loại chi phí ở mục 8 của chương chỉ mục. Script dưới chạy trên SQL Server 2019 Developer trong container Linux (môi trường ở mục 5). Bảng dbo.DonHang (cùng pf_DonHang_Ngay, ps_DonHang_Ngay) và dbo.SanPham giữ nguyên định nghĩa của BanHang ở chương lưu trữ và bài flash sale; đường dẫn file là của container.

-- sqlcmd -S localhost,14330 -U sa -P "<mật khẩu>" -C -f 65001 -i 0-tao.sql
CREATE DATABASE Kumeo_cdcct COLLATE Vietnamese_100_CI_AS;
GO
ALTER DATABASE Kumeo_cdcct ADD FILEGROUP FG_DATA;
ALTER DATABASE Kumeo_cdcct ADD FILEGROUP FG_ARCHIVE;
ALTER DATABASE Kumeo_cdcct ADD FILE (NAME = cdcct_data, FILENAME = '/var/opt/mssql/data/Kumeo_cdcct_data.ndf') TO FILEGROUP FG_DATA;
ALTER DATABASE Kumeo_cdcct ADD FILE (NAME = cdcct_arch, FILENAME = '/var/opt/mssql/data/Kumeo_cdcct_arch.ndf') TO FILEGROUP FG_ARCHIVE;
ALTER DATABASE Kumeo_cdcct MODIFY FILEGROUP FG_DATA DEFAULT;
ALTER DATABASE Kumeo_cdcct SET RECOVERY FULL;
ALTER DATABASE Kumeo_cdcct SET ALLOW_SNAPSHOT_ISOLATION ON;
GO
USE Kumeo_cdcct;
GO
CREATE PARTITION FUNCTION pf_DonHang_Ngay (datetime2(0))
AS RANGE RIGHT FOR VALUES (CAST('20250101' AS datetime2(0)), CAST('20260101' AS datetime2(0)), CAST('20270101' AS datetime2(0)));
CREATE PARTITION SCHEME ps_DonHang_Ngay AS PARTITION pf_DonHang_Ngay TO (FG_ARCHIVE, FG_ARCHIVE, FG_DATA, FG_DATA);
CREATE TABLE dbo.DonHang (
    DonHangId bigint NOT NULL,
    NgayTao datetime2(0) NOT NULL,
    KhachHangId int NOT NULL,
    TrangThai tinyint NOT NULL,
    TongTien decimal(18, 2) NOT NULL,
    CONSTRAINT PK_DonHang PRIMARY KEY CLUSTERED (NgayTao, DonHangId)
) ON ps_DonHang_Ngay (NgayTao);
CREATE INDEX IX_DonHang_KhachHang ON dbo.DonHang (KhachHangId, NgayTao) ON ps_DonHang_Ngay (NgayTao);
CREATE TABLE dbo.SanPham (
    SanPhamId int NOT NULL CONSTRAINT PK_SanPham PRIMARY KEY CLUSTERED,
    MaSanPham varchar(20) NOT NULL CONSTRAINT UQ_SanPham_Ma UNIQUE,
    Ten nvarchar(200) NOT NULL,
    DonGia decimal(18, 2) NOT NULL,
    TonKho int NOT NULL CONSTRAINT CK_SanPham_TonKho CHECK (TonKho >= 0)
);
INSERT INTO dbo.SanPham (SanPhamId, MaSanPham, Ten, DonGia, TonKho)   -- 5.000 sản phẩm
SELECT n, CONCAT('SP-', RIGHT(CONCAT('0000', n), 5)), CONCAT(N'Sản phẩm ', n), 1000000 + n % 50 * 100000, 1000000
FROM (SELECT TOP (5000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM sys.all_objects a CROSS JOIN sys.all_objects b) AS s(n);
UPDATE dbo.SanPham SET Ten = N'Điện thoại 128 GB', DonGia = 8990000, TonKho = 300 WHERE SanPhamId = 777;
GO
ALTER DATABASE Kumeo_cdcct SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);
ALTER TABLE dbo.SanPham ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);
ALTER TABLE dbo.DonHang ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);

Ba hàm làm hết việc đọc:

Hàm Trả về
CHANGE_TRACKING_CURRENT_VERSION() Phiên bản của giao dịch commit gần nhất. Consumer lưu nó làm mốc cho lần sau
CHANGETABLE(CHANGES t, @moc) Một dòng cho mỗi khóa đổi sau @moc: khóa, SYS_CHANGE_VERSION, SYS_CHANGE_OPERATION (I, U, D), SYS_CHANGE_COLUMNS
CHANGE_TRACKING_MIN_VALID_VERSION(id) Mốc nhỏ nhất còn đọc đúng cho bảng có object_id đó. Mốc cũ hơn thì phải tải lại toàn bộ

Điều kiện và giới hạn:

  • Bảng phải có khóa chính; thiếu thì nhận Msg 4997.
  • Thông tin đổi giữ theo CHANGE_RETENTION: mặc định 2 ngày, tối thiểu 1 phút, dọn tự động bật sẵn. Đến SQL Server 2022, luồng dọn chạy 30 phút một lần; SQL Server 2025 dọn bảng phụ lớn theo từng bước.
  • Microsoft "khuyến nghị mạnh" đọc trong một giao dịch SNAPSHOT, để mốc, MIN_VALID_VERSION và CHANGETABLE cùng thấy một thời điểm dù luồng dọn chạy giữa chừng. Cái giá của snapshot ở mục 8 bài transaction.
  • Bảng có CT không SWITCH được (Msg 4900). dbo.DonHang đẩy năm cũ ra bằng SWITCH, nên BanHang thật chỉ bật CT cho dbo.SanPham; database thử bật cả hai để so ở mục 4. TRUNCATE TABLE đẩy mốc tối thiểu lên, mọi consumer phải tải lại.
  • Có trên mọi edition. 302 lần sửa SP-00777 ở mục 4 chạy trên LocalDB 2019 CU27 (Express) cũng cho đúng 1 dòng CT, còn sys.sp_cdc_enable_db ở đó báo Msg 22988.

3. CDC: đọc lại từng lần đổi từ transaction log

sys.sp_cdc_enable_db (cần sysadmin) tạo schema cdc. sys.sp_cdc_enable_table tạo một capture instance: change table cdc.dbo_SanPham_CT, hàm cdc.fn_cdc_get_all_changes_dbo_SanPham, và với @supports_net_changes = 1 thêm hàm net changes cùng một chỉ mục trên change table. Bảng đầu tiên được bật kéo theo hai job của SQL Server Agent.

USE Kumeo_cdcct;
EXEC sys.sp_cdc_enable_db;
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'SanPham', @role_name = NULL, @supports_net_changes = 1;
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'DonHang', @role_name = NULL, @supports_net_changes = 1;
EXEC sys.sp_cdc_help_jobs;
Job Tham số, đọc từ sys.sp_cdc_help_jobs Việc làm
Capture maxtrans 500, maxscans 10, continuous 1, pollinginterval 5 Quét log tới 10 lần × 500 giao dịch, nghỉ 5 giây, lặp lại. Mỗi vòng commit một giao dịch riêng
Cleanup retention 4320, threshold 5000 2:00 mỗi ngày xóa dòng cũ hơn 4.320 phút (3 ngày), mỗi câu DELETE tối đa 5.000 dòng

Năm cột đầu của change table là metadata: __$start_lsn (LSN lúc commit, xếp thứ tự giao dịch), __$seqval (thứ tự trong một giao dịch), __$operation (1 xóa, 2 thêm, 3 ảnh trước của UPDATE, 4 ảnh sau), __$update_mask (cột bị sửa). Các cột còn lại chép cột của bảng nguồn. cdc.lsn_time_mapping nối LSN với giờ commit, nên sys.fn_cdc_map_time_to_lsn đổi "từ 20:00 đến 20:40" thành khoảng LSN cho hai hàm đọc.

Điều kiện và giới hạn:

  • Agent phải chạy. Agent dừng thì capture dừng, và log dồn lại như mục 5. Trên SQL Server 2019, CDC có ở Enterprise và Standard (Developer có đủ tính năng Enterprise), không có ở Web và Express. Bản Linux có CDC từ SQL Server 2017 CU18.
  • Mỗi bảng tối đa hai capture instance. Cột thêm vào bảng nguồn sau khi bật không được chép; cột bị xóa thành NULL. Muốn có cột mới thì tạo instance thứ hai, cho consumer chuyển sang, rồi bỏ instance cũ.
  • SWITCH không bị chặn (@allow_partition_switch mặc định 1, lúc bật có cảnh báo) nhưng không được ghi lại. SWITCH partition 2024 có 3 đơn ra bảng staging: _CT không có dòng xóa nào. Với kho kế toán, đẩy năm cũ ra lưu trữ không phải xóa nghiệp vụ nên thường đúng ý; consumer coi _CT là đủ mọi DELETE thì phải biết lịch SWITCH.
  • Azure SQL Managed Instance chạy CDC bằng Agent như bản tự cài. Azure SQL Database thay Agent bằng bộ lập lịch: capture mỗi 20 giây, cleanup mỗi giờ, không chỉnh được pollinginterval, cần vCore hoặc DTU từ S3, không có SLA cho độ trễ. Bật CDC còn tắt việc cắt log sớm của ADR, như chương Azure SQL đã nhắc.

4. 302 lần sửa SP-00777: mỗi cách trả mấy dòng

Script dưới chạy lại đợt sale, mỗi câu một giao dịch như endpoint thật, rồi đi hết vòng đời của đơn 10042: tạo 11:58 với 1.500.000, sửa thành 1.750.000, qua trạng thái 2, 3, 4.

USE Kumeo_cdcct;
SET NOCOUNT ON;
DECLARE @v0 bigint = CHANGE_TRACKING_CURRENT_VERSION(), @i int = 0;
UPDATE dbo.SanPham SET DonGia = 4990000 WHERE SanPhamId = 777;            -- 20:00
WHILE @i < 300
BEGIN
    UPDATE dbo.SanPham SET TonKho = TonKho - 1 WHERE SanPhamId = 777 AND TonKho >= 1;
    SET @i += 1;
END;
UPDATE dbo.SanPham SET DonGia = 8990000 WHERE SanPhamId = 777;            -- 20:40
INSERT INTO dbo.DonHang VALUES (10042, '2026-10-02T11:58:00', 7, 1, 1500000);
UPDATE dbo.DonHang SET TongTien = 1750000 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 2 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 3 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 4 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
WAITFOR DELAY '00:00:15';                                                 -- ba vòng quét của capture job
DECLARE @den binary(10) = sys.fn_cdc_get_max_lsn(), @sp binary(10) = sys.fn_cdc_get_min_lsn('dbo_SanPham'), @dh binary(10) = sys.fn_cdc_get_min_lsn('dbo_DonHang');
SELECT 'SanPham' AS bang,
    (SELECT COUNT(*) FROM CHANGETABLE(CHANGES dbo.SanPham, @v0) AS c) AS ct,
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_net_changes_dbo_SanPham(@sp, @den, N'all')) AS cdc_net,
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@sp, @den, N'all')) AS cdc_all,
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@sp, @den, N'all update old')) AS cdc_all_old
UNION ALL
SELECT 'DonHang',
    (SELECT COUNT(*) FROM CHANGETABLE(CHANGES dbo.DonHang, @v0) AS c),
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_net_changes_dbo_DonHang(@dh, @den, N'all')),
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_DonHang(@dh, @den, N'all')),
    (SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_DonHang(@dh, @den, N'all update old'));
Cách đọc SP-00777, 302 lần sửa Đơn 10042, thêm rồi sửa 4 lần
CT, CHANGETABLE(CHANGES ...) 1 dòng, U, version 302 1 dòng, I
CDC net changes 1 1, __$operation 2 với giá trị cuối
CDC all changes, all 302, chỉ ảnh sau 5
CDC all changes, all update old 604 9
Dòng trong bảng phụ: bảng nội bộ CT, _CT của CDC 302, 604 5, 9

302 lần sửa SP-00777: CT và net changes trả 1 dòng, all changes trả 302 hoặc 604

CT, CHANGETABLE1 dòngCDC net changes1 dòngCDC all302 dòngCDC all update old604 dòng
Đo trên SQL Server 2019 CU32 Developer trong container Linux. Một lần đổi giá xuống, 300 lần trừ kho, một lần trả giá, mỗi câu một giao dịch.
Bảng số liệu
Giá trị
CT, CHANGETABLE1 dòng
CDC net changes1 dòng
CDC all302 dòng
CDC all update old604 dòng

Dòng CT của SP-00777 có SYS_CHANGE_COLUMNS đánh dấu cả DonGia lẫn TonKho, nhưng không còn dấu vết của giá 4.990.000: JOIN sang dbo.SanPham chỉ thấy 8.990.000 và tồn 0. Đơn 10042 hiện là I vì được tạo sau mốc; trạng thái 2 và 3 biến mất. Với trang sản phẩm, đó đúng là thứ cần: một lần làm mới thay cho 302. Với kế toán, chỉ all update old giữ được 1.500.000 → 1.750.000 và 1 → 2 → 3 → 4. Hàng cuối hay bị bỏ qua: CT trả 1 dòng nhưng bảng nội bộ vẫn giữ 302 dòng tới khi bị dọn sau retention; net changes chỉ là cách đọc, _CT vẫn lưu đủ 604 dòng trong 3 ngày.

5. Chi phí khi ghi, độ trễ, và khi phía đọc dừng

Môi trường đo: SQL Server 2019 Developer, bản RTM-CU32-GDR (15.0.4490.9), trong container Linux giới hạn 4 GB RAM, trên laptop Intel Core Ultra 5 125U chạy Windows 11 đang làm việc khác. Thời gian dao động giữa các lần chạy; số đếm và số byte ổn định hơn.

Chi phí khi ghi

Mỗi lần đo tạo một database mới cùng filegroup và bảng như mục 2 nhưng không bật snapshot isolation, rồi bật một trong ba chế độ. Một phiên ghi 10.000 đơn, mỗi đơn một giao dịch: INSERT vào dbo.DonHang và trừ kho một trong 5.000 sản phẩm chọn bằng Random(42). Harness là một file .NET 10 dùng Microsoft.Data.SqlClient 7.1.1. Log của một giao dịch đọc từ sys.dm_tran_database_transactions trước khi commit, trung bình 50 đơn. Log ghi xuống đĩa đọc từ bộ đếm Log Bytes Flushed/sec của database, gồm cả phần capture job ghi vào _CT. Mỗi chế độ 3 lần, lấy trung vị.

10.000 đơn, trung vị 3 lần Không theo dõi CT CDC
Đơn mỗi giây 75 75 77
Log của một giao dịch ghi đơn 522 B 848 B 641 B
Log ghi xuống đĩa, gồm cả capture job 39,3 MiB 40,0 MiB 54,5 MiB
Bảng phụ sau khi ghi 0 2,1 MiB 6,0 MiB

10.000 đơn: CT thêm 0,7 MiB log ghi xuống đĩa, CDC thêm 15,2 MiB vì capture job chép lại từng thay đổi

Không theo dõi39,3 MiBCT40,0 MiBCDC54,5 MiB
Một phiên ghi, mỗi đơn một INSERT dbo.DonHang và một UPDATE dbo.SanPham. Bộ đếm Log Bytes Flushed/sec, trung vị 3 lần, SQL Server 2019 CU32 trong container.
Bảng số liệu
Giá trị
Không theo dõi39,3 MiB
CT40,0 MiB
CDC54,5 MiB

Thông lượng gần như bằng nhau vì mỗi commit chờ log xuống đĩa. 39,3 MiB cho 10.000 đơn là khoảng 4 KB mỗi commit dù giao dịch chỉ sinh 522 B log. Cách giải thích hợp với số đo, nhưng bài chưa kiểm bằng tài liệu: mỗi commit ghi trọn một khối log, làm tròn theo kích thước sector của đĩa. CT ghi thông tin đổi ngay trong giao dịch, thêm 326 B (+62%), nhưng phần thêm nằm gọn trong khối đã phải ghi nên tổng log chỉ thêm 2%. CDC thêm 119 B vào giao dịch ghi; chi phí chính đến sau, khi capture job chép 30.000 dòng (10.000 ảnh thêm, 20.000 ảnh trước và sau) vào _CT: tổng log +39%, bảng phụ gần gấp ba CT. Capture đọc kịp trung vị 4,9 giây sau đơn cuối.

Độ trễ của CDC

CT không có độ trễ: thông tin đổi commit cùng dòng dữ liệu. Với CDC, một phiên sửa DonGia của sản phẩm 42 bốn mươi lần, cách nhau 0,3 đến 1,7 giây (Random(42)); phiên thứ hai đọc liên tục cdc.dbo_SanPham_CT ở mức SNAPSHOT. Độ trễ tính từ lúc UPDATE trả về tới lúc phiên đọc thấy giá mới. Job chỉ đọc cấu hình lúc khởi động, nên đổi tham số xong phải dừng rồi chạy lại:

USE Kumeo_cdcct;
EXEC sys.sp_cdc_change_job @job_type = N'capture', @pollinginterval = 1;
EXEC sys.sp_cdc_stop_job @job_type = N'capture';
WAITFOR DELAY '00:00:05';
EXEC sys.sp_cdc_start_job @job_type = N'capture';
pollinginterval Trung vị p95 Lâu nhất CPU của job khi rảnh
5, mặc định 2,4 s 5,0 s 5,2 s khoảng 2 ms mỗi giây
1 0,53 s 1,0 s 1,06 s 9 ms mỗi giây
0 16 ms 34 ms 59 ms 840 đến 845 ms mỗi giây

Độ trễ CDC đi theo pollinginterval: 5 giây cho trung vị 2,4 s, 0 giây cho 16 ms

Trung vịp95
pollinginterval 52.417 ms4.991 mspollinginterval 1529 ms998 mspollinginterval 016 ms34 ms
40 lần sửa mỗi lần chạy, 3 lần chạy mỗi mức, lấy trung vị của từng chỉ số. Trục log. Một phiên ghi, container SQL Server 2019 CU32.
Bảng số liệu
Trung vịp95
pollinginterval 52.417 ms4.991 ms
pollinginterval 1529 ms998 ms
pollinginterval 016 ms34 ms

Độ trễ rải gần đều từ 0 tới đúng pollinginterval: thay đổi chờ vòng quét kế tiếp. Mức 0 cho vài chục mili giây nhưng job quét liên tục, ăn khoảng 0,84 giây CPU mỗi giây kể cả khi không ai ghi. Cột CPU là cpu_time của request job trong 20 giây rảnh, đo 1 lần ở mức 5, 2 lần ở mức 1 và 0. Trang sản phẩm chịu được 5 giây; kho kế toán nạp theo giờ thì không cần nghĩ tới. Trên production, sys.dm_cdc_log_scan_sessions ghi từng vòng quét, cột latency là số giây từ commit cuối được chép tới lúc vòng quét xong.

Khi capture job dừng

Giả sử Agent bị dừng để vá máy chiều thứ Sáu và quên bật lại tới sáng thứ Hai. Thử trên Kumeo_cdcct, recovery model FULL, log cấp sẵn 1 GB: dừng capture job, ghi 30.000 đơn bằng vòng lặp T-SQL, mỗi đơn một giao dịch như trên (khoảng 2,3 ngày đơn của BanHang, chỉ tính đơn và trừ kho), chạy log backup, rồi bật lại job. Log được giải phóng theo từng VLF, ở đây khoảng 127 MiB, nên con số trước khi dừng chưa về 0.

Bước Log đang dùng log_reuse_wait_desc ngay sau log backup
Trước khi dừng capture 79,8 MiB NOTHING
Capture dừng, 30.000 đơn, log backup 206,2 MiB REPLICATION
Log backup thêm một lần 209,5 MiB REPLICATION
Bật lại job, đọc kịp sau 31,4 giây, log backup 4,4 MiB NOTHING

Log backup vẫn chạy, vẫn chép log ra file, nhưng không giải phóng được phần capture chưa đọc. Lý do là REPLICATION vì CDC dùng chung bộ đọc log với transactional replication. Theo tài liệu, ở recovery model SIMPLE cũng vậy: CHECKPOINT không cắt được log khi capture chưa đọc. Trong BanHang, log lớn dần tới khi ổ L: đầy dù lịch log backup 15 phút của chương lưu trữ vẫn báo thành công. CT không có rủi ro này vì thông tin đổi nằm trong chính giao dịch. Cột log_reuse_wait_desc chỉ ghi lý do ở lần kiểm gần nhất, nên một lần đọc có thể ra LOG_BACKUP hay NOTHING; giám sát cần đọc lặp lại, kèm số log đang dùng:

USE Kumeo_cdcct;
SELECT d.log_reuse_wait_desc, u.used_log_space_in_bytes / 1048576 AS log_dung_mib
FROM sys.databases AS d CROSS JOIN sys.dm_db_log_space_usage AS u
WHERE d.database_id = DB_ID();

Khi consumer CT ngừng quá retention

Consumer CT ở mục 6 đang có mốc 63.675. Để khỏi chờ 2 ngày, hạ CHANGE_RETENTION xuống 1 phút, sửa vài dòng, chờ hơn 1 phút, rồi gọi sys.sp_flush_CT_internal_table_on_demand thay cho luồng dọn tự động. Thủ tục báo watermark 63.676 và xóa hơn 60.000 dòng cũ ở mỗi bảng nội bộ. CHANGE_TRACKING_MIN_VALID_VERSION của dbo.SanPham thành 63.676, lớn hơn mốc, nên lần chạy kế tiếp của consumer đi vào nhánh tải lại: 5.000 dòng.

Tải lại 5.000 sản phẩm là chuyện nhỏ, tải lại 10 triệu đơn thì không. Retention phải dài hơn lần ngừng lâu nhất của consumer, kể cả một cuối tuần, kèm cảnh báo khi mốc gần chạm MIN_VALID_VERSION. CT không giữ log khi consumer ngừng; thứ mất là thông tin đổi, bị dọn theo lịch dù consumer đã đọc hay chưa. CDC có giới hạn tương tự: theo tài liệu, mốc nhỏ hơn sys.fn_cdc_get_min_lsn nghĩa là cleanup đã xóa phần chưa đọc, và consumer ở mục 6 dừng với lỗi 50002 thay vì lặng lẽ bỏ qua.

6. Consumer: lưu mốc cùng giao dịch với dữ liệu đích

Consumer kéo một lô, ghi vào đích, rồi lưu mốc, cả ba trong một giao dịch. Lưu mốc trước rồi sập thì mất lô đó; ghi đích trước, lưu mốc sau rồi sập thì lần sau ghi lại cả lô. Phía ghi đích phải chịu được việc chạy lại, đúng lập luận của consumer idempotent trong bài hai vị tướng. Ví dụ đặt đích ở schema dich cùng database cho chạy được; đích ở hệ khác thì lưu mốc ngay trong hệ đó.

USE Kumeo_cdcct;
GO
CREATE SCHEMA dich;
GO
CREATE TABLE dich.MocDongBo (Nguon varchar(30) NOT NULL PRIMARY KEY, PhienBanCT bigint NULL, LsnCDC binary(10) NULL);
CREATE TABLE dich.SanPhamTimKiem (SanPhamId int NOT NULL PRIMARY KEY, DonGia decimal(18, 2) NOT NULL, TonKho int NOT NULL);
CREATE TABLE dich.LichSuGia (Lsn binary(10) NOT NULL, SeqVal binary(10) NOT NULL, SanPhamId int NOT NULL,
    GiaCu decimal(18, 2) NOT NULL, GiaMoi decimal(18, 2) NOT NULL, LucDoi datetime NOT NULL, PRIMARY KEY (Lsn, SeqVal, SanPhamId));
INSERT INTO dich.MocDongBo (Nguon) VALUES ('SanPham');
GO
CREATE OR ALTER PROCEDURE dich.usp_KeoCT          -- bản sao giá và tồn cho trang sản phẩm
AS
BEGIN
    SET NOCOUNT, XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
    BEGIN TRANSACTION;
    DECLARE @tu bigint = (SELECT PhienBanCT FROM dich.MocDongBo WHERE Nguon = 'SanPham');
    DECLARE @den bigint = CHANGE_TRACKING_CURRENT_VERSION();
    IF @tu IS NULL OR @tu < CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID('dbo.SanPham'))
    BEGIN                                          -- lần đầu, hoặc đã ngừng quá retention
        DELETE FROM dich.SanPhamTimKiem;
        INSERT INTO dich.SanPhamTimKiem SELECT SanPhamId, DonGia, TonKho FROM dbo.SanPham;
        SELECT N'tải lại toàn bộ' AS KetQua, @@ROWCOUNT AS SoDong;
    END
    ELSE
    BEGIN
        MERGE dich.SanPhamTimKiem AS d
        USING (SELECT c.SanPhamId, c.SYS_CHANGE_OPERATION AS Op, s.DonGia, s.TonKho
               FROM CHANGETABLE(CHANGES dbo.SanPham, @tu) AS c
               LEFT JOIN dbo.SanPham AS s ON s.SanPhamId = c.SanPhamId) AS n
        ON d.SanPhamId = n.SanPhamId
        WHEN MATCHED AND n.Op = 'D' THEN DELETE
        WHEN MATCHED THEN UPDATE SET DonGia = n.DonGia, TonKho = n.TonKho
        WHEN NOT MATCHED AND n.Op <> 'D' THEN INSERT VALUES (n.SanPhamId, n.DonGia, n.TonKho);
        SELECT N'kéo thay đổi' AS KetQua, @@ROWCOUNT AS SoDong;
    END;
    UPDATE dich.MocDongBo SET PhienBanCT = @den WHERE Nguon = 'SanPham';
    COMMIT;
END;
GO
CREATE OR ALTER PROCEDURE dich.usp_KeoCDC         -- lịch sử giá cho kho kế toán
AS
BEGIN
    SET NOCOUNT, XACT_ABORT ON;
    DECLARE @moc binary(10) = (SELECT LsnCDC FROM dich.MocDongBo WHERE Nguon = 'SanPham');
    DECLARE @min binary(10) = sys.fn_cdc_get_min_lsn('dbo_SanPham');
    DECLARE @tu binary(10) = IIF(@moc IS NULL, @min, sys.fn_cdc_increment_lsn(@moc));
    DECLARE @den binary(10) = sys.fn_cdc_get_max_lsn();
    IF @tu < @min THROW 50002, N'Cleanup đã xóa phần chưa đọc: phải nạp lại từ nguồn.', 1;
    IF @tu > @den BEGIN SELECT N'chưa có gì mới' AS KetQua, 0 AS SoDong; RETURN; END;
    BEGIN TRANSACTION;
    INSERT INTO dich.LichSuGia (Lsn, SeqVal, SanPhamId, GiaCu, GiaMoi, LucDoi)
    SELECT c.__$start_lsn, c.__$seqval, c.SanPhamId, c.GiaCu, c.DonGia, sys.fn_cdc_map_lsn_to_time(c.__$start_lsn)
    FROM (SELECT *, LAG(DonGia) OVER (PARTITION BY __$start_lsn, __$seqval, SanPhamId
                                      ORDER BY __$operation) AS GiaCu   -- ảnh trước (3) đứng trước ảnh sau (4)
          FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@tu, @den, N'all update old')) AS c
    WHERE c.__$operation = 4 AND c.GiaCu <> c.DonGia
      AND NOT EXISTS (SELECT 1 FROM dich.LichSuGia AS l                 -- khử trùng khi chạy lại
                      WHERE l.Lsn = c.__$start_lsn AND l.SeqVal = c.__$seqval AND l.SanPhamId = c.SanPhamId);
    SELECT N'ghi lịch sử giá' AS KetQua, @@ROWCOUNT AS SoDong;
    UPDATE dich.MocDongBo SET LsnCDC = @den WHERE Nguon = 'SanPham';
    COMMIT;
END;
GO

Chạy trên database của mục 4, theo thứ tự:

Bước EXEC dich.usp_KeoCT EXEC dich.usp_KeoCDC
Lần đầu, chưa có mốc Tải lại 5.000 sản phẩm 2 dòng: 8.990.000 → 4.990.000 → 8.990.000
Giá SP-00777 thành 7.990.000, nhập 3 máy, bán 2 Kéo 1 dòng 1 dòng; 3 lần đổi tồn bị bỏ qua
Chạy lại khi không có gì mới Kéo 0 dòng Chưa có gì mới
Xóa LsnCDC, chạy lại từ đầu 0 dòng mới, dich.LichSuGia vẫn 3 dòng

Bước cuối giả lập consumer mất mốc, như khi đích ở hệ khác và mốc chưa kịp lưu. CT không cần khử trùng vì MERGE ghi giá trị hiện tại: chạy hai lần cho cùng một bản sao. CDC chèn lịch sử, nên khóa (Lsn, SeqVal, SanPhamId) chặn lần giao lại thành dòng thứ hai. Debezium có connector SQL Server dựng trên chính CDC (SQL Server 2016 SP1 trở lên, Standard hoặc Enterprise), lưu commit LSN và change LSN làm offset; tài liệu của họ ghi offset được commit định kỳ nên sau sự cố có thể có sự kiện trùng. Phía nhận vẫn phải khử trùng như trên.

7. Chọn cách nào

Hệ nhận cần Dùng Vì sao
Giá, tồn hiện tại cho trang sản phẩm, cache, bộ tìm kiếm CT trên dbo.SanPham 1 dòng mỗi khóa, thấy ngay lúc commit, chạy cả trên Express
Đơn mới và đơn đổi trạng thái cho ERP của đại lý 42, thay cho gọi lại endpoint mỗi 15 phút CDC net changes trên dbo.DonHang dbo.DonHang cần SWITCH, CT chặn nó
Mọi lần đổi giá, mọi trạng thái đơn, giá trị trước và sau, cho kho kế toán CDC, all update old Chỉ CDC giữ ảnh trước
"Lúc 12:00 khách 42 tên gì", hỏi ngay trong BanHang Temporal table Lịch sử nằm cạnh bảng, hỏi bằng FOR SYSTEM_TIME AS OF
Dòng có bị sửa từ lúc mở màn hình kiểm kho không rowversion So một cột, không cần cơ chế nào chạy nền
Báo cho hệ khác "đơn đã thanh toán" Outbox CT và CDC cho biết dòng nào đổi, không cho biết vì sao

Cột rowversion cũng trả lời được "dòng nào đổi từ mốc X", nhưng không thấy dòng đã xóa, và giá trị gán lúc sửa chứ không phải lúc commit: giao dịch dài commit muộn có thể mang giá trị nhỏ hơn mốc đã lưu và bị bỏ sót. Microsoft nêu đúng điểm này khi so CT, vốn xếp theo thời điểm commit, với cách tự làm bằng cột timestamp.

Những chỗ hay hiểu sai

  • "CT cho biết dữ liệu đổi thế nào." Nó chỉ cho biết khóa nào đổi, cột nào bị đụng. 302 lần sửa ra 1 dòng, giá 4.990.000 không còn ở đâu.
  • "Net changes nhẹ hơn all changes." Đó chỉ là cách đọc. _CT vẫn lưu 604 dòng cho 302 lần sửa, đủ 3 ngày.
  • "CDC đọc log nên không tốn gì cho phía ghi." Giao dịch ghi đơn chỉ thêm 119 B log, nhưng capture job chép lại từng thay đổi: tổng log ghi xuống đĩa tăng 39%.
  • "Agent dừng một lúc thì CDC chỉ trễ." Không mất thay đổi, nhưng log giữ nguyên cho tới khi capture đọc kịp; log backup không giải phóng được nó.
  • "CT nhẹ nên bật cho mọi bảng." Bảng cần SWITCH partition như dbo.DonHang bị chặn với Msg 4900.

Đọc tiếp

Nguồn

Đọc tiếp