Что такое 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]]