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. Chọn theo bài toán
- 2. Accelerated Database Recovery
- 3. Commit chưa chờ log
- 4. Lịch sử dòng và hàng đợi đồng bộ
- 5. Che dữ liệu và khóa theo chi nhánh
- 6. Mã hóa file trên đĩa
- 7. Sẵn sàng cao
- 8. Việc chạy hằng ngày
- 9. Thứ tự nên bật cho BanHang
- 10. Áp dụng trong .NET
- Những chỗ hay hiểu sai
- Kết luận
- Đọc tiếp
- 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
DELETEnhầ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
TemporalAsOfcủa EF Core. - Bật
EnableRetryOnFailurethì 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 store
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.KhachHang
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_KhachHang
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:\Keys
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
- Recovery model
FULL, lịch full, differential, log, và một lần restore thử cóSTOPAT(chương lưu trữ). - 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.
- Query Store bật trước khi đổi chỉ mục hoặc compatibility level.
- 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.
READ_COMMITTED_SNAPSHOTkhi đo được đọc và ghi chặn nhau.- ADR khi rollback hoặc recovery của giao dịch dài vượt quá mức chấp nhận.
- TDE sau khi certificate đã được sao lưu ra ngoài máy.
- Log shipping hoặc Availability Group khi RTO của một lần restore full không còn đủ.
DBCC CHECKDBvà 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)
#: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à
STOPATlấ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
.mdfhoặc.bak; người cóSELECTvẫ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_healthvà 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
IsTemporalvà truyền giờ UTC (DateTime.UtcNow,DateTimeOffset.UtcDateTime) vàoTemporalAsOf. - Bật
EnableRetryOnFailurevà bọc mọiBeginTransactionAsynctrongCreateExecutionStrategy().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=ReadOnlyvàInitial Catalog. - Nếu dùng row-level security, đặt
ChiNhanhIdvàoSESSION_CONTEXTmỗi lần mở kết nối, ví dụ trongDbConnectionInterceptor.ConnectionOpenedAsync. - Diễn tập failover và restore
STOPATtrướ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
- Bài trước: Kỹ thuật thường dùng: lưu trữ và truy vấn, nén, columnstore,
SWITCH, RCSI và Query Store. - Bài sau: Kiểu dữ liệu: byte trên page và kiểu cho tiền.
- ADR và delayed durability trong bảng ACID của giao dịch: Giao dịch và XACT_ABORT.
- Các mức isolation: Mức isolation và SNAPSHOT.
- Đồng bộ thay đổi sang hệ khác, đo Change Tracking và CDC trên cùng một máy: Change Tracking và CDC.
- Chuỗi backup,
STOPATvà restore thử: Sao lưu và khôi phục theo thời điểm.
Nguồn
- Accelerated database recovery
- Temporal tables
- Transparent data encryption: mục Encryption hierarchy, TDE and backups.
- Availability groups
- Editions and supported features of SQL Server 2022
- High availability and disaster recovery with Microsoft.Data.SqlClient:
MultiSubnetFailover,ApplicationIntent, read-only routing, thử lại sau failover. - EF Core: SQL Server temporal tables
- EF Core: Connection resiliency và mã nguồn
SqlServerTransientExceptionDetector.