Spring Boot @Transactional: Advanced Transaction Propagation & Isolation SC Guide

ආයුබෝවන් යාළුවනේ! කොහොමද ඉතින්? අද අපි කතා කරන්න යන්නේ software development වල හරිම වැදගත්, ඒ වගේම ටිකක් සංකීර්ණ වෙන්න පුළුවන් මාතෘකාවක් ගැන – ඒ තමයි Transaction Management. විශේෂයෙන්ම අපි Spring Boot project වලදී කොහොමද මේ Transactions හරියට manage කරන්නේ කියන එකයි බලන්නේ. ගොඩක් වෙලාවට අපිට හුරුයි @Transactional
annotation එක use කරන්න. ඒත් ඒක ඇතුළේ තියෙන රහස් ටිකයි, ඒකෙන් අපිට ලැබෙන බලයයි ගැන හරියටම දන්නවද?
අද අපි බලමු @Transactional
වල තියෙන Propagation සහ Isolation Levels කියන advanced concepts දෙක මොනවද කියලා. මේ දෙක හරියට තේරුම් ගත්තොත් ඔයාලගේ applications වල data integrity එක උපරිමයෙන් maintain කරගන්න පුළුවන් වෙනවා විතරක් නෙවෙයි, performance එකත් optimize කරගන්න පුළුවන්.
Transactions කියන්නේ මොනවද? ඇයි මේවා වැදගත්?
ඔයාලා බැංකු ගිණුමකින් සල්ලි මාරු කරනවා කියලා හිතන්නකෝ. එක ගිණුමකින් සල්ලි අඩු වෙන්න ඕනේ, අනිත් ගිණුමට සල්ලි වැඩි වෙන්න ඕනේ. මේ දෙකම එකට වෙන්න ඕනේ, නැත්නම් එකක් විතරක් වෙන්න බැහැ. එකක් විතරක් වුණොත්? අර සල්ලි ටික වාතලෝකේ යයි, නැත්නම් නිකන්ම වැඩි වෙයි! මේ වගේ operations කීපයක් එකම unit එකක් විදියට handle කරන එක තමයි Transaction එකක් කියන්නේ.
Database transactions වලදී ප්රධාන ACID properties හතරක් තියෙනවා:
- Atomicity: Transaction එකක තියෙන හැම operation එකක්ම commit වෙන්න ඕනේ, නැත්නම් එකක්වත් commit වෙන්න බැහැ. ‘All or nothing’ කියන concept එක.
- Consistency: Transaction එකක් ඉවර වුණාම database එක consistent state එකක තියෙන්න ඕනේ. (Rules, constraints)
- Isolation: එක transaction එකක් අනිත් transactions වලට බලපාන්න බැහැ. ඒවා එකිනෙකින් වෙන් වෙලා, ಸ್ವತන්ත්රව execute වෙන්න ඕනේ.
- Durability: Transaction එකක් commit වුණාට පස්සේ, ඒ changes permanent වෙන්න ඕනේ, system crash වුණත් changes නැති වෙන්න බැහැ.
මේ properties ටික නිසා තමයි අපිට database එකක data integrity එක ගැන විශ්වාසය තියන්න පුළුවන් වෙන්නේ. විශේෂයෙන්ම E-commerce, Banking වගේ critical systems වලදී මේවා අත්යවශ්යයි.
@Transactional: මූලික දැනුම සහ බලය
Spring Framework එකේ @Transactional
annotation එක අපිට මේ Transaction Management එක හරිම ලේසියෙන් කරන්න උදව් කරනවා. ඔයාලා service layer එකක method එකක් උඩට මේ annotation එක දැම්මම, Spring එක automatically ඒ method එක database transaction එකක් ඇතුළේ execute කරනවා. Method එක සාර්ථකව ඉවර වුණොත් transaction එක commit කරනවා, exception එකක් ආවොත් rollback කරනවා.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public User createUser(User user) {
// Database operations go here
return userRepository.save(user);
}
@Transactional
public void transferFunds(Long fromAccountId, Long toAccountId, double amount) {
// Logic to deduct from one account and add to another
// If anything fails, the entire transaction will rollback
}
}
මේක හරිම පහසුයි නේද? ඒත් සමහර වෙලාවට අපිට මෙහෙම ප්රශ්නයක් එනවා. එක @Transactional
method එකක් ඇතුළෙන් තව @Transactional
method එකක් call කළොත් මොකද වෙන්නේ? එතනදි තමයි Transaction Propagation කියන concept එක වැදගත් වෙන්නේ.
Transaction Propagation Levels: Transactions එකිනෙකාට කතා කරන හැටි
Propagation කියන්නේ transactional context එකක් තියෙන method එකක්, තව transactional context එකක් තියෙන method එකක් call කරනකොට ඒ transaction එක හැසිරෙන විදිය. Spring Framework එකේ propagation levels කීපයක් තියෙනවා. අපි මේවා එකින් එක බලමු.
1. REQUIRED (Default)
මේක තමයි default setting එක. අලුතින් transaction එකක් ඕනෙද කියලා බලනවා. If a transaction exists, use it. If not, create a new one. හරිම සරලයි. ගොඩක් වෙලාවට මේක තමයි අපිට ඕනේ.
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional(propagation = Propagation.REQUIRED)
public void outerMethod() {
System.out.println("Outer method started.");
innerService.innerMethod(); // This will join the outer transaction
System.out.println("Outer method finished.");
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.REQUIRED)
public void innerMethod() {
System.out.println("Inner method started. Joining existing transaction.");
// Database operation
System.out.println("Inner method finished.");
}
}
මේ උදාහරණයේදී, outerMethod
එකේ transaction එක ඇතුලෙම innerMethod
එකත් execute වෙනවා. innerMethod
එක ඇතුලෙදී exception එකක් ආවොත්, outerMethod
එකේ transaction එකත් එක්කම rollback වෙනවා. ඒක හරිම වැදගත්.
2. REQUIRES_NEW
නමෙන් කියවෙන විදියටම, මේක හැමවිටම අලුත් transaction එකක් හදනවා. දැනට transaction එකක් තිබුණත් නැතත්, අලුත් transaction එකක් හදනවා. දැනට තියෙන transaction එක suspend කරනවා. මේක වැදගත් වෙන්නේ, අපිට පොඩි operation එකක් කරන්න ඕනෙ නම්, ඒක අනිත් transaction එකෙන් independent වෙන්න ඕනේ කියලා. උදාහරණයක් විදියට, log entry එකක් save කරනවා නම්. ඒක main transaction එක fail වුණත් save වෙන්න ඕනේ නේද?
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional
public void outerMethodWithNew() {
System.out.println("Outer method started. Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
// Some database operation for outer transaction
try {
innerService.logOperation(); // This will run in a NEW transaction
} catch (Exception e) {
System.out.println("Log operation failed, but outer transaction continues.");
}
// Another database operation for outer transaction
// throw new RuntimeException("Outer transaction failure"); // Uncomment to test outer rollback
System.out.println("Outer method finished. Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation() {
System.out.println("Log operation started in a NEW transaction. Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
// Save a log entry to database
// If this fails, only this new transaction rolls back, outer transaction is unaffected.
System.out.println("Log operation finished. Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
}
}
මෙතැනදී logOperation
එක fail වුණත්, outerMethodWithNew
එකේ transaction එකට බලපෑමක් වෙන්නේ නැහැ. ඒ වගේම outerMethodWithNew
එක fail වුණත් logOperation
එකේ changes commit වෙනවා.
3. SUPPORTS
මෙතැනදී, transaction එකක් තියෙනවා නම්, ඒකට join වෙනවා. නැත්නම්, non-transactionally execute වෙනවා. මේක හරිම ප්රවේසම් වෙලා use කරන්න ඕනේ, මොකද database operation එකක් non-transactionally run වුණොත්, error එකකදී rollback වෙන්නේ නැහැ.
4. NOT_SUPPORTED
මේක නිකන් SUPPORTS
එකේ අනිත් පැත්ත වගේ. Transaction එකක් තියෙනවා නම් ඒක suspend කරනවා, non-transactionally execute වෙනවා. මේකත් log වගේ operations වලට use කරන්න පුළුවන්, හැබැයි කිසිම transaction එකක් අවශ්ය නැති වෙලාවට.
5. NEVER
මේක නම් ටිකක් සැරයි. Transaction එකක් තියෙනවා නම් exception එකක් throw කරනවා. කිසිම වෙලාවක transaction එකක් ඇතුළේ execute වෙන්න බැරි method එකකට මේක දාන්න පුළුවන්.
6. MANDATORY
මේකත් ටිකක් සැරයි. Transaction එකක් අනිවාර්යයෙන්ම තියෙන්න ඕනේ. නැත්නම් exception එකක් throw කරනවා. External service එකක් call කරනකොට, ඒකට අනිවාර්යයෙන්ම transaction context එකක් යවන්න ඕනේ නම් මේක use කරන්න පුළුවන්.
7. NESTED
මේක තමයි ටිකක් සංකීර්ණම එක. මේක REQUIRED
වගේ existing transaction එකකට join වෙනවා. හැබැයි අලුත් savepoint එකක් හදනවා. මේකෙන් වෙන්නේ, NESTED
transaction එක fail වුණොත්, ඒකෙන් rollback වෙන්නේ savepoint එකට විතරයි. Main transaction එකට බලපෑමක් වෙන්නේ නැහැ. ඒත් Main transaction එක fail වුණොත්, NESTED
transaction එකත් rollback වෙනවා. මේක JDBC savepoints මත පදනම් වෙලා තියෙන්නේ. හැම database එකකම මේක support කරන්නේ නැහැ (e.g. MySQL doesn't fully support NESTED for all use cases, PostgreSQL does).
උදාහරණයක් විදියට:
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional
public void outerMethodWithNested() {
System.out.println("Outer method started.");
// Some operation
try {
innerService.nestedMethod(); // Runs as a nested transaction
} catch (Exception e) {
System.out.println("Nested method failed, but outer transaction continues from savepoint.");
}
// Another operation in outer transaction
// throw new RuntimeException("Outer transaction fails completely");
System.out.println("Outer method finished.");
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
System.out.println("Nested method started.");
// Database operation
// throw new RuntimeException("Nested transaction failure"); // Uncomment to test nested rollback
System.out.println("Nested method finished.");
}
}
මෙහිදී nestedMethod
එක fail වුණොත්, outerMethodWithNested
එක rollback වෙන්නේ නැහැ, ඒක continue කරනවා. ඒත් outerMethodWithNested
එක fail වුණොත්, nestedMethod
එකේ changes ටිකත් rollback වෙනවා.
Transaction Isolation Levels: concurrency ප්රශ්න විසඳන හැටි
අපි දන්නවා transactions එකිනෙකින් isolated වෙන්න ඕනේ කියලා. ඒත් කොච්චර isolated වෙන්න ඕනෙද කියන එක තමයි Isolation Level එකකින් තීරණය වෙන්නේ. වැඩිපුර isolation කියන්නේ වැඩිපුර performance overhead එකක්. ඒ නිසා අවශ්ය මට්ටමට isolation තෝරා ගැනීම හරිම වැදගත්.
Transactions එකිනෙකට බලපාන විදියට ඇතිවෙන පොදු ප්රශ්න තුනක් තියෙනවා:
- Dirty Reads: එක transaction එකක් commit නොකරපු data, අනිත් transaction එකකට කියවන්න පුළුවන් වීම. මේ data පස්සේ rollback වුණොත්, කියවපු data වැරදියි.
- Non-repeatable Reads: එක transaction එකක් ඇතුළේ එකම query එක දෙපාරක් run කරනකොට, result එක වෙනස් වීම. මේක වෙන්නේ අනිත් transaction එකක් මැදදී commit වීම නිසා.
- Phantom Reads: එක transaction එකක් ඇතුළේ එකම query එක දෙපාරක් run කරනකොට, දෙවෙනි පාරට අලුත් rows එනවා. මේක වෙන්නේ අනිත් transaction එකක් අලුත් rows add කරලා commit කිරීම නිසා.
මේ ප්රශ්න විසඳන්න විවිධ Isolation Levels තියෙනවා:
1. READ_UNCOMMITTED
අඩුම isolation level එක. මේක Dirty Reads වලට ඉඩ දෙනවා. performance එක උපරිමයි, නමුත් data integrity එක අවදානමේ. ගොඩක් වෙලාවට මේක use කරන්නේ නැහැ.
2. READ_COMMITTED (Most common default)
Dirty Reads නවත්වනවා. මේක තමයි ගොඩක් databases වල default isolation level එක (e.g. PostgreSQL, SQL Server, Oracle). Non-repeatable Reads සහ Phantom Reads වලට ඉඩ දෙනවා.
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUserById(Long id) {
// This will not read uncommitted changes from other transactions
return userRepository.findById(id).orElse(null);
}
3. REPEATABLE_READ
Dirty Reads සහ Non-repeatable Reads දෙකම නවත්වනවා. MySQL වල default isolation level එක. Phantom Reads වලට ඉඩ දෙනවා.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<Order> getOrdersForUser(Long userId) {
// If you query for orders multiple times within this transaction,
// you will get the same set of orders, preventing non-repeatable reads.
return orderRepository.findByUserId(userId);
}
4. SERIALIZABLE
උපරිම isolation level එක. මේක හැම concurrency ප්රශ්නයක්ම (Dirty Reads, Non-repeatable Reads, Phantom Reads) නවත්වනවා. හැබැයි performance එකට ලොකුම බලපෑම මේකෙන් වෙනවා, මොකද transactions serialized විදියට execute වෙන නිසා. concurrency එක හරිම අඩු වෙනවා. විශේෂයෙන්ම critical reports වගේ operations වලට use කරන්න පුළුවන්.
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calculateTotalRevenue() {
// Ensures that no other transactions can modify data while this one is running,
// providing a perfectly consistent view for critical calculations.
// High performance overhead expected.
return orderRepository.findAll().stream().map(Order::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
}
ඔයාලා මේවා තෝරගන්නකොට හොඳට හිතන්න ඕනේ. අවශ්යම නැත්නම් ඉහළ isolation level එකක් use කරන එකෙන් performance එකට හානි වෙන්න පුළුවන්. ගොඩක් වෙලාවට default එක (READ_COMMITTED) ඇති.
ප්රායෝගික අභ්යාසය: තනිවම අත්හදා බලමු!
දැන් මේ theory ටික අපි ප්රායෝගිකව කරලා බලමු. Spring Boot project එකක් හදලා, පොඩි Database (H2 database එකක් පාවිච්චි කරන්න, හරිම ලේසියි) එකක් connect කරලා මේවා test කරන්න පුළුවන්.
- Service Layer එකක් හදන්න: User Management system එකක් වගේ පොඩි දෙයක් හදන්න.
UserService
එකක්,UserRepository
එකක් වගේ. - Propagation test කරන්න:
UserService
එක ඇතුළේ method දෙකක් හදන්න. එක@Transactional
method එකක් ඇතුළෙන් තව@Transactional
method එකක් call කරන්න. (e.g.,createUserAndLog()
method එකක් ඇතුළේsaveUser()
සහlogAction()
). - Exception එකක් දාන්න:
logAction()
method එකේදීRuntimeException
එකක් throw කරලා බලන්න.Propagation.REQUIRED
දාලා test කරන්න.createUserAndLog()
එකත් rollback වෙනවා. REQUIRES_NEW
test කරන්න:logAction()
method එකටPropagation.REQUIRES_NEW
දාලා බලන්න.logAction()
එක fail වුණත්saveUser()
එක commit වෙනවාද කියලා බලන්න.- Isolation test කරන්න: User table එකක් හදලා, එකම User record එක read කරන methods දෙකක් හදන්න. ඒ methods වලට වෙන වෙනම
Isolation.READ_COMMITTED
සහIsolation.REPEATABLE_READ
දාලා බලන්න. - Concurrency test කරන්න: Multi-threading environment එකක් simulate කරලා (
ExecutorService
use කරලා) එකම record එකට different transactions වලින් access කරලා බලන්න. DB client දෙකක් ඇරලා එකකින් uncommitted changes දාලා, අනිත් එකෙන් read කරලා බලන්න (READ_UNCOMMITTED test කරන්න).
මේවා කරලා බලනකොට ඔයාලට මේ concepts තව දුරටත් පැහැදිලි වෙයි. මතක තියාගන්න, හොඳ software engineer කෙනෙක් වෙන්න නම් theory විතරක් මදි, ප්රායෝගිකව අත්හදා බැලීම් කිරීම අත්යවශ්යයි.
අවසාන වශයෙන්...
ඉතින් යාළුවනේ, අද අපි Spring Boot වල Advanced Transaction Management ගැන ගොඩක් දේවල් කතා කළා. Propagation Levels
වලින් අපිට පුළුවන් එකිනෙකට සම්බන්ධ transactions කොහොමද හැසිරෙන්නේ කියලා control කරන්න. ඒ වගේම Isolation Levels
වලින් අපිට පුළුවන් concurrent transactions අතර data consistency එක කොයි තරම් දුරට තියාගන්න ඕනෙද කියලා තීරණය කරන්න.
මේවා ටිකක් සංකීර්ණයි වගේ පෙනුනත්, මේවා හරිම වැදගත් concepts. විශේෂයෙන්ම ඔයාලා enterprise level applications හදනකොට, data integrity එකයි, performance එකයි දෙකම balance කරගන්න මේ දැනුම අත්යවශ්යයි. අනිවාර්යයෙන්ම මම කියපු විදියට පොඩි project එකක් හදලා මේවා test කරලා බලන්න. එතකොට ඔයාලට මේවා අමුතු දේවල් නෙවෙයි, හුරු පුරුදු දේවල් වෙයි.
මේ blog post එක ගැන ඔයාලගේ අදහස්, ප්රශ්න තියෙනවා නම් පහළින් comment section එකේ දාන්න. අපි ඊළඟ post එකෙන් තවත් මෙවැනිම වැදගත් මාතෘකාවකින් හම්බවෙමු! දැනට එච්චරයි! ජය වේවා!