مدیریت همزمانی (Concurrency Management) در Entity Framework Core
مقدمه: چرا مدیریت همزمانی اهمیت دارد؟
در یک دنیای ایدهآل، کاربران یکی پس از دیگری دادهها را ویرایش میکنند. اما در دنیای واقعی، موقعیتی به نام Race Condition رخ میدهد. فرض کنید دو کاربر (آرش و سارا) همزمان قصد دارند موجودی یک حساب بانکی که ۱۰۰ واحد است را تغییر دهند:
-
آرش موجودی را میخواند (۱۰۰).
-
سارا موجودی را میخواند (۱۰۰).
-
آرش ۱۰ واحد اضافه میکند و ذخیره میکند (۱۱۰).
-
سارا ۲۰ واحد کم میکند و ذخیره میکند (۸۰).
در اینجا تغییرات آرش کاملاً از بین میرود! این پدیده را Lost Update مینامند. برای جلوگیری از این مشکل، ما از مکانیزمهای مدیریت همزمانی استفاده میکنیم.
انواع مدلهای مدیریت همزمانی
بهطور کلی دو رویکرد اصلی برای مدیریت همزمانی وجود دارد:
۱. همزمانی خوشبینانه (Optimistic Concurrency)
در این مدل، ما فرض میکنیم تداخل بهندرت رخ میدهد. بنابراین، رکوردی را قفل نمیکنیم. هنگام ذخیرهسازی، بررسی میکنیم که آیا دادهها از زمانی که ما آنها را خواندهایم تغییر کردهاند یا خیر. اگر تغییر کرده باشند، یک خطا صادر میشود.
-
مزایا: کارایی بالا، عدم قفل شدن دیتابیس.
-
معایب: نیاز به مدیریت خطا در کد برنامه.
۲. همزمانی بدبینانه (Pessimistic Concurrency)
در این مدل، به محض اینکه کاربری دادهای را برای ویرایش میخواند، آن رکورد قفل میشود تا کار تمام شود.
-
مزایا: تضمین عدم تداخل.
-
معایب: کاهش شدید کارایی (Performance) و احتمال وقوع Deadlock.
مدیریت همزمانی خوشبینانه در EF Core
EF Core بهصورت پیشفرض از الگوی "Last-in-Wins" استفاده میکند، یعنی آخرین تغییر، تغییرات قبلی را بازنویسی میکند. برای تغییر این رفتار، باید از Concurrency Tokens استفاده کنیم.
روش اول: استفاده از ویژگی [ConcurrencyCheck]
شما میتوانید روی هر فیلدی که میخواهید حساس به همزمانی باشد، این ویژگی را قرار دهید.
public class Product
{
public int Id { get; set; }
[ConcurrencyCheck]
public string Name { get; set; }
public decimal Price { get; set; }
}
وقتی این فیلد را تغییر میدهید، EF Core در دستور UPDATE خود، مقدار قدیمی را در بخش WHERE چک میکند:
UPDATE Products SET Name = @NewName
WHERE Id = @Id AND Name = @OldName;
اگر سطر مربوطه تغییر کرده باشد، هیچ ردیفی ویرایش نمیشود و EF Core یک استثنا از نوع DbUpdateConcurrencyException پرتاب میکند.
روش دوم: استفاده از RowVersion یا Timestamp (بهترین روش)
در SQL Server، نوع دادهای به نام rowversion وجود دارد که با هر تغییر در سطر، دیتابیس بهصورت خودکار مقدار آن را عوض میکند. این بهترین راه برای مدیریت همزمانی است.
در کلاس مدل:
public class Employee
{
public int Id { get; set; }
public string FullName { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
با استفاده از Fluent API:
modelBuilder.Entity()
.Property(e => e.RowVersion)
.IsRowVersion();
مدیریت استثنای DbUpdateConcurrencyException
وقتی تداخلی رخ میدهد، شما باید تصمیم بگیرید که چه اتفاقی بیفتد. سه استراتژی رایج وجود دارد:
-
Database Wins: تغییرات کاربر فعلی نادیده گرفته شود و دادههای دیتابیس حفظ شود.
-
Client Wins: تغییرات کاربر فعلی به زور روی دیتابیس اعمال شود (مشابه رفتار پیشفرض).
-
Merging: مقادیر با هم مقایسه شوند و فقط فیلدهایی که تداخل ندارند ادغام شوند.
مثال کد برای مدیریت خطا:
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
// سطر توسط کاربر دیگری حذف شده است
throw new Exception("آیتم مورد نظر حذف شده است.");
}
// در اینجا میتوانید منطق دلخواه برای ادغام را بنویسید
// مثلاً: Client Wins
entry.OriginalValues.SetValues(databaseValues);
await _context.SaveChangesAsync();
}
همزمانی بدبینانه (Pessimistic Concurrency) در EF Core
EF Core مستقیماً API خاصی برای قفل کردن سطرها (مانند SELECT FOR UPDATE) ندارد، اما میتوانید از تراکنشها و SQL خام استفاده کنید.
استفاده از Transaction Isolation Levels
شما میتوانید سطح انزوای تراکنش را روی RepeatableRead یا Serializable قرار دهید:
using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
try
{
var account = await _context.Accounts.FindAsync(1);
account.Balance -= 100;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
}
نکته: استفاده از Serializable قفلهای سنگینی روی جداول ایجاد میکند و فقط در موارد بسیار حساس مالی توصیه میشود.
مقایسه روشها
|
ویژگی |
Concurrency Check |
RowVersion/Timestamp |
Pessimistic Locking |
|
پیچیدگی پیادهسازی |
متوسط |
ساده |
بالا |
|
فشار بر دیتابیس |
کم |
بسیار کم |
زیاد |
|
دقت |
محدود به فیلد خاص |
کل سطر |
کل تراکنش |
|
بهترین کاربرد |
سناریوهای خاص |
اکثر پروژههای تجاری |
سیستمهای بانکی حساس |
بهترین تمرینها (Best Practices)
-
همیشه از RowVersion استفاده کنید: اگر از SQL Server استفاده میکنید، استفاده از byte[] با ویژگی [Timestamp] استانداردترین روش است.
-
مدت زمان تراکنش را کوتاه نگه دارید: هر چه تراکنش طولانیتر باشد، احتمال بروز بنبست (Deadlock) بیشتر است.
-
به کاربر اطلاع دهید: در صورت بروز تداخل، به جای نمایش یک خطای گنگ، به کاربر بگویید که دادهها توسط شخص دیگری تغییر کرده و دکمهای برای بازنشانی (Reload) قرار دهید.
-
از تراکنشهای صریح (Explicit Transactions) استفاده کنید: زمانی که چندین عملیات وابسته به هم دارید، حتماً از BeginTransaction استفاده کنید تا یکپارچگی دادهها حفظ شود.
نتیجهگیری
مدیریت همزمانی در EF Core انتخابی بین کارایی و دقت است. در اکثر اپلیکیشنهای وب، Optimistic Concurrency با استفاده از RowVersion بهترین تعادل را ایجاد میکند. این روش اجازه میدهد سیستم بدون قفلهای سنگین مقیاسپذیر بماند و در عین حال از فساد دادهها جلوگیری کند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.