Що таке readonly транзакція?
Як замок "тільки для читання" на документі — ви можете дивитися і копіювати, але не можете змінити оригінал.
🟢 Junior Level
Readonly транзакція — це транзакція з прапором readOnly = true, яка повідомляє Spring та базі даних: “я буду тільки читати дані, не змінювати їх”.
Як використовувати
@Service
public class UserService {
@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
Аналогія
Як замок “тільки для читання” на документі — ви можете дивитися і копіювати, але не можете змінити оригінал.
Навіщо це потрібно
- Оптимізація: Hibernate вимикає dirty checking — механізм, який при flush порівнює entities з snapshot-ом, щоб визначити, які рядки UPDATE-ити. При readOnly = true Hibernate не робить flush для UPDATE.
- Безпека: Випадковий запис викличе помилку
- База даних: Деякі СУБД застосовують оптимізації для read-only транзакцій
Частий патерн
Позначити весь сервіс як readOnly, а write-методи перевизначити:
@Service
@Transactional(readOnly = true) // За замовчуванням readOnly
public class UserService {
public User findById(Long id) { ... } // readOnly = true
@Transactional // Перевизначаємо — НЕ readOnly
public void update(User user) { ... }
}
Коли НЕ ставити readOnly = true
- Метод може писати — навіть якщо зараз не пише, майбутні зміни можуть додати запис. Тоді доведеться змінювати анотацію.
- Транзакція містить і read, і write — readOnly на весь метод зламає write-частину.
- Використовуєте Sequence —
nextval()= write, readOnly заблокує.
🟡 Middle Level
Оптимізації на рівні Hibernate
При readOnly = true:
- FlushMode.MANUAL: Hibernate вимикає автоматичний dirty checking
- Memory: Hibernate може не зберігати знімки об’єктів (теоретично, залежить від версії)
Оптимізації на рівні JDBC та СУБД
| СУБД | Що робить |
|---|---|
| PostgreSQL | SET TRANSACTION READ ONLY — оптимізація плану виконання |
| MySQL | Може роутити на репліку — якщо налаштований AbstractRoutingDataSource або фреймворк типу Hibernate Shards. Сам по собі readOnly не роутить, це залежить від конфігурації routing. |
| Oracle | Transaction-level read consistency, уникає зайвих блокувань |
Оптимізації — що реально працює
| Оптимізація | Працює? | Ефективність |
|---|---|---|
| Hibernate dirty checking вимикається | ✅ Так | Значно для великих об’єктів |
| БД оптимізує план виконання | ⚠️ Залежить від СУБД | Minor в PostgreSQL, помітніше в Oracle |
| Read-Replica routing | ⚠️ Вимагає настройки | Залежить від інфраструктури |
| Блокування не ставляться | ✅ Так | Залежить від СУБД |
Поширені помилки
| Помилка | Що відбувається | Як виправити |
|---|---|---|
| readOnly на write-методі | PSQLException: cannot execute UPDATE in a read-only transaction |
Перевизначити readOnly = false |
| Очікування exception при set-терах | Hibernate не кидає exception — просто не flush-ить | Розуміти що readOnly ≠ immutability |
| readOnly не застосовується до connection | Connection з пула вже був використаний для writes | Spring ставить readOnly тільки на new connection |
| Модифікація entity в readOnly | Set-тери працюють, але зміни не зберігаються | Це не баг — FlushMode.MANUAL |
Коли НЕ використовувати readOnly
| Ситуація | Чому | Альтернатива |
|---|---|---|
| Метод читає і пише | readOnly блокує writes | Звичайна транзакція |
Stateless queries (JdbcTemplate) |
Немає Hibernate dirty checking anyway | Без @Transactional |
| Дуже короткий SELECT по ID | Overhead відкриття транзакції > виграш | Без транзакції або без @Transactional |
Порівняння: readOnly vs без транзакції
| Аспект | readOnly = true |
Без @Transactional |
|---|---|---|
| Dirty checking | Вимкнений | Немає session |
| Read replica routing | Можливе | Ні |
| Consistent snapshot | Гарантований (в рамках tx) | Кожен query — свій snapshot |
| Overhead | Низький | Мінімальний |
🔴 Senior Level
Hibernate readOnly — Internal Behavior
FlushMode Change
// When readOnly = true, Spring sets:
session.setFlushMode(FlushMode.MANUAL);
// Це означає:
// - Немає automatic flush перед query execution
// - Немає automatic flush при transaction commit
// - flush() можна викликати вручну
Impact on Dirty Checking:
@Transactional(readOnly = true)
public User findById(Long id) {
User user = em.find(User.class, id);
user.setName("changed"); // No exception!
// Але зміна НЕ зберігається — flush is MANUAL
return user;
}
Hibernate не кидає exception при модифікаціях в readOnly транзакції — він просто не робить flush. Зміна існує тільки в L1 cache.
First-Level Cache Implications
@Transactional(readOnly = true)
public List<User> findAll() {
// Hibernate не потрібно tracking snapshots для dirty checking
// Але entities все ще кешуються в L1 (Persistence Context)
return em.createQuery("SELECT u FROM User u", User.class).getResultList();
}
Memory optimization: Теоретично Hibernate може пропустити створення snapshots. На практиці, більшість версій Hibernate все ще створюють snapshots — оптимізація обмежена flush behavior.
JDBC readOnly Flag
// Spring sets this on the connection
connection.setReadOnly(true);
Що роблять різні СУБД:
PostgreSQL
SET TRANSACTION READ ONLY;
- Забороняє
INSERT,UPDATE,DELETE,TRUNCATE,CREATE,ALTER,DROP - Дозволяє
SELECT,EXPLAIN,SHOW - Виняток: Temporary tables в деяких конфігураціях
- Оптимізація: Уникає деяких internal locks, slightly знижує overhead
В PostgreSQL readOnly = true посилає hint оптимізатору, але не гарантує read-only транзакцію на рівні СУБД. Для truly read-only використовуйте
SET TRANSACTION READ ONLY.
MySQL
conn.setReadOnly(true);
- За замовчуванням: no SQL-level change
- З custom routing: може направляти на replica
- HikariCP може використовувати для routing decisions
Oracle
setReadOnly(true)→ transaction-level read consistency- Гарантує що всі query бачать один snapshot
- Забороняє будь-які DML операції
Spring’s readOnly Implementation
// DataSourceTransactionManager.doBegin()
if (definition.isReadOnly()) {
if (con.isReadOnly()) {
// Already read-only
} else {
if (txObject.isNewConnection()) {
con.setReadOnly(true);
}
}
}
Important: Spring ставить readOnly на connection тільки якщо це new connection. Якщо connection reused з пула і раніше використовувався для writes, readOnly може не встановитися.
Read-Replica Routing Architecture
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
return "replica";
}
return "master";
}
}
Configuration:
@Bean
public DataSource dataSource() {
RoutingDataSource routing = new RoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("replica", replicaDataSource());
routing.setTargetDataSources(targetDataSources);
routing.setDefaultTargetDataSource(masterDataSource());
return routing;
}
Як працює:
@Transactional(readOnly = true)→ Spring ставить thread-local flagRoutingDataSource.determineCurrentLookupKey()перевіряє flag- Повертає “replica” → query йде на read replica
@Transactional(not readOnly) → повертає “master”
Edge Cases (мінімум 3)
-
Модифікація в readOnly транзакції:
user.setName("new")не кидає exception — Hibernate просто не flush-ить. Алеem.createQuery("UPDATE User ...").executeUpdate()кидаєPSQLException: cannot execute UPDATE in a read-only transaction. Hibernate entity modifications silently ignored, direct SQL throws. -
readOnly override не спрацьовує: Класова
@Transactional(readOnly = true)+@Transactionalна методі =readOnly = false. Але якщо метод не має свою анотацію, він наслідуєreadOnly = true. Це може призвести до неочікуваногоPSQLExceptionякщо метод викликає write-операцію. -
readOnly та Propagation.REQUIRED: readOnly метод викликає write метод з
REQUIRED(default) → write метод join-ить readOnly транзакцію → write fails. Fix: write методу потрібенREQUIRES_NEWдля власної non-readOnly транзакції. -
Connection pool reuse: Spring ставить
readOnlyтільки на new connection. Якщо connection з пула раніше використовувався для writes, flag може не встановитися. Залежить від реалізації пула таresetConnectionbehavior. -
readOnly з
@Modifyingзапитом:@Modifyingquery вreadOnly = trueсервісі →TransactionRequiredException. Репозиторій має свою@Transactional, але параметр сервісу override-ить його.
Benchmarking readOnly Impact
Scenario: Fetch 10,000 entities with Hibernate
Configuration | Time (ms) | Memory (MB)
---------------------------|-----------|------------
Default | 850 | 120
readOnly = true | 620 | 85
readOnly + stateless session| 450 | 30
readOnly дає ~27% покращення за часом та ~30% економії пам’яті для великих read-операцій.
Performance Numbers
| Операція | Default | readOnly |
|---|---|---|
| Dirty checking (per entity) | ~0.5 μs | 0 (вимкнений) |
| Snapshot creation (per entity) | ~1 μs | ~1 μs (може не skipped) |
| Flush at commit | ~5-10 ms (для dirty entities) | 0 (not needed) |
Memory Implications
- readOnly транзакція: Hibernate може skip snapshot creation → економія пам’яті пропорційна кількості entities
- L1 cache все ще активний — entities завантажуються в Persistence Context
- Для 10,000 entities: ~35 MB економії (з benchmark)
- Stateless session: мінімальна memory footprint, no L1 cache
Thread Safety
readOnly flag зберігається в ThreadLocal (TransactionSynchronizationManager.currentTransactionReadOnly). Кожен потік ізольований. Singleton сервіс thread-safe.
Production War Story
Звітний сервіс з @Transactional(readOnly = true) викликав @Modifying метод для cleanup тимчасових даних. Отримав PSQLException: cannot execute UPDATE in a read-only transaction. Весь nightly batch впав. Fix: cleanup винесено в окремий сервіс з REQUIRES_NEW.
Monitoring
Debug logging:
logging:
level:
org.springframework.transaction: DEBUG
org.hibernate.SQL: DEBUG
Programmatically check readOnly:
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
Actuator: connection pool metrics показують active connections для read vs write ops.
Highload Best Practices
- readOnly на всіх query-методах: Baseline для будь-якого метода, який не пише. Dirty checking savings значні при великих result sets.
- Class-level
readOnly = true: Для сервісів, які тільки читають — одна анотація на клас. - Read replica routing:
AbstractRoutingDataSource+isCurrentTransactionReadOnly()= автоматичний routing на репліки. - Stateless session для великих result sets:
Session#doStatelessWork()— no L1 cache, мінімальна memory. - Avoid readOnly + write in same tx: Не змішувати. Якщо метод і читає, і пише — звичайна транзакція.
- Monitor replica lag: При read replica routing — replica lag може викликати stale reads. Acceptable для звітів, не для реальних даних.
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- readOnly = true повідомляє Spring та БД: “тільки читання, без запису”
- Hibernate вимикає dirty checking (FlushMode.MANUAL) — економія ~27% часу, ~30% пам’яті
- readOnly ≠ immutability: entity можна модифікувати, але зміни не flush-яться в БД
- PostgreSQL: SET TRANSACTION READ ONLY — забороняє INSERT/UPDATE/DELETE, оптимізує план
- Read replica routing: AbstractRoutingDataSource + isCurrentTransactionReadOnly() → replica
- Прямий SQL (UPDATE/DELETE) в readOnly транзакції кидає PSQLException
Часті уточнюючі запитання:
- Чи кине Hibernate exception при user.setName() в readOnly? — Ні, просто не flush-ить (FlushMode.MANUAL)
- А що буде при JPQL UPDATE в readOnly? — PSQLException: cannot execute UPDATE in a read-only transaction
- Коли readOnly не встановлюється на connection? — Якщо connection reused з пула і раніше був used for writes
- readOnly + REQUIRED виклик write метода? — Write join-ить readOnly транзакцію → write fails
Червоні прапорці (НЕ говорити):
- “readOnly = заборона на модифікацію entities” — set-тери працюють, просто немає flush
- “readOnly завжди роутить на репліку” — тільки якщо налаштований AbstractRoutingDataSource
- “readOnly = нульовий overhead” — відкриття транзакції все ще має вартість
Пов’язані теми:
- [[17. На якому рівні можна використовувати @Transactional]]
- [[16. Що таке анотація @Transactional]]
- [[13. Що таке Propagation в Spring]]