Saya memiliki tabel yang digunakan oleh aplikasi lawas sebagai pengganti IDENTITY
bidang di berbagai tabel lainnya.
Setiap baris dalam tabel menyimpan ID yang terakhir digunakan LastID
untuk bidang yang disebutkan dalam IDName
.
Terkadang proc yang disimpan menemui jalan buntu - Saya yakin saya telah membangun penangan kesalahan yang sesuai; namun saya tertarik untuk melihat apakah metodologi ini berfungsi seperti yang saya kira, atau jika saya menggonggong pohon yang salah di sini.
Saya cukup yakin harus ada cara untuk mengakses tabel ini tanpa ada deadlock sama sekali.
Basis data itu sendiri dikonfigurasi dengan READ_COMMITTED_SNAPSHOT = 1
.
Pertama, ini adalah tabelnya:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
Dan indeks nonclustered di IDName
lapangan:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] ASC
)
WITH (
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF
, DROP_EXISTING = OFF
, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 80
);
GO
Beberapa data sampel:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
Prosedur tersimpan digunakan untuk memperbarui nilai-nilai yang disimpan dalam tabel, dan mengembalikan ID berikutnya:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Contoh eksekusi dari proc yang disimpan:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDIT:
Saya telah menambahkan indeks baru, karena indeks yang ada IX_tblIDs_Name tidak digunakan oleh SP; Saya berasumsi prosesor permintaan menggunakan indeks berkerumun karena membutuhkan nilai yang disimpan dalam LastID. Bagaimanapun, indeks ini digunakan oleh rencana eksekusi aktual:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
EDIT # 2:
Saya telah menerima saran yang diberikan @AaronBertrand dan memodifikasinya sedikit. Gagasan umum di sini adalah untuk memperbaiki pernyataan untuk menghilangkan penguncian yang tidak perlu, dan secara keseluruhan untuk membuat SP lebih efisien.
Kode di bawah ini menggantikan kode di atas dari BEGIN TRANSACTION
ke END TRANSACTION
:
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
Karena kode kita tidak pernah menambahkan catatan ke tabel ini dengan 0 di LastID
kita dapat membuat asumsi bahwa jika @NewID adalah 1 maka maksudnya adalah menambahkan ID baru ke daftar, kalau tidak kita memperbarui baris yang ada dalam daftar.
sumber
SERIALIZABLE
sini.Jawaban:
Pertama, saya akan menghindari melakukan perjalanan pulang pergi ke database untuk setiap nilai. Misalnya, jika aplikasi Anda tahu bahwa ia memerlukan 20 ID baru, jangan lakukan 20 perjalanan pulang pergi. Buat hanya satu panggilan prosedur yang tersimpan, dan tambahkan penghitung dengan 20. Juga mungkin lebih baik untuk membagi meja Anda menjadi beberapa.
Dimungkinkan untuk menghindari kebuntuan sama sekali. Saya tidak punya deadlock sama sekali di sistem saya. Ada beberapa cara untuk mencapai itu. Saya akan menunjukkan bagaimana saya akan menggunakan sp_getapplock untuk menghilangkan kebuntuan. Saya tidak tahu apakah ini akan bekerja untuk Anda, karena SQL Server adalah sumber tertutup, jadi saya tidak dapat melihat kode sumber, dan karena itu saya tidak tahu apakah saya telah menguji semua kemungkinan kasus.
Yang berikut ini menjelaskan apa yang berhasil untuk saya. YMMV.
Pertama, mari kita mulai dengan skenario di mana kita selalu mendapatkan kebuntuan yang cukup besar. Kedua, kita akan menggunakan menghilangkan sp_getapplock mereka. Poin paling penting di sini adalah untuk menguji stres solusi Anda. Solusi Anda mungkin berbeda, tetapi Anda perlu mengeksposnya ke konkurensi tinggi, seperti yang akan saya tunjukkan nanti.
Prasyarat
Mari kita buat tabel dengan beberapa data uji:
Dua prosedur berikut ini kemungkinan besar akan mengalami kebuntuan:
Kebuntuan mereproduksi
Loop berikut harus mereproduksi lebih dari 20 deadlock setiap kali Anda menjalankannya. Jika Anda mendapatkan kurang dari 20, tambah jumlah iterasi.
Dalam satu tab, jalankan ini;
Di tab lain, jalankan skrip ini.
Pastikan Anda memulai keduanya dalam beberapa detik.
Menggunakan sp_getapplock untuk menghilangkan kebuntuan
Ubah kedua prosedur, jalankan kembali loop, dan pastikan Anda tidak lagi memiliki deadlock:
Menggunakan tabel dengan satu baris untuk menghilangkan kebuntuan
Alih-alih memanggil sp_getapplock, kita dapat memodifikasi tabel berikut:
Setelah tabel ini dibuat dan diisi, kita dapat mengganti baris berikut
dengan yang ini, dalam kedua prosedur:
Anda dapat menjalankan kembali tes stres, dan lihat sendiri bahwa kami tidak memiliki kebuntuan.
Kesimpulan
Seperti yang telah kita lihat, sp_getapplock dapat digunakan untuk membuat serialisasi akses ke sumber daya lain. Dengan demikian dapat digunakan untuk menghilangkan kebuntuan.
Tentu saja, ini dapat memperlambat modifikasi secara signifikan. Untuk mengatasinya, kita perlu memilih perincian yang tepat untuk kunci eksklusif, dan jika memungkinkan, bekerja dengan set alih-alih baris individual.
Sebelum menggunakan pendekatan ini, Anda perlu melakukan stress test sendiri. Pertama, Anda harus memastikan Anda mendapatkan setidaknya beberapa lusin kebuntuan dengan pendekatan awal Anda. Kedua, Anda seharusnya tidak mendapatkan deadlock ketika Anda menjalankan kembali script repro yang sama menggunakan prosedur tersimpan yang dimodifikasi.
Secara umum, saya tidak berpikir ada cara yang baik untuk menentukan apakah T-SQL Anda aman dari kebuntuan hanya dengan melihatnya atau melihat rencana eksekusi. IMO satu-satunya cara untuk menentukan apakah kode Anda rentan terhadap deadlock adalah dengan mengeksposnya ke konkurensi tinggi.
Semoga berhasil dengan menghilangkan kebuntuan! Kami tidak memiliki kebuntuan dalam sistem kami sama sekali, yang sangat bagus untuk keseimbangan kehidupan kerja kami.
sumber
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
mencegah kebuntuan?Penggunaan
XLOCK
petunjuk padaSELECT
pendekatan Anda atau yang berikut iniUPDATE
harus kebal terhadap jenis kebuntuan ini:Akan kembali dengan beberapa varian lain (jika tidak dikalahkan!).
sumber
XLOCK
akan mencegah penghitung yang ada diperbarui dari banyak koneksi, tidakkah Anda perluTABLOCKX
untuk mencegah beberapa koneksi menambahkan penghitung baru yang sama?Mike Defehr menunjukkan kepada saya cara yang elegan untuk mencapai ini dengan cara yang sangat ringan:
(Untuk kelengkapan, berikut adalah tabel yang terkait dengan proc yang disimpan)
Ini adalah paket eksekusi untuk versi terbaru:
Dan ini adalah rencana eksekusi untuk versi asli (rentan kebuntuan):
Jelas, versi baru menang!
Sebagai perbandingan, versi perantara dengan
(XLOCK)
dll, menghasilkan paket berikut:Saya akan mengatakan itu kemenangan! Terima kasih atas bantuan semua orang!
sumber
SERIALIZABLE
tidak ada untuk mencegah hantu. Itu ada untuk memberikan semantik isolasi serializable , yaitu efek persisten yang sama pada database seolah-olah transaksi yang terlibat telah dieksekusi secara seri dalam beberapa urutan yang tidak ditentukan.Bukan untuk mencuri guntur Mark Storey-Smith, tetapi ia ke sesuatu dengan jabatannya di atas (yang notabene menerima paling banyak suara positif). Saran yang saya berikan kepada Max berpusat di sekitar "UPDATE set @variable = column = kolom + value" yang menurut saya sangat keren, tapi saya pikir mungkin tidak berdokumen (harus didukung, karena ada khusus untuk TCP tolok ukur).
Berikut adalah variasi jawaban Mark - karena Anda mengembalikan nilai ID baru sebagai rekaman, Anda dapat menghapus variabel skalar sepenuhnya, tidak ada transaksi eksplisit yang diperlukan juga, dan saya akan setuju bahwa bermain-main dengan tingkat isolasi tidak diperlukan demikian juga. Hasilnya sangat bersih dan cukup apik ...
sumber
Saya memperbaiki kebuntuan serupa dalam suatu sistem tahun lalu dengan mengubah ini:
Untuk ini:
Secara umum, memilih
COUNT
hanya untuk menentukan ada atau tidaknya cukup boros. Dalam hal ini karena 0 atau 1 tidak seperti itu banyak pekerjaan, tetapi (a) kebiasaan itu dapat berdarah ke dalam kasus-kasus lain di mana itu akan jauh lebih mahal (dalam kasus-kasus itu, gunakanIF NOT EXISTS
bukanIF COUNT() = 0
), dan (b) pemindaian tambahan sama sekali tidak perlu. ItuUPDATE
dasarnya melakukan pemeriksaan yang sama.Juga, ini sepertinya bau kode serius bagi saya:
Apa gunanya di sini? Mengapa tidak menggunakan kolom identitas atau mendapatkan urutan yang menggunakan
ROW_NUMBER()
pada waktu permintaan?sumber
IDENTITY
. Tabel ini mendukung beberapa kode lawas yang ditulis dalam MS Access yang akan cukup terlibat untuk retrofit. TheSET @NewID=
garis hanya akan menambahkan nilai yang disimpan dalam tabel untuk ID yang diberikan (tetapi Anda sudah tahu itu). Bisakah Anda memperluas bagaimana saya bisa menggunakanROW_NUMBER()
?LastID
sebenarnya berarti dalam model Anda. Apa tujuannya? Nama itu tidak terlalu jelas. Bagaimana Access menggunakannya?GetNextID('WhatevertheIDFieldIsCalled')
untuk mendapatkan ID berikutnya untuk digunakan, kemudian memasukkannya ke baris baru bersama dengan data apa pun yang diperlukan.