Lesson 0021 · Database · ±30 menit (15 konsep + kuis, 15 hands-on) · Hari 4

Transaction, isolation level, locking, dan deadlock

ACID, transaction yang benar di T-SQL dan EF Core, lima isolation level beserta anomalinya, dan blocking serta deadlock yang Anda buat sendiri di dua jendela query

Target lesson ini: Anda bisa menjawab pertanyaan interview "Jelaskan isolation level di SQL Server. Apa itu deadlock dan bagaimana mencegahnya?" tanpa melihat catatan, dan sudah melihat sendiri blocking, dirty read, phantom, dan deadlock antara dua sesi di ProductCatalogLab.

Lanjutan lesson 0020: pola SET XACT_ABORT ON + TRY/CATCH + THROW di usp_UpdatePrice kini dijelaskan alasannya. Padanan Node: prisma.$transaction(...) atau BEGIN/COMMIT lewat pg. Konsep isolation level dan deadlock sama di semua database relasional; yang khas SQL Server adalah default-nya (READ COMMITTED berbasis lock) dan nama fiturnya.

Catatan versi: dokumentasi Microsoft terbaru banyak menyebut optimized locking. Fitur itu hanya ada di Azure SQL dan SQL Server 2025 [11]. Container sql2022 memakai perilaku "without optimized locking", dan itulah yang diajarkan di sini.

1. ACID dan dasar transaction

"A transaction is a sequence of operations performed as a single logical unit of work." Empat properti yang wajib dimilikinya: [1]

PropertiArti (kutipan Microsoft Learn)Contoh Product Catalog
Atomicity"either all of its data modifications are performed, or none of them are performed"Transfer Stock: kurangi Product A dan tambah Product B, tidak boleh salah satu saja
Consistency"all rules must be applied to the transaction's modifications to maintain all data integrity"CHECK Stock >= 0, FK ke Category tetap terpenuhi
Isolation"Modifications made by concurrent transactions must be isolated from the modifications made by any other concurrent transactions"Dua request mengubah Product yang sama tidak saling melihat data setengah jadi
Durability"its effects are permanently in place in the system. The modifications persist even in the event of a system failure"Setelah COMMIT, server mati pun perubahan tetap ada

Latihan 1 — ACID

Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review database 0024.

Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0024.

2. Transaction yang benar di T-SQL

SET XACT_ABORT ON;                         -- error run-time apa pun me-rollback SELURUH transaction
BEGIN TRY
    BEGIN TRANSACTION;
        UPDATE dbo.Product SET Stock = Stock - 5 WHERE Id = 1;
        UPDATE dbo.Product SET Stock = Stock + 5 WHERE Id = 2;
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;   -- 1 (masih aktif) atau -1 (uncommittable)
    THROW;                                     -- lempar ulang ke aplikasi (lesson 0020)
END CATCH;

Padanan Node: await prisma.$transaction(async tx => { ... }) otomatis rollback bila callback melempar error. Di T-SQL, perilaku "semua atau tidak sama sekali" itu harus Anda minta sendiri lewat XACT_ABORT dan TRY/CATCH.

Latihan 2 — XACT_ABORT dan @@TRANCOUNT

3. Transaction di EF Core

// Pola dari dokumentasi EF Core. StockMovement hanya contoh tabel riwayat (tidak ada di mini project).
await using var tx = await context.Database.BeginTransactionAsync();   // opsional: (IsolationLevel.Serializable)
try
{
    from.Stock -= qty;
    await context.SaveChangesAsync();

    context.StockMovements.Add(new StockMovement { ProductId = from.Id, Delta = -qty });
    await context.SaveChangesAsync();

    await tx.CommitAsync();       // tanpa Commit, transaction di-rollback saat tx di-dispose
}
catch (Exception)
{
    // log; transaction otomatis di-rollback saat dispose
    throw;
}

Latihan 3 — EF Core

4. Isolation level dan anomalinya

Tiga anomali yang menjadi dasar semua pertanyaan isolation level [1]:

Isolation levelDirty readNon-repeatable readPhantomCara kerja (tanpa optimized locking)
READ UNCOMMITTEDbisabisabisatidak meminta S lock; setara hint NOLOCK
READ COMMITTED (default)tidakbisabisaS lock dilepas begitu baris selesai dibaca
REPEATABLE READtidaktidakbisaS lock ditahan sampai akhir transaction, tanpa range lock
SNAPSHOTtidaktidaktidakrow versioning: membaca versi saat transaction dimulai
SERIALIZABLEtidaktidaktidakkey-range lock atas rentang WHERE

Tabel diringkas dari [1] dan [7]. Dipasang per sesi dengan SET TRANSACTION ISOLATION LEVEL ...; di dalam SP, level kembali ke semula saat SP selesai.

Row versioning: RCSI vs SNAPSHOT (ringkas)

READ COMMITTED SNAPSHOT (RCSI)SNAPSHOT isolation
Diaktifkan denganALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ONALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON, lalu SET TRANSACTION ISOLATION LEVEL SNAPSHOT di sesi
Data yang dibaca"committed before the start of each statement""committed before the start of each transaction"
Konflik UPDATEtidak dideteksidideteksi: Msg 3960 "Snapshot isolation transaction aborted due to update conflict"
Bagi aplikasitransparan: tetap READ COMMITTED biasaharus diminta eksplisit per transaction

Latihan 4 — Isolation level

5. Lock dan blocking

"Locking is a mechanism used by the Database Engine to synchronize access by multiple users to the same piece of data at the same time." "All locks held by a transaction are released when the transaction completes (either commits or rolls back)." [1]

ModeDipakai untuk
Shared (S)membaca; di READ COMMITTED dilepas "as soon as the read operation completes"
Update (U)tahap "cari baris" sebelum mengubah; "Prevents a common form of deadlock"
Exclusive (X)INSERT/UPDATE/DELETE; ditahan sampai transaction selesai
Intent (IS, IX)tanda di level page/tabel bahwa ada lock di bawahnya ("lock hierarchy")
Key-rangedipakai SERIALIZABLE untuk mencegah phantom
Diminta ↓ / sudah dipegang →SUX
Sbolehbolehmenunggu
Ubolehmenunggumenunggu
Xmenunggumenunggumenunggu

Latihan 5 — Lock dan blocking

6. Deadlock

A: X lock Product 1→ mintaProduct 2 (milik B) · B: X lock Product 2→ mintaProduct 1 (milik A)
-- T-SQL: kunci baris untuk diubah sejak dibaca (U lock ditahan sampai COMMIT)
BEGIN TRANSACTION;
SELECT @stock = Stock FROM dbo.Product WITH (UPDLOCK) WHERE Id = @Id;
IF @stock >= @Qty
    UPDATE dbo.Product SET Stock = Stock - @Qty WHERE Id = @Id;
COMMIT TRANSACTION;
// C#: tangani 1205 dengan mengulang SELURUH unit kerja setelah jeda acak.
for (var attempt = 1; ; attempt++)
{
    try
    {
        await using var tx = await context.Database.BeginTransactionAsync();
        // ... baca dan ubah Product ...
        await context.SaveChangesAsync();
        await tx.CommitAsync();
        break;
    }
    catch (Exception ex) when (ex.GetBaseException() is SqlException { Number: 1205 } && attempt < 3)
    {
        context.ChangeTracker.Clear();                       // buang state yang sudah di-rollback
        await Task.Delay(Random.Shared.Next(50, 250));       // jeda acak
    }
}

Dasar pola retry: "applications should have an error handler that can handle error 1205", dan "The application should pause briefly before resubmitting its query ... Randomizing the duration of the pause minimizes the likelihood of the deadlock reoccurring". [8] Error 1205 juga bisa ditangkap di blok CATCH T-SQL.

Latihan 6 — Deadlock

7. Hands-on (±15 menit): dua jendela, dua sesi

  1. Pastikan container jalan (docker start sql2022) dan setup lab sudah dijalankan (6 Category, 100000 Product). Kalau ragu, jalankan ulang dotnet/lab/00-setup-product-catalog-lab.sql.
  2. Buka dua jendela query di Azure Data Studio atau VS Code mssql, masing-masing koneksi sendiri ke localhost,1433, user sa. Jendela kiri: dotnet/lab/0021-sesi-a.sql. Jendela kanan: dotnet/lab/0021-sesi-b.sql.
  3. Jalankan satu langkah saja setiap kali (pilih blok di antara dua penanda -- Langkah N, lalu Run selection), ikuti urutan di tabel. Setiap langkah diakhiri penanda >>> lanjut yang menyebut jendela berikutnya. Jangan menjalankan seluruh file: langkah yang menunggu akan menggantung.
  4. Sebelum menjalankan, tebak dulu: langkah mana yang akan menunggu (ikon query berputar), dan angka berapa yang muncul?
BagianUrutan langkah (A = Sesi A, B = Sesi B)Yang harus terlihat
0 · Persiapan1 A → 2 Bdua session id berbeda; is_read_committed_snapshot_on = 0, snapshot OFF
1 · Atomicity3 A3a: Stock Id 1 = 36 (setengah transaction tersimpan); 3b: XactState -1, error 547, Stock tetap 31 dan 62; 3c: @@TRANCOUNT 2 → 1 → 0, Stock 31
2 · Blocking4 A → 5 B (menunggu) → 6 A → 7 AB diblok A dengan LCK_M_S; A: X pada KEY GRANTED, B: S pada KEY WAIT; setelah ROLLBACK, B langsung menampilkan 80.19
3 · Dirty read8 A → 9 B → 10 A → 11 BB membaca 99999.99 tanpa menunggu, lalu 80.19 setelah A ROLLBACK
4 · Non-repeatable read12 B → 13 A → 14 B → 15 B → 16 A (menunggu) → 17 BREAD COMMITTED: 80.19 lalu 90.00 di transaction yang sama; REPEATABLE READ: 90.00 dua kali, UPDATE A menunggu sampai B COMMIT
5 · Phantom18 B → 19 A → 20 B → 21 B → 22 A (menunggu) → 23 B → 24 AREPEATABLE READ: 6 lalu 7; SERIALIZABLE: 7 dua kali, INSERT A menunggu; akhir: 100000 Product
6 · Deadlock25 A → 26 B → 27 A (menunggu) → 28 B → 29 A dan B → (30 A) → 31 A → 32 B (menunggu) → 33 A → 34 Bdalam ±5 detik satu sesi mendapat Msg 1205; deadlock graph di system_health; dengan urutan akses sama hanya terjadi blocking
7 · SNAPSHOT (opsional)35 A → 36 B → 37 A → 38 B → 39 A → 40 B → 41 B → 42 AB tidak menunggu dan tetap membaca 80.19; UPDATE di B gagal dengan Msg 3960; akhir: lab kembali seperti setup

Tanpa GUI: buka dua terminal, masing-masing docker exec -it sql2022 /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Dev_Passw0rd!' -C, tempel satu langkah, lalu ketik GO. Kalau sebuah langkah tidak menunggu padahal seharusnya, cek Langkah 1: RCSI mungkin ON. Kalau lab berantakan, jalankan IF @@TRANCOUNT > 0 ROLLBACK; di kedua jendela, lalu setup ulang.

Jendela 1: dotnet/lab/0021-sesi-a.sql

-- Lesson 0021 · Transaction, isolation level, locking, deadlock · SESI A
-- Pasangan file ini: dotnet/lab/0021-sesi-b.sql. Buka KEDUA file di dua jendela query (dua koneksi
-- terpisah ke localhost,1433) setelah dotnet/lab/00-setup-product-catalog-lab.sql dijalankan.
--
-- JANGAN jalankan seluruh file sekaligus. Jalankan satu langkah saja (blok selection
-- antara dua penanda "Langkah"), lalu ikuti penanda ">>> lanjut" ke jendela berikutnya.
-- Langkah bernomor urut di kedua file: Langkah 1 (A), Langkah 2 (B), Langkah 3 (A), dst.
-- Langkah yang "menunggu" (ikon query berputar) memang disengaja: itulah blocking.
--
-- Data yang dipakai (dari setup): Product Id 1 = Price 80.19, Stock 31; Product Id 2 = Price 159.38,
-- Stock 62. Semua langkah mengembalikan data ke nilai ini.

-- ============ Bagian 0 · Persiapan ============================================================

-- Langkah 1 (Sesi A) -----------------------------------------------------------------------
USE ProductCatalogLab;
SELECT @@SPID AS SesiA;                        -- catat angkanya: session id jendela ini

-- Default SQL Server 2022: READ COMMITTED berbasis lock (RCSI OFF), SNAPSHOT tidak diizinkan.
-- Harus 0 dan OFF. Kalau is_read_committed_snapshot_on = 1, demo blocking di Bagian 2 tidak terjadi.
SELECT name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
FROM sys.databases
WHERE name = N'ProductCatalogLab';
-- >>> lanjut: Sesi B, Langkah 2

-- ============ Bagian 1 · Atomicity, XACT_ABORT, TRY/CATCH (Sesi A saja) ======================

-- Langkah 3 (Sesi A) -----------------------------------------------------------------------
-- 3a) JEBAKAN: XACT_ABORT OFF (default) tanpa TRY/CATCH. Error 547 hanya membatalkan
--     STATEMENT yang gagal; batch lanjut, COMMIT tetap jalan, UPDATE pertama tersimpan.
SET XACT_ABORT OFF;
BEGIN TRANSACTION;
    UPDATE dbo.Product SET Stock = Stock + 5 WHERE Id = 1;   -- berhasil
    UPDATE dbo.Product SET Stock = -1 WHERE Id = 2;          -- gagal: CK_Product_Stock (547)
COMMIT TRANSACTION;
SELECT Id, Stock FROM dbo.Product WHERE Id IN (1, 2);        -- Id 1 = 36: setengah transaction tersimpan
UPDATE dbo.Product SET Stock = 31 WHERE Id = 1;              -- kembalikan

-- 3b) POLA BENAR: XACT_ABORT ON + TRY/CATCH. Error apa pun membatalkan SELURUH transaction.
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;
        UPDATE dbo.Product SET Stock = Stock + 5 WHERE Id = 1;
        UPDATE dbo.Product SET Stock = -1 WHERE Id = 2;
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- XACT_STATE: 1 = masih bisa di-commit, -1 = uncommittable (hanya boleh ROLLBACK), 0 = tidak ada.
    SELECT XACT_STATE() AS XactState, ERROR_NUMBER() AS ErrorNumber;   -- -1 dan 547
    IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
    -- Di Stored Procedure, akhiri CATCH dengan THROW; (lesson 0020). Di sini tidak, supaya demo lanjut.
END CATCH;
SELECT Id, Stock FROM dbo.Product WHERE Id IN (1, 2);        -- 31 dan 62: tidak ada yang berubah
SET XACT_ABORT OFF;

-- 3c) Nested transaction bukan transaction sungguhan: COMMIT dalam hanya mengurangi @@TRANCOUNT.
BEGIN TRANSACTION;
    BEGIN TRANSACTION;
        SELECT @@TRANCOUNT AS TranCount;                     -- 2
        UPDATE dbo.Product SET Stock = 999 WHERE Id = 1;
    COMMIT TRANSACTION;                                      -- tidak menyimpan apa pun
    SELECT @@TRANCOUNT AS TranCount;                         -- 1
ROLLBACK TRANSACTION;                                        -- membatalkan SEMUANYA, termasuk yang "di-commit"
SELECT @@TRANCOUNT AS TranCount, Stock FROM dbo.Product WHERE Id = 1;   -- 0 dan 31
-- >>> lanjut: Langkah 4 (masih Sesi A)

-- ============ Bagian 2 · Blocking dan cara melihatnya ========================================

-- Langkah 4 (Sesi A) -----------------------------------------------------------------------
-- Ambil exclusive (X) lock pada baris Id 1 dan JANGAN commit.
BEGIN TRANSACTION;
UPDATE dbo.Product SET Price = 99999.99 WHERE Id = 1;
SELECT @@TRANCOUNT AS TranCount;                             -- 1: transaction masih terbuka
-- >>> lanjut: Sesi B, Langkah 5

-- Langkah 6 (Sesi A) -----------------------------------------------------------------------
-- Sesi B sedang menunggu. Siapa yang mem-block siapa?
SELECT session_id, status, blocking_session_id, wait_type, wait_time, wait_resource
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0;        -- session Sesi B, diblok oleh Sesi A, wait_type LCK_M_S

-- Lock apa saja yang dipegang dan diminta di database ini?
SELECT request_session_id, resource_type, request_mode, request_status, resource_description
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID(N'ProductCatalogLab')
  AND resource_type <> 'DATABASE'
ORDER BY request_session_id, resource_type;
-- Sesi A: X pada KEY (GRANTED), IX pada PAGE dan OBJECT (intent lock).
-- Sesi B: S pada KEY yang sama (WAIT), IS pada PAGE dan OBJECT.

-- Langkah 7 (Sesi A) -----------------------------------------------------------------------
ROLLBACK TRANSACTION;          -- lock dilepas, SELECT di Sesi B langsung selesai: 80.19
-- >>> lanjut: Langkah 8 (masih Sesi A)

-- ============ Bagian 3 · Dirty read (READ UNCOMMITTED) =======================================

-- Langkah 8 (Sesi A) -----------------------------------------------------------------------
BEGIN TRANSACTION;
UPDATE dbo.Product SET Price = 99999.99 WHERE Id = 1;        -- belum di-commit
-- >>> lanjut: Sesi B, Langkah 9

-- Langkah 10 (Sesi A) ----------------------------------------------------------------------
ROLLBACK TRANSACTION;          -- 99999.99 yang dibaca Sesi B tidak pernah ada secara resmi
-- >>> lanjut: Sesi B, Langkah 11

-- ============ Bagian 4 · Non-repeatable read (READ COMMITTED vs REPEATABLE READ) =============

-- Langkah 13 (Sesi A) ----------------------------------------------------------------------
UPDATE dbo.Product SET Price = 90.00 WHERE Id = 1;           -- autocommit; tidak menunggu
-- >>> lanjut: Sesi B, Langkah 14

-- Langkah 16 (Sesi A) ----------------------------------------------------------------------
-- Sesi B (REPEATABLE READ) memegang S lock sampai transaction-nya selesai: UPDATE ini MENUNGGU.
UPDATE dbo.Product SET Price = 80.19 WHERE Id = 1;           -- selesai setelah Sesi B COMMIT (Langkah 17)
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 80.19
-- >>> lanjut: Sesi B, Langkah 17

-- ============ Bagian 5 · Phantom (REPEATABLE READ vs SERIALIZABLE) ===========================

-- Langkah 19 (Sesi A) ----------------------------------------------------------------------
-- Id baru (IDENTITY) > 100000, jadi masuk ke rentang "Id >= 99995" yang sedang dibaca Sesi B.
INSERT INTO dbo.Product (Name, Price, Stock, CategoryId, CreatedAt)
VALUES (N'Phantom 0021 RR', 10.00, 1, 6, SYSDATETIME());     -- langsung berhasil
-- >>> lanjut: Sesi B, Langkah 20

-- Langkah 22 (Sesi A) ----------------------------------------------------------------------
-- Sesi B (SERIALIZABLE) memegang key-range lock atas rentang itu: INSERT ini MENUNGGU.
INSERT INTO dbo.Product (Name, Price, Stock, CategoryId, CreatedAt)
VALUES (N'Phantom 0021 SER', 10.00, 1, 6, SYSDATETIME());    -- selesai setelah Sesi B COMMIT (Langkah 23)
-- >>> lanjut: Sesi B, Langkah 23

-- Langkah 24 (Sesi A) ----------------------------------------------------------------------
DELETE FROM dbo.Product WHERE Name LIKE N'Phantom 0021%';    -- bersihkan
SELECT COUNT(*) AS ProductCount FROM dbo.Product;            -- 100000
-- >>> lanjut: Langkah 25 (masih Sesi A)

-- ============ Bagian 6 · Deadlock, lalu pencegahannya ========================================

-- Langkah 25 (Sesi A) ----------------------------------------------------------------------
BEGIN TRANSACTION;
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 1;       -- A memegang X lock baris 1
-- >>> lanjut: Sesi B, Langkah 26

-- Langkah 27 (Sesi A) ----------------------------------------------------------------------
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 2;       -- MENUNGGU baris 2 milik Sesi B
-- >>> lanjut: Sesi B, Langkah 28 (jangan tunggu langkah ini selesai)

-- Langkah 29 (Sesi A DAN Sesi B) -----------------------------------------------------------
-- Satu sesi mendapat Msg 1205 (deadlock victim, transaction-nya sudah di-rollback engine);
-- UPDATE di sesi lain selesai. Jalankan blok ini di KEDUA jendela.
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
SELECT @@TRANCOUNT AS TranCount, Id, Stock FROM dbo.Product WHERE Id IN (1, 2);   -- 0; 31 dan 62

-- Langkah 30 (Sesi A, opsional) ------------------------------------------------------------
-- Deadlock graph dicatat otomatis oleh extended event session bawaan system_health.
-- Klik kolom XML untuk melihat proses, resource, dan siapa victim-nya.
SELECT x.ev.value('@timestamp', 'datetime2') AS EventTime, x.ev.query('.') AS DeadlockReport
FROM (
    SELECT CAST(st.target_data AS xml) AS TargetData
    FROM sys.dm_xe_session_targets AS st
    INNER JOIN sys.dm_xe_sessions AS s ON s.address = st.event_session_address
    WHERE s.name = N'system_health' AND st.target_name = N'ring_buffer'
) AS t
CROSS APPLY t.TargetData.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(ev)
ORDER BY EventTime DESC;

-- Langkah 31 (Sesi A) ----------------------------------------------------------------------
-- PENCEGAHAN: kedua sesi mengakses baris dalam URUTAN YANG SAMA (Id 1 dulu, lalu Id 2).
BEGIN TRANSACTION;
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 1;
-- >>> lanjut: Sesi B, Langkah 32

-- Langkah 33 (Sesi A) ----------------------------------------------------------------------
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 2;       -- tidak menunggu: baris 2 bebas
ROLLBACK TRANSACTION;          -- lock dilepas; Sesi B (yang hanya blocking, bukan deadlock) lanjut
-- >>> lanjut: Sesi B, Langkah 34

-- ============ Bagian 7 · SNAPSHOT isolation (ringkas, opsional) ==============================

-- Langkah 35 (Sesi A) ----------------------------------------------------------------------
-- Pastikan kedua sesi tidak punya transaction terbuka dulu.
ALTER DATABASE ProductCatalogLab SET ALLOW_SNAPSHOT_ISOLATION ON;
SELECT snapshot_isolation_state_desc FROM sys.databases WHERE name = N'ProductCatalogLab';   -- ON
-- >>> lanjut: Sesi B, Langkah 36

-- Langkah 37 (Sesi A) ----------------------------------------------------------------------
BEGIN TRANSACTION;
UPDATE dbo.Product SET Price = 70.00 WHERE Id = 1;           -- X lock, belum commit
-- >>> lanjut: Sesi B, Langkah 38

-- Langkah 39 (Sesi A) ----------------------------------------------------------------------
COMMIT TRANSACTION;            -- Price kini 70.00
-- >>> lanjut: Sesi B, Langkah 40

-- Langkah 42 (Sesi A) ----------------------------------------------------------------------
-- Kembalikan lab ke kondisi setup.
UPDATE dbo.Product SET Price = 80.19 WHERE Id = 1;
ALTER DATABASE ProductCatalogLab SET ALLOW_SNAPSHOT_ISOLATION OFF;
SELECT name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
FROM sys.databases
WHERE name = N'ProductCatalogLab';                           -- 0 dan OFF
SELECT Id, Price, Stock FROM dbo.Product WHERE Id IN (1, 2);  -- 80.19/31 dan 159.38/62
-- Selesai.

Jendela 2: dotnet/lab/0021-sesi-b.sql

-- Lesson 0021 · Transaction, isolation level, locking, deadlock · SESI B
-- Pasangan file ini: dotnet/lab/0021-sesi-a.sql. Mulai dari Langkah 1 di Sesi A.
-- JANGAN jalankan seluruh file sekaligus: jalankan satu langkah, lalu ikuti penanda ">>> lanjut".
-- SET TRANSACTION ISOLATION LEVEL berlaku untuk seluruh sesi sampai diganti, jadi setiap
-- bagian menyetel ulang isolation level-nya sendiri.

-- ============ Bagian 0 · Persiapan ============================================================

-- Langkah 2 (Sesi B) -----------------------------------------------------------------------
USE ProductCatalogLab;
SELECT @@SPID AS SesiB;                        -- session id jendela ini, berbeda dari Sesi A
-- >>> lanjut: Sesi A, Langkah 3

-- ============ Bagian 2 · Blocking ============================================================

-- Langkah 5 (Sesi B) -----------------------------------------------------------------------
-- READ COMMITTED berbasis lock butuh shared (S) lock untuk membaca; baris Id 1 dipegang X lock
-- oleh Sesi A, jadi query ini MENUNGGU. Tanpa LOCK_TIMEOUT, ia menunggu selamanya.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT Id, Name, Price FROM dbo.Product WHERE Id = 1;        -- berputar... lalu 80.19 setelah Langkah 7
-- >>> lanjut: Sesi A, Langkah 6 (biarkan query ini tetap menunggu)

-- ============ Bagian 3 · Dirty read ==========================================================

-- Langkah 9 (Sesi B) -----------------------------------------------------------------------
-- READ UNCOMMITTED tidak meminta S lock (sama dengan hint WITH (NOLOCK)): tidak menunggu,
-- tapi membaca data yang belum di-commit.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 99999.99, langsung
-- >>> lanjut: Sesi A, Langkah 10

-- Langkah 11 (Sesi B) ----------------------------------------------------------------------
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 80.19: yang tadi adalah dirty read
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- >>> lanjut: Langkah 12 (masih Sesi B)

-- ============ Bagian 4 · Non-repeatable read =================================================

-- Langkah 12 (Sesi B) ----------------------------------------------------------------------
-- READ COMMITTED: S lock dilepas begitu baris selesai dibaca, bukan di akhir transaction.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 80.19
-- >>> lanjut: Sesi A, Langkah 13

-- Langkah 14 (Sesi B) ----------------------------------------------------------------------
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 90.00: transaction yang SAMA, hasil beda
COMMIT TRANSACTION;
-- >>> lanjut: Langkah 15 (masih Sesi B)

-- Langkah 15 (Sesi B) ----------------------------------------------------------------------
-- REPEATABLE READ: S lock ditahan sampai akhir transaction.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 90.00
-- >>> lanjut: Sesi A, Langkah 16

-- Langkah 17 (Sesi B) ----------------------------------------------------------------------
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- tetap 90.00: UPDATE Sesi A masih menunggu
COMMIT TRANSACTION;            -- S lock dilepas; UPDATE di Sesi A langsung selesai (80.19)
-- >>> lanjut: Langkah 18 (masih Sesi B)

-- ============ Bagian 5 · Phantom =============================================================

-- Langkah 18 (Sesi B) ----------------------------------------------------------------------
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
SELECT COUNT(*) AS Jumlah FROM dbo.Product WHERE Id >= 99995;   -- 6 (Id 99995..100000)
-- >>> lanjut: Sesi A, Langkah 19

-- Langkah 20 (Sesi B) ----------------------------------------------------------------------
SELECT COUNT(*) AS Jumlah FROM dbo.Product WHERE Id >= 99995;   -- 7: baris baru muncul = phantom
COMMIT TRANSACTION;
-- >>> lanjut: Langkah 21 (masih Sesi B)

-- Langkah 21 (Sesi B) ----------------------------------------------------------------------
-- SERIALIZABLE: range lock atas rentang WHERE, jadi tidak ada baris baru yang bisa masuk.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
SELECT COUNT(*) AS Jumlah FROM dbo.Product WHERE Id >= 99995;   -- 7
-- >>> lanjut: Sesi A, Langkah 22

-- Langkah 23 (Sesi B) ----------------------------------------------------------------------
SELECT COUNT(*) AS Jumlah FROM dbo.Product WHERE Id >= 99995;   -- tetap 7: INSERT Sesi A masih menunggu
COMMIT TRANSACTION;            -- range lock dilepas; INSERT di Sesi A langsung selesai
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- >>> lanjut: Sesi A, Langkah 24

-- ============ Bagian 6 · Deadlock ============================================================

-- Langkah 26 (Sesi B) ----------------------------------------------------------------------
BEGIN TRANSACTION;
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 2;       -- B memegang X lock baris 2
-- >>> lanjut: Sesi A, Langkah 27

-- Langkah 28 (Sesi B) ----------------------------------------------------------------------
-- B meminta baris 1 (milik A), A meminta baris 2 (milik B): siklus. Deadlock monitor
-- (default memeriksa tiap 5 detik) memilih satu victim: Msg 1205, transaction-nya di-rollback.
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 1;
-- >>> lanjut: Langkah 29 di KEDUA jendela

-- Langkah 29 (Sesi A DAN Sesi B) -----------------------------------------------------------
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
SELECT @@TRANCOUNT AS TranCount, Id, Stock FROM dbo.Product WHERE Id IN (1, 2);   -- 0; 31 dan 62
-- >>> lanjut: Sesi A, Langkah 30 atau 31

-- Langkah 32 (Sesi B) ----------------------------------------------------------------------
-- Urutan sama dengan Sesi A: Id 1 dulu. Ini hanya BLOCKING (menunggu A selesai), bukan deadlock.
BEGIN TRANSACTION;
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 1;       -- menunggu sampai Sesi A ROLLBACK
-- >>> lanjut: Sesi A, Langkah 33

-- Langkah 34 (Sesi B) ----------------------------------------------------------------------
UPDATE dbo.Product SET Stock = Stock - 1 WHERE Id = 2;
ROLLBACK TRANSACTION;
SELECT Id, Stock FROM dbo.Product WHERE Id IN (1, 2);        -- 31 dan 62
-- >>> lanjut: Sesi A, Langkah 35 (opsional) atau selesai

-- ============ Bagian 7 · SNAPSHOT isolation (ringkas, opsional) ==============================

-- Langkah 36 (Sesi B) ----------------------------------------------------------------------
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 80.19 (snapshot diambil di sini)
-- >>> lanjut: Sesi A, Langkah 37

-- Langkah 38 (Sesi B) ----------------------------------------------------------------------
-- Baris Id 1 sedang dipegang X lock oleh Sesi A, tapi SNAPSHOT membaca versi lama dari
-- version store (tempdb): tidak menunggu.
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- 80.19, langsung
-- >>> lanjut: Sesi A, Langkah 39

-- Langkah 40 (Sesi B) ----------------------------------------------------------------------
SELECT Id, Price FROM dbo.Product WHERE Id = 1;              -- tetap 80.19 walau Sesi A sudah commit 70.00
-- Mengubah baris yang sudah diubah transaction lain setelah snapshot diambil:
-- Msg 3960 (update conflict), transaction ini dibatalkan.
UPDATE dbo.Product SET Price = Price + 1 WHERE Id = 1;
-- >>> lanjut: Langkah 41 (masih Sesi B)

-- Langkah 41 (Sesi B) ----------------------------------------------------------------------
SELECT @@TRANCOUNT AS TranCount;                             -- 0: sudah di-rollback engine
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- >>> lanjut: Sesi A, Langkah 42

8. Latihan tulis query

Tulis sendiri di editor SQL, jalankan di lab, baru buka jawaban. Tidak dinilai otomatis; bandingkan hasil dan bentuk kodenya.

A. Tulis Stored Procedure dbo.usp_TransferStock dengan parameter @FromId int, @ToId int, @Qty int. SP memindahkan @Qty Stock dari satu Product ke Product lain secara atomik, menolak bila Stock asal tidak cukup (THROW 50003), dan tidak bisa deadlock walau dua request memanggil (1, 2) dan (2, 1) bersamaan. Uji: pindahkan 5 dari Id 1 ke Id 2, lalu coba pindahkan 100000, lalu kembalikan 5 dari Id 2 ke Id 1.

Lihat jawaban
CREATE OR ALTER PROCEDURE dbo.usp_TransferStock
    @FromId int,
    @ToId   int,
    @Qty    int
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    DECLARE @FirstId  int = IIF(@FromId < @ToId, @FromId, @ToId),
            @SecondId int = IIF(@FromId < @ToId, @ToId, @FromId),
            @FirstStock int, @SecondStock int;

    BEGIN TRY
        BEGIN TRANSACTION;

        -- Kunci kedua baris dalam urutan Id naik, sama untuk SEMUA pemanggil: tidak ada siklus.
        -- UPDLOCK: lock ditahan sampai COMMIT, dan dua pembaca UPDLOCK tidak bisa sama-sama lolos.
        SELECT @FirstStock  = Stock FROM dbo.Product WITH (UPDLOCK) WHERE Id = @FirstId;
        SELECT @SecondStock = Stock FROM dbo.Product WITH (UPDLOCK) WHERE Id = @SecondId;

        IF @FirstStock IS NULL OR @SecondStock IS NULL
            THROW 50001, N'Product tidak ditemukan.', 1;
        IF IIF(@FromId = @FirstId, @FirstStock, @SecondStock) < @Qty
            THROW 50003, N'Stock asal tidak cukup.', 1;

        UPDATE dbo.Product SET Stock = Stock - @Qty WHERE Id = @FromId;
        UPDATE dbo.Product SET Stock = Stock + @Qty WHERE Id = @ToId;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
        THROW;
    END CATCH
END;
GO

EXEC dbo.usp_TransferStock @FromId = 1, @ToId = 2, @Qty = 5;
SELECT Id, Stock FROM dbo.Product WHERE Id IN (1, 2);           -- 26 dan 67

BEGIN TRY
    EXEC dbo.usp_TransferStock @FromId = 1, @ToId = 2, @Qty = 100000;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS Message;   -- 50003
END CATCH;

EXEC dbo.usp_TransferStock @FromId = 2, @ToId = 1, @Qty = 5;   -- kembali 31 dan 62
SELECT Id, Stock FROM dbo.Product WHERE Id IN (1, 2);
GO

Poin yang dinilai interviewer: XACT_ABORT ON + TRY/CATCH + THROW; (atomicity dan error sampai ke aplikasi); urutan akses yang sama (Id kecil dulu) sebagai pencegah deadlock; UPDLOCK supaya pola baca-lalu-ubah tidak berubah menjadi deadlock S→X atau lost update. Uji deadlock-nya sendiri: jalankan EXEC dbo.usp_TransferStock 1, 2, 1 dan 2, 1, 1 bersamaan di dua jendela, dibungkus BEGIN TRAN tanpa COMMIT: yang kedua hanya menunggu, tidak ada 1205 (lalu ROLLBACK di kedua jendela). Kalau ditanya "kenapa tidak cukup CHECK constraint?": constraint tetap penjaga terakhir (547), tapi 50003 lebih jelas untuk dipetakan ke response 409.

B. Tulis query "siapa mem-block siapa" untuk dipakai saat aplikasi terasa macet: tampilkan session_id yang menunggu, blocking_session_id, wait_type, lama menunggu dalam detik, nama isolation level sesi yang menunggu (bukan angkanya), dan teks SQL yang sedang menunggu. Jalankan di jendela ketiga saat lab sedang di Langkah 5 (Sesi B menunggu Sesi A).

Lihat jawaban
SELECT r.session_id,
       r.blocking_session_id,
       r.wait_type,
       r.wait_time / 1000.0 AS WaitSeconds,
       CASE r.transaction_isolation_level
           WHEN 1 THEN 'READ UNCOMMITTED'
           WHEN 2 THEN 'READ COMMITTED'
           WHEN 3 THEN 'REPEATABLE READ'
           WHEN 4 THEN 'SERIALIZABLE'
           WHEN 5 THEN 'SNAPSHOT'
           ELSE 'UNSPECIFIED'
       END AS IsolationLevel,
       t.text AS WaitingSql
FROM sys.dm_exec_requests AS r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;

blocking_session_id bukan 0 berarti request itu diblok; angka isolation level (1–5) dipetakan sesuai dokumentasi sys.dm_exec_requests. CROSS APPLY memanggil function per baris (mirip JOIN ke table-valued function di lesson 0020). Langkah berikutnya di dunia nyata: lihat apa yang dikerjakan sesi pemblokir (biasanya transaction yang lupa di-commit atau terlalu panjang), bukan langsung KILL.

Latihan 7 — Ucapkan dengan suara keras

Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Jelaskan isolation level di SQL Server: anomali apa yang dicegah masing-masing, apa default-nya, dan level mana yang Anda pilih." (60–90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"Isolation level menentukan seberapa jauh transaction terlindung dari perubahan transaction lain yang berjalan bersamaan. Ada tiga anomali: dirty read, yaitu membaca data yang belum di-commit; non-repeatable read, membaca baris yang sama dua kali dengan hasil berbeda; dan phantom, query yang sama mengembalikan himpunan baris berbeda karena ada INSERT. READ UNCOMMITTED membolehkan ketiganya, sama dengan NOLOCK. READ COMMITTED, default SQL Server, mencegah dirty read. REPEATABLE READ menahan shared lock sampai akhir transaction sehingga mencegah non-repeatable read, tapi phantom masih bisa. SERIALIZABLE menambah range lock sehingga phantom pun tercegah, dengan biaya blocking paling besar. SNAPSHOT mencegah ketiganya lewat row versioning, pembaca tidak memblok penulis, tapi harus diaktifkan dengan ALLOW_SNAPSHOT_ISOLATION dan bisa gagal dengan update conflict 3960. Di SQL Server 2022 READ_COMMITTED_SNAPSHOT default OFF, jadi pembaca menunggu penulis; Microsoft menyarankan RCSI untuk kebanyakan aplikasi. Apa pun levelnya, UPDATE tetap memegang X lock sampai commit. Pilihan saya: default READ COMMITTED, idealnya dengan RCSI, dan level lebih tinggi hanya untuk transaction tertentu yang memang butuh."

Checklist: apakah Anda menyebut…

  • tiga anomali dengan definisi: dirty read, non-repeatable read, phantom?
  • READ UNCOMMITTED (= NOLOCK) membolehkan dirty read; READ COMMITTED adalah default dan mencegahnya?
  • REPEATABLE READ menahan shared lock sampai akhir transaction, phantom masih bisa; SERIALIZABLE memakai range lock?
  • SNAPSHOT memakai row versioning: pembaca tidak memblok penulis, perlu ALLOW_SNAPSHOT_ISOLATION, bisa update conflict?
  • trade-off: level lebih tinggi berarti lebih banyak blocking dan risiko deadlock; RCSI default OFF di SQL Server 2022?
  • pilihan yang dibela: READ COMMITTED (idealnya RCSI), level tinggi hanya bila perlu; UPDATE tetap memegang X lock?

Lulus kalau minimal 4 dari 6.

"Apa itu deadlock, apa bedanya dengan blocking, dan bagaimana Anda mencegah serta menanganinya di aplikasi .NET?" (60–90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"Blocking itu normal: transaction B menunggu lock yang dipegang transaction A, dan begitu A commit atau rollback, B lanjut. Deadlock adalah siklus: A memegang baris 1 dan menunggu baris 2, sementara B memegang baris 2 dan menunggu baris 1, jadi tidak ada yang bisa lanjut. SQL Server punya deadlock monitor yang memeriksa secara berkala, default tiap 5 detik; ia memilih satu victim, biasanya yang paling murah di-rollback, me-rollback transaction-nya, dan mengirim error 1205. Pencegahannya: akses tabel dan baris dalam urutan yang sama di semua kode, transaction sependek mungkin tanpa menunggu input user, jangan pakai REPEATABLE READ atau SERIALIZABLE kalau tidak perlu, pertimbangkan RCSI supaya pembaca tidak memblok penulis, dan untuk pola baca-lalu-ubah pakai UPDLOCK saat membaca. Karena deadlock tidak bisa dihilangkan seratus persen, aplikasi harus menangkap SqlException nomor 1205 lalu mengulang seluruh transaction setelah jeda singkat yang diacak. Untuk investigasi, deadlock graph sudah dicatat otomatis oleh extended event system_health."

Checklist: apakah Anda menyebut…

  • blocking = menunggu lock lalu lanjut setelah dilepas; deadlock = siklus saling menunggu?
  • contoh konkret dua transaction yang mengunci dua baris dengan urutan terbalik?
  • deadlock monitor memilih victim (paling murah di-rollback), me-rollback, error 1205?
  • pencegahan: urutan akses sama, transaction pendek, tanpa input user di tengah transaction?
  • hindari isolation level tinggi bila tidak perlu, RCSI, atau UPDLOCK untuk pola baca-lalu-ubah?
  • aplikasi menangkap 1205 dan retry seluruh transaction dengan jeda acak; deadlock graph di system_health?

Lulus kalau minimal 4 dari 6.

"Apa itu ACID? Tunjukkan cara menulis transaction yang benar di T-SQL dan di EF Core." (60–90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"ACID adalah empat jaminan transaction: Atomicity, semua perubahan tersimpan atau tidak sama sekali; Consistency, data selalu memenuhi aturan seperti constraint; Isolation, transaction yang berjalan bersamaan tidak saling mengganggu, diatur lewat isolation level dan lock; Durability, yang sudah di-commit tetap ada walau server crash, dijamin transaction log. Di T-SQL tiap statement sudah autocommit, tapi kalau beberapa statement harus atomik saya tulis SET XACT_ABORT ON, lalu BEGIN TRY, BEGIN TRANSACTION, statement-statementnya, COMMIT. Di BEGIN CATCH: IF XACT_STATE() <> 0 ROLLBACK, lalu THROW supaya error sampai ke aplikasi. XACT_ABORT penting karena default-nya hanya statement yang gagal yang di-rollback, sehingga tanpa itu COMMIT bisa menyimpan setengah transaction. Saya juga tahu nested BEGIN TRAN tidak independen: COMMIT dalam hanya mengurangi @@TRANCOUNT. Di EF Core, satu SaveChanges sudah otomatis satu transaction. Kalau beberapa SaveChanges harus atomik, saya pakai await using var tx = await context.Database.BeginTransactionAsync(), bisa dengan IsolationLevel, lalu CommitAsync; kalau exception, transaction di-rollback saat dispose. Kalau retry on failure aktif, bungkus dengan execution strategy."

Checklist: apakah Anda menyebut…

  • keempat huruf ACID dengan arti yang benar?
  • autocommit: tiap statement sudah transaction; explicit transaction untuk beberapa statement?
  • SET XACT_ABORT ON + BEGIN TRY/BEGIN TRANSACTION/COMMIT?
  • CATCH: IF XACT_STATE() <> 0 (atau @@TRANCOUNT > 0) ROLLBACK lalu THROW;?
  • EF Core: satu SaveChanges = satu transaction; BeginTransactionAsync untuk beberapa SaveChanges, lalu CommitAsync?
  • jebakan: tanpa XACT_ABORT hanya statement gagal yang di-rollback, atau nested COMMIT hanya mengurangi @@TRANCOUNT?

Lulus kalau minimal 4 dari 6. Review 0024 akan menanyakan salah satunya lagi.

Bacaan utama (±15 menit) Microsoft Learn: Transaction locking and row versioning guide. Baca bagian "Transaction basics", "Concurrency effects", dan "Isolation levels in the Database Engine" (lewati paragraf "with optimized locking"), lalu bagian "Minimize deadlocks" di Deadlocks guide.

Ringkasan untuk dicetak: Reference: Transaction, isolation level, locking, deadlock · Istilah: GLOSSARY.md (bagian "Database (SQL Server)")

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "kenapa UPDATE yang memakai index bisa deadlock dengan SELECT?", "kapan RCSI justru berbahaya untuk logika bisnis?", atau "bagaimana optimistic concurrency EF Core menulis SQL-nya?"

Sumber: [1] Transaction locking and row versioning guide · [2] BEGIN TRANSACTION · [3] SET XACT_ABORT · [4] XACT_STATE · [5] EF Core: Transactions · [6] BeginTransaction(IsolationLevel) · [7] SET TRANSACTION ISOLATION LEVEL · [8] Deadlocks guide · [9] sys.dm_tran_locks · [10] sys.dm_exec_requests · [11] Optimized locking · [12] EF Core: Connection resiliency · [13] EF Core: Handling concurrency conflicts · [14] SET LOCK_TIMEOUT · [15] MSSQLSERVER_1222