در این مقاله، به بررسی دقیق مفهوم Concurrency Token، نحوه عملکرد آن در لایههای دسترسی به داده (مخصوصاً Entity Framework Core) و پیادهسازی آن با مثالهای کاربردی میپردازیم.
قبل از اینکه بگوییم توکن همزمانی چیست، باید بدانیم چه مشکلی را حل میکند. در پایگاه داده، وقتی دو کاربر همزمان یک رکورد را میخوانند و هر دو سعی میکنند آن را تغییر دهند، سناریوی زیر رخ میدهد:
کاربر الف موجودی حساب را میبیند: ۱۰۰ هزار تومان.
کاربر ب موجودی همان حساب را میبیند: ۱۰۰ هزار تومان.
کاربر الف ۲۰ هزار تومان واریز میکند و موجودی را به ۱۲۰ هزار تومان تغییر داده و ذخیره میکند.
کاربر ب ۱۰ هزار تومان برداشت میکند و بر اساس همان ۱۰۰ تومانی که دیده بود، موجودی را به ۹۰ هزار تومان تغییر داده و ذخیره میکند.
نتیجه فاجعهبار: واریز ۲۰ هزار تومانی کاربر الف کاملاً نادیده گرفته میشود و موجودی نهایی به جای ۱۱۰ هزار تومان، روی ۹۰ هزار تومان میماند. به این اتفاق Lost Update میگوییم.
دو راه کلی برای جلوگیری از این مشکل وجود دارد:
Pessimistic Locking (بدبینانه): به محض اینکه کاربر الف رکورد را خواند، دیتابیس آن را قفل میکند تا هیچکس دیگر حتی نتواند آن را بخواند. این کار سرعت سیستم را به شدت کاهش میدهد.
Optimistic Concurrency (خوشبینانه): ما به هیچکس قفل نمیدهیم، اما هنگام ذخیره کردن چک میکنیم که آیا دادهها از زمانی که کاربر آنها را خوانده، تغییر کردهاند یا خیر.
Concurrency Token ابزاری است که برای پیادهسازی روش خوشبینانه استفاده میشود.
Concurrency Token یک ویژگی (Property) در مدل دادهای شماست که نقش "برچسب وضعیت" را بازی میکند. وقتی میخواهید رکوردی را آپدیت کنید، پایگاه داده چک میکند که آیا مقدار این توکن هنوز همان مقداری است که در زمان "خواندن" بود یا خیر.
اگر مقدار تغییر کرده باشد، یعنی شخص دیگری در این فاصله رکورد را آپدیت کرده است. در این حالت، سیستم اجازه ذخیرهسازی نمیدهد و یک خطا (Exception) صادر میکند.
وقتی شما یک فیلد را به عنوان Concurrency Token معرفی میکنید، در پشت صحنه، دستور UPDATE در SQL تغییر میکند.
بدون توکن:
UPDATE Accounts SET Balance = 90000 WHERE Id = 1
با توکن:
UPDATE Accounts SET Balance = 90000 WHERE Id = 1 AND VersionToken = 'ABC'
اگر کاربر دیگری در این فاصله VersionToken را به 'XYZ' تغییر داده باشد، شرط WHERE برقرار نمیشود، هیچ ردیفی آپدیت نمیگردد و برنامه متوجه تداخل میشود.
در EF Core، شما به دو روش میتوانید یک ویژگی را به عنوان توکن همزمانی معرفی کنید:
روش اول: استفاده از ویژگیهای موجود
فرض کنید میخواهیم فیلد Email در کلاس کاربر، توکن ما باشد:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[ConcurrencyCheck] // این فیلد تبدیل به توکن همزمانی میشود
public string Email { get; set; }
}
روش دوم: استفاده از RowVersion (بهترین روش)
در SQL Server، نوع دادهای به نام rowversion وجود دارد که با هر بار تغییر رکورد، به صورت خودکار توسط خود دیتابیس تغییر میکند. این بهترین گزینه برای Concurrency Token است.
public class Product
{
public int Id { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
[Timestamp] // در دیتابیس به rowversion تبدیل میشود
public byte[] RowVersion { get; set; }
}
حالا ببینیم در کد برنامه چطور با این تداخل برخورد میکنیم. وقتی دو کاربر همزمان تلاش کنند، EF Core خطای DbUpdateConcurrencyException را پرتاب میکند.
try
{
var product = context.Products.First(p => p.Id == 1);
product.Price = 500;
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// اوه! یک تداخل رخ داده است.
foreach (var entry in ex.Entries)
{
if (entry.Entity is Product)
{
var proposedValues = entry.CurrentValues; // مقداری که ما میخواستیم ذخیره کنیم
var databaseValues = entry.GetDatabaseValues(); // مقداری که الان در دیتابیس است
if (databaseValues == null)
{
Console.WriteLine("این کالا توسط شخص دیگری حذف شده است.");
}
else
{
Console.WriteLine("این کالا توسط شخص دیگری ویرایش شده. دوباره تلاش کنید.");
// در اینجا میتوانید تصمیم بگیرید:
// ۱. مقدار جدید دیتابیس را به کاربر نشان دهید
// ۲. یا تغییرات خودتان را به زور اعمال کنید (Client Wins)
}
}
}
}
استفاده از این توکنها هزینه پردازشی کمی دارد، بنابراین وسوسهانگیز است که برای همه جداول استفاده شود. اما بهتر است اولویتبندی کنید:
حیاتی: جداول مالی، موجودی انبار، سیستمهای رزرو بلیط.
متوسط: پروفایل کاربری، تنظیمات اپلیکیشن.
غیرضروری: جداول لاگ (Log)، کامنتهای زیر پست (معمولاً تداخل در کامنتها بحران ایجاد نمیکند).
| روش | مزایا | معایب |
| RowVersion (Timestamp) | خودکار توسط دیتابیس مدیریت میشود و بسیار دقیق است. | وابسته به دیتابیس (SQL Server) است. |
| ConcurrencyCheck | روی هر فیلد دلخواهی قابل اجراست. | اگر فیلد تغییر نکند، تداخل تشخیص داده نمیشود. |
| Shadow Properties | کد کلاس مدل را شلوغ نمیکند. | پیادهسازی آن در Fluent API کمی پیچیدهتر است. |
Concurrency Token یک مکانیزم دفاعی برای حفظ یکپارچگی دادهها در محیطهای چندکاربره است. با استفاده از روش Optimistic Concurrency، شما به کاربران اجازه میدهید بدون معطلی و قفل شدن دیتابیس کار کنند، و تنها در لحظه نهایی از "بروزرسانیهای گمشده" جلوگیری میکنید. در اکثر پروژههای داتنتی، استفاده از ویژگی [Timestamp] به همراه مدیریت خطای DbUpdateConcurrencyException استانداردترین راه حل ممکن است.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.