Питання 21 · Розділ 11

Що таке readonly транзакція?

Як замок "тільки для читання" на документі — ви можете дивитися і копіювати, але не можете змінити оригінал.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Readonly транзакція — це транзакція з прапором readOnly = true, яка повідомляє Spring та базі даних: “я буду тільки читати дані, не змінювати їх”.

Як використовувати

@Service
public class UserService {

    @Transactional(readOnly = true)
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Аналогія

Як замок “тільки для читання” на документі — ви можете дивитися і копіювати, але не можете змінити оригінал.

Навіщо це потрібно

  1. Оптимізація: Hibernate вимикає dirty checking — механізм, який при flush порівнює entities з snapshot-ом, щоб визначити, які рядки UPDATE-ити. При readOnly = true Hibernate не робить flush для UPDATE.
  2. Безпека: Випадковий запис викличе помилку
  3. База даних: Деякі СУБД застосовують оптимізації для 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

  1. Метод може писати — навіть якщо зараз не пише, майбутні зміни можуть додати запис. Тоді доведеться змінювати анотацію.
  2. Транзакція містить і read, і write — readOnly на весь метод зламає write-частину.
  3. Використовуєте Sequencenextval() = 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;
}

Як працює:

  1. @Transactional(readOnly = true) → Spring ставить thread-local flag
  2. RoutingDataSource.determineCurrentLookupKey() перевіряє flag
  3. Повертає “replica” → query йде на read replica
  4. @Transactional (not readOnly) → повертає “master”

Edge Cases (мінімум 3)

  1. Модифікація в 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.

  2. readOnly override не спрацьовує: Класова @Transactional(readOnly = true) + @Transactional на методі = readOnly = false. Але якщо метод не має свою анотацію, він наслідує readOnly = true. Це може призвести до неочікуваного PSQLException якщо метод викликає write-операцію.

  3. readOnly та Propagation.REQUIRED: readOnly метод викликає write метод з REQUIRED (default) → write метод join-ить readOnly транзакцію → write fails. Fix: write методу потрібен REQUIRES_NEW для власної non-readOnly транзакції.

  4. Connection pool reuse: Spring ставить readOnly тільки на new connection. Якщо connection з пула раніше використовувався для writes, flag може не встановитися. Залежить від реалізації пула та resetConnection behavior.

  5. readOnly з @Modifying запитом: @Modifying query в 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

  1. readOnly на всіх query-методах: Baseline для будь-якого метода, який не пише. Dirty checking savings значні при великих result sets.
  2. Class-level readOnly = true: Для сервісів, які тільки читають — одна анотація на клас.
  3. Read replica routing: AbstractRoutingDataSource + isCurrentTransactionReadOnly() = автоматичний routing на репліки.
  4. Stateless session для великих result sets: Session#doStatelessWork() — no L1 cache, мінімальна memory.
  5. Avoid readOnly + write in same tx: Не змішувати. Якщо метод і читає, і пише — звичайна транзакція.
  6. 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]]