Cơ sở dữ liệuSQL Server, phần 6/24

Kỹ thuật thường dùng: an toàn và sẵn sàng

ADR, delayed durability, temporal table, masking, row-level security, TDE, Availability Group và việc vận hành hằng ngày trên database BanHang, kèm cấu hình EF Core và chuỗi kết nối.

Mục lục
  1. 1. Chọn theo bài toán
  2. 2. Accelerated Database Recovery
  3. 3. Commit chưa chờ log
  4. 4. Lịch sử dòng và hàng đợi đồng bộ
  5. 5. Che dữ liệu và khóa theo chi nhánh
  6. 6. Mã hóa file trên đĩa
  7. 7. Sẵn sàng cao
  8. 8. Việc chạy hằng ngày
  9. 9. Thứ tự nên bật cho BanHang
  10. 10. Áp dụng trong .NET
  11. Những chỗ hay hiểu sai
  12. Kết luận
  13. Đọc tiếp
  14. Nguồn

Dữ liệu của BanHang phải qua được một giao dịch dài bị hủy, câu hỏi "hôm qua khách này tên gì", một file backup bị mang ra ngoài và một máy chủ chết giữa giờ cao điểm. Mỗi rủi ro có tính năng riêng, nhưng thiếu điều kiện đi kèm, như certificate đã sao lưu, thì bật cũng như không. Đọc xong, bạn biết bật gì theo thứ tự nào, và cấu hình EF Core cùng chuỗi kết nối cho chúng.

Đọc nhanh

  • TDE chỉ an toàn khi certificate đã được sao lưu ra máy khác.
  • Availability Group không thay backup: một DELETE nhầm cũng được redo sang máy phụ.
  • Temporal table trả lời "lúc đó dữ liệu là gì", và mốc thời gian là giờ UTC, kể cả khi đọc bằng TemporalAsOf của EF Core.
  • Bật EnableRetryOnFailure thì mọi giao dịch tự mở phải chạy trong execution strategy, nếu không EF Core báo lỗi ngay.

1. Chọn theo bài toán

Bài trước lo cho database nhanh; bài này lo cho dữ liệu không mất, không lộ, và luôn có máy phục vụ. Mốc là SQL Server 2019; từ 2016 SP1, Standard đã có CDC, row-level security và dynamic data masking.

Bài toán trên BanHang Kỹ thuật nên xét trước
Rollback và recovery sau giao dịch dài quá lâu Accelerated Database Recovery, từ SQL Server 2019
Cần biết hôm qua TongTien của đơn 10042 là bao nhiêu Bảng temporal
Màn hình tổng đài không hiện đủ số điện thoại Dynamic data masking
Nhân viên chi nhánh chỉ thấy đơn của mình Row-level security
Ổ đĩa hoặc file backup ra khỏi tủ TDE, cộng sao lưu certificate
Mất một máy mà chỉ được mất vài phút dữ liệu Log shipping, Basic Availability Group, hoặc Availability Group
Biết backup có lành không DBCC CHECKDB và restore thử

2. Accelerated Database Recovery

Từ SQL Server 2019, ADR đổi cách undo và recovery. Giao dịch dài không còn kéo thời gian rollback và thời gian mở lại database theo số dòng nó đã sửa, và log được cắt sớm hơn dù còn transaction chưa commit. Có trên Standard, Enterprise và Web; Express không có.

Persisted version store (PVS) mặc định nằm trên filegroup PRIMARY, với BanHang là tệp 512 MB trên D:. Chỉ định filegroup ngay lúc bật, để phiên bản dòng nằm trên ổ nhanh và có chỗ lớn:

ALTER DATABASE BanHang SET ACCELERATED_DATABASE_RECOVERY = ON
    (PERSISTENT_VERSION_STORE_FILEGROUP = FG_DATA);

Lệnh cần khóa độc quyền: nó chờ mọi phiên khác rời BanHang, và phiên mới xếp hàng sau nó. Chạy ngoài giờ cao điểm, thêm WITH ROLLBACK IMMEDIATE nếu chấp nhận ngắt các phiên đang mở. PVS đã ở PRIMARY thì muốn chuyển phải tắt ADR, chờ PVS dọn về 0, rồi bật lại kèm filegroup mới. ADR không thay log backup: điểm phục hồi theo thời điểm vẫn do log backup quyết định.

Theo dõi dung lượng persisted version storeSQL · 3 dòng
SELECT persistent_version_store_size_kb / 1024.0 AS pvs_mb
FROM sys.dm_tran_persistent_version_store_stats
WHERE database_id = DB_ID(N'BanHang');

3. Commit chưa chờ log

Commit thường chờ log record xuống L: rồi mới trả về. Delayed durability bỏ bước chờ đó: thông lượng tăng khi đĩa log là nút cổ chai, đổi lại mất các giao dịch đã báo thành công nếu mất điện trước lần flush kế tiếp.

ALTER DATABASE BanHang SET DELAYED_DURABILITY = ALLOWED;
GO

BEGIN TRAN;
    UPDATE dbo.DonHang
    SET TrangThai = 2
    WHERE DonHangId = 10042
      AND NgayTao = CAST('2026-10-02T11:58:00' AS datetime2(0));
COMMIT TRAN WITH (DELAYED_DURABILITY = ON);

Dùng cho luồng ghi dày mà mất vài giao dịch cuối vẫn chấp nhận được, ví dụ nhật ký truy cập. Đơn đã thu tiền giữ commit bền, tức không gắn cờ này.

4. Lịch sử dòng và hàng đợi đồng bộ

Ba cơ chế cùng trả lời "dữ liệu đã đổi gì", với độ nặng tăng dần.

Cơ chế Giữ cái gì Khi nào hợp
Change Tracking Khóa của dòng đã đổi kể từ một mốc Ứng dụng tự kéo danh sách đơn cần đồng bộ. Nhẹ
Temporal table Cả dòng tại từng khoảng thời gian Hỏi "lúc 12:00 khách này tên gì". Có từ SQL Server 2016
Change Data Capture Ảnh trước và sau của dòng, đọc bằng SQL Agent Kho dữ liệu hoặc hệ khác cần nội dung cũ và mới. Không có trên Express

dbo.KhachHang của BanHang đã là bảng temporal: hai cột SysStart, SysEnd và bảng lịch sử dbo.KhachHang_LichSu. Sau vài lần UPDATE, câu này trả tên đúng tại thời điểm trước lần sửa:

SELECT KhachHangId, Ten
FROM dbo.KhachHang
FOR SYSTEM_TIME AS OF '2026-10-02T12:00:00'
WHERE KhachHangId = 42;

SQL Server ghi SysStart, SysEnd theo giờ UTC, nên mốc trong AS OF cũng là giờ UTC; 12:00 ở Việt Nam là 05:00 UTC.

Định nghĩa bảng temporal dbo.KhachHangSQL · 12 dòng
CREATE TABLE dbo.KhachHang (
    KhachHangId int NOT NULL CONSTRAINT PK_KhachHang PRIMARY KEY CLUSTERED,
    ChiNhanhId int NOT NULL,
    Ten nvarchar(200) NOT NULL,
    SoDienThoai varchar(20) NULL,
    SysStart datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    SysEnd datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME (SysStart, SysEnd)
)
WITH (
    SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.KhachHang_LichSu)
);

Bảng lịch sử lớn theo số lần sửa, không theo số khách, và nằm trên filegroup mặc định FG_DATA. Muốn để nó trên FG_ARCHIVE, tạo bảng lịch sử trên filegroup đó trước rồi mới bật SYSTEM_VERSIONING. Lịch sử đi theo full backup, không thay log backup. Chính sách lọc dòng ở mục 5 không tự áp lên bảng lịch sử.

Change Tracking hợp khi chỉ cần biết đơn nào đổi để đồng bộ sang hệ tìm kiếm. CDC hợp khi bên nhận cần cả TongTien cũ và mới; nó phụ thuộc SQL Server Agent, Agent dừng thì hàng đợi CDC đứng.

5. Che dữ liệu và khóa theo chi nhánh

Dynamic data masking đổi cách một cột hiện với người không có quyền UNMASK; người đó vẫn SELECT được dòng. Đây là lớp hiển thị cho ứng dụng tổng đài, không phải lớp chống người có quyền đọc trang dữ liệu. Người có db_owner hoặc quyền UNMASK thấy số đầy đủ.

ALTER TABLE dbo.KhachHang
    ALTER COLUMN SoDienThoai ADD MASKED WITH (FUNCTION = 'partial(0,"*****",2)');

GRANT SELECT ON dbo.KhachHang TO NhanVienTongDai;
-- Tài khoản ứng dụng quản trị được UNMASK khi cần gọi lại khách.

Row-level security lọc dòng. Nhân viên mang ChiNhanhId trong SESSION_CONTEXT chỉ thấy khách của chi nhánh đó. Hàm lọc và chính sách ở khối dưới; ứng dụng đặt ngữ cảnh sau khi đăng nhập bằng EXEC sys.sp_set_session_context @key = N'ChiNhanhId', @value = 3;.

Hàm lọc dbo.fn_KhachHang_ChiNhanh và chính sách dbo.pol_KhachHangSQL · 13 dòng
CREATE FUNCTION dbo.fn_KhachHang_ChiNhanh (@ChiNhanhId int)
RETURNS TABLE
WITH SCHEMABINDING
AS
    RETURN
    SELECT 1 AS cho_phep
    WHERE @ChiNhanhId = CAST(SESSION_CONTEXT(N'ChiNhanhId') AS int);
GO

CREATE SECURITY POLICY dbo.pol_KhachHang
ADD FILTER PREDICATE dbo.fn_KhachHang_ChiNhanh (ChiNhanhId)
    ON dbo.KhachHang
WITH (STATE = ON);

Chủ database và người có quyền sửa chính sách vẫn thiết kế được đường đọc khác. Row-level security là kiểm soát trong ứng dụng dùng chung một bảng, không thay quyền GRANT tối thiểu.

Always Encrypted đi xa hơn: driver mã hóa cột trước khi gửi, nên người quản trị database không đọc được cột đó. Tìm theo đẳng thức được với mã hóa xác định, theo khoảng giá trị thì không. Khóa nằm ở ứng dụng hoặc Windows, nên đây là một dự án riêng, không phải một cờ bật.

6. Mã hóa file trên đĩa

TDE mã hóa file dữ liệu, file log và bản backup lúc nằm trên đĩa. Ai chép .mdf hoặc .bak sang máy khác mà không có certificate thì không mở được. Người đã kết nối vào BanHang và có quyền SELECT vẫn đọc đơn bình thường. Trên SQL Server 2022, Standard có TDE; trên SQL Server 2019, TDE thuộc Enterprise.

Mỗi tầng khóa bảo vệ tầng dưới nó. Certificate nằm trong master, không nằm trong file backup của BanHang:

flowchart TD
  DP["DPAPI của Windows"] --> SMK["Service master key của instance"]
  SMK --> DMK["Database master key trong master"]
  DMK --> CERT["Certificate TdeCert_BanHang trong master"]
  CERT --> DEK["Database encryption key trong BanHang"]
  DEK --> F["File dữ liệu, file log, bản backup"]
  CERT -.->|"BACKUP CERTIFICATE"| B["Ổ B: file .cer và .pvk"]

Mất certificate là mất khả năng restore sang máy khác, kể cả khi file .bak còn nguyên. Sao lưu certificate ra máy khác ngay lúc bật TDE, và giữ mật khẩu file khóa riêng.

Bật TDE cho BanHang và sao lưu certificate ra B:\KeysSQL · 27 dòng
USE master;
GO

CREATE MASTER KEY ENCRYPTION BY PASSWORD = N'<mat-khau-master-key>';
GO

CREATE CERTIFICATE TdeCert_BanHang
    WITH SUBJECT = N'TDE BanHang';
GO

BACKUP CERTIFICATE TdeCert_BanHang
    TO FILE = N'B:\Keys\TdeCert_BanHang.cer'
    WITH PRIVATE KEY (
        FILE = N'B:\Keys\TdeCert_BanHang.pvk',
        ENCRYPTION BY PASSWORD = N'<mat-khau-file-khoa>'
    );
GO

USE BanHang;
GO

CREATE DATABASE ENCRYPTION KEY
    WITH ALGORITHM = AES_256
    ENCRYPTION BY SERVER CERTIFICATE TdeCert_BanHang;
GO

ALTER DATABASE BanHang SET ENCRYPTION ON;

Đặt mật khẩu thật ở kho mật khẩu, không ghi vào script trong git. Thư mục B:\Keys không nằm trên cùng đĩa với E: và G:. Mã hóa riêng từng bản backup khi chưa bật TDE thì dùng BACKUP ... WITH ENCRYPTION; khóa đó cũng phải có trên máy restore.

7. Sẵn sàng cao

Backup giải quyết mất dữ liệu đến mốc sao lưu cuối; sẵn sàng cao giải quyết thêm thời gian máy đứng. Chọn theo edition và số phút được phép mất.

Cách Edition Dữ liệu Máy hỏng thì sao
Full + log backup Standard trở lên cho lịch Agent Một bản Restore, RPO bằng chu kỳ log, RTO bằng thời gian restore
Log shipping Standard, Enterprise Bản log được restore liên tục sang máy phụ Máy phụ mở được sau khi áp nốt log. Thường trễ một chu kỳ log
Failover cluster instance Standard 2 nút, Enterprise nhiều nút hơn Một bản trên đĩa dùng chung Tiến trình SQL chuyển nút. Hỏng đĩa dùng chung thì vẫn phải restore
Basic Availability Group Standard, từ SQL Server 2016 Hai bản, một database, hai replica Tự chuyển khi đồng bộ. Máy phụ không đọc báo cáo được
Availability Group Enterprise Nhiều database, tới nhiều replica Tự chuyển nếu đồng bộ. Replica phụ đọc được nếu cấp quyền

BanHang một database, bản Standard, chấp nhận vài phút mất dữ liệu: log shipping hoặc Basic Availability Group. Cần vừa tự chuyển vừa cho báo cáo đọc trên máy phụ: Availability Group trên Enterprise, replica đồng bộ cho chuyển tự động, replica bất đồng bộ cho báo cáo nếu chấp nhận trễ.

flowchart TD
  Q1["Restore full và log có kịp RTO?"]
  Q1 -->|Kịp| R1["Full + log backup"]
  Q1 -->|Không| Q2["Cần báo cáo đọc trên máy phụ?"]
  Q2 -->|Có| R2["Availability Group, Enterprise"]
  Q2 -->|Không| Q3["Cần tự chuyển khi máy chính hỏng?"]
  Q3 -->|Có| R3["Basic Availability Group, Standard"]
  Q3 -->|Không| R4["Log shipping, Standard"]
  R2 --> K["Vẫn chạy chuỗi full, differential, log backup"]
  R3 --> K
  R4 --> K

Availability Group dùng đúng log của chương lưu trữ: máy phụ redo log của máy chính. Recovery model phải là FULL, và chuỗi log backup vẫn phải chạy. Một DELETE nhầm lúc 12:07 được redo sang máy phụ; muốn lấy lại dữ liệu trước lệnh xóa vẫn restore theo STOPAT. Ứng dụng trỏ vào listener, không trỏ vào tên từng máy; chuỗi kết nối ở mục 10.

8. Việc chạy hằng ngày

DBCC CHECKDB đọc cấu trúc trang và đối chiếu chỉ mục. Chạy bản đủ mỗi tuần lúc tải thấp; giữa tuần, trên database lớn, WITH PHYSICAL_ONLY bắt trang hỏng sớm hơn. Lỗi CHECKDB được xử lý từ backup lành gần nhất; không có backup thì CHECKDB chỉ cho biết database đã hỏng.

DBCC CHECKDB (N'BanHang') WITH NO_INFOMSGS, ALL_ERRORMSGS;

Deadlock đã được session sẵn có system_health ghi lại; mở system_health*.xel bằng SSMS khi cần đồ thị deadlock. Cảnh báo SQL Server Agent nên có ít nhất: log dùng quá 80%, lỗi severity 19 đến 25, job backup thất bại, và CHECKDB thất bại. Log backup 15 phút mà log vẫn tăng thì tìm giao dịch mở bằng DBCC OPENTRAN.

Ba bộ script cộng đồng được dùng rộng khi vận hành nhiều instance. Đọc script trước khi chạy, ghim một phiên bản, và giữ lịch backup theo chuỗi full, differential, log có CHECKSUM và restore thử:

Bộ Việc nó gánh
Ola Hallengren Maintenance Solution Job backup, DBCC CHECKDB, và bảo trì chỉ mục theo kích thước cùng mức phân mảnh
First Responder Kit (sp_Blitz, sp_BlitzCache, sp_BlitzIndex) Lượt xem nhanh cấu hình, câu nặng, và chỉ mục thừa hoặc thiếu
dbatools Backup, restore, sao chép login và di chuyển instance từ PowerShell

9. Thứ tự nên bật cho BanHang

  1. Recovery model FULL, lịch full, differential, log, và một lần restore thử có STOPAT (chương lưu trữ).
  2. Tách ổ data, log, backup. Cấp sẵn kích thước file. Bật instant file initialization cho tài khoản dịch vụ SQL Server.
  3. Query Store bật trước khi đổi chỉ mục hoặc compatibility level.
  4. Chỉ mục cho câu chạy nhiều nhất, rồi mới nén trang hoặc thêm columnstore cho câu tổng hợp.
  5. READ_COMMITTED_SNAPSHOT khi đo được đọc và ghi chặn nhau.
  6. ADR khi rollback hoặc recovery của giao dịch dài vượt quá mức chấp nhận.
  7. TDE sau khi certificate đã được sao lưu ra ngoài máy.
  8. Log shipping hoặc Availability Group khi RTO của một lần restore full không còn đủ.
  9. DBCC CHECKDB và cảnh báo Agent chạy trước khi thêm tính năng mới.

Bước 3 đến 5 nằm ở bài trước. Trên Azure SQL Database, backup, TDE, ADR, Query Store và RCSI đã do nền tảng bật; phần vận hành đổi nằm ở Azure SQL.

10. Áp dụng trong .NET

EF Core ánh xạ dbo.KhachHang có sẵn bằng IsTemporal, với SysStart, SysEnd là shadow property. TemporalAsOf sinh FOR SYSTEM_TIME AS OF, nên mốc phải là giờ UTC:

e.ToTable("KhachHang", "dbo", t => t.IsTemporal(h =>
{
    h.HasPeriodStart("SysStart");
    h.HasPeriodEnd("SysEnd");
    h.UseHistoryTable("KhachHang_LichSu", "dbo");
}));

// mocUtc lấy từ DateTime.UtcNow, không phải DateTime.Now
var tenLucDo = await db.KhachHang.TemporalAsOf(mocUtc)
    .Where(k => k.KhachHangId == 42).Select(k => k.Ten).SingleAsync();

Bản thử ghi khách 42 tên "Đại lý A", nhớ mốc, rồi đổi tên. Máy chạy giờ Việt Nam, nên DateTime.Now đi trước UTC 7 giờ và rơi vào sau lần sửa:

AS OF 14:36:40 (UTC):     Đại lý A
AS OF 21:36:40 (giờ máy): Đại lý A - Chi nhánh 3

EnableRetryOnFailure cho EF Core tự chạy lại khi gặp lỗi mà nó xếp là tạm thời, trong đó có đứt kết nối (10054) và deadlock (1205). Khi bật, giao dịch tự mở bằng BeginTransactionAsync phải nằm trong execution strategy, để cả khối được chạy lại như một đơn vị:

var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
    await using var tx = await db.Database.BeginTransactionAsync();
    // đọc, sửa, SaveChangesAsync
    await tx.CommitAsync();
});

Mở giao dịch ngoài strategy rồi truy vấn, EF Core ném InvalidOperationException: "The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions." Cả khối có thể chạy hai lần nếu kết nối đứt sau khi commit đã tới server, nên khối ghi tiền phải có khóa chống trùng như Idempotency key.

Với Availability Group, SqlConnectionStringBuilder dựng hai chuỗi trỏ vào listener: ghi vào replica chính, báo cáo xin replica đọc.

var ghi = new SqlConnectionStringBuilder
{
    DataSource = "tcp:BanHang-lsn,1433", InitialCatalog = "BanHang",
    IntegratedSecurity = true, MultiSubnetFailover = true,
    ApplicationIntent = ApplicationIntent.ReadWrite,
};
var baoCao = new SqlConnectionStringBuilder(ghi.ConnectionString)
    { ApplicationIntent = ApplicationIntent.ReadOnly };

MultiSubnetFailover=True thử song song mọi IP của listener, nhưng không rút ngắn thời gian failover của server. Định tuyến chỉ đọc cần client trỏ vào listener, có Initial Catalog là database trong nhóm và ApplicationIntent=ReadOnly, còn server phải cấu hình read-only routing.

Bản đầy đủ dưới chạy trên LocalDB SQL Server 2019 (15.0.4382), .NET 10.0.401, Microsoft.EntityFrameworkCore.SqlServer 10.0.12, database Kumeo_kythuat. Phần temporal và retry chạy thật; phần Availability Group chỉ dựng và in chuỗi kết nối, chưa thử với listener thật vì LocalDB không có Availability Group. Dòng #:property PublishAot=false cần vì file-based app mặc định bật PublishAot, mà EF Core không dựng model lúc chạy dưới chế độ đó.

KhachHangLichSu.cs: temporal, execution strategy và chuỗi kết nối listener (dotnet run KhachHangLichSu.cs)C# · 148 dòng
#:package Microsoft.EntityFrameworkCore.SqlServer@10.0.12
#:property PublishAot=false
// Bảng temporal dbo.KhachHang đọc bằng EF Core, retry khi lỗi tạm thời, và chuỗi kết nối cho Availability Group.
// Chạy: dotnet run KhachHangLichSu.cs   (LocalDB, tạo database Kumeo_kythuat nếu chưa có)
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

const string May = @"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;TrustServerCertificate=true";
const string Cs = May + ";Database=Kumeo_kythuat";
await TaoBang();

// 1. Ghi rồi sửa tên khách 42, nhớ mốc trước lần sửa (giờ UTC).
await using (var db = new BanHangDb(Cs))
{
    db.KhachHang.Add(new KhachHang { KhachHangId = 42, ChiNhanhId = 3, Ten = "Đại lý A" });
    await db.SaveChangesAsync();
}
await Task.Delay(1000);
var truocKhiSuaUtc = DateTime.UtcNow;
var truocKhiSuaGioMay = DateTime.Now;
await Task.Delay(1000);
await using (var db = new BanHangDb(Cs))
{
    var k = await db.KhachHang.SingleAsync(k => k.KhachHangId == 42);
    k.Ten = "Đại lý A - Chi nhánh 3";
    await db.SaveChangesAsync();
}

// 2. Đọc lại theo thời điểm.
await using (var db = new BanHangDb(Cs))
{
    var q = db.KhachHang.TemporalAsOf(truocKhiSuaUtc).Where(k => k.KhachHangId == 42).Select(k => k.Ten);
    Console.WriteLine(q.ToQueryString());
    Console.WriteLine();
    Console.WriteLine($"AS OF {truocKhiSuaUtc:HH:mm:ss} (UTC):     {await q.SingleAsync()}");
    var sai = await db.KhachHang.TemporalAsOf(truocKhiSuaGioMay).Where(k => k.KhachHangId == 42).Select(k => k.Ten).SingleAsync();
    Console.WriteLine($"AS OF {truocKhiSuaGioMay:HH:mm:ss} (giờ máy): {sai}");
    Console.WriteLine();

    var lichSu = await db.KhachHang.TemporalAll()
        .Where(k => k.KhachHangId == 42)
        .OrderBy(k => EF.Property<DateTime>(k, "SysStart"))
        .Select(k => new { k.Ten, Tu = EF.Property<DateTime>(k, "SysStart"), Den = EF.Property<DateTime>(k, "SysEnd") })
        .ToListAsync();
    foreach (var d in lichSu) Console.WriteLine($"{d.Ten,-24} {d.Tu:yyyy-MM-dd HH:mm:ss} → {d.Den:yyyy-MM-dd HH:mm:ss}");
    Console.WriteLine();
}

// 3. Retry: EnableRetryOnFailure không nhận giao dịch tự mở ngoài execution strategy.
var coRetry = new DbContextOptionsBuilder<BanHangDb>()
    .UseSqlServer(Cs, sql => sql.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorNumbersToAdd: null))
    .Options;
await using (var db = new BanHangDb(coRetry))
{
    try
    {
        await using var tx = await db.Database.BeginTransactionAsync();
        var k = await db.KhachHang.SingleAsync(k => k.KhachHangId == 42);
        Console.WriteLine("Mở giao dịch trực tiếp rồi truy vấn: được");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Mở giao dịch trực tiếp: {ex.GetType().Name}");
        Console.WriteLine(ex.Message);
    }

    var strategy = db.Database.CreateExecutionStrategy();
    await strategy.ExecuteAsync(async () =>
    {
        await using var tx = await db.Database.BeginTransactionAsync();
        var k = await db.KhachHang.SingleAsync(k => k.KhachHangId == 42);
        k.SoDienThoai = "0900000042";
        await db.SaveChangesAsync();
        await tx.CommitAsync();
    });
    Console.WriteLine("Giao dịch trong CreateExecutionStrategy().ExecuteAsync: đã commit");
    Console.WriteLine();
}

// 4. Chuỗi kết nối tới listener của Availability Group (chỉ in ra, không kết nối).
var ghi = new SqlConnectionStringBuilder
{
    DataSource = "tcp:BanHang-lsn,1433", InitialCatalog = "BanHang",
    IntegratedSecurity = true, MultiSubnetFailover = true,
    ApplicationIntent = ApplicationIntent.ReadWrite,
};
var baoCao = new SqlConnectionStringBuilder(ghi.ConnectionString)
    { ApplicationIntent = ApplicationIntent.ReadOnly };
Console.WriteLine(ghi.ConnectionString);
Console.WriteLine(baoCao.ConnectionString);

async Task TaoBang()
{
    await using (var m = new SqlConnection(May))
    {
        await m.OpenAsync();
        await using var c = new SqlCommand("IF DB_ID(N'Kumeo_kythuat') IS NULL CREATE DATABASE Kumeo_kythuat COLLATE Vietnamese_100_CI_AS;", m);
        await c.ExecuteNonQueryAsync();
    }
    await using var cn = new SqlConnection(Cs);
    await cn.OpenAsync();
    // Đúng định nghĩa dbo.KhachHang dùng chung của BanHang. Xóa bản cũ để chạy lại được.
    await using var cmd = new SqlCommand("""
        IF OBJECT_ID(N'dbo.KhachHang') IS NOT NULL
        BEGIN
            ALTER TABLE dbo.KhachHang SET (SYSTEM_VERSIONING = OFF);
            DROP TABLE dbo.KhachHang;
            DROP TABLE dbo.KhachHang_LichSu;
        END;
        CREATE TABLE dbo.KhachHang (
            KhachHangId int NOT NULL CONSTRAINT PK_KhachHang PRIMARY KEY CLUSTERED,
            ChiNhanhId int NOT NULL,
            Ten nvarchar(200) NOT NULL,
            SoDienThoai varchar(20) NULL,
            SysStart datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
            SysEnd datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
            PERIOD FOR SYSTEM_TIME (SysStart, SysEnd)
        ) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.KhachHang_LichSu));
        """, cn);
    await cmd.ExecuteNonQueryAsync();
}

class KhachHang
{
    public int KhachHangId { get; set; }
    public int ChiNhanhId { get; set; }
    public string Ten { get; set; } = "";
    public string? SoDienThoai { get; set; }
}

class BanHangDb : DbContext
{
    public BanHangDb(string cs) : base(new DbContextOptionsBuilder<BanHangDb>().UseSqlServer(cs).Options) { }
    public BanHangDb(DbContextOptions<BanHangDb> o) : base(o) { }
    public DbSet<KhachHang> KhachHang => Set<KhachHang>();
    protected override void OnModelCreating(ModelBuilder m) => m.Entity<KhachHang>(e =>
    {
        e.ToTable("KhachHang", "dbo", t => t.IsTemporal(h =>
        {
            h.HasPeriodStart("SysStart");
            h.HasPeriodEnd("SysEnd");
            h.UseHistoryTable("KhachHang_LichSu", "dbo");
        }));
        e.Property(k => k.KhachHangId).ValueGeneratedNever();
        e.Property(k => k.Ten).HasMaxLength(200);
        e.Property(k => k.SoDienThoai).HasMaxLength(20).IsUnicode(false);
    });
}

Những chỗ hay hiểu sai

  • "Có Availability Group thì không cần backup." Máy phụ redo cả lệnh xóa nhầm; chỉ log backup và STOPAT lấy lại được dữ liệu trước đó.
  • "TDE chặn người đọc trộm dữ liệu." TDE chặn người cầm file .mdf hoặc .bak; người có SELECT vẫn đọc bình thường.
  • "Masking là bảo mật cột." Masking là lớp hiển thị; cần người vận hành không đọc được thì dùng Always Encrypted.
  • "Profiler chạy cả ngày để bắt câu chậm." Profiler thêm tải; dùng session Extended Events ngắn, system_health và Query Store.
  • "Autogrowth theo phần trăm cho tiện." Dùng bước cố định, như 1 GB cho data file và cho log của BanHang.
  • "Log đầy thì co file log về 1 MB." Tìm giao dịch mở hoặc chuỗi log backup đứt, rồi để log đủ lớn cho giờ cao điểm.
  • "Gọi linked server trong từng vòng lặp cũng được." Một câu kéo tập khóa cần đồng bộ rồi xử lý tại chỗ; linked server để cho tác vụ thưa.

Kết luận

An toàn của BanHang nằm ở điều kiện đi kèm mỗi tính năng: certificate đã sao lưu, log backup vẫn chạy, mốc thời gian đúng UTC, giao dịch chạy lại được.

Trong dự án .NET của bạn:

  • Ánh xạ bảng temporal bằng IsTemporal và truyền giờ UTC (DateTime.UtcNow, DateTimeOffset.UtcDateTime) vào TemporalAsOf.
  • Bật EnableRetryOnFailure và bọc mọi BeginTransactionAsync trong CreateExecutionStrategy().ExecuteAsync; khối ghi tiền có khóa chống trùng.
  • Trỏ chuỗi kết nối vào listener với MultiSubnetFailover=True; báo cáo dùng chuỗi riêng có ApplicationIntent=ReadOnly và Initial Catalog.
  • Nếu dùng row-level security, đặt ChiNhanhId vào SESSION_CONTEXT mỗi lần mở kết nối, ví dụ trong DbConnectionInterceptor.ConnectionOpenedAsync.
  • Diễn tập failover và restore STOPAT trước khi tin vào cấu hình, và đo thời gian ứng dụng mất kết nối.

Đọc tiếp

Nguồn

Đọc tiếp

Trong Azure SQL

Khác gì so với SQL Server tự cài

Azure SQL Database, Managed Instance và máy ảo. Backup tự động, tầng dịch vụ, trần log, point-in-time restore, và ứng dụng .NET đăng nhập bằng Entra, thử lại khi gặp lỗi tạm thời.

14 phút đọc

Trong SQL Server

Ngày giờ, múi giờ và kiểu tham số

Vì sao BETWEEN '23:59:59.997' làm sót hoặc đếm trùng đơn, lưu giờ Việt Nam hay UTC, và vì sao kiểu tham số mà Dapper, ADO.NET hay EF Core gửi xuống quyết định cả kết quả lẫn kế hoạch.

12 phút đọc