⚖️ ءOptimistic Locking vs Pessimistic Locking
فرض کن دو کاربر همزمان موجودی یک کیف پول را تغییر میدهند.
موجودی اولیه:
Balance = 1000
کاربر اول میخواهد 700 تومان برداشت کند.
کاربر دوم همزمان میخواهد 500 تومان برداشت کند.
اگر هر دو درخواست مقدار
1000 را بخوانند:User A reads: 1000
User B reads: 1000
User A writes: 300
User B writes: 500
نتیجه:
Final Balance = 500
اما در واقع باید فقط یکی از برداشتها موفق شود؛ چون موجودی برای هر دو کافی نیست.
این مشکل یکی از نمونههای معروف Race Condition و Lost Update است.
اینجا دو رویکرد مهم داریم:
Optimistic Locking
Pessimistic Locking
🟢 ءOptimistic Locking چیست؟
در Optimistic Locking فرض میکنیم برخورد همزمانی معمولاً کم است.
پس هنگام خواندن داده، آن را قفل نمیکنیم.
بهجای قفلکردن، هنگام ذخیره بررسی میکنیم:
آیا دادهای که من خواندم هنوز همان نسخهی قبلی است؟
اگر در این فاصله شخص دیگری داده را تغییر داده باشد، عملیات شکست میخورد و باید:
دوباره داده را بخوانیم؛
تصمیم را از نو محاسبه کنیم؛
یا خطای Conflict به کاربر بدهیم.
مثال با Version
فرض کن رکورد حساب اینطور است:
AccountId = 10
Balance = 1000
Version = 5
کاربر A رکورد را میخواند:
Balance = 1000
Version = 5
کاربر B هم همین نسخه را میخواند:
Balance = 1000
Version = 5
کاربر A ذخیره میکند:
UPDATE Accounts
SET Balance = 300,
Version = 6
WHERE AccountId = 10
AND Version = 5;
این Query موفق میشود.
حالا کاربر B میخواهد ذخیره کند:
UPDATE Accounts
SET Balance = 500,
Version = 6
WHERE AccountId = 10
AND Version = 5;
اما دیگر رکوردی با
Version = 5 وجود ندارد.پس:
Affected Rows = 0
یعنی:
داده در فاصلهی خواندن تا ذخیره تغییر کرده است.
این همان Optimistic Concurrency Conflict است.
🧠 نکتهی مهم
ءOptimistic Locking الزاماً به معنی استفاده از یک ستون به نام
Version نیست.میتوان از موارد زیر هم استفاده کرد:
ء
rowversion در SQL Server؛ءTimestamp یا Version Number؛
مقدار Hash؛
بررسی مقدار قبلی چند ستون؛
شرط روی
UpdatedAt؛ءConcurrency Tokenء در EF Core.
اما برای تشخیص Conflict، باید یک مقدار قابلاعتماد برای مقایسه داشته باشیم.
🟦 ءOptimistic Locking
در SQL Server میتوان از
rowversion استفاده کرد:public sealed class Account
{
public int Id { get; set; }
public decimal Balance { get; set; }
public byte[] RowVersion { get; set; } = [];
}
پیکربندی:
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Account>()
.Property(x => x.RowVersion)
.IsRowVersion();
}
حالا EF Core هنگام Update، مقدار
RowVersion قبلی را در شرط قرار میدهد.اگر شخص دیگری رکورد را تغییر داده باشد، EF Core معمولاً با این Exception مواجه میشود:
DbUpdateConcurrencyException
🔴 ءPessimistic Locking چیست؟
در Pessimistic Locking فرض میکنیم برخورد همزمانی محتمل است.
پس از همان ابتدا داده را قفل میکنیم.
یعنی:
تا وقتی من در حال کار روی این رکورد هستم، دیگران نباید بتوانند آن را به شکل ناسازگار تغییر دهند.
مثلاً:
Transaction A:
Lock Account 10
Read Balance = 1000
Withdraw 700
Commit
Release Lock
Transaction B:
Wait for Account 10
Read Balance = 300
Withdraw 500
Reject
در این روش، کاربر دوم باید منتظر آزادشدن Lock بماند.
مثال SQL Server با
UPDLOCKیک الگوی رایج در SQL Server:
BEGIN TRANSACTION;
SELECT Balance
FROM Accounts WITH (UPDLOCK, ROWLOCK)
WHERE Id = 10;
-- Validate balance
-- Update balance
UPDATE Accounts
SET Balance = Balance - 700
WHERE Id = 10;
COMMIT;
ءUPDLOCK باعث میشود SQL Server هنگام خواندن، قفل Update بگیرد تا عملیات تغییر همزمان کنترل شود.اما باید دقت کرد:
ءLock تا پایان Transaction باقی میماند؛
ءTransaction باید کوتاه باشد؛
ءLock طولانی میتواند باعث Blocking شود؛
چند Lock ناسازگار میتوانند Deadlock ایجاد کنند.
🎯 مثال واقعی: ویرایش پروفایل کاربر
فرض کن دو کاربر یا دو Tab، پروفایل یک نفر را باز کردهاند.
نسخهی اولیه:
Name = Milad
Version = 3
Tab A:
Name = Milad Heidarpour
Version = 3
Tab B:
Name = Milad Developer
Version = 3
اگر Tab A زودتر ذخیره کند:
Version = 4
Tab B دیگر نباید بیخبر تغییرات Tab A را Overwrite کند.
با Optimistic Locking:
Tab B → 409 Conflict
و UI میتواند بگوید:
این اطلاعات توسط کاربر دیگری تغییر کرده است. لطفاً دادهی جدید را بررسی کنید.