عنوان:

‫جایگزینی برای راهنمای پرخطر NOLOCK در SQL Server


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۰۲ ۰۹:۰۵
آدرس: www.dntips.ir
یکی از پرتکرارترین چالش‌ها در سیستم‌های عملیاتی با ترافیک بالا (OLTP)، افت ناگهانی کارایی و قفل شدن پایگاه داده به‌دلیل همزمانی خواندن و نوشتن است. این راهنما مکانیزم‌های واقعی قفل‌گذاری، باورهای غلط رایج، و استراتژی‌های استاندارد مدیریت همزمانی را در SQL Server، PostgreSQL و MySQL بررسی می‌کند.

۱. مکانیزم همزمانی: مدل بدبینانه در برابر مدل چندنسخه‌ای
برای مدیریت درخواست‌های همزمان دو رویکرد اصلی وجود دارد:
  • مدل مبتنی بر قفل بدبینانه (Pessimistic Locking): فرض می‌کند تداخل حتماً رخ می‌دهد؛ بنابراین خوانندگان روی داده‌ها قفل اشتراکی (Shared Lock یا S-Lock) و نویسندگان قفل انحصاری (Exclusive Lock یا X-Lock) می‌گذارند. تا زمانی که نویسنده کارش تمام نشود، خواننده منتظر می‌ماند و برعکس.
  • مدل کنترل همزمانی چندنسخه‌ای (MVCC - Multi-Version Concurrency Control): فرض می‌کند خواندن نباید مانع نوشتن شود. وقتی داده‌ای ویرایش می‌شود، موتور پایگاه داده نسخه قبلی آن را در فضایی مجزا نگه می‌دارد. در نتیجه خوانندگان نسخه‌ی تأییدشده‌ی قبلی (Snapshot) را می‌خوانند و هیچ فرآیندی معطل دیگری نمی‌ماند.

۲. تصحیح ۴ باور غلط و پرخطر درباره قفل‌گذاری

باور غلط اول: کوئری‌های SELECT به‌طور پیش‌فرض کل جدول را قفل می‌کنند.
واقعیت: در سیستم‌های مدرن، دستور SELECT به‌صورت پیش‌فرض قفل‌های ریزدانه در سطح ردیف (Row) یا صفحه (Page) می‌گیرد و پس از خواندن بلافاصله آن‌ها را آزاد می‌کند. قفل شدن کل جدول تنها زمانی رخ می‌دهد که پدیده ترفیع قفل (Lock Escalation) فعال شود؛ یعنی زمانی که حجم ردیف‌های درگیر از یک آستانه (معمولاً حدود ۵۰۰۰ قفل خرد در SQL Server) عبور کند و سیستم برای جلوگیری از پر شدن حافظه، قفل‌ها را به سطح جدول ارتقا دهد.

باور غلط دوم: هینت WITH (NOLOCK) بهترین شروع برای تمام کوئری‌ها است.
واقعیت: استفاده کورکورانه از NOLOCK سطح جداسازی READ UNCOMMITTED را فعال می‌کند و خطرات جدی به همراه دارد:
  • خواندن کثیف (Dirty Read): خواندن داده‌های خامی که وسط یک تراکنش هستند و ممکن است با خطایی Rollback شوند.
  • ردیف‌های تکراری یا گم‌شده: اگر در لحظه خواندن داده، تقسیم صفحه داده‌ای (Page Split) رخ دهد، کوئری شما ممکن است یک سطر را دو بار بخواند یا کلاً نادیده بگیرد.

باور غلط سوم: تراکنش‌ها همیشه کل جدول را قفل می‌کنند.
واقعیت: تراکنش‌ها فقط روی ردیف‌ها و صفحات درگیر قفل انحصاری (X-Lock) اعمال می‌کنند.

باور غلط دوم (MySQL): دستور ALTER TABLE ... LOCK=NONE برای کوئری‌های SELECT است.
واقعیت: این دستور صرفاً برای تغییر ساختار آنلاین جدول (Online DDL) به کار می‌رود و ربطی به خواندن یا فیلتر کردن داده‌های روزمره ندارد.

۳. مقایسه تطبیقی رفتار موتورهای پایگاه داده

پایگاه دادهمکانیزم پیش‌فرض خواندنآیا نویسنده خواننده را مسدود می‌کند؟راهکار استاندارد خواندن بدون بلاک
PostgreSQLMVCC ذاتیخیررفتار پیش‌فرض بدون نیاز به تنظیم خاص
MySQL (InnoDB)MVCC ذاتی (Undo Logs)خیررفتار پیش‌فرض بدون نیاز به تنظیم خاص
SQL ServerLocking بدبینانهبله (در سطح پیش‌فرض)فعال‌سازی RCSI در سطح دیتابیس

۴. پیاده‌سازی راهکار استاندارد در SQL Server: فعال‌سازی RCSI
بهترین جایگزین برای هینت پرخطر NOLOCK در SQL Server، فعال‌سازی Read Committed Snapshot Isolation (RCSI) است. اینکار رفتار سطح جداسازی پیش‌فرض SQL Server یعنی READ COMMITTED را از حالت مبتنی بر قفل بدبینانه (Pessimistic Locking) به حالت مبتنی بر نسخه‌بندی ردیف‌ها (Row Versioning / Optimistic) تغییر می‌دهد. با فعال‌سازی این قابلیت، کوئری‌های خواندن (SELECT) دیگر قفل اشتراکی (S-Lock) نگرفته و منتظر پایان تراکنش‌های نوشتن نمی‌مانند؛ در عوض، آخرین نسخه تاییدشده (Committed) رکوردها را از فضای tempdb می‌خوانند.

۱. چک‌لیست و ارزیابی ریسک‌ها قبل از فعال‌سازی
پیش از فعال‌سازی RCSI در محیط Production، ارزیابی ۴ بعد کلیدی زیر ضروری است:
الف)فشار برtempdbو I/O دیسک (بزرگ‌ترین ریسک)
  • مکانیزم: هر بار که داده‌ای تغییر می‌کند (UPDATE یا DELETE)، نسخه قدیمی آن در Version Store واقع در tempdb ذخیره می‌شود.
  • اقدام پیشگیرانه: مطمئن شوید دیسک tempdb روی درایوهای بسیار سریع (NVMe SSD) قرار دارد، چندین دیتافایل هم‌اندازه برای آن ایجاد شده و فضای خالی کافی دارد.

ب) افزایش حجم ردیف‌ها و پدیده Page Split
  • مکانیزم: به ازای هر رکورد تغییریافته، ۱۴ بایت متادیتا (اشاره‌گر به نسخه قدیمی در tempdb) به انتهای ردیف اضافه می‌شود.
  • ریسک: در جداولی که طول ردیف به سقف اندازه صفحه (۸۰۶۰ بایت) نزدیک است، افزودن این ۱۴ بایت باعث تقسیم صفحه (Page Split) و ایجاد تکه‌تکه‌شدن ایندکس (Fragmentation) می‌شود.

ج) تغییر منطق رفتاری کدهای مبتنی بر قفل (Application Logic)
  • ریسک: اگر برنامه‌نویسان کدها را با فرض اینکه "یک SELECT مانع ویرایش همزمان رکورد می‌شود" نوشته باشند، فعال‌سازی RCSI باعث همزمانی ناخواسته و از دست رفتن داده (Lost Updates) می‌شود؛ مگر اینکه به صورت صریح از هینت‌هایی مثل WITH (UPDLOCK) استفاده شده باشد.

۲. مراحل صحیح و ایمن فعال‌سازی RCSI
برای فعال‌سازی RCSI پایگاه داده نیاز به قفل انحصاری (Exclusive Lock) دارد. اگر نشست‌های (Sessions) فعال باز باشند، دستور در صف انتظار متوقف می‌شود (Block می‌شود).

1.بررسی وضعیت فعلی دیتابیس:ابتدا مطمئن شوید آیا RCSI از قبل فعال است یا خیر:
SELECT name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
FROM sys.databases
WHERE name = 'YourDatabaseName';
اگر مقدار is_read_committed_snapshot_on برابر با 0 باشد، غیرفعال است.

2.فعال‌سازی با قطع آنی اتصالات مسدودکننده (توصیه‌شده):برای جلوگیری از مسدود شدن سیستم و اعمال سریع دستور، از ساختار ROLLBACK IMMEDIATE استفاده کنید (بهتر است در ساعات کم‌ترافیک انجام شود):
USE master;
GO

ALTER DATABASE YourDatabaseName
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
(گزینه ROLLBACK IMMEDIATE تمام تراکنش‌های باز جاری را لغو و اتصالات را قطع می‌کند تا قفل لازم بلافاصله به دست آید).

3.صحت‌سنجی فعال‌سازی:مجدداً وضعیت پایگاه داده را بررسی کنید تا تایید شود که مقدار به 1 تغییر یافته است:
SELECT name, is_read_committed_snapshot_on 
FROM sys.databases 
WHERE name = 'YourDatabaseName';

۳. مانیتورینگ عملکرد پس از فعال‌سازی
پس از فعال‌سازی، با کوئری‌های زیر بار tempdb و استفاده از Version Store را پایش کنید:
-- بررسی میزان فضای مصرفی Version Store در tempdb (بر حسب مگابایت)
SELECT 
    SUM(version_store_reserved_page_count) * 8 / 1024.0 AS VersionStore_MB
FROM sys.dm_db_file_space_usage;

-- شناسایی تراکنش‌هایی که بیشترین حجم نسخه را در tempdb باز نگه داشته‌اند
SELECT 
    at.transaction_id,
    at.name,
    at.transaction_begin_time,
    DATEDIFF(minute, at.transaction_begin_time, GETDATE()) AS active_minutes
FROM sys.dm_tran_active_snapshot_database_transactions ast
JOIN sys.dm_tran_active_transactions at ON ast.transaction_id = at.transaction_id
ORDER BY ast.elapsed_time_seconds DESC;

مقایسه RCSI با Snapshot Isolation کامل (ALLOW_SNAPSHOT_ISOLATION)
ویژگیREAD_COMMITTED_SNAPSHOT (RCSI)ALLOW_SNAPSHOT_ISOLATION (SI)
تغییر کد برنامهخیر (سطح پیش‌فرض READ COMMITTED ارتقا می‌یابد)بله (باید قبل از کوئری SET TRANSACTION ISOLATION LEVEL SNAPSHOT صدا زده شود)
سطح پایداری (Statement vs Transaction)در سطح هر Statement آخرین نسخه خوانده می‌شوددر سطح کل Transaction نسخه از زمان شروع تراکنش خوانده می‌شود
خطای Update Conflictخیر (پشت قفل نوشتن منتظر می‌ماند)بله (اگر دو تراکنش همزمان یک سطر را تغییر دهند خطای ۳۹۶۰ رخ می‌دهد)

معماری پایدار برای کوئری‌ها و گزارش‌های سنگین
برای پایگاه‌های داده عملیاتی پرفشار، تکیه بر تنظیمات قفل به‌تنهایی کافی نیست. راهکارهای قطعی مهندسی عبارتند از:
  • کوتاه نگه‌داشتن تراکنش‌ها: در بلاک‌های BEGIN TRAN ... COMMIT فقط دستورات ضروری نوشتن را قرار دهید و منطق پردازشی، فراخوانی وب‌سرویس‌ها یا محاسبات طولانی را خارج از تراکنش انجام دهید.
  • خرد کردن کوئری‌ها (Batching): در حذف یا ویرایش‌های میلیونی، داده‌ها را در دسته‌های ۵۰۰۰ تایی تغییر دهید تا از Lock Escalation جلوگیری شود.
  • جداسازی بار خواندن و نوشتن (Read Replicas): کوئری‌های سنگین گزارش‌گیری و هوش تجاری (BI) را از طریق قابلیت‌هایی مثل Always On Availability Groups به پایگاه‌های داده ثانویه (فقط‌خواندنی) هدایت کنید تا هیچ اثری بر عملکرد کاربران سامانه زنده نگذارند.