ACID, Isolation level, @Transactional
1. In general, Spring does not create proxy classes by default for stereotype-annotated classes (such as those annotated with @Component
, @Service
, @Repository
, etc.). Spring only creates a proxy for a class when it needs to apply some kind of aspect-oriented programming (AOP), typically for features like transaction management, security, or custom-defined interceptors.
Here are the conditions under which Spring creates a proxy:
AOP Requirements: When there is a specific aspect (e.g.,
@Transactional
,@Cacheable
, or@Async
), Spring uses a proxy to implement that aspect without modifying the class itself.Interfaces: If a class implements an interface and Spring needs to create a proxy (e.g., for
@Transactional
), Spring will use JDK dynamic proxies by default, which only proxy interfaces. For classes without an interface, Spring uses CGLIB to create a subclass proxy.
In the example you provided, B
is annotated with @Component
, but it doesn’t have any aspect requirements. So Spring will treat it as a regular bean, and no proxy will be created unless you add annotations or configurations that require one.
package com.appsdeveloperblog.ws.demo_transactional;
import org.springframework.stereotype.Component;
@Component
public class B {
public void m2() {
System.out.println("m2 in B class");
}
}
package com.appsdeveloperblog.ws.demo_transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Component
@RequiredArgsConstructor
public class A {
private final B b;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void m1() {
System.out.println(b.getClass().getName());
System.out.println(this.getClass());
}
}
package com.appsdeveloperblog.ws.demo_transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication
@RequiredArgsConstructor
public class DemoTransactionalApplication implements CommandLineRunner {
private final A a;
public static void main(String[] args) {
SpringApplication.run(DemoTransactionalApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
a.m1();
}
}
Now if put @Transaction:
package com.appsdeveloperblog.ws.demo_transactional;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class B {
@Transactional
public void m2() {
System.out.println("m2 in B class");
}
}
com.appsdeveloperblog.ws.demo_transactional.B$$SpringCGLIB$$0
class com.appsdeveloperblog.ws.demo_transactional.A
This is how @Transactional annotation works under the hood:
package com.appsdeveloperblog.ws.demo_transactional.service;
import com.appsdeveloperblog.ws.demo_transactional.entity.Account;
import com.appsdeveloperblog.ws.demo_transactional.repo.AccountRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository repository;
private final EntityManagerFactory entityManagerFactory;
public void transferProxy(Long fromId, Long toId, Integer amount) {
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
Account from = em.find(Account.class, 1L);
Account to = em.find(Account.class, 2L);
from.setBalance(from.getBalance() - amount);
if(true) throw new RuntimeException("Error");
to.setBalance(to.getBalance() + amount);
} catch (RuntimeException e) {
transaction.rollback();
} finally {
transaction.commit();
em.close();
}
}
@Transactional
public void transfer(Account from, Account to, Integer amount) {
if (from.getBalance() < amount) {
throw new RuntimeException("Amount is not sufficient");
}
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
if (true) {
throw new RuntimeException("Error");
}
}
}
*** In RuntimeExceptions and Error it rolls back, but in Exception it does not.
--- *** Isolation Levels
MySQL ucun default isolation level Repeatable Read sayilir.
Bu zaman eger hansisa bir row ucun data update edirikse hemin row-nu lock-layir.
Dirty Read
Ferz edekki, Transaction1 report cixarmaq isteyir ve satilan mallarin umumi odeniwini cicarir, bu zaman Transaction2 table-da update edir ve tutalim ki transaction kecmir veya xeta baw verir veya imtina edir, bu zaman Transaction1-in hesabati yalniw olacaq.
Lakin bu o demek deyilki Dirty Read pis bir weydi. Mes: sosial webekede like sayi hazirki an ucun o qederde vacib bir wey deyil.
Non-repeatable Read
Bu o demekdir ki, eyni query ferqli netice gostere biler.
Ferz edekki Transaction1 hesabah aparmaq ucun query atir ve x bir deyer alir, bu zaman Transaction2 hemin table-da update edir, bu zaman Transaction1 yene eyni query generate edir ve ferqli bir netice alir.
Phantom Read
Ferz edek ki, Transaction1 hesabat cixarmaq isteyir ve x bir edek alir, bu zaman Transaction2 hemin table-a insert edir, bir nece saniye sonra yeniden hesabah cixarmaq isteyende ferqli bir netice alir.
Phantom reads and non-repeatable reads are both types of isolation-level issues that can occur in transactional databases. They affect the consistency of data when transactions are running concurrently. Here’s a breakdown of each:
### 1. **Non-Repeatable Read**
- **Definition**: A non-repeatable read occurs when a transaction reads the same row twice but gets different values because another transaction modified that row in between reads.
- **Example**: Suppose Transaction A reads the value of a row, then Transaction B updates that row, and when Transaction A reads it again, the value has changed. This inconsistency can lead to unexpected results in Transaction A.
- **Isolation Level**: This is prevented by the **REPEATABLE READ** isolation level, which ensures that once a transaction reads a row, that row cannot be modified by other transactions until the transaction completes.
### 2. **Phantom Read**
- **Definition**: A phantom read happens when a transaction reads a set of rows that satisfy a condition, but another transaction inserts, updates, or deletes rows that would also satisfy that condition, causing the initial transaction to get a different result set if it re-runs the query.
- **Example**: Transaction A reads all rows where `status = 'active'`. Meanwhile, Transaction B inserts a new row with `status = 'active'`. If Transaction A re-reads rows with `status = 'active'`, it will see an additional "phantom" row that wasn’t there in the first read.
- **Isolation Level**: This is prevented by the **SERIALIZABLE** isolation level, which ensures that no other transactions can modify or insert rows that would affect the initial transaction's result set.
### **Key Difference**
- **Scope**: Non-repeatable reads affect individual rows, while phantom reads affect the entire set of rows returned by a query.
- **Cause**: Non-repeatable reads occur due to **updates** or **deletes** on existing rows, while phantom reads are due to **inserts** or **deletes** that affect the result set of a query.
### Isolation Level Summary
- **Read Committed**: Prevents dirty reads but allows non-repeatable and phantom reads.
- **Repeatable Read**: Prevents dirty and non-repeatable reads but allows phantom reads.
- **Serializable**: Prevents dirty, non-repeatable, and phantom reads by locking the whole data set.
******** Extremely important part
The behavior you're observing stems from how **transaction management and SQL execution timing** differ between the MySQL CLI and your Spring Boot application using JPA/Hibernate. Here's why your second CLI session waits in the CLI scenario but doesn't when interacting with the Spring Boot application.
---
### **Scenario Breakdown**
#### **1. Two MySQL CLI Sessions**
**Session 1:**
```sql
START TRANSACTION;
UPDATE account SET balance = 0 WHERE id = 1;
-- Session holds an exclusive lock on the row with id = 1
```
**Session 2:**
```sql
START TRANSACTION;
UPDATE account SET balance = 800 WHERE id = 1;
-- This session waits because Session 1 holds a lock on the same row
```
#### **2. Spring Boot Application and MySQL CLI Session**
**Spring Boot Application:**
```java
@Transactional
public void transfer() throws Exception {
Account parvin = repository.findById(1L).get();
Account rovshan = repository.findById(2L).get();
parvin.setBalance(parvin.getBalance() - 50);
Thread.sleep(30000); // Pauses for 30 seconds
rovshan.setBalance(rovshan.getBalance() + 50);
}
```
**MySQL CLI Session:**
```sql
START TRANSACTION;
UPDATE account SET balance = 0 WHERE id = 1;
-- Session holds an exclusive lock on the row with id = 1
```
---
### **Why Does the Second CLI Session Wait?**
- **Immediate Execution and Locking:**
- In the MySQL CLI, when you execute `UPDATE account SET balance = ... WHERE id = 1;`, the statement is sent immediately to the database.
- The database acquires an **exclusive lock** on the row with `id = 1` for the duration of the transaction.
- **Conflict Detection:**
- The second session attempts to update the same row, but it **cannot acquire the lock** because the first session already holds it.
- Therefore, it **waits** until the first session commits or rolls back.
---
### **Why Doesn't the Spring Boot Transaction Cause the CLI Session to Wait?**
- **Deferred Execution in Hibernate/JPA:**
- When you modify an entity in a Spring Boot application using JPA/Hibernate, the change is made **in memory** within the persistence context (first-level cache).
- **SQL statements are not sent to the database immediately**; they are deferred until the persistence context is flushed.
- **No Immediate Lock Acquisition:**
- Since the `UPDATE` statement hasn't been sent to the database yet, **no lock is acquired** on the row with `id = 1`.
- During the `Thread.sleep(30000);`, the database is unaware of any pending changes, so the CLI session can proceed to acquire the lock.
- **Resulting Behavior:**
- The Spring Boot transaction doesn't prevent other transactions from modifying the same data during its sleep period.
- When it finally flushes and commits, it may overwrite changes made by other transactions or encounter conflicts.
---
### **How to Align Spring Boot Behavior with CLI Sessions**
To make your Spring Boot transaction behave like the CLI session (i.e., to acquire locks immediately), you need to **ensure that SQL statements are sent to the database promptly**. Here's how:
#### **1. Manually Flush the Persistence Context**
```java
@Autowired
private EntityManager entityManager;
@Transactional
public void transfer() throws Exception {
Account parvin = repository.findById(1L).get();
parvin.setBalance(parvin.getBalance() - 50);
// Force the SQL UPDATE to be executed now
entityManager.flush();
Thread.sleep(30000);
Account rovshan = repository.findById(2L).get();
rovshan.setBalance(rovshan.getBalance() + 50);
entityManager.flush();
}
```
- **Explanation:**
- `entityManager.flush();` sends all pending SQL statements to the database.
- The database then acquires the necessary locks.
- Other transactions attempting to modify the same data will now wait.
#### **2. Use Pessimistic Locking**
Modify your repository to lock the entities when fetching them:
```java
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);
}
```
Update your service method:
```java
@Transactional
public void transfer() throws Exception {
Account parvin = repository.findByIdForUpdate(1L);
Account rovshan = repository.findByIdForUpdate(2L);
parvin.setBalance(parvin.getBalance() - 50);
rovshan.setBalance(rovshan.getBalance() + 50);
// Locks are held until the transaction commits
}
```
- **Explanation:**
- `@Lock(LockModeType.PESSIMISTIC_WRITE)` acquires a database lock when the entity is read.
- Other transactions attempting to read or write to the same row will wait.
- Ensures data consistency and prevents concurrent modifications.
#### **3. Use `saveAndFlush` Method**
Force immediate write to the database:
```java
@Transactional
public void transfer() throws Exception {
Account parvin = repository.findById(1L).get();
parvin.setBalance(parvin.getBalance() - 50);
repository.saveAndFlush(parvin); // Flushes immediately
Thread.sleep(30000);
Account rovshan = repository.findById(2L).get();
rovshan.setBalance(rovshan.getBalance() + 50);
repository.saveAndFlush(rovshan);
}
```
---
### **Understanding the Difference in Locking Mechanisms**
- **CLI Sessions:**
- Statements are executed immediately.
- Locks are acquired as soon as the statement runs.
- Transactions are straightforward; there's no intermediate caching.
- **Spring Boot with JPA/Hibernate:**
- Uses a persistence context that caches changes.
- SQL statements are deferred until flush or commit time.
- No locks are acquired until the SQL statements are sent to the database.
---
### **Additional Points**
- **Transaction Isolation Levels:**
- **Repeatable Read** isolation level ensures consistent reads within a transaction but doesn't prevent other transactions from modifying the data.
- **Serializable** isolation level can prevent concurrent writes but may lead to decreased performance due to increased locking.
- **Optimistic vs. Pessimistic Locking:**
- **Optimistic Locking** assumes conflicts are rare and checks for conflicts at commit time.
- **Pessimistic Locking** immediately locks the data to prevent conflicts.
- **Flush Modes:**
- **AUTO (default):** Persistence context is flushed before query execution and at transaction commit.
- **COMMIT:** Flush occurs only at transaction commit.
---
### **Summary**
- **CLI Sessions Acquire Locks Immediately:**
- Direct execution of SQL statements leads to immediate lock acquisition.
- Concurrent transactions wait if they try to access the same data.
- **Spring Boot Defers SQL Execution:**
- Changes are made in-memory and flushed at commit time.
- No locks are held until the flush occurs.
- Other transactions can modify the data in the meantime.
- **To Synchronize Behavior:**
- **Force Flushing:** Use `entityManager.flush()` or `saveAndFlush()` to send SQL statements to the database immediately.
- **Use Locking:** Apply pessimistic locking to prevent other transactions from accessing the data.
---
### **Let Me Know If You Need Further Assistance**
- **Implementing the Solutions:** If you need help modifying your Spring Boot application to implement these changes, feel free to ask.
- **Understanding Transaction Management:** I can provide more details on how JPA/Hibernate handles transactions and flushing.
- **Exploring Locking Strategies:** If you're interested in the pros and cons of optimistic vs. pessimistic locking, I can elaborate.
By adjusting your Spring Boot application to flush changes immediately or to use locking mechanisms, you can ensure that it behaves similarly to your CLI sessions, preventing concurrent modifications and ensuring data consistency.
*** But when we do in this way:
private final EntityManager entityManager;
// @Transactional(isolation = Isolation.SERIALIZABLE)
@Transactional
public void transfer() throws Exception {
Account parvin = repository.findById(1L).get();
parvin.setBalance(parvin.getBalance() - 50);
// Force the SQL UPDATE to be executed now
entityManager.flush();
Thread.sleep(30000);
Account rovshan = repository.findById(2L).get();
rovshan.setBalance(rovshan.getBalance() + 50);
entityManager.flush();
// Account parvin = repository.findById(1L).get();
// Account rovshan = repository.findById(2L).get();
// parvin.setBalance(parvin.getBalance() - 50);
//// Thread.sleep(30000);
// rovshan.setBalance(rovshan.getBalance() + 50);
}
**********
Certainly! Let's work through how to simulate a **phantom read** in MySQL using two CLI sessions. I'll explain why you might not be seeing it and provide detailed steps to reproduce it.
---
### **Understanding Phantom Reads**
A **phantom read** occurs when:
- **Transaction T1** reads a set of rows matching a condition.
- **Transaction T2** inserts (or deletes) new rows that match the same condition.
- **Transaction T1** re-reads the data and finds new (or missing) rows—these are the "phantoms."
**Phantom reads** are possible under the **Read Committed** and **Repeatable Read** isolation levels, depending on the database system. However, MySQL's default **Repeatable Read** isolation level uses **next-key locking** and **MVCC** (Multi-Version Concurrency Control) to prevent phantom reads.
---
### **Why You're Not Seeing Phantom Reads in MySQL**
- **MySQL's Default Behavior**: By default, MySQL's **Repeatable Read** isolation level prevents phantom reads using **gap locks** and **next-key locks**.
- **Need to Use Read Committed Isolation Level**: To observe phantom reads in MySQL, you need to set the isolation level to **Read Committed**.
- **Table Type Matters**: Make sure you're using the **InnoDB** storage engine, as MyISAM doesn't support transactions.
---
### **Steps to Simulate Phantom Reads in MySQL**
#### **Preparation**
1. **Ensure InnoDB Engine is Used**
```sql
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
balance INT,
name VARCHAR(100)
) ENGINE=InnoDB;
INSERT INTO account (balance, name) VALUES (1000, 'Parvin'), (1000, 'Rovshan');
```
2. **Open Two MySQL CLI Sessions**
- **Session 1**: We'll call this **Transaction T1**.
- **Session 2**: We'll call this **Transaction T2**.
#### **Simulating Phantom Read**
##### **Session 1 (Transaction T1)**
1. **Set Isolation Level to Read Committed**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
2. **Start Transaction**
```sql
START TRANSACTION;
```
3. **Execute Initial Query**
```sql
SELECT * FROM account WHERE balance >= 1000;
```
- **Expected Result**:
```
+----+---------+---------+
| id | balance | name |
+----+---------+---------+
| 1 | 1000 | Parvin |
| 2 | 1000 | Rovshan |
+----+---------+---------+
```
##### **Session 2 (Transaction T2)**
1. **Set Isolation Level to Read Committed**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
2. **Start Transaction**
```sql
START TRANSACTION;
```
3. **Insert a New Row Matching the Condition**
```sql
INSERT INTO account (balance, name) VALUES (1500, 'Aydin');
```
4. **Commit Transaction**
```sql
COMMIT;
```
##### **Back to Session 1 (Transaction T1)**
1. **Re-Execute the Query**
```sql
SELECT * FROM account WHERE balance >= 1000;
```
- **Expected Result**:
```
+----+---------+---------+
| id | balance | name |
+----+---------+---------+
| 1 | 1000 | Parvin |
| 2 | 1000 | Rovshan |
| 3 | 1500 | Aydin |
+----+---------+---------+
```
- **Observation**: The new row ('Aydin') appears in the result set, which wasn't there in the first query—this is the **phantom read**.
2. **Commit or Rollback Transaction**
```sql
COMMIT;
```
---
### **Explanation**
- **Transaction T1** starts and reads the set of rows where `balance >= 1000`.
- **Transaction T2** inserts a new row with `balance = 1500`, which satisfies the condition.
- **Transaction T2** commits, making the new row visible to other transactions.
- When **Transaction T1** re-executes the query, it sees the new row, demonstrating a phantom read.
---
### **Why It Didn't Work Before**
- If you had both sessions using the **Repeatable Read** isolation level (MySQL's default), **Transaction T1** would not see the new row inserted by **Transaction T2** until **Transaction T1** ends. This is because **Repeatable Read** in MySQL prevents phantom reads using gap locks.
---
### **Key Points**
- **Isolation Level Matters**: To observe phantom reads in MySQL, you need to set the isolation level to **Read Committed**.
- **Repeatable Read Prevents Phantom Reads in MySQL**: Due to MySQL's implementation of **Repeatable Read**, phantom reads are prevented using next-key locking.
- **Ensure Transactions Are Open**: Both sessions must have active transactions to observe the effects.
---
### **Additional Example: Phantom Reads with Updates**
You can also simulate phantom reads using `UPDATE` statements.
#### **Session 1 (Transaction T1)**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT COUNT(*) FROM account WHERE balance >= 1000;
```
- **Initial Count**: 2
#### **Session 2 (Transaction T2)**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
INSERT INTO account (balance, name) VALUES (1200, 'Leyla');
COMMIT;
```
#### **Back to Session 1 (Transaction T1)**
```sql
SELECT COUNT(*) FROM account WHERE balance >= 1000;
```
- **New Count**: 3 (Phantom row 'Leyla' appears)
---
### **Preventing Phantom Reads**
If you want to prevent phantom reads, you can:
- **Use Repeatable Read Isolation Level**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
```
- **Use Serializable Isolation Level**
```sql
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
```
- This level is the strictest and can impact performance due to increased locking.
---
### **Final Notes**
- **Consistency is Key**: Always ensure both sessions are using the intended isolation level.
- **Testing Different Scenarios**: Try inserting, updating, and deleting rows in **Transaction T2** to see how it affects **Transaction T1** under different isolation levels.
- **Understanding MySQL's Behavior**: MySQL's handling of isolation levels may differ from other databases due to its use of MVCC and locking strategies.
---
### **Let Me Know If You Need Further Assistance**
Feel free to ask if you have any questions or need clarification on any of the steps. I'm here to help you understand and observe phantom reads in your MySQL environment.
****
in application.yaml file when we include this:
spring:
aop:
proxy-target-class: false
it switches to jdk proxy
Комментарии
Отправить комментарий