Conteúdo do Curso
Spring Boot Backend
Spring Boot Backend
Transactions
Imagine a bank transfer operation, where money is moved from one account to another. This process involves two steps:
- Deducting money from one account;
- Depositing money into another account.
If the transaction fails after deducting money but before depositing it, the funds could be "lost." A transaction ensures that both operations are either fully completed or fully reversed.
Basics of Transaction Management
The @Transactional
annotation: This is used to declare methods or classes that should be executed within the context of a transaction.
When a method with this annotation is called, Spring starts a new transaction. If the method completes successfully, the transaction is committed; otherwise, it is rolled back.
Let’s provide an example based on the real-life scenario mentioned earlier.
BankService
@Service public class BankService { private AccountRepository accountRepository; @Transactional public void transferMoney(Long fromAccountId, Long toAccountId, double amount) { Account fromAccount = accountRepository.findById(fromAccountId); Account toAccount = accountRepository.findById(toAccountId); fromAccount.withdraw(amount); toAccount.deposit(amount); accountRepository.save(fromAccount); accountRepository.save(toAccount); } }
When the transferMoney
method is marked with the @Transactional
annotation, it means that all changes happening within this method will be executed within a single transaction.
When we call fromAccount.withdraw(amount)
followed by toAccount.deposit(amount)
, both of these actions must be successfully completed. If, for instance, an error occurs during the toAccount.deposit(amount)
operation, the transaction will automatically roll back the changes made during the fromAccount.withdraw(amount)
step.
This ensures that either both operations are executed and the money is transferred, or, in the event of an error, neither operation is performed, preventing any loss of funds. The transaction guarantees that the database will never be left in an inconsistent state.
Practical Application of Transactions
//VIDEO
Available Isolation Levels
Isolation.READ_UNCOMMITTED
This level allows reading data that has not yet been committed by other transactions, which can result in "dirty reads."
Example
First, Transaction 1
updates a record but hasn’t committed the changes yet. Meanwhile, Transaction 2
comes in and reads that same record, seeing the uncommitted changes made by Transaction 1
. If Transaction 1
later decides to roll back its changes, the data that Transaction 2
read becomes invalid, as it was based on incomplete, uncommitted information.
Main
@Transactional(isolation = Isolation.READ_UNCOMMITTED) public void readUncommittedData() { // May see uncommitted changes from another transaction Book book = bookRepository.findById(1L); // Here, book may contain uncommitted changes }
Isolation.READ_COMMITTED
Ensures that you only read committed data, preventing "dirty reads".
The transaction can only see data that has been committed by other transactions at the time of reading. When the transaction attempts to read data, it locks only the committed records, avoiding any "dirty reads".
Example
First, Transaction 1
updates a record and commits the changes. Meanwhile, Transaction 2
reads that same record and can only see the committed data. This ensures that Transaction 2
does not encounter any uncommitted changes made by Transaction 1
, thereby preventing any "dirty reads".
Main
@Transactional(isolation = Isolation.READ_COMMITTED) public void readCommittedData() { // Reads only committed data Book book = bookRepository.findById(1L); // If the book is updated in another transaction, we won't see the changes until they are committed }
Isolation.REPEATABLE_READ
Guarantees that if you read data within a transaction, you will get the same data upon subsequent reads.
When data is first read, the transaction locks its state, preventing other transactions from making changes until it is completed. If another transaction attempts to modify this data, it will wait until the first transaction is finished, thereby eliminating any "phantom reads".
Example
Transaction 1
updates the record and commits the changes. After that, Transaction 2
attempts to retrieve the updated record. However, because Transaction 1
finished before Transaction 2
began, Transaction 2
doesn’t see the changes made by Transaction 1
.
Main
@Transactional(isolation = Isolation.REPEATABLE_READ) public void repeatableReadExample() { Book book = bookRepository.findById(1L); // The book remains unchanged within this transaction // Even if another process updates this record, we won't see the changes }
Isolation.SERIALIZABLE
This is the strictest level of isolation, which prevents any conflicts between transactions but can lead to locking and decreased performance.
All transactions operate as if they were executed sequentially, one after the other. This is achieved by locking all records that the transaction reads or modifies, thereby preventing any "phantom reads" and ensuring the highest level of data integrity.
Example
Transaction 1
begins its operation and updates the record. Meanwhile, Transaction 2
tries to read the same record but has to wait for Transaction 1
to finish. This ensures that Transaction 2
doesn’t access the data until it’s safe, maintaining transaction integrity.
Main
@Transactional(isolation = Isolation.SERIALIZABLE) public void serializableExample() { Book book = bookRepository.findById(1L); // Other transactions won't be able to read or modify this record until the current transaction is complete }
Summary
Transaction isolation levels define the interaction between operations in different transactions and impact data integrity. A transaction is a logical unit of work that ensures that all operations are completed successfully or none at all, maintaining consistency in the database.
Tudo estava claro?