در این بخش به بررسی نحوه ست کردن تنظیمات Spring Transaction و چگونگی استفاده از Transactional@ و مشکلات آن میپردازیم
اساسا دو راه مجزا برای ست کردن تنظیمات Transaction ها وجود دارد :
- استفاده از Annotation
- AOP
که هر کدام مزایای خود را دارند
EnableTransactionManagement@ در Spring 3.1 معرفی شد که در کلاس Configuration@ استفاده میشود تا قابلیت Transaction فعال پوشش داده شود
@Configuration @EnableTransactionManagement public class PersistenceJPAConfig{ @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(){ //... } @Bean public PlatformTransactionManager transactionManager(){ JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory( entityManagerFactoryBean().getObject() ); return transactionManager; } }
* چنانچه از Spring Boot استفاده کنیم و در لیست وابستگی ها spring-data-* یا spring-tx وجود داشته باشد، بصورت پیش فرض قابلیت Transaction فعال است
قبل از Spring 3.1 با XML میتوانستیم Transaction را فعال کنیم :
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="myEmf" /> </bean> <tx:annotation-driven transaction-manager="txManager" />
بعد از اینکه قابلیت Transaction را فعال کردیم میتوانیم از Transactional@ در سطح کلاس و یا متد استفاده کنیم
@Service @Transactional public class FooService { //... }
با این Annotation میتوانیم برخی ویژگی ها را روی transaction ست کنیم مثل :
- propagation که نوع تراکنش را تعیین میکند
- Isolation که سطح تراکنش را تعیین میکند
- Timeout حداکثر زمان فعال ماندن تراکنش حین عملیات دیتابیس را ست میکند
- readOnly یک flag است که مشخص میکند عملیات دیتابیسی فوق تغییری در دیتا ایجاد نمیکند و فقط قرار است دیتا را دریافت کند
- Rollback قواعد roll back را برای تراکنش تعیین میکند
* بصورت پیش فرض عمل rollback برای تراکنش هایی که حین عملیات با خطا روبرو شوند و آن خطا گرفته نشود فعال است و خطاهایی که catch میشوند عمل rollback بصورت اتوماتیک انجام نمیشود البته ما میتوانیم عملیات rollback را برای exception ای بوسیله پارامترهای rollbackFor و noRollbackFor ست کنیم
مشکلات احتمالی :
Transaction ها و Proxy :
Spring برای تمامی کلاس هایی که دارای Transactional@ در سطح کلاس و یا متد هستند از Proxy استفاده میکند. Proxy به Spring این اجازه را میدهد که بتواند منطق Transaction را قبل و بعد از اجرای متد پیاده سازی کند (بیشتر برای شروع Transaction و Commit و Rollback کردن )
*اگر Transactional@ در سطح کلاس داشته باشیم فقط روی متدهای public تاثیر خواهد داشت
این نکته مهم است که به یاد داشته باشیم اگر یک Transactional Bean از یک اینترفیس پیاده سازی کند Proxy از نوع Dynamic Proxy خواهد بود و این بدین معنی است که Method Call های خارج از کلاس که از طریق پروکسی باشند جلوگیری خواهد شد (مطمئن نیستم باید صحت آن چک شود)
و هرگونه self-invocation call یا internal call باعث میشود هیچگونه transaction ای را ایجاد نکند حتی اگر با Transactional@ مشخص شده باشد
فرض کنید متدی داریم بنام callMethod و با Transactional@ علامت گذاری کردیم پیاده سازی آن توسط Proxy شبیه به کد زیر خواهد بود :
createTransactionIfNecessary(); try { callMethod(); commitTransactionAfterReturning(); } catch (exception) { completeTransactionAfterThrowing(); throw exception; }
نکته دیگر تنها متد های public باید Transactionale@ باشند و دیگر modifier ها توسط proxy نادیده گرفته خواهند شد حتی اگر از این annotation روی متد private یا protected استفاده کنیم Spring بدون خطایی آنها را نادیده خواهد گرفت
توصیه نمیشود از Transactional@ روی interface استفاده کنیم ولی در صورتی که آن اینترفیس یک Repository@ در Spring Data باشد مورد قبول است
ما میتوانیم Transactional@ را در یک کلاس تعریف کنیم و تنظیمات Transaction موجود در Interface یا Super class پدر را Override کنیم
@Service @Transactional public class TransferServiceImpl implements TransferService { @Override public void transfer(String user1, String user2, double val) { // ... } }
Transaction Propagation :
تنظیمات مربوط به propagation منطق در اختیار گرفتن Transaction را مشخص میکند که بر چند نوع است. Spring با توجه به Propagation ست شده بوسیله TransactionManager::getTransaction از Transaction جاری استفاده میکند یا یکی جدید ایجاد میکند
انواع Propagation :
REQUIRED propagation :
propagation بصورت پیش فرض روی REQUIRED ست شده است یعنی اگر در Transactional@ پارامتری برای propagation ست نشده بود REQUIRED در نظر گرفته میشود که در این صورت Spring ابتدا چک میکند که آیا Transaction فعالی وجود دارد یا خیر و اگر وجود نداشت یکی ایجاد میکند
@Transactional(propagation = Propagation.REQUIRED) public void requiredExample(String user) { // ... }
چون پیش فرض روی REQUIRED ست شده میتوانیم آنرا ننویسیم :
@Transactional public void requiredExample(String user) { // ... }
کد شبیه سازی منطق REQUIRED :
if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } return createNewTransaction();
SUPPORTS Propagation :
در این حالت ابتدا Spring چک میکند که آیا Transaction فعالی وجود دارد آنرا استفاده میکند و اگر وجود نداشت بصورت non-transactional کد اجرا خواهد شد
@Transactional(propagation = Propagation.SUPPORTS) public void supportsExample(String user) { // ... }
شبیه سازی SUPPORTS :
if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } return emptyTransaction;
MANDATORY Propagation :
وقتی Transaction موجود باشد آنرا استفاده میکند و اگر موجود نباشد Spring یک Exception پرتاب میکند
@Transactional(propagation = Propagation.MANDATORY) public void mandatoryExample(String user) { // ... }
شبیه سازی MANDATORY :
if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } throw IllegalTransactionStateException;
NEVER Propagation :
در این حالت اگر Transaction فعالی موجود باشد یک Exception پرتاب خواهد شد
@Transactional(propagation = Propagation.NEVER) public void neverExample(String user) { // ... }
شبیه سازی NEVER :
if (isExistingTransaction()) { throw IllegalTransactionStateException; } return emptyTransaction;
NOT_SUPPORTED Propagation :
اگر Transaction جاری فعال باشد آنرا غیر فعال میکند و کد را بدون Transaction اجرا میکند
@Transactional(propagation = Propagation.NOT_SUPPORTED) public void notSupportedExample(String user) { // ... }
* برای پیاده سازی این حالت اگر از JTATransactionManager استفاده شود تعلیق Transaction بصورت واقعی و پشت پرده انجام میشود و در غیر اینصورت این حالت بدین صورت شبیه سازی میشود که یک نمونه از Transaction موجود نگهداری میشود و آن از Thread مربوطه حذف میشود
REQUIRED_NEW Propagation :
Transaction جاری در صورتی که فعال باشد تعلیق شده و یکی جدید ایجاد میشود
@Transactional(propagation = Propagation.REQUIRES_NEW) public void requiresNewExample(String user) { // ... }
* مشابه حالت NOT_SUPPORTED برای تعلیق Transaction به JTATransactionManager نیاز داریم
شبیه سازی REQUIRED_NEW :
if (isExistingTransaction()) { suspend(existing); try { return createNewTransaction(); } catch (exception) { resumeAfterBeginException(); throw exception; } } return createNewTransaction();
NESTED Propagation :
Spring چک میکند اگر Transaction موجود بود ابتدا یک Save Point ایجاد میکند که در صورتی که اجرای کد همراه با Exception بود Transaction با Rollback در آن Save Point دیتا را ذخیره کند و اگر Transaction فعالی وجود نداشت همانند REQUIRED رفتار خواهد کرد
@Transactional(propagation = Propagation.NESTED) public void nestedExample(String user) { // ... }
* DataSourceTransactionManager از این Propagation حمایت میکند
* همچنین برخی پیاده سازی های JTATransactionManager ممکن است این Propagation را ساپورت کنند
* JpaTransactionManager از NESTED در مواقعی که از JDBC Connection استفاده میکنیم حمایت میکند و اگر مقدار nestedTransactionAllowed روی true بود و JDBC استفاده شده از ویژگی savepoint حمایت میکرد امکان استفاده از NESTED در JPA Trasaction بوسیله دسترسی ای که JDBC فراهم میکرد قابل استفاده میشد
Transaction Isolation :
isolation یکی از ویژگی های ACID است (Atomicity, Consistency, Isolation, Durability)
isolation مشخص میکند که چگونه تغییرات حال حاضر Transaction توسط دیگر Transaction ها قابل دیدن باشد به طور جزیی تر، هنگام عملیات CRUD روی یک رکورد اطلاعات باید از دیگر Transaction ها ایزوله باشد و نوع ایزوله شدن را ما میتوانیم تعیین کنیم
هر سطح از isolation میتواند عوارض جانبی ای برای دیگر Trasnaction های همزمان ایجاد کند
تغییر دادن Isolation Level :
ما میتوانیم Transaction Isolation Level را بدین صورت تغییر دهیم :
@Transactional(isolation = Isolation.SERIALIZABLE)
* اهمیت Isolation بیشتر در سیستم هایی که تعداد تراکنش بالا و همزمان دارند کاملا مشهود است و میتوانیم سطح آنرا از از کم به زیاد تعیین کنیم و حتی دیتاهای Commit نشده هم کنترل داشته باشیم
هر چقدر سطح isolation قوی تر باشد یکتایی اطلاعات مطمئن تر خواهد بود و در این صورت Locking هم روی رکورد ها خواهیم داشت
اصطلاحات Side Effect Isolation :
- Dirty read : تغییرات Commit نشده قابل خواندن باشد این یعنی چون دیتا Commit نشده احتمال Rollback وجود دارد و اصطلاحا select ما کثیف است و ممکن است با select دیگری دیتا متفاوتی دریافت کنیم و یا نکنیم
- Nonrepeatable read : دریافت مقادیر متفاوتی از یک رکورد در دو select از دو Transaction همزمان هنگام update شدن رکورد، یک select موقعی که دیتا commit نشده بوده زده شده و مقدار قدیمی را دریافت کرده و select دوم بعد از commit شدن زده شده و دیتای متفاوتی از select اول دریافت کرده است
- Phantom read : به select ای گفته میشود که نتیجه آن در اثر تغییرات row های دیگر یا اضافه شدن row جدید، تغییر کند اصطلاحا میگوییم phantom read اتفاق افتاده است مثلا (*)select COUNT گرفته ایم که همزمان رکورد جدیدی اضافه شده است و نتیجه select اول را تحت اشعاع قرار داده است
سطوح Isolation ای که روی Transaction در بدنه Transactional@ قابل تغییر است بر چند نوع است :
DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
DEFAULT :
مقدار پیش فرض روی DEFAULT ای که RDBMS به عنوان وضعیت پیش فرض تعیین کرده است ست میشود و موقع انتخاب یا تغییر RDBMS باید به این نکته دقت کنیم که Isolation پیش فرض چه چیزی میباشد
** موقعی که یک سلسله متد های Transactional@ داریم که Isolation های متفاوتی دارند باید دقت کنیم که هنگامی که Transaction ساخته میشود همان Isolation تعریف شده را ست میکند و به هر دلیلی اگر بخواهیم متد Transactional ما با Isolation متفاوت از چیزی که برایش تعریف شده اجرا نشود مقدار TransactionManager::setValidateExistingTransaction را باید true کنیم
شبه کد آن بدین شکل است :
if (isolationLevel != ISOLATION_DEFAULT) { if (currentTransactionIsolationLevel() != isolationLevel) { throw IllegalTransactionStateException } }
READ_UNCOMMITED :
پایینترین نوع Isolation است و هر سه side effect را ایجاد میکند و عملا هیچ قفلی روی رکورد ها اعمال نمیشود اگر تراکنش های همزمانی داشته باشیم یکی میتواند رکورد را آپدیت کند دیگری میتواند آنرا تغییر دهد و دیگری آنرا بخواند قفل از اینکه Commit شود
@Transactional(isolation = Isolation.READ_UNCOMMITTED) public void log(String message) { // ... }
* دیتابیس Postgres از READ_UNCOMMITED پشتیبانی نمیکند و اگر روی این حالت ست شود اتوماتیک سطح READ_COMMITED انتخاب میشود همچنین Oracle هم اجازه READ_UNCOMMITED را نمیدهد و پشتیبانی هم نمیکند
READ_COMMITED :
حالتی است که از عوارض جانبی Dirty read جلوگیری میکند ولی بقیه side effect ها ممکن است اتفاق بیوفتند بنابراین تغییراتی که هنوز commit نشده اند تاثیری در select ما نخواهند داشت و اگر دوباره select بگیریم ممکن است تفاوت داشته باشد چون اطلاعات commit شده هم جز کوئری خواهند بود
@Transactional(isolation = Isolation.READ_COMMITTED) public void log(String message){ // ... }
* READ_COMMITED حالت پیش فرض در Oracle , Postgres و SQL Server است
REPEATABLE_READ :
از Dirty read و Non-Repeatable read جلوگیری میکند و تغییرات commit نشده تاثیری در تراکنش های همزمان ندارد به عبارتی یک رکورد قفل میشود و اجازه نمیدهد تراکنش دیگری وارد آن شود تا زمانی که کار تراکنش قبلی به اتمام برسد، تراکنش های ثانویه باید منتظر بمانند تا تراکنش قبلی کارش تمام شود حتی دسترسی select را هم نمیدهیم این باعث میشود در حین چند update درستی اطلاعات تضمین شود ولی همچنان Phantom read را داریم
@Transactional(isolation = Isolation.REPEATABLE_READ) public void log(String message){ // ... }
* Mysql بصورت پیش فرض روی REPEATABLE_READ تنظیم شده است . Oracle از REPEATABLE_READ پشتیبانی نمیکند
SERIALIZABLE :
بالاترین سطح Isolation است که حتی از Phantom read هم جلوگیری میکند و قفل روی table اعمال میشود اما بسیار پر هزینه است و اجازه دسترسی همزمان transaction ها به حداقل خود میرسد و دسترسی ها متوالی انجام میشود و احتمال time out شدن بالا میرود
@Transactional(isolation = Isolation.SERIALIZABLE) public void log(String message){ // ... }
تراکنش های Read-Only :
Read-Only flag اغلب گیج کننده است مخصوصا موقعی که با JPA کار میکنیم چون در این حالت میتوان تراکنش های insert و update را انجام داد ولی هیچ تضمینی برای commit شدن آن وجود ندارد
read-only بودن فقط در transaction context معنی پیدا میکند و خارج از آن نادیده گرفته میشود مانند این حالت که روی متد ست شده باشد :
@Transactional( propagation = Propagation.SUPPORTS,readOnly = true )
در این حالت transaction ای ایجاد نخواهد شد و طبیعتا readOnly بودن نادیده گرفته میشود
Log گرفتن Transaction :
روشی مفید برای فهمیدن مشکلات مربوط به Transaction ست کردن درست Log Level برای پکیج های transactional است که در Spring در آدرس org.springframework.transaction قرار گرفته است که میبایست روی TRACE تنظیم شود