Привет, Хабр!

Большинство JVM-бэкендов работают с БД через JDBC. Даже если в коде только Spring Data, JPA, Slick, Quill, jOOQ или MyBatis, внизу почти всегда один и тот же стек: DataSourceConnectionStatement/PreparedStatementResultSet.

Проблема в том, что JDBC обычно изучают «снизу вверх по методам API»: вот executeQuery, вот commit. Для повседневной разработки этого хватает. Для проектирования, рефакторинга и оптимизации под нагрузкой нужен другой взгляд: общая архитектура, главные части и связи между ними.

Оглавление

  1. Карта архитектуры JDBC

  2. Главные части и связи между ними

  3. Driver и автоматическая загрузка реализации


1. Карта архитектуры JDBC

1.1. Слои стека

JDBC — стандартный Java API для взаимодействия с реляционными БД. Он является частью многослойной архитектуры, в которой API отделён от драйвера, пула соединений и сетевого протокола СУБД:

┌─────────────────────────────────────────────────────────────┐
│  Application / ORM / SQL-framework                          │
│  (Spring Data, JPA, jOOQ, Slick, Quill, MyBatis, JDBC API)  │
├─────────────────────────────────────────────────────────────┤
│  Connection pool (часто реализует DataSource)               │
│  HikariCP, Tomcat JDBC, ...                                 │
├─────────────────────────────────────────────────────────────┤
│  JDBC API  (Java SE / JDBC specification)                   │
│  java.sql.* / javax.sql.*                                   │
├─────────────────────────────────────────────────────────────┤
│  JDBC Driver (vendor implementation)                        │
│  postgresql-*.jar, mysql-connector-j, ojdbc, ...            │
├─────────────────────────────────────────────────────────────┤
│  DB wire protocol + session                                 │
│  Postgres Frontend/Backend, MySQL protocol, ...             │
├─────────────────────────────────────────────────────────────┤
│  СУБД                                                       │
└─────────────────────────────────────────────────────────────┘

Frontend/Backend Protocol в PostgreSQL — официальный термин для клиент-серверного протокола БД. Frontend здесь означает клиент, например JDBC-драйвер, а Backend — серверный процесс PostgreSQL. К веб-фронтенду это отношения не имеет.

Слой

Что делает

Чего не делает

JDBC API

Описывает интерфейсы и контракты

Не знает Postgres/MySQL, не шлёт пакеты

Driver

Реализует API, говорит на протоколе СУБД

Не управляет пулом приложения

Pool

Управляет числом и lifecycle physical Connection

Не ускоряет SQL и не заменяет транзакционную модель

App/ORM

Доменные операции, mapping, кэши

Часто скрывает JDBC, но не убирает его из runtime

Ключевое разделение: спецификация (API) ≠ реализация (driver) ≠ эксплуатационная обёртка (pool). Тюнинг «JDBC» почти всегда означает тюнинг связки driver + pool + SQL + transaction boundaries.

1.2. Объектная модель

На уровне API основные объекты JDBC связаны порядком создания и общим жизненным циклом:

Driver / DataSource
        │
        ▼
   Connection  ──────────────────────────────┐
        │                                    │
        ├── Statement                        │  transaction state
        ├── PreparedStatement                │  autoCommit, isolation,
        ├── CallableStatement                │  savepoints
        │         │                          │
        │         ▼                          │
        │      ResultSet  ◄── fetch/cursor ──┘
        │
        └── DatabaseMetaData (опционально)

autoCommit. Это режим Connection, который определяет, кто завершает транзакцию. По умолчанию он включён: каждый успешно завершённый statement JDBC автоматически фиксирует отдельным commit. Для запроса statement считается завершённым после обработки связанного ResultSet. При autoCommit=false несколько statement’ов можно объединить в одну транзакцию, но приложение обязано явно вызвать commit() или rollback(). Отключённый autoCommit без корректного завершения транзакции удерживает connection, а на стороне СУБД может удерживать locks и состояние idle in transaction.

Что означает commit на уровне БД. Вызов Connection.commit() завершает текущую транзакцию и фиксирует выполненные в ней изменения. После этого изменения становятся доступны другим транзакциям с учётом их уровня изоляции, а связанные с завершённой транзакцией locks освобождаются. COMMIT не закрывает JDBC Connection: соединение остаётся открытым или возвращается в pool после close().

Savepoint — именованная точка внутри текущей транзакции. Вызов rollback(savepoint) отменяет операции, выполненные после этой точки, но сохраняет более ранние изменения и не завершает транзакцию.

Основные правила жизненного цикла:

  • Connection — корень сессии с БД со стороны клиента;

  • Statement/PreparedStatement создаются из Connection и живут в его контексте;

  • ResultSet создаётся statement’ом и обычно привязан к нему;

  • транзакционное состояние (autoCommit, isolation, open transaction) живёт на Connection, не на Statement;

  • при закрытии Connection драйвер также закрывает созданные через него Statement и ResultSet. Использовать эти объекты после закрытия соединения нельзя.

1.3. Два пакета JDBC API в Java SE

JDBC API входит в Java SE и разнесён по двум пакетам:

  • java.sql — базовый клиентский контракт: Driver, Connection, Statement, ResultSet, SQLException;

  • javax.sql — расширения для работы с источниками данных, пулами и распределёнными транзакциями: DataSource, ConnectionPoolDataSource, XADataSource, RowSet и т.п.

Префикс javax здесь не означает Jakarta EE: оба пакета относятся к JDBC в Java SE. Jakarta EE использует JDBC и интегрирует его с JPA, JTA и контейнерной инфраструктурой, но не определяет основной JDBC API.

В современном приложении вы почти всегда работаете через DataSource, даже если пишете «голый JDBC». DriverManager остаётся низкоуровневым механизмом discovery/connect.


2. Главные части и связи между ними

Сначала определим основные компоненты JDBC и прямые связи между ними. В следующих разделах отдельно разберём назначение и жизненный цикл каждого компонента.

Часть

Роль в архитектуре

С чем связана напрямую

Driver + SPI

Обнаружение и загрузка vendor-реализации в JVM

URL, DriverManager, classloader

DataSource / Pool / Connection

Получение и удержание сессии

Driver, transactions, statements

Statement hierarchy

Выполнение SQL / bind / batch

Connection, ResultSet, plan/prepare

ResultSet

Чтение результата, fetch, cursor

Statement, Connection, heap/network

Transaction state

Границы транзакции и правила видимости данных

Connection, locks на СУБД, pool checkout

Types / timeouts / errors

Сквозные механизмы

Все уровни: API → driver → DB

SPI (Service Provider Interface) — стандартный механизм Java для поиска и загрузки реализаций интерфейса во время запуска. В JDBC через SPI обнаруживаются реализации java.sql.Driver из JAR-файлов в classpath. Благодаря этому в JDBC 4+ обычно не требуется явно вызывать Class.forName(...).

Checkout connection — выдача соединения из пула приложению при вызове DataSource.getConnection(). Checkout заканчивается после Connection.close(), когда соединение возвращается в пул. Долгий checkout означает, что приложение долго удерживает соединение и уменьшает число доступных слотов пула.

2.1. Последовательность работы: от загрузки драйвера до возврата соединения в пул

1) Driver регистрируется (SPI / explicit)
2) DataSource/pool создаёт или переиспользует Connection
3) App checkout Connection
4) На Connection выставляется transaction mode
5) Создаётся Statement/PreparedStatement
6) Execute → (опционально) ResultSet
7) Чтение ResultSet / обработка update count
8) commit/rollback (если не autoCommit)
9) close ResultSet/Statement
10) close Connection (= return to pool в типичном setup)

Ошибка на любом шаге — это не локальный fail метода. Она затрагивает:

  • состояние connection (нужен rollback / eviction);

  • занятость pool slot;

  • возможные locks/cursors на стороне СУБД.

Что такое eviction. В контексте connection pool это принудительное удаление physical connection из пула. Пул закрывает соединение и больше не выдаёт его приложению, например после fatal error драйвера, разрыва сокета, failed validation или истечения maxLifetime. rollback завершает неуспешную транзакцию на исправном connection, а eviction полностью удаляет connection, который больше нельзя безопасно переиспользовать.

2.2. Ресурсы с жёсткими ограничениями

В высоконагруженных системах важны не только объекты JDBC API, но и ограниченные системные ресурсы, которые за ними стоят:

Ресурс

Где учитывается

Чем исчерпывается

Physical TCP/TLS session

Driver + DB max_connections

Создание connection без pool / слишком большой pool

Pool slot

Hikari/etc maximumPoolSize

Долгий checkout, открытый ResultSet, широкая TX

App thread

Thread pool / virtual thread

Blocking wait на execute / fetch

Server cursor / portal

СУБД session

Незакрытый ResultSet, server-side cursor

Client heap

JVM

Загрузка большого ResultSet в коллекцию

Plan/statement cache

Driver session + СУБД

Нестабильный SQL, отсутствие bind

Широкая TX — транзакция с большой областью действия: она долго выполняется или включает SQL, вычисления и внешние вызовы. Всё это время connection занят, а БД может удерживать locks.

Server cursor хранит на стороне БД состояние запроса и позволяет получать результат порциями. Portal — близкий механизм в PostgreSQL wire protocol. Пока cursor/portal открыт, он занимает ресурсы сессии и обычно удерживает JDBC connection.

JDBC API не предоставляет единой модели учёта этих ресурсов. Их использование зависит от жизненного цикла связанных объектов. Например, открытый ResultSet удерживает соединение из пула. При порционном чтении СУБД также хранит состояние незавершённого запроса до закрытия результата.

2.3. За что отвечают JVM, приложение, драйвер, пул и СУБД

Ответственность распределена так:

  • JVM/SPI — какие driver implementation видны;

  • Pool — сколько physical connection существует и кто их арендует;

  • Application — границы транзакций, SQL, время удержания connection;

  • Driver — формирование сообщений протокола, режим prepare, стратегия fetch, кодирование типов;

  • СУБД — locks, isolation semantics, plan cache, session limits.

Protocol framing — формирование и разбор сетевых сообщений между драйвером и БД: кодирование SQL и параметров, добавление служебных полей, обработка строк результата и ошибок.

Plan cache — механизм СУБД для повторного использования ранее подготовленного плана выполнения SQL. Он уменьшает затраты на повторный разбор и оптимизацию запроса; конкретное поведение зависит от СУБД и типа statement.


3. Driver и SPI: как появляется реализация

3.1. Место в архитектуре

Driver — точка входа vendor-кода в JDBC-модель. Без driver’а API — набор интерфейсов без runtime.

Связи:

  • DriverManager использует список Driver для getConnection(url);

  • DataSource конкретной реализации (или pool) внутри тоже опирается на driver/connect path;

  • URL jdbc:<subprotocol>:... — ключ маршрутизации к нужному driver.

3.2. Discovery

До JDBC 4 драйвер загружали явно:

Class.forName("org.postgresql.Driver");
Connection c = DriverManager.getConnection(url, user, password);

Начиная с JDBC 4 драйвер загружается автоматически. Java использует ServiceLoader, чтобы найти реализацию java.sql.Driver, указанную в файле META-INF/services/java.sql.Driver внутри JAR драйвера.

Процесс открытия соединения в упрощённом виде:

  1. обход зарегистрированных Driver;

  2. acceptsURL(url);

  3. connect(url, info);

  4. TCP/TLS + auth + session setup → Connection.

3.3. Что важно на практике

  • Версия драйвера — часть контракта поведения (fetch defaults, binary mode, batch rewrite, prepare cache).

  • Один и тот же JDBC-код на разных driver’ах даёт разный runtime profile.

  • В приложении driver часто «невидим», потому что спрятан за pool/ORM, но именно он определяет wire-поведение.


Далее мы рассмотрим DataSource, пул и Connection, а затем Statement, prepare и batch.


Если Вам понравилась статья, то буду рад видеть в моем tg канале!


Безопасных вам релизов!

Комментарии (0)