Привет, Хабр!
Большинство 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 Protocol в PostgreSQL — официальный термин для клиент-серверного протокола БД. Frontend здесь означает клиент, например JDBC-драйвер, а Backend — серверный процесс PostgreSQL. К веб-фронтенду это отношения не имеет.
Слой |
Что делает |
Чего не делает |
|---|---|---|
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. Это режимConnection, который определяет, кто завершает транзакцию. По умолчанию он включён: каждый успешно завершённый statement JDBC автоматически фиксирует отдельнымcommit. Для запроса statement считается завершённым после обработки связанногоResultSet. ПриautoCommit=falseнесколько statement’ов можно объединить в одну транзакцию, но приложение обязано явно вызватьcommit()илиrollback(). ОтключённыйautoCommitбез корректного завершения транзакции удерживает connection, а на стороне СУБД может удерживать locks и состояниеidle in transaction.
Что означает
commitна уровне БД. ВызовConnection.commit()завершает текущую транзакцию и фиксирует выполненные в ней изменения. После этого изменения становятся доступны другим транзакциям с учётом их уровня изоляции, а связанные с завершённой транзакцией locks освобождаются.COMMITне закрывает JDBCConnection: соединение остаётся открытым или возвращается в 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, |
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 |
Создание 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 — близкий механизм в 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 драйвера.
Процесс открытия соединения в упрощённом виде:
обход зарегистрированных
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 канале!
Безопасных вам релизов!