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

Mức isolation và SNAPSHOT

Mức isolation nào chặn dirty read, non-repeatable read, phantom và write skew trên BanHang, SNAPSHOT khác RCSI ở đâu, và cách đặt mức isolation đúng từ EF Core và TransactionScope.

Mục lục
  1. 1. Mức isolation và các hiện tượng đồng thời
  2. 2. SNAPSHOT
  3. 3. Áp dụng trong .NET
  4. Những chỗ hay hiểu sai
  5. Kết luận
  6. Đọc tiếp
  7. Nguồn

Báo cáo cuối ngày của BanHang đọc tổng tiền rồi đọc danh sách đơn, và hai con số lệch nhau vì một đơn commit đúng lúc giữa hai câu. Chọn sai mức isolation thì báo cáo in số chưa từng commit, đếm sót đơn mới, hoặc để hai phiên cùng phá một quy tắc nghiệp vụ. Đọc xong, bạn biết mức nào chặn hiện tượng nào, khi nào cần SNAPSHOT, và đặt mức isolation đúng từ .NET.

Đọc nhanh

  • Mức isolation chỉ đổi cách câu đọc lấy khóa hoặc lấy phiên bản; RCSI không phải mức riêng mà đổi cách READ COMMITTED đọc.
  • NOLOCK đọc số chưa commit và có thể đọc trùng hoặc sót dòng, kể cả khi RCSI đang bật.
  • SERIALIZABLE chặn phantom bằng khóa phạm vi trên chỉ mục câu đọc dùng; write skew lọt qua mọi mức thấp hơn, kể cả SNAPSHOT.
  • SNAPSHOT nhất quán theo cả giao dịch và báo lỗi 3960 khi dòng đã bị sửa, còn RCSI chỉ nhất quán theo từng câu.

Bài thứ ba trong năm bài về giao dịch, cùng mốc với bài Giao dịch và XACT_ABORT: SQL Server 2019, READ_COMMITTED_SNAPSHOT và ADR tắt. Các chế độ khóa S, U, X ở bài Khóa và leo thang khóa.

1. Mức isolation và các hiện tượng đồng thời

Mức isolation đặt cho từng phiên bằng SET TRANSACTION ISOLATION LEVEL, mặc định READ COMMITTED. RCSI không phải mức riêng: nó là tùy chọn database đổi cách READ COMMITTED đọc. SNAPSHOT cần bật ALLOW_SNAPSHOT_ISOLATION trước (mục 2). Mức đặt trong thủ tục trở về mức cũ khi thủ tục kết thúc. Với connection pool, kết nối trả về pool giữ mức đặt lần cuối và mang sang lần dùng sau (mục 3 đo điều này), nên đặt mức ngay trong thủ tục hoặc ngay sau khi lấy kết nối.

  • Dirty read: đọc giá trị chưa commit.
  • Non-repeatable read: đọc lại cùng dòng trong một giao dịch, thấy giá trị khác.
  • Phantom: chạy lại cùng điều kiện, thấy tập dòng khác vì có dòng mới chèn.
  • Lost update: hai giao dịch đọc cùng giá trị, mỗi bên tính rồi ghi đè, một lần ghi biến mất.
  • Write skew: hai giao dịch đọc cùng tập dữ liệu, ghi vào hai chỗ khác nhau. Từng bên đúng quy tắc, gộp lại thì sai.
Mức Câu đọc dùng Dirty read Non-repeatable read Phantom Lost update Write skew
READ UNCOMMITTED Không khóa S Có Có Có Có Có
READ COMMITTED, có khóa S nhả sau từng dòng Không Có Có Có Có
READ COMMITTED SNAPSHOT Bản commit tại đầu câu lệnh Không Có Có Có Có
REPEATABLE READ S giữ đến hết giao dịch Không Không Có Không, thành deadlock 1205 Có
SERIALIZABLE S và khóa phạm vi đến hết giao dịch Không Không Không Không, thành deadlock 1205 Không
SNAPSHOT Bản commit tại câu đầu tiên của giao dịch Không Không Không Không, lỗi 3960 Có

Cột lost update tính cho kiểu đọc ở một câu rồi ghi ở câu sau; timeline và cách sửa nằm ở bài Mất cập nhật và upsert. Cột write skew theo ví dụ ở dưới, nơi hai phiên chèn dòng mới. Nếu hai phiên chỉ sửa các dòng cả hai đã đọc, REPEATABLE READ giữ S nên kết cục là chờ hoặc deadlock, không phải write skew.

Dirty read

Đúng giao dịch lúc 12:04 của bộ bài, nhìn từ một báo cáo dùng NOLOCK:

Thời điểm Phiên A — ứng dụng Phiên B — báo cáo, WITH (NOLOCK)
12:04:00 BEGIN TRAN. UPDATE đơn 10042: TongTien 1.500.000 thành 1.750.000
12:04:01 SELECT TongTien đơn 10042: 1.750.000
12:04:02 COMMIT

Lần này A commit, nên con số B đọc trùng kết quả cuối. Nếu bước sau của A lỗi và A rollback, báo cáo đã in 1.750.000, giá trị chưa từng tồn tại ở trạng thái commit. NOLOCK còn có thể đọc một dòng hai lần hoặc bỏ sót dòng khi page đang tách, và có thể gặp lỗi 601.

Bỏ NOLOCK, cùng câu đọc ở READ COMMITTED có khóa: B chờ LCK_M_S từ 12:04:01 đến 12:04:02, rồi đọc 1.750.000 đã commit. Ở RCSI: B không chờ, đọc 1.500.000, bản commit tại lúc câu lệnh bắt đầu. Gợi ý NOLOCK thắng cả RCSI: có nó, câu đọc vẫn là dirty read.

Non-repeatable read

Thời điểm Phiên A — đối soát, READ COMMITTED Phiên B — ứng dụng
12:03:58 BEGIN TRAN. SELECT TongTien đơn 10042: 1.500.000
12:04:00 UPDATE thành 1.750.000, commit
12:04:10 SELECT TongTien đơn 10042: 1.750.000
12:04:12 COMMIT

Khóa S của câu đọc đầu nhả ngay sau khi đọc, nên B không phải chờ. RCSI cho cùng kết quả: câu thứ hai bắt đầu sau 12:04:00 nên thấy bản mới. Ở REPEATABLE READ, A giữ S đến 12:04:12, UPDATE của B chờ đến lúc đó, và A đọc 1.500.000 cả hai lần. Ở SNAPSHOT, B không chờ, A vẫn đọc 1.500.000 hai lần.

Phantom

Khách 42 có khoảng 300 đơn mỗi ngày. Lúc 12:10 có 3 đơn ngày 2026-10-02 đang ở TrangThai = 1.

Thời điểm Phiên A — đối soát, REPEATABLE READ Phiên B — ứng dụng
12:10:00 BEGIN TRAN. Đếm đơn khách 42, TrangThai = 1, ngày 2026-10-02: 3
12:10:05 INSERT đơn 10047, khách 42, TrangThai = 1: thành công ngay
12:10:08 UPDATE TongTien của một trong 3 đơn A đã đọc: chờ S của A
12:10:10 Đếm lại: 4
12:10:12 COMMIT. UPDATE của B chạy tiếp
SELECT COUNT(*) AS so_don_mo
FROM dbo.DonHang
WHERE KhachHangId = 42 AND TrangThai = 1
  AND NgayTao >= CAST('20261002' AS datetime2(0))
  AND NgayTao < CAST('20261003' AS datetime2(0));

REPEATABLE READ khóa ba dòng đã đọc, không khóa khoảng trống giữa chúng. Ở SERIALIZABLE, câu đếm seek trên một chỉ mục có KhachHangId đứng đầu (IX_DonHang_DangMo hoặc IX_DonHang_KhachHang) và đặt khóa phạm vi trên đoạn khóa (42, 2026-10-02) đến (42, 2026-10-03) của chỉ mục đó. Đơn 10047 phải chèn một khóa vào đúng đoạn này ở cả hai chỉ mục, nên INSERT chờ đến 12:10:12, và A đếm 3 cả hai lần.

Khóa phạm vi bám chỉ mục mà câu đọc dùng. Không có chỉ mục phù hợp, câu đọc quét clustered index và khóa phạm vi rộng tương ứng. Cách chỉ mục quyết định seek hay scan nằm ở Chỉ mục B-tree, seek, scan và key lookup.

Write skew

Quy tắc công nợ: đại lý 42 không được có quá 5 đơn ở TrangThai = 1. Lúc 12:20 khách đang có 4 đơn như vậy: 3 đơn mở từ sáng và đơn 10047. Ứng dụng đếm trước rồi mới chèn, cả hai phiên ở READ COMMITTED.

sequenceDiagram
  participant A as Phiên A
  participant D as Đơn mở của khách 42
  participant B as Phiên B
  A->>D: 12:20:00 BEGIN TRAN, đếm được 4
  B->>D: 12:20:01 BEGIN TRAN, đếm được 4
  A->>D: 12:20:02 4 nhỏ hơn 5, INSERT đơn 10048
  B->>D: 12:20:03 4 nhỏ hơn 5, INSERT đơn 10049
  A->>D: 12:20:04 COMMIT
  B->>D: 12:20:05 COMMIT
  Note over A,B: Khách 42 có 6 đơn mở, vượt hạn mức 5

Hai phiên ghi hai dòng khác nhau. Không khóa nào xung đột, nên RCSI và REPEATABLE READ cho cùng kết cục. SNAPSHOT cũng để lọt: không có dòng nào bị cả hai cùng sửa, nên không có xung đột để báo 3960.

Ở SERIALIZABLE, cả hai câu đếm giữ RangeS-S trên đoạn khóa của khách 42. Mỗi INSERT cần RangeI-N trong đoạn đó và bị khóa của bên kia chặn: deadlock, một phiên nhận 1205. Phiên thử lại đếm được 5 và từ chối. Cách gọn hơn là đếm với WITH (UPDLOCK, HOLDLOCK): khóa phạm vi chế độ U không tương thích với nhau, nên B chờ ngay ở câu đếm và đếm được 5 sau khi A commit, không qua deadlock.

2. SNAPSHOT

ALTER DATABASE BanHang SET ALLOW_SNAPSHOT_ISOLATION ON;

SELECT name, snapshot_isolation_state_desc, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = N'BanHang';

Lệnh này không đòi ngắt các kết nối khác như RCSI, nhưng chỉ trả về khi mọi giao dịch đang mở trong BanHang đã kết thúc. Từ lúc bật, mọi lệnh sửa dữ liệu sinh phiên bản dòng, kể cả khi chưa phiên nào dùng SNAPSHOT. Ứng dụng muốn dùng thì phải chủ động SET TRANSACTION ISOLATION LEVEL SNAPSHOT.

Xung đột cập nhật

Lúc 15:30, sau đơn thành công lúc 15:00, sản phẩm 42 còn 2.

Thời điểm Phiên A — SNAPSHOT Phiên B — READ COMMITTED
15:30:00 BEGIN TRAN. SELECT TonKho: 2. Ảnh chụp bắt đầu ở câu này
15:30:02 UPDATE ... SET TonKho = TonKho - 1 WHERE ... TonKho >= 1: còn 1, commit
15:30:04 SELECT TonKho: vẫn 2
15:30:05 UPDATE ... SET TonKho = TonKho - 1 WHERE ... TonKho >= 1: Msg 3960, giao dịch bị rollback
Msg 3960, Level 16, State 2
Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'dbo.SanPham' directly or indirectly in database 'BanHang' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.

Giao dịch SNAPSHOT sửa một dòng đã bị giao dịch khác sửa và commit sau khi ảnh chụp của nó bắt đầu thì bị hủy. Cách xử lý giống deadlock: thử lại cả giao dịch (vòng thử lại). Lần thử lại có ảnh chụp mới, thấy 1, trừ còn 0. Giao dịch bắt đầu từ câu đầu tiên đọc dữ liệu, không phải từ BEGIN TRAN.

Khác RCSI ở đâu

RCSI SNAPSHOT
Bật bằng READ_COMMITTED_SNAPSHOT ON, không được còn kết nối nào khác ALLOW_SNAPSHOT_ISOLATION ON, chờ các giao dịch đang mở
Ứng dụng phải đổi Không. READ COMMITTED đổi cách đọc Có. Đặt mức SNAPSHOT cho phiên
Nhất quán trong phạm vi Từng câu lệnh Cả giao dịch
Câu sửa gặp dòng đã bị sửa Chờ khóa, rồi sửa trên bản commit mới nhất Lỗi 3960, giao dịch rollback

Báo cáo cuối ngày chạy hai câu trong một giao dịch: tổng TongTien ngày 2026-10-02, rồi danh sách đơn. Dưới RCSI, một đơn commit giữa hai câu có trong danh sách mà không có trong tổng. Dưới SNAPSHOT, hai câu thấy cùng một ảnh chụp. Mục 3 chạy đúng báo cáo này.

Phiên bản nằm ở version store trong tempdb khi ADR tắt. Khi ADR bật, chúng nằm trong persisted version store của chính BanHang, mặc định trên filegroup PRIMARY nếu lúc bật ADR không chỉ định PERSISTENT_VERSION_STORE_FILEGROUP (đọc không chặn ghi, ADR). Mỗi dòng được sửa hoặc chèn sau khi bật RCSI hay SNAPSHOT mang thêm tối đa 14 byte thông tin phiên bản. Dòng DonHang 35 byte của chương lưu trữ thành tối đa 49 byte, cộng 2 byte slot là 51 byte, nên page lá toàn những dòng như vậy chứa 8.096 / 51 = 158 dòng thay vì 218. Page đầy có thể tách khi các dòng cũ lần lượt bị sửa.

Mang thông tin phiên bản, một page lá của DonHang chứa 158 dòng thay vì 218

Không phiên bản, 37 byte mỗi dòng218 dòngCó phiên bản, 51 byte mỗi dòng158 dòng
Số tính, không phải số đo: 8.096 byte dữ liệu mỗi page chia cho kích thước dòng cộng 2 byte slot. Dòng 35 byte, tối đa 49 byte khi mang thông tin phiên bản.
Bảng số liệu
Giá trị
Không phiên bản, 37 byte mỗi dòng218 dòng
Có phiên bản, 51 byte mỗi dòng158 dòng

Giao dịch SNAPSHOT mở lâu giữ version store không dọn được. sys.dm_tran_active_snapshot_database_transactions sắp theo elapsed_time_seconds giảm dần chỉ ra giao dịch đó và session_id của nó.

3. Áp dụng trong .NET

EF Core đặt mức isolation qua BeginTransactionAsync(IsolationLevel), chuyển xuống SqlConnection.BeginTransaction(IsolationLevel) của SqlClient. Báo cáo cuối ngày viết bằng EF Core, hai câu trong một giao dịch SNAPSHOT:

await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.Snapshot, ct);
var homNay = db.DonHang.Where(d => d.NgayTao >= ngay && d.NgayTao < ngay.AddDays(1));
decimal tong = await homNay.SumAsync(d => d.TongTien, ct);           // ảnh chụp bắt đầu ở đây
var danhSach = await homNay.Select(d => d.TongTien).ToListAsync(ct); // cùng ảnh chụp
await tx.CommitAsync(ct);

MucIsolation.cs chạy báo cáo này trên LocalDB (SQL Server 2019, 15.0.4382), .NET 10.0.401, EF Core 10.0.12, một lần ở READ COMMITTED với RCSI bật và một lần ở SNAPSHOT. Giữa hai câu, một phiên khác chèn và commit đơn 10061 giá 300.000:

Mức Câu 1: tổng Câu 2: danh sách
RCSI 2.650.000 3 đơn, cộng lại 2.950.000
SNAPSHOT 2.650.000 2 đơn, cộng lại 2.650.000

TransactionScope là bẫy thứ hai. new TransactionScope() không tham số mở giao dịch Serializable, và cùng chương trình cho thấy mức đó đi theo kết nối về pool:

TransactionScope mặc định: transaction_isolation_level = 4
  kết nối lấy lại từ pool sau scope: 4
TransactionScope với ReadCommitted: 2

Mức 4 là SERIALIZABLE, mức 2 là READ COMMITTED. Một request sau đó lấy đúng kết nối này sẽ chạy ở SERIALIZABLE và lấy khóa phạm vi mà không ai trong code yêu cầu. Luôn truyền mức isolation:

var docDaCommit = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted };
using var scope = new TransactionScope(TransactionScopeOption.Required, docDaCommit,
    TransactionScopeAsyncFlowOption.Enabled);

Chương trình bật RCSI bằng ALTER DATABASE CURRENT ... WITH ROLLBACK IMMEDIATE rồi tắt lại, nên chỉ chạy trên database thử. Dòng #:property PublishAot=false cần vì file-based app của .NET 10 bật Native AOT mặc định, còn EF Core dựng model lúc chạy.

MucIsolation.cs, chạy bằng dotnet run MucIsolation.csC# · 86 dòng
#:package Microsoft.EntityFrameworkCore.SqlServer@10.0.12
#:property PublishAot=false
// Báo cáo hai câu dưới RCSI và dưới SNAPSHOT; mức isolation của TransactionScope.
// Chạy: dotnet run MucIsolation.cs   (BANHANG_DB: database thử có dbo.DonHang; chương trình bật rồi tắt RCSI)
using System.Data;
using System.Transactions;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

string cs = Environment.GetEnvironmentVariable("BANHANG_DB")
    ?? @"Server=(localdb)\MSSQLLocalDB;Database=BanHang_Thu;Integrated Security=true;TrustServerCertificate=true";
System.Globalization.CultureInfo.CurrentCulture = new("vi-VN");

await QuanTri("ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE");
await QuanTri("ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON");
await QuanTri("UPDATE dbo.DonHang SET TongTien = 1750000.00 WHERE DonHangId = 10042 AND NgayTao = '2026-10-02T11:58:00'");

// 1. Báo cáo cuối ngày 2026-10-02: câu 1 lấy tổng, câu 2 lấy danh sách. Giữa hai câu, đơn 10061 commit.
foreach (var muc in new[] { System.Data.IsolationLevel.ReadCommitted, System.Data.IsolationLevel.Snapshot })
{
    await QuanTri("DELETE dbo.DonHang WHERE DonHangId = 10061");
    await using var db = new BanHangDb(cs);
    await using var tx = await db.Database.BeginTransactionAsync(muc);
    decimal tong = await db.DonHang.Where(d => d.NgayTao >= new DateTime(2026, 10, 2) && d.NgayTao < new DateTime(2026, 10, 3))
                                   .SumAsync(d => d.TongTien);
    await QuanTri("INSERT INTO dbo.DonHang (DonHangId, NgayTao, KhachHangId, TrangThai, TongTien) VALUES (10061, '2026-10-02T20:00:00', 42, 2, 300000.00)");
    var ds = await db.DonHang.Where(d => d.NgayTao >= new DateTime(2026, 10, 2) && d.NgayTao < new DateTime(2026, 10, 3))
                             .Select(d => d.TongTien).ToListAsync();
    await tx.CommitAsync();
    Console.WriteLine($"{(muc == System.Data.IsolationLevel.Snapshot ? "SNAPSHOT" : "RCSI    ")}: câu 1 tổng {tong:N0}; câu 2 có {ds.Count} đơn, cộng lại {ds.Sum():N0}");
}
await QuanTri("DELETE dbo.DonHang WHERE DonHangId = 10061");

// 2. TransactionScope
const string Muc = "SELECT transaction_isolation_level FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
string api = cs + ";Application Name=BanHang.Api";
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    await using var c = new SqlConnection(api);
    await c.OpenAsync();
    Console.WriteLine($"TransactionScope mặc định: transaction_isolation_level = {await new SqlCommand(Muc, c).ExecuteScalarAsync()}");
    scope.Complete();
}
await using (var c = new SqlConnection(api))
{
    await c.OpenAsync();
    Console.WriteLine($"  kết nối lấy lại từ pool sau scope: {await new SqlCommand(Muc, c).ExecuteScalarAsync()}");
}
var docDaCommit = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted };
using (var scope = new TransactionScope(TransactionScopeOption.Required, docDaCommit, TransactionScopeAsyncFlowOption.Enabled))
{
    await using var c = new SqlConnection(api);
    await c.OpenAsync();
    Console.WriteLine($"TransactionScope với ReadCommitted: {await new SqlCommand(Muc, c).ExecuteScalarAsync()}");
    scope.Complete();
}

SqlConnection.ClearAllPools();
await QuanTri("ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT OFF WITH ROLLBACK IMMEDIATE");

async Task QuanTri(string sql)
{
    await using var c = new SqlConnection(cs + ";Pooling=false");
    await c.OpenAsync();
    await new SqlCommand(sql, c).ExecuteNonQueryAsync();
}

public class DonHang
{
    public long DonHangId { get; set; }
    public DateTime NgayTao { get; set; }
    public decimal TongTien { get; set; }
}

public class BanHangDb(string cs) : DbContext
{
    public DbSet<DonHang> DonHang => Set<DonHang>();
    protected override void OnConfiguring(DbContextOptionsBuilder o) => o.UseSqlServer(cs);
    protected override void OnModelCreating(ModelBuilder m) => m.Entity<DonHang>(e =>
    {
        e.ToTable("DonHang", "dbo");
        e.HasKey(d => new { d.NgayTao, d.DonHangId });
        e.Property(d => d.NgayTao).HasColumnType("datetime2(0)");
        e.Property(d => d.TongTien).HasColumnType("decimal(18, 2)");
    });
}
Output của MucIsolation.cs trên LocalDB5 dòng
RCSI    : câu 1 tổng 2.650.000; câu 2 có 3 đơn, cộng lại 2.950.000
SNAPSHOT: câu 1 tổng 2.650.000; câu 2 có 2 đơn, cộng lại 2.650.000
TransactionScope mặc định: transaction_isolation_level = 4
  kết nối lấy lại từ pool sau scope: 4
TransactionScope với ReadCommitted: 2

Những chỗ hay hiểu sai

Hiểu sai Đúng là
NOLOCK chỉ đọc thêm dữ liệu chưa commit Còn đọc trùng hoặc sót dòng khi page tách, và có thể lỗi 601
SERIALIZABLE khóa cả bảng Khóa phạm vi trên chỉ mục câu đọc dùng. Rộng đến cả bảng khi câu đọc phải quét
SNAPSHOT và RCSI là một RCSI nhất quán theo câu lệnh và không báo xung đột. SNAPSHOT nhất quán theo giao dịch và báo 3960
TransactionScope dùng mức mặc định của SQL Server Mặc định Serializable, và mức này theo kết nối về pool

Kết luận

Mức isolation quyết định câu đọc thấy gì, không quyết định câu ghi nhả khóa khi nào; báo cáo cần số khớp nhau thì dùng SNAPSHOT, còn quy tắc nghiệp vụ dựa trên một lần đếm thì phải khóa đúng khoảng mình đã đếm.

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

  • Truyền mức isolation tường minh cho mọi TransactionScope và BeginTransactionAsync; tìm new TransactionScope() không tham số trong code.
  • Báo cáo đọc nhiều câu trong một giao dịch dùng BeginTransactionAsync(IsolationLevel.Snapshot) sau khi bật ALLOW_SNAPSHOT_ISOLATION.
  • Gỡ WITH (NOLOCK) khỏi truy vấn báo cáo; bật RCSI nếu cần đọc không chặn ghi, và theo dõi version store trong tempdb.
  • Quy tắc kiểu "tối đa 5 đơn mở" đếm với WITH (UPDLOCK, HOLDLOCK) trong cùng giao dịch với câu chèn.

Đọc tiếp

Nguồn

Đọc tiếp

Bài tiếp theo trong series

Mất cập nhật tồn kho và upsert

Vì sao đọc tồn kho rồi ghi lại làm mất cập nhật ở cả READ COMMITTED lẫn RCSI, ba cách sửa (UPDATE có điều kiện, UPDLOCK, rowversion), upsert với UPDLOCK HOLDLOCK, và cách viết từng cách bằng EF Core.

11 phút đọc

Trong SQL Server

Khóa và leo thang khóa

SQL Server khóa những gì khi sửa một dòng, các chế độ khóa tương thích ra sao, khi nào khóa dòng leo thẳng lên khóa bảng, và cách chia lô job hàng loạt trong EF Core để không chặn bán hàng.

14 phút đọc

Trong SQL Server

Blocking, deadlock và thử lại giao dịch

Tìm phiên đầu chuỗi khi API timeout hàng loạt, xử lý giao dịch bị bỏ quên, đọc deadlock từ system_health, sửa bằng thứ tự truy cập, và thử lại cả giao dịch bằng execution strategy của EF Core.

13 phút đọc

Trong PostgreSQL

MVCC, HOT update và VACUUM

UPDATE trong PostgreSQL ghi phiên bản dòng mới ngay trong heap; HOT giữ chỉ mục khỏi bị chạm, VACUUM dọn phiên bản cũ; code .NET dùng xmin làm concurrency token và không để giao dịch mở lâu.

14 phút đọc