Spring Boot Deadlocks විසඳමු: Database & Application Deadlocks Sinhala Guide
ආයුබෝවන් කට්ටියට! අද අපි කතා කරන්න යන්නේ Spring Boot applications develop කරන අපිට, විශේෂයෙන්ම concurrent systems එක්ක වැඩ කරනකොට නිතරම වගේ මුහුණ දෙන්න වෙන පොදු ගැටලුවක් ගැන – ඒ තමයි Deadlocks.
Deadlocks කියන්නේ application එකක් run වෙනකොට ඇතිවෙන්න පුළුවන් serious issue එකක්. මොකද මේ නිසා අපේ application එකේ performance අඩු වෙන්න, users ලාට respond කරන්න බැරිව යන්න, සමහර විට සම්පූර්ණයෙන්ම crash වෙන්නත් පුළුවන්. ඉතින් මේ Deadlocks මොනවද, ඒවා ඇතිවෙන්නේ කොහොමද, සහ ඒවා හඳුනාගෙන විසඳගන්නේ කොහොමද කියලා මේ ලිපියෙන් අපි විස්තරාත්මකව බලමු. අපි practical code උදාහරණ එක්ක මේක තවදුරටත් පැහැදිලි කරගමු.
Deadlocks කියන්නේ මොනවද?
සරලව කිව්වොත්, Deadlock එකක් කියන්නේ එකිනෙකා බලාපොරොත්තුවෙන් ඉන්න resource දෙකක් හෝ වැඩි ගාණක් නිසා, කිසිම දෙයක් ඉදිරියට කරගෙන යන්න බැරිව යන තත්වයක්. නිකන් හිතන්න පාරවල් දෙකක් හමුවෙන තැනක වාහන දෙකක් ඉන්නවා කියලා. වාහන A ට යන්න ඕන වාහන B ඉන්න තැනට, වාහන B ට යන්න ඕන වාහන A ඉන්න තැනට. දැන් දෙන්නටම එහා පැත්තට යන්න බෑ, මොකද අනිත් වාහනේ ඉවත් වෙනකන් දෙන්නම බලාගෙන ඉන්නවා. මේක තමයි Deadlock එකක් කියන්නේ.
Software Engineering වලදී මේ 'resources' වෙන්න පුළුවන් database records, files, memory locations, හෝ වෙනත් shared objects වගේ දේවල්. 'වාහන' වෙන්න පුළුවන් different threads, processes, හෝ database transactions.
Deadlock එකක් ඇතිවෙන්න ප්රධාන කොන්දේසි හතරක් (Coffman conditions) තියෙනවා:
- Mutual Exclusion (අන්යෝන්ය බැහැර කිරීම): Resource එකක් එකවර එක process එකකට පමණක් පාවිච්චි කරන්න පුළුවන් වෙන්න ඕන. (e.g., database record එකක් එක transaction එකකට විතරක් lock කරන්න පුළුවන්)
- Hold and Wait (අල්ලාගෙන සිටීම සහ බලා සිටීම): Process එකක් එක resource එකක් අල්ලාගෙන ඉඳිමින් තවත් resource එකක් නිදහස් වෙනකන් බලාගෙන ඉන්නවා.
- No Preemption (අත්පත් කරගැනීමක් නොමැතිකම): Resource එකක් process එකකින් බලහත්කාරයෙන් ගන්න බෑ. ඒ process එක ඉවර වෙනකන්ම ඒ resource එක release කරන්නේ නෑ.
- Circular Wait (චක්රීය පොරොත්තු): Process A, Process B විසින් අල්ලාගෙන ඉන්න resource එකක් බලාගෙන ඉන්නවා. Process B, Process C විසින් අල්ලාගෙන ඉන්න resource එකක් බලාගෙන ඉන්නවා. ඒ වගේම Process C, Process A විසින් අල්ලාගෙන ඉන්න resource එකක් බලාගෙන ඉන්නවා. (නැත්නම් Process B, Process A විසින් අල්ලාගෙන ඉන්න resource එකක් බලාගෙන ඉන්නවා.) මේක තමයි උඩ කියපු වාහන උදාහරණය.
මේ කොන්දේසි හතරම එකවර සපුරාලුනොත්, Deadlock එකක් ඇතිවෙන්න පුළුවන්.
Spring Boot Application එකක Deadlock එකක් හැදෙන්නේ කොහොමද?
අපේ Spring Boot application එකක Deadlock එකක් ඇතිවෙන්න ප්රධාන හේතු දෙකක් තියෙනවා:
- Database Deadlocks: මේවා තමයි ගොඩක්ම පොදු. Database එකේ rows, tables, හෝ indexes වලට concurrent access කරනකොට මේවා ඇතිවෙන්න පුළුවන්. Transaction එකක් row එකක් lock කරගෙන ඉද්දී, අනිත් transaction එක තව row එකක් lock කරගෙන, එකිනෙකාට අවශ්ය row එක release වෙනකන් බලන් ඉන්නකොට Deadlock එකක් හැදෙනවා.
- Application-level Deadlocks: මේවා Java code එකේම
synchronizedblocks හෝLockobjects වැරදි විදියට පාවිච්චි කරනකොට ඇතිවෙන්න පුළුවන්. අපි Thread දෙකක් එකිනෙකාට අවශ්ය shared resources දෙකක් ගන්න උත්සාහ කරද්දී, වැරදි order එකකට ගත්තොත් Deadlock එකක් ඇතිවෙන්න පුළුවන්.
ප්රායෝගික උදාහරණයක්: Deadlock එකක් සාදා ගැනීම (Creating a Deadlock)
අපි හිතමු අපිට බැංකු ගිණුම් කළමනාකරණය කරන Spring Boot application එකක් තියෙනවා කියලා. ඒකේ Account කියන Entity එකක් තියෙනවා. අපි දැන් උත්සාහ කරන්නේ ගිණුම් දෙකක් අතර මුදල් මාරු කරන්න (transfer funds). Deadlock එකක් ඇතිවෙන්නේ කොහොමද කියලා බලමු.
1. Entity එක හදාගනිමු: Account.java
package com.example.deadlockdemo.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double balance;
public Account() {
}
public Account(Long id, double balance) {
this.id = id;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", balance=" + balance +
'}';
}
}
2. Repository එක හදාගනිමු: AccountRepository.java
package com.example.deadlockdemo.repository;
import com.example.deadlockdemo.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
}
3. Deadlock ඇතිකරන Service එක: AccountService.java
මේ service එකේ අපි method දෙකක් හදනවා. මේ method දෙකම කරන්නේ ගිණුම් දෙකක් අතර මුදල් මාරු කරන එක තමයි. නමුත්, මේවා ගිණුම් lock කරගන්නා අනුපිළිවෙල (order of locking) වෙනස්. මේක තමයි Deadlock එකක් ඇතිවෙන්න ප්රධාන හේතුව.
package com.example.deadlockdemo.service;
import com.example.deadlockdemo.entity.Account;
import com.example.deadlockdemo.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
public void createAccount(Long id, double initialBalance) {
if (accountRepository.findById(id).isEmpty()) {
Account account = new Account(id, initialBalance);
accountRepository.save(account);
System.out.println("Created account: " + account);
}
}
public long countAccounts() {
return accountRepository.count();
}
// --- DEADLOCK SCENARIO METHODS ---
// Scenario 1: Locks fromAccount (ID 1) then toAccount (ID 2)
@Transactional
public void transferFundsDeadlockScenario1(Long fromAccountId, Long toAccountId, double amount) {
System.out.println(Thread.currentThread().getName() + ": Started transfer from " + fromAccountId + " to " + toAccountId);
// Get and implicitly lock fromAccount
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new RuntimeException("Account " + fromAccountId + " not found"));
// Introduce an artificial delay to increase the chance of a deadlock
try {
System.out.println(Thread.currentThread().getName() + ": Acquired lock on Account " + fromAccountId + ", waiting to acquire " + toAccountId);
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Transfer interrupted", e);
}
// Get and implicitly lock toAccount
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new RuntimeException("Account " + toAccountId + " not found"));
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient funds in account " + fromAccountId);
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
System.out.println(Thread.currentThread().getName() + ": Successfully transferred " + amount +
" from " + fromAccountId + " to " + toAccountId);
}
// Scenario 2: Locks toAccount (ID 2) then fromAccount (ID 1)
@Transactional
public void transferFundsDeadlockScenario2(Long fromAccountId, Long toAccountId, double amount) {
System.out.println(Thread.currentThread().getName() + ": Started transfer from " + fromAccountId + " to " + toAccountId);
// Get and implicitly lock toAccount (Reverse order)
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new RuntimeException("Account " + toAccountId + " not found"));
// Introduce an artificial delay to increase the chance of a deadlock
try {
System.out.println(Thread.currentThread().getName() + ": Acquired lock on Account " + toAccountId + ", waiting to acquire " + fromAccountId);
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Transfer interrupted", e);
}
// Get and implicitly lock fromAccount (Reverse order)
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new RuntimeException("Account " + fromAccountId + " not found"));
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient funds in account " + fromAccountId);
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
System.out.println(Thread.currentThread().getName() + ": Successfully transferred " + amount +
" from " + fromAccountId + " to " + toAccountId);
}
}
4. Deadlock එක Trigger කරන Controller එක: AccountController.java
දැන් අපි AccountService එකේ method දෙක, වෙන වෙනම Threads දෙකක එකවර run කරලා Deadlock එකක් ඇතිකරන හැටි බලමු.
package com.example.deadlockdemo.controller;
import com.example.deadlockdemo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/init")
public String initializeAccounts() {
// Initialize accounts if they don't exist
if (accountService.countAccounts() == 0) {
accountService.createAccount(1L, 1000.0);
accountService.createAccount(2L, 1000.0);
return "Accounts 1 and 2 initialized.";
}
return "Accounts already exist.";
}
@PostMapping("/deadlock-test")
public String testDeadlock() {
// Ensure accounts are initialized
initializeAccounts();
// Start two concurrent threads to create a deadlock scenario
new Thread(() -> {
try {
accountService.transferFundsDeadlockScenario1(1L, 2L, 100.0);
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error in Scenario 1: " + e.getMessage());
}
}, "Thread-A").start();
new Thread(() -> {
try {
accountService.transferFundsDeadlockScenario2(2L, 1L, 100.0);
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error in Scenario 2: " + e.getMessage());
}
}, "Thread-B").start();
return "Deadlock scenario initiated. Check your application logs!";
}
}
ඔබ මේ application එක run කරලා /accounts/deadlock-test endpoint එකට request එකක් යැව්වොත් (e.g., Postman වලින් POST request එකක්), ටික වෙලාවකින් logs වල ඔබට Deadlock error එකක් දකින්න පුළුවන්. Database එක අනුව මේ error message එක වෙනස් වෙන්න පුළුවන්. (උදා: MySQL වල Deadlock found when trying to get lock)
මෙහිදී වෙන්නේ මෙහෙමයි:
- Thread-A:
Account 1lock කරනවා, ඊටපස්සේAccount 2lock කරන්න උත්සාහ කරනවා. - Thread-B:
Account 2lock කරනවා, ඊටපස්සේAccount 1lock කරන්න උත්සාහ කරනවා.
Thread.sleep(50) දාලා තියෙන්නේ Deadlock එකක් ඇතිවෙන probability එක වැඩි කරන්න. මේ නිසා Thread-A, Account 1 අල්ලගෙන ඉන්නකොට Thread-B, Account 2 අල්ලගෙන ඉඳලා, එකිනෙකාට අවශ්ය අනිත් Account එක release වෙනකන් බලන් ඉන්නවා. මේක තමයි Circular Wait condition එක.
Deadlock එකක් හොයාගන්නේ කොහොමද?
Deadlock එකක් වුණාම ගොඩක් වෙලාවට අපිට logs වල මෙහෙම error එකක් වගේ පේන්න පුළුවන්:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in file [...]
### The error occurred while committing a transaction.
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
හෝ
org.springframework.dao.PessimisticLockingFailureException: Deadlock detected;
nested exception is org.hibernate.PessimisticLockException: Deadlock detected
මේ වගේ errors දකිනකොට අපිට තේරෙන්න ඕන Deadlock එකක් ඇතිවෙලා කියලා. මේවා හඳුනාගන්න තව ක්රම කිහිපයක් තියෙනවා:
- Application Logs: උඩ පෙන්නපු වගේ
Deadlock found,PessimisticLockingFailureExceptionවැනි errors සොයන්න. - Database Monitoring Tools: MySQL Workbench, pgAdmin, SQL Server Management Studio වැනි tools වලට Deadlock detection tools තියෙනවා. ඒවායින් Deadlock graphs පවා පෙන්නන්න පුළුවන්.
- Java Thread Dumps:
jstackcommand එක පාවිච්චි කරලා JVM එකේ running threads වල dump එකක් ගන්න පුළුවන්. ඒකෙන්WAITINGහෝBLOCKEDstates වල තියෙන threads සහ ඒවා බලාගෙන ඉන්න locks මොනවද කියලා බලාගන්න පුළුවන්.
Deadlocks විසඳන්නේ කොහොමද?
Deadlocks වළක්වා ගන්න හෝ ඒවා නිරාකරණය කරගන්න ක්රම කිහිපයක් තියෙනවා. මේවායින් සමහරක් Deadlock එකක් ඇතිවෙන එක වළක්වන අතර, සමහරක් Deadlock එකක් ඇතිවුණොත් recover වෙන්න උදව් කරනවා.
1. Consistent Locking Order (ස්ථාවර Lock Order එකක් පාවිච්චි කිරීම)
මේක තමයි Deadlocks වළක්වා ගන්න තියෙන වැදගත්ම ක්රමය. අපි ගිණුම් lock කරගන්නකොට හැමවිටම එකම අනුපිළිවෙලකට (e.g., Account ID අනුව, කුඩාම ID එක මුලින්) lock කරනවා නම්, Circular Wait condition එක ඇතිවෙන්නේ නෑ. මොකද හැම thread එකක්ම එකම order එකට resources ඉල්ලන නිසා.
ප්රායෝගික උදාහරණයක්: Deadlock එකක් විසඳීම (Fixing a Deadlock)
අපි කලින් හදපු AccountService එකේ transferFunds method එක මේ විදියට වෙනස් කරමු:
package com.example.deadlockdemo.service;
import com.example.deadlockdemo.entity.Account;
import com.example.deadlockdemo.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
// ... (createAccount, countAccounts methods remain the same)
// --- FIXED TRANSFER METHOD ---
@Transactional
public void transferFundsFixed(Long fromAccountId, Long toAccountId, double amount) {
System.out.println(Thread.currentThread().getName() + ": Started FIXED transfer from " + fromAccountId + " to " + toAccountId);
// Ensure a consistent locking order: always lock the account with the smaller ID first
Long firstAccountId = Math.min(fromAccountId, toAccountId);
Long secondAccountId = Math.max(fromAccountId, toAccountId);
// Retrieve and implicitly lock accounts in a consistent order
Account firstAccount = accountRepository.findById(firstAccountId)
.orElseThrow(() -> new RuntimeException("Account " + firstAccountId + " not found"));
Account secondAccount = accountRepository.findById(secondAccountId)
.orElseThrow(() -> new RuntimeException("Account " + secondAccountId + " not found"));
// Determine which account is 'from' and 'to' based on the original request
Account actualFromAccount = (firstAccountId.equals(fromAccountId)) ? firstAccount : secondAccount;
Account actualToAccount = (secondAccountId.equals(toAccountId)) ? secondAccount : firstAccount;
if (actualFromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient funds in account " + fromAccountId);
}
actualFromAccount.setBalance(actualFromAccount.getBalance() - amount);
actualToAccount.setBalance(actualToAccount.getBalance() + amount);
// Save both. Spring Data JPA will handle updates within the transaction.
accountRepository.save(firstAccount);
accountRepository.save(secondAccount);
System.out.println(Thread.currentThread().getName() + ": Successfully completed FIXED transfer of " + amount +
" from " + fromAccountId + " to " + toAccountId);
}
}
දැන් AccountController එකේ testDeadlock method එක වෙනුවට, මේ transferFundsFixed method එක පාවිච්චි කරන්න:
package com.example.deadlockdemo.controller;
import com.example.deadlockdemo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
// ... (initializeAccounts method remains the same)
@PostMapping("/fixed-test")
public String testFixedTransfer() {
initializeAccounts();
// Start two concurrent threads with the fixed transfer logic
new Thread(() -> {
try {
accountService.transferFundsFixed(1L, 2L, 100.0);
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error in Fixed Scenario 1: " + e.getMessage());
}
}, "Thread-C").start();
new Thread(() -> {
try {
accountService.transferFundsFixed(2L, 1L, 100.0);
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error in Fixed Scenario 2: " + e.getMessage());
}
}, "Thread-D").start();
return "Fixed transfer scenario initiated. Check your application logs!";
}
}
දැන් ඔබ /accounts/fixed-test endpoint එකට request එකක් යැව්වොත්, Deadlock එකක් ඇතිවෙන එක වළකිනවා. මොකද Threads දෙකම මුලින්ම Account ID 1 Lock කරගන්න බලනවා. එක thread එකකට ඒක ගන්න පුළුවන් වුණාම, අනිත් thread එක ඒක release වෙනකන් ඉන්නවා, ඒත් Deadlock වෙන්නේ නෑ.
2. Shorter Transactions (කෙටි Transactions)
Transactions හැකි තරම් කෙටි කරන්න. Transaction එකක් වැඩි වෙලාවක් Lock එකක් අල්ලාගෙන ඉන්නකොට, Deadlock එකක් ඇතිවෙන්න තියෙන ඉඩකඩ වැඩි වෙනවා. අනවශ්ය operations transaction එකෙන් එළියට ගන්න.
3. Retries with Backoff (Retries සහ Backoff)
Database Deadlock එකක් වුණොත්, සමහර වෙලාවට transaction එක retry කරන එක හොඳ විසඳුමක්. Deadlock එකක් ඇතිවුණාම database එක විසින් එක් transaction එකක් 'Deadlock Victim' එකක් විදියට තෝරගෙන ඒක rollback කරනවා. අපිට පුළුවන් මේ rollback වුණු transaction එක ටික වෙලාවක් ඉඳලා ආයෙත් execute කරන්න (retry with backoff).
Spring Framework එකේ Spring Retry project එක මේ සඳහා පාවිච්චි කරන්න පුළුවන්. pom.xml එකට මේ dependency එක එකතු කරන්න:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
Application එකේ main class එකේ @EnableRetry annotation එක දාන්න:
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableRetry
public class DeadlockDemoApplication {
public static void main(String[] args) {
SpringApplication.run(DeadlockDemoApplication.class, args);
}
}
දැන් service method එකට @Retryable annotation එක එකතු කරන්න. Database එකේ deadlock error එක අල්ලගන්න පුළුවන් exception එකක් specify කරන්න (e.g., DeadlockLoserDataAccessException for Spring Data JPA/JDBC or PessimisticLockingFailureException for Hibernate specific lock exceptions):
package com.example.deadlockdemo.service;
import com.example.deadlockdemo.entity.Account;
import com.example.deadlockdemo.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DeadlockLoserDataAccessException; // Import the correct exception
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
// ... (other methods)
@Retryable(value = DeadlockLoserDataAccessException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
@Transactional
public void transferFundsWithRetry(Long fromAccountId, Long toAccountId, double amount) {
System.out.println(Thread.currentThread().getName() + ": Attempting transfer with retry from " + fromAccountId + " to " + toAccountId);
// The fixed logic from transferFundsFixed goes here
Long firstAccountId = Math.min(fromAccountId, toAccountId);
Long secondAccountId = Math.max(fromAccountId, toAccountId);
Account firstAccount = accountRepository.findById(firstAccountId)
.orElseThrow(() -> new RuntimeException("Account " + firstAccountId + " not found"));
Account secondAccount = accountRepository.findById(secondAccountId)
.orElseThrow(() -> new RuntimeException("Account " + secondAccountId + " not found"));
Account actualFromAccount = (firstAccountId.equals(fromAccountId)) ? firstAccount : secondAccount;
Account actualToAccount = (secondAccountId.equals(toAccountId)) ? secondAccount : firstAccount;
if (actualFromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient funds in account " + fromAccountId);
}
actualFromAccount.setBalance(actualFromAccount.getBalance() - amount);
actualToAccount.setBalance(actualToAccount.getBalance() + amount);
accountRepository.save(firstAccount);
accountRepository.save(secondAccount);
System.out.println(Thread.currentThread().getName() + ": Successfully completed transfer with retry of " + amount +
" from " + fromAccountId + " to " + toAccountId);
}
}
දැන් transferFundsWithRetry method එක call කළොත්, Deadlock එකක් ඇතිවුණොත් Spring Retry විසින් transaction එක maxAttempts ගණනක් backoff delay එකක් එක්ක retry කරනවා.
4. Pessimistic Locking (SELECT ... FOR UPDATE)
වඩාත් critical operations සඳහා, database එකේ row එකක් lock කරන්න පුළුවන්. Spring Data JPA වලදී @Lock(LockModeType.PESSIMISTIC_WRITE) annotation එක මේ සඳහා පාවිච්චි කරන්න පුළුවන්. මේකෙන් database එකට SELECT ... FOR UPDATE query එකක් යවනවා. ඒකෙන් අදාල row එක transaction එක ඉවර වෙනකන්ම අනිත් transactions වලට lock කරනවා.
// AccountRepository.java
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdWithPessimisticLock(Long id);
}
// In your service method:
@Transactional
public void transferFundsWithPessimisticLock(Long fromAccountId, Long toAccountId, double amount) {
Account fromAccount = accountRepository.findByIdWithPessimisticLock(fromAccountId).orElseThrow();
Account toAccount = accountRepository.findByIdWithPessimisticLock(toAccountId).orElseThrow();
// ... (rest of transfer logic)
}
මෙහිදීත් Locking Order එක වැදගත්. හැමවිටම එකම Order එකකට Locks ගන්න එක Deadlocks වළක්වනවා.
5. Timeout Mechanisms (කාල සීමා)
Database server එකේ transaction timeouts configure කරන එකත් වැදගත්. Deadlock එකක් detect කරලා විසඳන්න බැරි වුණොත්, transaction එකක් අසීමිතව බලාගෙන ඉන්නේ නැතුව timeout වෙලා rollback වෙන්න මේකෙන් උදව් වෙනවා.
6. Avoid Nested Locks (Nested Locks වලින් වැළකීම)
synchronized blocks හෝ Lock objects එකිනෙකා ඇතුලේ (nested) පාවිච්චි කරන එක පුළුවන් තරම් අඩු කරන්න. මේවා නිසා code එකේ complexity එක වැඩිවෙලා Deadlocks ඇතිවෙන්න තියෙන ඉඩකඩ වැඩි වෙනවා.
නිගමනය
අද අපි Deadlocks කියන්නේ මොනවද, ඒවා Spring Boot application එකක ඇතිවෙන්නේ කොහොමද, ඒ වගේම ඒවා හොයාගෙන විසඳගන්නේ කොහොමද කියලා විස්තරාත්මකව කතා කළා. Deadlocks කියන්නේ concurrent systems වලදී ඇතිවෙන්න පුළුවන් ඉතා සංකීර්ණ ගැටලුවක් වුණත්, consistent locking order, shorter transactions, retries, pessimistic locking වගේ techniques හරහා අපිට ඒවා හොඳින් manage කරගන්න පුළුවන්.
ඔබේ Spring Boot applications වලදී මේ Deadlock handling techniques පාවිච්චි කරලා, stable සහ high-performing systems develop කරන්න පුළුවන් කියලා මම හිතනවා. මතක තියාගන්න, Deadlocks වළක්වන එක, ඒවා ඇතිවුණාට පස්සේ fix කරනවට වඩා පහසුයි.
ඔබේ අත්දැකීම් කොහොමද? ඔබ Deadlocks විසඳන්න වෙනත් මොනවා හරි ක්රම පාවිච්චි කරලා තියෙනවද? පහත comment section එකේ ඔබේ අදහස් සහ අත්දැකීම් බෙදාගන්න!