برنامه نویسی

برنامه نویسی

وبلاگ برنامه نویسی
برنامه نویسی

برنامه نویسی

وبلاگ برنامه نویسی

یک Transaction چیست؟

در یک application database، اغلب با موقعیتی روبرو می شوید که نیاز به اجرای دو یا چند فرمان SQL دارید، به طوریکه اگر یکی از عبارات اجرا نشود، آنگاه هیچ عبارت دیگری قادر به تغییر database نخواهد بود. مثال کلاسیک این مورد، انتقال پول از یک حساب بانکی به حساب دیگر است.

UPDATE Accounts SET Balance = Balance □ 10 WHERE Customer = 1;
UPDATE Accounts SET Balance = Balance + 10 WHERE Customer = 2;

اگر قرار بود عبارت اول SQL، اجرا شود و عبارت دوم SQL اجرا نشود، آنگاه 10 دلار از حساب مشتری اول کم می شود، اما هرگز به حساب مشتری دوم واریز نمی شود. وقتی 10 دلار به طور کلی ناپدید شود، زیاد جالب نیست.

یک راه برای مقابله با این مشکل، کنترل وضعتت در database access codeتان است. می توان این کار را توسط گرفتن System.Data.SqlClient.SqlException هنگام انجام database access، انجام دهید. اما این موقعیت آنقدر که ابتدا به نظر می رسد آسان نیست. ممکن است عبارات SQL در جاهایی اجرا نشود:

  • قبل از اینکه اولین عبارت SQL اجرا شود.
  • بعد از اینکه اولین عبارت SQL اجرا شود.
  • بعد از اینکه دومین عبارت SQL اجرا شود.

این بدین معناست که شما باید ترازهای هر دو حساب را قبل از اتمام انتقال تعیین کنید، و مشخص کنید تراز کنونی کدام حساب با تراز ابتدایی یکی نیست.، و یک عبارت UPDATE اجرا کنید.

این شرایط توسط این واقعیت که SQL Server یک محیط دارای چندین کاربر است، بدتر هم می شود. در هر زمان، کاربرهای دیگر ممکن است به جدول حسابها دسترسی پیدا کنند و اگر به آن جدولی که بین اجرا نشدن عبارت شما و تصحیحات جدول وجود دارد دسترسی پیدا کنند، به خوبی می توانند به داده های نامعتبر دسترسی پیدا کنند. این بسیار بد خواهد بود، و ممکن است به مشکل شدن پیدا کردن bugها در کد شما منجر شود. (پیدا کردن bugهای مربوط به داده های همسان سازی (synchronization) در طول چندین فرایند بسیار مشکل است، زیرا آنها نام مخصوص خود را دارند: Heisenbugها )

نظریه database مدرن، پیشنهاد می کند که در یک دنیای transaction بی نقص، یک database دارای یک سری از خصوصیاتی (property) باشد که تحت عنوان ACID شناخته شوند. این خصوصیات عبارتند از:

Atomic: همه عبارات موجوددر یک گروه باید اجرا شوند، یا هیچ عبارتی نباید اجرا شود.

Consistent: به طور طبیعی از atomic تبعیت می کند؛ گروهی از عبارات SQL باید یک database را از یک حالت شناخته شده شروع تا یک حالت شناخته شده پایان بگیرند. اگر عبارات اجرا شوند، database باید در حالت شناخته شده پایانی باشد. اگر عبارت اجرا نشود، database باید در حالت شناخته شده شروع باشد.

Isolated: گروهی از عبارات باید مستقل از هر گروه عبارت دیگری که همزمان در حال اجرا شدن هستند، اجرا شوند. اگر اینگونه نباشد، ممکن نیست گروههای عبارت consistent (به هم پیوسته) باشند. ممکن است حالت شناخته شده پایانی توسط یک کد که شما هیچ کنترل یا اطلاعی از آن ندارید، تغییر کند. این، یکی از مفاهیمی است که در تئوری عالی است، اما Isolation کلی، تاثیرات اجرایی مهمی در دنیایی واقعی دارد. در مورد اینکه SQL Server چگونه این را اجرا می کند، بعداً بیشتر توضیح داده خواهد شد.

Durable: وقتی گروهی از عبارات SQL اجرا می شوند، نتایج باید در یک رسانه دایم ذخیره شود، که اگر database درست بعد از اینکه گروهی از عبارات SQL اجرا می شوند، از بین بروند، بازگرداندن حالت database به نقطه ای که بعد از اینکه آخرین transaction به حافظه سپرده میشود، ممکن باشد.

 

در SQL Server، ACID بودن، توسط مفهوم transactionها مهیا می شود. Transaction راهی برای گروه بندی عبارات SQL است، طوری که وقتی اجرا می شوند، transaction از اصول ACID تبعیت می کند. یک transaction با یک اتصال به database فعال می شود و به تمامی فرمانهای اجرا شده در آن اتصال اعمال می شود، تا وقتی که transaction تمام شود. زمانی که شما یک transaction دارید، دو کار می توانید با آن بکنید. یا می توانید transaction را بعد از اتمام به حافظه database بسپارید، یا می توانید transactionرا رها کنید و تغییرات بوجود آمده درآنرا به حالت اول برگردانید.

طبق Transact-SQL، سه فرمان مهم برای کنترل یک transaction وجود دارد. TRANSACTION BEGIN: یک transaction را شروع می کند،

COMMIT TRANSACTION: transaction را به حافظه database می سپارد.

ROLLBACK TRANSACTION: transaction را به حالت اول برمی گرداند.

این عبارات ممکن است واقعاً کمی پیچیده باشند، می توانید به documentation سایت MSDN که راجع به این عبارات هستند رجوع کنید.

Transactionهای .NET

در .NET، transactionها با کلاس System.Data.SqlClient.SqlTransaction کنترل می شوند. بازهم یک transaction روی یک object SqlConnection وجود دارد و بدین ترتیب شما همه objectهای SqlCommand را با استفاده از آن اتصال ایجاد می کنید. بایید نگاهی به این مثال بیاندازیم.

public class TransactionDemo
{
public TransactionDemo()
{

}

[STAThread]
public static void Main()
{
Demo1();
}

private static void Demo1()
{
SqlConnection db = new SqlConnection("connstringhere");
SqlTransaction transaction;

db.Open();
transaction = db.BeginTransaction();
try
{
new SqlCommand("INSERT INTO TransactionDemo " +
"(Text) VALUES ('Row1');", db, transaction)
.ExecuteNonQuery();
new SqlCommand("INSERT INTO TransactionDemo " +
"(Text) VALUES ('Row2');", db, transaction)
.ExecuteNonQuery();
new SqlCommand("INSERT INTO CrashMeNow VALUES " +
"('Die', 'Die', 'Die');", db, transaction)
.ExecuteNonQuery();
transaction.Commit();
}
catch (SqlException sqlError)
{
transaction.Rollback();
}
db.Close();
}

همانطور که در مثال بالا دیدید، ابتدا به یک database متصل شویم. سپس، متد BeginTransaction ازconnection را فرا می خوانیم و مرجعی را به Object که برمی گرداند نگه می داریم. در این نقطه، connection، محدود به object SqlTransaction می شود که برگردانده شده. این بدین معنی است که هر SqlCommand اجرا شده در آن اتصال، در درون transaction خواهد بود. در بلوک (block) try{، می بینید که ما سه object SqlCommand ایجاد و اجرا کردیم. گرچه متوجه خواهید شد که در این مورد ما ازاین stringهای استفاده می کنیم:

constructor شی SqlCommand دارای یک overload می باشد که شی transaction را به عنوان پارامتر ورودی به آن پاس می دهیم.

دلیلش هم این است که SqlCommand object، نیاز به رد شدن از محدوده transaction به یک connection دارد. قصور در انجام این کار منجر به روی دادن خطای زمان اجرا خواهد شد. به نظر من، این، نقطه ضعف این مدل است، زیرا یک transaction کاملاً محدود به connection است، و SqlCommand باید قادر به بیرون کشیدنSqlTransaction object از SqlConnection باشد.

در مثال بالا، در فرمان نخستین SqlCommand کاملاً معتبر هستند و TransactionDemo در database وجود دارند. اما جدول CrashMeNow وجود ندارد. از آنجاییکه این جدول وجود ندارد، یک شی SqlException ، روی ExecuteNonQuery ، پرتاب خواهد شد. تشخیص اینکه داشتن یک transaction، یک خطای زمان اجرای استاندارد را، که مکانیزم را کنترل می کند، تعویض نمی کند. اگر فکر می کنید ممکن است عبارات شما اجرا نشوند، باید SqlException را catch کنید، و در بلوک catchتان، transaction را به حالت اول برگردانید.

دو عملیات وجود دارد که می توانید روی SqlTransaction object انجام دهید. Rollback، transaction را کنسل و تمامی تغییرات اعمال شده را لغو می کند. Commit، باعث نوشته شدن دایمی transaction در database می شود. هر دو عملیات، باعث اتمام transaction می شوند.

اگر کد بالا را اجرا کنید و به جدول TransactionDemo نگاه کنید، خواهید دید که هیچ ردیفی (row) اضافه نشده است، transaction ، بعد از روی دادن خطای زمان اجرا ،به حالت اول برگردانده شد. اگر خط مزاحم SQL را حذف و برنامه را دوباره اجرا و دوباره به آن نگاه کنید، خواهید دید که دو ردیف اضافه شده است. در واقع بدین معنی است که transaction فعال است.

Transactionهای پیشرفته و سطوح (level) isolation

اینجا، مرز transactionها نیست. همانطور که قبلاً در توصیف از خصوصیات ACID گفتم، transactionها، لزوماً با دقسق ترین تعریف isolated هماهنگ نیست. دلیلش این است که وقتی شما transaction را ایجاد می کنید، می توانید سطح isolation یک transaction را configure کنید.

چرا می خواهید این کار را بکنید؟ performance. در حالی که isolation در تئوری بسیار جالب است، کمونیزم همه اینگونه بود. در واقعیت، قفل کردن گروهی از ردیفها وقتی که یک transaction روی آن کار می کند، ممکن است به خاطر performance شدنی نباشد. لزوماً نباید هر reader را از خواندن یک جدول متوقف کنید، زیرا transaction شما همه چیز را قفل می کند.

برای سبک کردن این نگرانی، .NET دارای قابلیت تعیین کردن سطوح isolation هنگام ایجاد یک transaction است. انجام این کار فقط نیاز به supply کردن یک System.Data.IsolationLevel value به متد BeginTransaction است. Valueهای در دسترس برای SQL عبارتند از:

ReadUncommitted: این، isolation نیست. هر کسی می تواند داده های قرارداده شده در جدول یا داده های update شده ای را که بلافاصله بعد از اینکه عبارت SQL باعث تغییر می شود، بخواند. نیازی به commit نیست. ممکن است این موضوع به فرایندی منجر شود که دارای داده های قدیمی (out-of-date) باشد. ممکن است از ورژنی از داده هااستفاده کند که از جدول باز گردانده شده اند.

ReadCommitted: این، کمی isolate تر است. در این مورد، یک transaction می تواند فقط داده هایی از جدول بخواند که قبلاً commit شده اند. وقتی یک transaction می خواهد یک داده ها را update کند، نیاز به یک قفل(lock) مشترک(shared) روی آن داده ها دارد و (اگر با موفقیت قفل را بدست آورد) داده ها را update می کند.transactionهای خارج از آن transaction نمی توانند داده های آن جدول را update کنند. این، فقط کمی isolateتر است ، اما یک عبارت SQL که دوبار در یک transaction اجرا شده است، می تواند نتایج متفاوتی را بازگرداند، اگر transaction دیگری، داده هایی را که عبارات SQL در بین دو عبارت اجرا می کنند، تغییر و commit کند. این، سطح isolation پیش فرض برای SqlTransaction است.

RepeatableRead: آرام آرام isolate تر می شود. در این مورد، یک قفل مشترک، روی تمامی داده های query شده در یک transaction اعمال می شود. این بدین معناست که هیچ transaction دیگری نمی تواند داده های استفاده شده در transaction شما را تغییر دهند.

Serializable: قفلها روی دامنه های (range) جداولی که استفاده می کنید قرار می گیرند و اجازه نمی دهند کاربرهای دیگر داده های شما را تغییر دهند یا ردیفهای جدیدی اضافه کنند. این، isolateترین سطح isolation است.

در SQL Server 2005، یک سطح جدید isolation به نام " snapshot isolation" اضافه خواهد شد. در snapshot isolation، وقتی در یک transaction به ردیفها دسترسی پیدا می شود،version میشوند. این بدین معناست که زمانیکه یک transaction، به یک سری از valueها دسترسی پیدا می کند، ماندن آن valueها به همان صورت، تا زمانی که شما transaction را commit یا rollback کنید، تضمین می شود. Transactionهای دیگر که وسط transaction اول شروع شده اند، یک کپی از database اصلی می گیرند تا عملیات را روی آن انجام دهند. گرچه قبل از اینکه هر transaction تمام شود، SQL Server داده های اصلی را که داشتند رویشان عملیات انجام می گرفت را تست می کند تا مطمئن شود که همان داده های کنونی در database هستند. اگر اینگونه شود، transaction تمام می شود؛ وگرنه، transaction به همان حالت اول بازمی گردد و کاربر مجبور خواهد بود batch را دوباره امتحان کند.

نتیجه گیری

Transactionها، برای چندین چیز دیگر نیز مفید هستند. اولاً، راهی را جهت به حالت اول برگرداندن گروهی از عبارات SQL در اختیار می گذارد. به یاد داشته باشید که failure، می تواند بیشتر از یک error که برگردانده می شود، باشد. همچنین یک failure می تواند منطقی باشد – در مثال بالا، شاید حسابی که از آن پول منتقل می شد، پول کافی جهت انتقال ندارد. در این صورت، می توانید transaction را، هنگامی که به این واقعیت پی می برید، به حالت اول بازگردانید. دوماً، آنها راهی را برای isolate کردن داده هایی که رویشان کار می کند در اختیار می گذارد، به طوریکه دیگر در مورد surpriseها نگران نیستید. در همه موارد، باید سطح isolation را که واقعاً نیاز دارید امتحان کنید و از تاثیرات performance همه آنها آگاه باشید.

نظرات 2 + ارسال نظر
حسین چهارشنبه 10 مهر 1392 ساعت 15:11

واقعا دمت گرم
توضیحات قشنگ و جالبی بود.

[ بدون نام ] یکشنبه 26 آبان 1392 ساعت 00:15

عالی

برای نمایش آواتار خود در این وبلاگ در سایت Gravatar.com ثبت نام کنید. (راهنما)
ایمیل شما بعد از ثبت نمایش داده نخواهد شد