Cơ sở dữ liệuSQL Server, phần 3/24
Write-ahead log, VLF và recovery model
Vì sao COMMIT chỉ chờ log, ai ghi page dữ liệu xuống đĩa, recovery làm gì sau sự cố, cấp log thế nào để tránh hàng nghìn VLF, recovery model quyết định khi nào log được cắt, và một BackgroundService .NET theo dõi điều đó.
COMMIT trả về khi log record đã xuống tệp .ldf, không phải khi page dữ liệu đã nằm trên đĩa. Không hiểu điều này thì một giao dịch quên đóng, hay một database FULL thiếu log backup, sẽ làm log phình đến lúc không đơn nào commit được. Đọc xong, bạn biết log được giữ đến khi nào, cấp log ra sao để tránh hàng nghìn VLF, và theo dõi điều đó từ một dịch vụ .NET.
Đọc nhanh
- Quy tắc write-ahead logging: log của một lần sửa phải xuống đĩa trước page dữ liệu của lần sửa đó.
- Mất điện sau
COMMITkhông mất giao dịch, vì recovery dùng log để redo phần đã commit và undo phần chưa commit. - Nới log từng bước nhỏ sinh hàng nghìn VLF, nên cấp sẵn log đủ lớn ngay lúc tạo database.
- Trong
FULL, chỉ log backup mới cắt log, và một giao dịch mở lâu giữ log dù lịch backup đều.
1. Đường ghi dữ liệu
Sửa một dòng không có nghĩa là ghi ngay page đó xuống .mdf. Đường đi gồm bộ nhớ, nhật ký, rồi mới tới tệp dữ liệu.
Page dữ liệu được đọc vào buffer pool trong RAM. Sửa dòng là một lần ghi logic trên page trong bộ nhớ, và page đó được đánh dấu dirty. Nhiều lần sửa có thể dồn trên cùng một dirty page trước khi page được ghi vật lý ra đĩa.
Mỗi lần sửa tạo log record trong log cache. Quy tắc write-ahead logging (WAL): log record của lần sửa phải đã nằm trên đĩa trước khi dirty page tương ứng được ghi xuống tệp dữ liệu. Khi giao dịch COMMIT, engine làm cứng log của giao dịch xuống .ldf rồi mới báo thành công cho client. Dirty page có thể vẫn nằm trong RAM sau commit. Mất điện lúc này không mất giao dịch đã commit, vì recovery đọc log và làm lại thay đổi trên page.
Thứ tự đúng của một UPDATE đã commit:
- Page dữ liệu nằm trong buffer pool, hoặc được đọc lên nếu chưa có.
- Log record được ghi vào log cache.
- Page trong bộ nhớ bị sửa và trở thành dirty page.
- Lúc
COMMIT, log record được flush xuống.ldf. - Client nhận kết quả thành công.
- Sau đó checkpoint, lazy writer, hoặc eager writer mới ghi dirty page xuống
.mdfhoặc.ndf.
sequenceDiagram participant App as Ứng dụng participant BP as Buffer pool participant LC as Log cache participant LDF as Tệp log trên L participant NDF as Tệp dữ liệu trên E hoặc G App->>BP: UPDATE đơn 10042 BP->>LC: log record BP->>BP: page thành dirty App->>LC: COMMIT LC->>LDF: flush log LDF-->>App: thành công BP->>NDF: checkpoint hoặc lazy writer ghi page, sau đó
Bước 6 được phép xảy ra trước commit nếu log của lần sửa đó đã ở trên đĩa. Điều WAL cấm là ghi dirty page khi log tương ứng chưa bền.
2. Sửa đơn 10042, theo từng thời điểm
Đơn 10042 tạo lúc 11:58 ngày 2026-10-02, TongTien = 1500000.00, nằm trên FG_DATA (xem bài tệp và filegroup). Lúc 12:04 ứng dụng chạy:
BEGIN TRAN;
UPDATE dbo.DonHang
SET TongTien = 1750000.00
WHERE DonHangId = 10042
AND NgayTao = CAST('2026-10-02T11:58:00' AS datetime2(0));
-- Chưa COMMIT. Page trong RAM đã là 1.750.000 và được đánh dấu dirty.
-- Log record đang ở log cache. Tệp .ndf trên E: hoặc G: vẫn là 1.500.000.
-- Giao dịch khác ở READ COMMITTED vẫn đọc thấy 1.500.000.
COMMIT;
-- Log record đã được ghi xuống L:\SqlLog\BanHang_log.ldf.
-- Client nhận thành công. Phiên khác đọc thấy 1.750.000 từ buffer pool.
-- Tệp .ndf có thể vẫn đang chứa 1.500.000 cho đến checkpoint hoặc lazy writer.
Ba cách mất điện ngay sau đó:
| Thời điểm mất điện | Trên đĩa dữ liệu | Trên đĩa log | Sau khi SQL Server mở lại |
|---|---|---|---|
Sau UPDATE, trước COMMIT, log chưa flush |
1.500.000 | Không có bản ghi hoàn chỉnh của lần sửa | Đơn vẫn là 1.500.000. Giao dịch không tồn tại |
Sau UPDATE, trước COMMIT, nhưng log buffer đã flush vì một commit khác |
1.500.000 hoặc đã là dirty page được ghi | Có log record của giao dịch chưa kết thúc | Undo trả TongTien về 1.500.000 |
Sau COMMIT, trước checkpoint |
Vẫn có thể là 1.500.000 | Có log record đã commit | Redo ghi 1.750.000 lên page. Khách không mất lần sửa |
Trường hợp thứ ba là lý do commit có thể trả về trước khi .ndf đổi. Độ bền nằm ở L:\SqlLog\BanHang_log.ldf, không nằm ở việc page dữ liệu đã xuống E: hay chưa.
3. Ai ghi page xuống đĩa, và recovery lúc khởi động
| Tiến trình | Việc làm |
|---|---|
| Log flush | Đẩy log cache xuống .ldf khi commit, khi checkpoint, hoặc khi buffer log đầy |
| Checkpoint | Quét dirty page của database và ghi xuống tệp dữ liệu, tạo một mốc mà recovery không phải làm lại từ quá khứ xa |
| Lazy writer | Khi buffer pool thiếu trang trống, ghi bớt dirty page và giải phóng bộ đệm |
| Eager writer | Ghi các trang mới của thao tác minimal logging, chẳng hạn bulk insert đủ điều kiện, song song với chính thao tác đó |
Checkpoint tự chạy theo mục tiêu thời gian recovery, theo lượng log phát sinh, và khi có thay đổi cấu trúc như thêm hoặc gỡ tệp. Người quản trị có thể gọi CHECKPOINT. Database tạo trên SQL Server 2016 trở đi dùng indirect checkpoint theo mặc định: engine dàn việc ghi dirty page để thời gian recovery gần với TARGET_RECOVERY_TIME.
Checkpoint ghi dirty page và ghi một mốc vào log. Việc phần log cũ có được tái sử dụng hay không thì phụ thuộc recovery model, nói ở mục 5.
Sau sự cố, recovery đi từ checkpoint gần nhất. Redo áp lại log record của thay đổi đã ghi log nhưng dirty page chưa kịp xuống đĩa. Undo hoàn tác giao dịch chưa commit. Thời gian recovery tỷ lệ với lượng log phải redo và undo, nên giữ log nhỏ bằng log backup đều và giữ mục tiêu recovery hợp lý trực tiếp rút ngắn lúc SQL Server mở lại database.
4. Tăng trưởng log và VLF
Tệp dữ liệu được nới nhanh nhờ instant file initialization. Tệp log thì luôn được điền số 0 khi nới, nên autogrowth của log làm giao dịch đang chờ ghi log phải dừng cho đến khi phần mới sẵn sàng. Nới log 1 GB ghi số 0 trên toàn bộ phần mới, và trên đĩa chậm có thể giữ commit trong nhiều giây.
Autogrowth nhỏ lặp lại nhiều lần còn cắt log thành rất nhiều VLF. Quá nhiều VLF làm chậm recovery lúc khởi động, chậm log backup, và chậm các thao tác quét log. Với thuật toán dùng từ SQL Server 2014 đến 2019, mỗi lần tạo hoặc nới log sinh số VLF theo kích thước lần nới đó:
| Kích thước lần tạo hoặc nới | Số VLF thêm vào |
|---|---|
| Đến 64 MB | 4 |
| Trên 64 MB đến 1 GB | 8 |
| Trên 1 GB | 16 |
Từ SQL Server 2022, nếu bước nới nhỏ hơn 1/8 dung lượng log hiện tại thì lần nới đó chỉ thêm 1 VLF.
Ví dụ xấu trên SQL Server 2019: tạo log 64 MB, FILEGROWTH = 64MB, rồi để autogrowth đưa log lên 64 GB. Có 1.024 lần cấp 64 MB, mỗi lần 4 VLF, tổng khoảng 4.096 VLF. Khởi động sau sự cố phải mở hàng nghìn VLF trước khi redo.
Nếu tạo log 8 GB ngay từ đầu, như cột mục tiêu của BanHang, lần tạo lớn hơn 1 GB nên ra khoảng 16 VLF, mỗi VLF khoảng 512 MB. FILEGROWTH = 1GB đúng bằng 1 GB nên mỗi lần nới, đến SQL Server 2019, thêm 8 VLF. Log lên 16 GB thì còn khoảng 80 VLF. Script tạo database của series để log 4 GB cho máy thử: vẫn lớn hơn 1 GB, nên cũng khoảng 16 VLF, mỗi VLF khoảng 256 MB.
Đếm VLF từ SQL Server 2017 (và SQL Server 2016 SP2). Phép đo ở mục 6 dùng đúng truy vấn này: log 64 MB ra 4 VLF, sau một lần autogrowth 64 MB ra 8 VLF, khớp bảng trên.
SELECT
COUNT(*) AS vlf_count,
SUM(vlf_size_mb) AS log_mb,
SUM(CASE WHEN vlf_active = 1 THEN 1 ELSE 0 END) AS active_vlf_count
FROM sys.dm_db_log_info(DB_ID(N'BanHang'));
Nếu vlf_count lên vài nghìn, tạo lại log ở kích thước đủ dùng trong cửa sổ bảo trì thay vì tiếp tục nới từng bước nhỏ.
5. Recovery model
Recovery model quyết định log được giữ đến khi nào và bản sao lưu nào có nghĩa.
SIMPLE |
FULL |
BULK_LOGGED |
|
|---|---|---|---|
| Log backup | Không dùng được | Bắt buộc nếu muốn giữ log và muốn phục hồi theo thời điểm | Bắt buộc |
| Cắt log | Tự cắt khi checkpoint | Khi log backup thành công | Khi log backup thành công |
| Mất dữ liệu sau sự cố | Mọi thay đổi sau full hoặc differential gần nhất | Về được một thời điểm trong chuỗi log nếu log còn | Có khoảng không phục hồi theo thời điểm nếu log backup chứa thao tác bulk-logged |
| Log shipping | Không dùng | Dùng được | Dùng được, với hạn chế quanh bulk log |
| Always On, database mirroring | Không dùng | Bắt buộc FULL |
Không dùng. Cần chuyển về FULL |
| Phiên bản mặc định | Express | Enterprise, Standard, và các bản đầy đủ khác | Không phải mặc định |
BULK_LOGGED là biến thể của FULL cho lúc nạp dữ liệu lớn. Thao tác bulk đủ điều kiện được ghi log tối thiểu, nên nhanh hơn và log nhỏ hơn, nhưng log backup phải kéo theo các extent đã đổi (đọc từ BCM) để vẫn restore được. Trong khoảng có bulk-logged change, không restore về một giây nằm giữa khoảng đó.
Đổi từ SIMPLE sang FULL hoặc BULK_LOGGED chưa mở chuỗi log. Hãy lấy một full backup ngay sau khi đổi. Bản full đó là mốc đầu, và log backup lấy sau mốc này mới tạo được chuỗi khôi phục theo thời điểm.
SELECT name, recovery_model_desc
FROM sys.databases
WHERE name = N'BanHang';
ALTER DATABASE BanHang SET RECOVERY FULL;
Với database giao dịch, model làm việc là FULL, kèm lịch log backup. SIMPLE phù hợp database có thể nạp lại, hoặc nơi chấp nhận mất phần phát sinh từ bản full hay differential gần nhất. Recovery model còn quyết định mất bao nhiêu đơn khi sự cố; bài sao lưu và khôi phục so ba cấu hình cho cùng sự cố lúc 14:00.
Log trong FULL tăng cho đến khi có log backup. Full backup không cắt log, differential backup cũng không. Kể cả khi log backup chạy đều, log vẫn có thể bị giữ. Nếu phiên giữ BEGIN TRAN từ 09:00 và không commit, các log backup sau 09:00 vẫn chạy nhưng, khi ADR tắt, không giải phóng được VLF cũ hơn giao dịch đó. Log đầy dần dù lịch backup đều. DBCC OPENTRAN('BanHang') cho thấy giao dịch cũ nhất và thời điểm nó bắt đầu. ADR, mặc định tắt trên bản tự cài, đổi cách cắt log này; Kỹ thuật thường dùng: an toàn và sẵn sàng nói riêng phần đó.
6. Áp dụng trong .NET
Giao dịch mở lâu thường không đến từ DBA mà từ code ứng dụng: mở SqlTransaction hoặc TransactionScope, ghi đơn, rồi gọi cổng thanh toán đối tác qua HTTP trong lúc giao dịch còn mở. Cổng chậm 30 giây thì giao dịch, cùng mọi phần log từ lúc nó bắt đầu, bị giữ 30 giây. Cách đúng là ghi đơn ở trạng thái chờ thanh toán, COMMIT, rồi mới gọi cổng, và cập nhật kết quả bằng một giao dịch ngắn thứ hai hoặc qua webhook.
Phía vận hành, một BackgroundService trong API đọc lý do log chưa được tái sử dụng, phần trăm log đã dùng, số VLF và tuổi giao dịch ghi cũ nhất:
SELECT d.log_reuse_wait_desc, u.used_log_space_in_percent, u.total_log_size_in_bytes / 1048576,
(SELECT COUNT(*) FROM sys.dm_db_log_info(DB_ID())),
(SELECT DATEDIFF(SECOND, MIN(t.database_transaction_begin_time), SYSDATETIME())
FROM sys.dm_tran_database_transactions AS t
WHERE t.database_id = DB_ID() AND t.database_transaction_log_bytes_used > 0)
FROM sys.databases AS d CROSS JOIN sys.dm_db_log_space_usage AS u
WHERE d.database_id = DB_ID();
protected override async Task ExecuteAsync(CancellationToken stop)
{
var daCanhBao = false;
using var timer = new PeriodicTimer(chuKy);
while (await timer.WaitForNextTickAsync(stop))
{
await using var cn = new SqlConnection(connectionString);
await cn.OpenAsync(stop);
await using var r = await new SqlCommand(Sql, cn).ExecuteReaderAsync(stop);
await r.ReadAsync(stop);
var s = new TrangThaiLog(r.GetString(0), r.GetFloat(1), r.GetInt64(2), r.GetInt32(3),
r.IsDBNull(4) ? null : r.GetInt32(4));
MoiNhat = s.ToString();
if (s.LyDo == "ACTIVE_TRANSACTION" && s.TuoiGiaoDichCuGiay > nguong.TotalSeconds && !daCanhBao)
{
log.LogWarning("Log bị giữ bởi giao dịch mở {Giay} s, log đã dùng {PhanTram:F0}%",
s.TuoiGiaoDichCuGiay, s.PhanTramDung);
daCanhBao = true;
}
if (s.TuoiGiaoDichCuGiay is null) daCanhBao = false;
}
}
Tài khoản chạy truy vấn cần quyền VIEW SERVER STATE để đọc sys.dm_tran_database_transactions. Kịch bản thử trên LocalDB SQL Server 2019 tái hiện giao dịch mở từ 09:00 ở thu nhỏ: database FULL, log 64 MB nới 64 MB, phiên A sửa đơn 10042 rồi để giao dịch mở, phiên B ghi ba đợt 200.000 đơn, mỗi đợt có BACKUP LOG. Worker đọc mỗi giây:
| Bước | log_reuse_wait_desc |
Log | Đã dùng | VLF |
|---|---|---|---|---|
| Sau full backup | NOTHING |
63 MB | 1% | 4 |
Phiên A mở giao dịch, chưa COMMIT |
NOTHING |
63 MB | 1% | 4 |
Đợt 1: ghi đơn, BACKUP LOG |
ACTIVE_TRANSACTION |
63 MB | 42% | 4 |
| Đợt 2 | ACTIVE_TRANSACTION |
127 MB | 43% | 8 |
| Đợt 3 | ACTIVE_TRANSACTION |
127 MB | 64% | 8 |
Phiên A COMMIT, rồi BACKUP LOG |
NOTHING |
127 MB | 2% | 8 |
Log backup chạy sau mỗi đợt mà log vẫn nới từ 64 MB lên 128 MB (cột Log in phần nguyên của số MB), vì giao dịch của phiên A giữ mọi VLF từ lúc nó bắt đầu. Một lần COMMIT cộng một log backup đưa phần đã dùng về 2%. log_reuse_wait_desc phản ánh lần checkpoint gần nhất, nên ngay sau BEGIN TRAN nó vẫn là NOTHING; kịch bản gọi CHECKPOINT sau mỗi log backup để đọc ổn định. Đo bằng .NET 10.0.12, Microsoft.Data.SqlClient 7.1.1, Intel Core Ultra 5 125U, Windows 11. Tuổi giao dịch trong output đổi theo từng lần chạy.
GiamSatNhatKy.cs — bản đầy đủ, chạy bằng dotnet run GiamSatNhatKy.cs
#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.Data.SqlClient@7.1.1
// dotnet run GiamSatNhatKy.cs (.NET 10, LocalDB SQL Server 2019)
// Kumeo_luutru ở FULL, log 64 MB nới 64 MB. Phiên A mở giao dịch rồi để đó, phiên B ghi đơn đều,
// log backup vẫn chạy. BackgroundService đọc log_reuse_wait_desc, % log đã dùng, số VLF, tuổi giao dịch cũ nhất.
using Microsoft.Data.SqlClient;
const string Master = @"Server=(localdb)\MSSQLLocalDB;Database=master;Integrated Security=true;TrustServerCertificate=true";
const string Db = @"Server=(localdb)\MSSQLLocalDB;Database=Kumeo_luutru;Integrated Security=true;TrustServerCertificate=true";
var dir = Path.Combine(Path.GetTempPath(), "kumeo-luutru");
Directory.CreateDirectory(dir);
await Exec(Master, "IF DB_ID('Kumeo_luutru') IS NOT NULL BEGIN ALTER DATABASE Kumeo_luutru SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE Kumeo_luutru; END");
await Exec(Master, $"""
CREATE DATABASE Kumeo_luutru
ON PRIMARY (NAME = N'BanHang_data', FILENAME = N'{dir}\BanHang.mdf', SIZE = 256MB)
LOG ON (NAME = N'BanHang_log', FILENAME = N'{dir}\BanHang_log.ldf', SIZE = 64MB, FILEGROWTH = 64MB);
ALTER DATABASE Kumeo_luutru SET RECOVERY FULL;
""");
await Exec(Db, """
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));
INSERT dbo.DonHang VALUES (10042, '2026-10-02T11:58:00', 1, 1, 1500000.00);
""");
await Exec(Master, $@"BACKUP DATABASE Kumeo_luutru TO DISK = N'{dir}\full.bak' WITH INIT;"); // mở chuỗi log
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddSimpleConsole(o => o.SingleLine = true).SetMinimumLevel(LogLevel.Warning);
builder.Services.AddHostedService(sp => new GiamSatNhatKy(Db, chuKy: TimeSpan.FromSeconds(1), nguong: TimeSpan.FromSeconds(5),
sp.GetRequiredService<ILogger<GiamSatNhatKy>>()));
var app = builder.Build();
await app.StartAsync();
await InTrangThai("Sau full backup");
await using var phienA = new SqlConnection(Db);
await phienA.OpenAsync();
var tx = phienA.BeginTransaction();
await new SqlCommand("UPDATE dbo.DonHang SET TongTien = 1750000.00 WHERE DonHangId = 10042", phienA, tx).ExecuteNonQueryAsync();
await InTrangThai("Phiên A: BEGIN TRAN, sửa đơn 10042, chưa COMMIT");
for (var dot = 1; dot <= 3; dot++)
{
await Exec(Db, $"""
INSERT dbo.DonHang (DonHangId, NgayTao, KhachHangId, TrangThai, TongTien)
SELECT TOP (200000) {dot} * 1000000 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
'2026-10-02T12:00:00', 1, 1, 990000.00
FROM sys.all_columns AS a CROSS JOIN sys.all_columns AS b;
""");
await Exec(Master, $@"BACKUP LOG Kumeo_luutru TO DISK = N'{dir}\log_{dot}.trn' WITH INIT;");
await Exec(Db, "CHECKPOINT;"); // log_reuse_wait_desc phản ánh lần checkpoint gần nhất
await InTrangThai($"Phiên B: ghi 200.000 đơn rồi BACKUP LOG, đợt {dot}");
}
await tx.CommitAsync();
await Exec(Master, $@"BACKUP LOG Kumeo_luutru TO DISK = N'{dir}\log_4.trn' WITH INIT;");
await InTrangThai("Phiên A: COMMIT, rồi BACKUP LOG");
await app.StopAsync();
await phienA.CloseAsync();
SqlConnection.ClearAllPools();
await Exec(Master, "ALTER DATABASE Kumeo_luutru SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE Kumeo_luutru;");
await Exec(Master, "EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'Kumeo_luutru';");
foreach (var f in Directory.GetFiles(dir, "*.bak").Concat(Directory.GetFiles(dir, "*.trn"))) File.Delete(f);
static async Task InTrangThai(string buoc)
{
await Task.Delay(2500); // chờ worker đọc ít nhất một lần
Console.WriteLine($"{buoc}\n {GiamSatNhatKy.MoiNhat}");
}
static async Task Exec(string cs, string sql)
{
await using var cn = new SqlConnection(cs);
await cn.OpenAsync();
await new SqlCommand(sql, cn) { CommandTimeout = 300 }.ExecuteNonQueryAsync();
}
record TrangThaiLog(string LyDo, double PhanTramDung, long LogMb, int SoVlf, int? TuoiGiaoDichCuGiay)
{
public override string ToString() =>
$"{LyDo}, log {LogMb} MB dùng {PhanTramDung:F0}%, {SoVlf} VLF, giao dịch ghi cũ nhất: " +
(TuoiGiaoDichCuGiay is int s ? $"{s} s" : "không có");
}
// ---------- BackgroundService: vì sao log chưa được tái sử dụng ----------
sealed class GiamSatNhatKy(string connectionString, TimeSpan chuKy, TimeSpan nguong, ILogger<GiamSatNhatKy> log)
: BackgroundService
{
public static volatile string MoiNhat = "";
const string Sql = """
SELECT d.log_reuse_wait_desc, u.used_log_space_in_percent, u.total_log_size_in_bytes / 1048576,
(SELECT COUNT(*) FROM sys.dm_db_log_info(DB_ID())),
(SELECT DATEDIFF(SECOND, MIN(t.database_transaction_begin_time), SYSDATETIME())
FROM sys.dm_tran_database_transactions AS t
WHERE t.database_id = DB_ID() AND t.database_transaction_log_bytes_used > 0)
FROM sys.databases AS d CROSS JOIN sys.dm_db_log_space_usage AS u
WHERE d.database_id = DB_ID();
""";
protected override async Task ExecuteAsync(CancellationToken stop)
{
var daCanhBao = false;
using var timer = new PeriodicTimer(chuKy);
while (await timer.WaitForNextTickAsync(stop))
{
await using var cn = new SqlConnection(connectionString);
await cn.OpenAsync(stop);
await using var r = await new SqlCommand(Sql, cn).ExecuteReaderAsync(stop);
await r.ReadAsync(stop);
var s = new TrangThaiLog(r.GetString(0), r.GetFloat(1), r.GetInt64(2), r.GetInt32(3),
r.IsDBNull(4) ? null : r.GetInt32(4));
MoiNhat = s.ToString();
if (s.LyDo == "ACTIVE_TRANSACTION" && s.TuoiGiaoDichCuGiay > nguong.TotalSeconds && !daCanhBao)
{
log.LogWarning("Log bị giữ bởi giao dịch mở {Giay} s, log đã dùng {PhanTram:F0}%",
s.TuoiGiaoDichCuGiay, s.PhanTramDung);
daCanhBao = true;
}
if (s.TuoiGiaoDichCuGiay is null) daCanhBao = false;
}
}
}
Output của GiamSatNhatKy.cs
Sau full backup
NOTHING, log 63 MB dùng 1%, 4 VLF, giao dịch ghi cũ nhất: không có
Phiên A: BEGIN TRAN, sửa đơn 10042, chưa COMMIT
NOTHING, log 63 MB dùng 1%, 4 VLF, giao dịch ghi cũ nhất: 3 s
warn: GiamSatNhatKy[0] Log bị giữ bởi giao dịch mở 6 s, log đã dùng 42%
Phiên B: ghi 200.000 đơn rồi BACKUP LOG, đợt 1
ACTIVE_TRANSACTION, log 63 MB dùng 42%, 4 VLF, giao dịch ghi cũ nhất: 8 s
Phiên B: ghi 200.000 đơn rồi BACKUP LOG, đợt 2
ACTIVE_TRANSACTION, log 127 MB dùng 43%, 8 VLF, giao dịch ghi cũ nhất: 13 s
Phiên B: ghi 200.000 đơn rồi BACKUP LOG, đợt 3
ACTIVE_TRANSACTION, log 127 MB dùng 64%, 8 VLF, giao dịch ghi cũ nhất: 20 s
Phiên A: COMMIT, rồi BACKUP LOG
NOTHING, log 127 MB dùng 2%, 8 VLF, giao dịch ghi cũ nhất: không có
Những chỗ hay hiểu sai
- “
COMMITxong là page dữ liệu đã nằm trên đĩa.” Sai:COMMITchỉ chờ log xuống.ldf; page được checkpoint hoặc lazy writer ghi sau. - “Full backup cắt log.” Sai: trong
FULL, full và differential không cắt log; chỉ log backup cắt. - “Lịch log backup đều thì log không thể đầy.” Sai: khi ADR tắt, một giao dịch mở lâu giữ mọi VLF từ lúc nó bắt đầu, như phép đo ở mục 6.
- “Đổi sang
FULLlà có ngay phục hồi theo thời điểm.” Sai: cần một full backup ngay sau khi đổi để mở chuỗi log.
Kết luận
Độ bền của một lần sửa nằm ở log, và log chỉ được tái sử dụng khi đã sao lưu và không còn giao dịch nào cần nó. Code .NET quyết định phần sau nhiều hơn người ta nghĩ.
Trong dự án .NET của bạn:
- Không giữ
SqlTransactionhayTransactionScopequa lời gọi HTTP, hàng đợi, hay bước chờ người dùng;COMMITtrước rồi mới gọi cổng thanh toán. - Chạy một
BackgroundServiceđọclog_reuse_wait_desc,sys.dm_db_log_space_usagevà tuổi giao dịch ghi cũ nhất mỗi phút, cảnh báo khiACTIVE_TRANSACTIONkéo dài quá vài phút; cấpVIEW SERVER STATEcho tài khoản giám sát. - Đưa truy vấn đếm VLF vào kiểm tra sau triển khai, và tạo log đủ lớn ngay khi tạo database thay vì để autogrowth 64 MB.
- Database
FULLphải có log backup; health check tuổi log backup nằm ở bài sau.
Đọc tiếp
- Page, dòng và extent: bài trước trong series, page mà log record ở đây mô tả thay đổi.
- Sao lưu và khôi phục theo thời điểm: bài tiếp theo, log backup, tail-log và chuỗi restore cho sự cố 12:07.
- Blocking, deadlock và thử lại giao dịch: giao dịch mở từ 09:00 nhìn từ phía khóa và blocking.
- Kỹ thuật thường dùng: an toàn và sẵn sàng: ADR và delayed durability, hai thứ đổi cách log được giữ và được chờ.