Привет, Хабр!
Большинство JVM-бэкендов работают с БД через JDBC. Даже если в коде только Spring Data, JPA, Slick, Quill, jOOQ или MyBatis, внизу почти всегда один и тот же стек: DataSource → Connection → Statement/PreparedStatement → ResultSet.
Проблема в том, что JDBC обычно изучают «снизу вверх по методам API»: вот executeQuery, вот commit. Для повседневной разработки этого хватает. Для проектирования, рефакторинга и оптимизации под нагрузкой нужен другой взгляд: общая архитектура, главные части и связи между ними.

Оглавление
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 в протоколе PostgreSQL
Frontend/Backend Protocol в PostgreSQL — официальный термин для клиент-серверного протокола БД. Frontend здесь означает клиент, например JDBC-драйвер, а Backend — серверный процесс PostgreSQL. К веб-фронтенду это отношения не имеет.
Ответственность слоёв JDBC-стека
Слой | Что делает | Чего не делает |
|---|---|---|
JDBC API | Описывает интерфейсы и контракты | Не знает Postgres/MySQL, не шлёт пакеты |
Driver | Реализует API, говорит на протоколе СУБД | Не управляет пулом приложения |
Pool | Управляет числом и lifecycle physical | Не ускоряет 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
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 позволяет откатить часть транзакции
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 и прямые связи между ними. В следующих разделах отдельно разберём назначение и жизненный цикл каждого компонента.
Основные компоненты JDBC и их связи
Часть | Роль в архитектуре | С чем связана напрямую |
|---|---|---|
Driver + SPI | Обнаружение и загрузка vendor-реализации в JVM | URL, |
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 |
Как JDBC загружает драйвер через SPI
SPI (Service Provider Interface) — стандартный механизм Java для поиска и загрузки реализаций интерфейса во время запуска. В JDBC через SPI обнаруживаются реализации java.sql.Driver из JAR-файлов в classpath. Благодаря этому в JDBC 4+ обычно не требуется явно вызывать Class.forName(...).
Когда начинается и заканчивается checkout connection
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 eviction — это принудительное удаление physical connection из пула. Пул закрывает соединение и больше не выдаёт его приложению, например после fatal error драйвера, разрыва сокета, failed validation или истечения maxLifetime. rollback завершает неуспешную транзакцию на исправном connection, а eviction полностью удаляет connection, который больше нельзя безопасно переиспользовать.
2.2. Ресурсы с жёсткими ограничениями
В высоконагруженных системах важны не только объекты JDBC API, но и ограниченные системные ресурсы, которые за ними стоят:
Ограниченные ресурсы JDBC-приложения
Ресурс | Где учитывается | Чем исчерпывается |
|---|---|---|
Physical TCP/TLS session | Driver + DB | Создание connection без pool / слишком большой pool |
Pool slot | Hikari/etc | Долгий checkout, открытый ResultSet, широкая TX |
App thread | Thread pool / virtual thread | Blocking wait на |
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 удерживают соединение
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
Protocol framing — формирование и разбор сетевых сообщений между драйвером и БД: кодирование SQL и параметров, добавление служебных полей, обработка строк результата и ошибок.
Как plan cache ускоряет повторные запросы
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 драйвера.
Процесс открытия соединения в упрощённом виде:
обход зарегистрированных
Driver;acceptsURL(url);connect(url, info);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 канале!
Безопасных вам релизов!
