Aplikasi web modern melayani ratusan permintaan per detik, dan hampir tiap permintaan butuh database. Jika setiap permintaan membuka Connection baru lewat DriverManager, aplikasi mengulang ritual mahal yang sama terus-menerus: buka soket TCP, jabat tangan TLS, autentikasi, siapkan sesi — lalu membuang semuanya begitu permintaan selesai. Connection pooling membalik pola itu: koneksi dibuka sedikit, disimpan di kolam, dan dipakai bergantian. Chapter ini membahas biaya yang disembunyikan sebuah koneksi, cara kerja pool, peran DataSource sebagai pintu standar, dan parameter pool yang perlu disetel — ditutup implementasi pool mini yang bisa kita jalankan langsung.
Biaya Sebuah Koneksi #
Membuka koneksi bukan sekadar new pada objek Java. Untuk database berjaringan, urutannya kira-kira: resolusi DNS, pembukaan soket TCP (dan ulang kalau gagal), negosiasi TLS, pertukaran kredensial autentikasi, lalu penyiapan status sesi di server. Totalnya bisa beberapa sampai puluhan milidetik per koneksi — dan pada server database, tiap sesi juga memakan memori. Karena itu pola “buka-pakai-tutup” per permintaan cepat menghabiskan dua sumber daya sekaligus: waktu tunggu klien dan kapasitas server.
Pool menyingkat ritual itu menjadi sekali di awal. Koneksi yang sudah selesai dipakai tidak ditutup fisik, melainkan dikembalikan ke kolam dan menunggu pemakai berikutnya — penghematan terbesar justru datang dari penggunaan ulang koneksi yang masih hangat.
Cara Kerja Pool #
Alur pemakaian lewat pool hanya mengganti dua titik dari alur biasa:
Mekanisme di balik layar: aplikasi memanggil dataSource.getConnection(), pool mengeluarkan koneksi bebas dari kolamnya dan menandainya “sedang dipakai”. Saat aplikasi memanggil close() pada koneksi tersebut, koneksi tidak ditutup fisik — pembungkus dari pool mengalihkan close() menjadi pengembalian ke kolam, memeriksa kondisinya, dan menyiapkannya untuk pemanggil berikutnya. Jika kolam kosong, pemanggil menunggu selama connection timeout sebelum gagal; jika koneksi sudah terlalu tua (max lifetime), pool menutupnya sungguhan dan membuat yang baru di tempatnya — menjaga koneksi tidak pernah lebih tua dari batas server.
DataSource sebagai Pintu Standar #
Dalam aplikasi ber-pool, DriverManager tidak lagi dipanggil langsung. Perannya digantikan javax.sql.DataSource — interface yang method getConnection()-nya bisa disediakan pool mana pun. Tabel berikut merangkum perbedaan keduanya:
| Aspek | DriverManager | DataSource |
|---|---|---|
| Pemanggilan | getConnection(url, ...) statis | dataSource.getConnection() dari instance |
| URL dan kredensial | ditulis di kode pemanggil | tersembunyi di dalam implementasi |
| Pooling | tidak ada | implementasi pool tinggal pasang |
| Pemakaian lazim | skrip, alat bantu, uji cepat | aplikasi server, framework |
Pemisahan ini penting pada aplikasi besar: kode bisnis hanya mengenal DataSource, sementara tim operasional mengganti implementasi pool atau memindahkan database tanpa menyentuh kode. Pool paling populer di ekosistem Java adalah HikariCP — ringan, cepat, dan menjadi default Spring Boot. Konfigurasinya (ditampilkan sebagai contoh; HikariCP bukan bagian JDK):
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/toko");
config.setUsername("toko_app");
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(10); // batas koneksi simultan
config.setMinimumIdle(2); // koneksi siaga minimal
config.setConnectionTimeout(3000); // ms menunggu saat kolam kosong
config.setMaxLifetime(600_000); // ms umur maksimum sebuah koneksi
try (HikariDataSource ds = new HikariDataSource(config)) {
try (Connection conn = ds.getConnection()) { // pinjam
// bekerja seperti biasa
} // close = kembalikan ke pool
}Pool Mini dengan Semaphore #
Bagaimana sebenarnya sebuah pool menjaga jumlah peminjaman? Permisinya dijaga java.util.concurrent.Semaphore: satu izin per slot koneksi — acquire sebelum memberikan koneksi, release saat koneksi kembali. Implementasi mini berikut memakai pola itu dan membuktikan tiga perilaku inti pool: penolakan saat habis, pemakaian ulang objek yang sama, dan jumlah koneksi yang jauh lebih kecil daripada jumlah pemakaian:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.Semaphore;
public class PoolMini {
public static void main(String[] args) {
PoolKoneksi pool = new PoolKoneksi(3);
KoneksiSimulasi c = null;
try (KoneksiSimulasi a = pool.pinjam();
KoneksiSimulasi b = pool.pinjam();
KoneksiSimulasi c1 = pool.pinjam()) {
c = c1; // simpan referensi untuk uji pemakaian ulang
System.out.println("memegang koneksi : #" + a.nomor
+ ", #" + b.nomor + ", #" + c1.nomor);
// permintaan keempat saat seluruh koneksi sedang dipakai
KoneksiSimulasi tolak = pool.pinjamBilaAda();
System.out.println("pinjaman keempat : " + tolak);
} // close otomatis: c1, b, a kembali ke pool dalam urutan kebalikan
try (KoneksiSimulasi d = pool.pinjam()) {
System.out.println("pinjaman baru : koneksi #" + d.nomor
+ ", objek sama dengan sebelumnya: " + (d == c));
}
System.out.println("koneksi dibuat : " + pool.getDibuat());
System.out.println("pinjaman dilayani: " + pool.getDilayani());
}
}
/** Pool sederhana: Semaphore menjaga jumlah peminjaman maksimum. */
class PoolKoneksi {
private final Deque<KoneksiSimulasi> bebas = new ArrayDeque<>();
private final Semaphore izin;
private int dibuat = 0;
private int dilayani = 0;
PoolKoneksi(int ukuran) {
this.izin = new Semaphore(ukuran);
for (int i = 1; i <= ukuran; i++) {
bebas.add(new KoneksiSimulasi(this));
dibuat++;
}
System.out.println("[pool] dibuat " + ukuran + " koneksi sejak awal");
}
/** Versi memblokir: menunggu sampai ada koneksi bebas. */
KoneksiSimulasi pinjam() {
izin.acquireUninterruptibly();
KoneksiSimulasi k = bebas.poll();
dilayani++;
System.out.println("[pool] pinjam koneksi #" + k.nomor
+ " (bebas: " + bebas.size() + ")");
return k;
}
/** Versi tidak memblokir: null bila pool sedang habis. */
KoneksiSimulasi pinjamBilaAda() {
if (!izin.tryAcquire()) {
System.out.println("[pool] DITOLAK: seluruh koneksi sedang dipakai");
return null;
}
KoneksiSimulasi k = bebas.poll();
dilayani++;
System.out.println("[pool] pinjam koneksi #" + k.nomor);
return k;
}
void kembali(KoneksiSimulasi k) {
bebas.add(k);
izin.release();
System.out.println("[pool] kembali koneksi #" + k.nomor
+ " (bebas: " + bebas.size() + ")");
}
int getDibuat() {
return dibuat;
}
int getDilayani() {
return dilayani;
}
}
/** Koneksi simulasi: close() mengembalikan ke pool, bukan menutup fisik. */
class KoneksiSimulasi implements AutoCloseable {
final int nomor;
private final PoolKoneksi pool;
private static int penomor = 0;
KoneksiSimulasi(PoolKoneksi pool) {
this.pool = pool;
this.nomor = ++penomor;
}
@Override
public void close() {
pool.kembali(this);
}
}Output yang Dihasilkan:
[pool] dibuat 3 koneksi sejak awal
[pool] pinjam koneksi #1 (bebas: 2)
[pool] pinjam koneksi #2 (bebas: 1)
[pool] pinjam koneksi #3 (bebas: 0)
memegang koneksi : #1, #2, #3
[pool] DITOLAK: seluruh koneksi sedang dipakai
pinjaman keempat : null
[pool] kembali koneksi #3 (bebas: 1)
[pool] kembali koneksi #2 (bebas: 2)
[pool] kembali koneksi #1 (bebas: 3)
[pool] pinjam koneksi #3 (bebas: 2)
pinjaman baru : koneksi #3, objek sama dengan sebelumnya: true
[pool] kembali koneksi #3 (bebas: 3)
koneksi dibuat : 3
pinjaman dilayani: 4Mari kita telaah perilaku pool pada output di atas:
pinjaman keempat : null: ketiga izinSemaphoresedang terpakai sehinggatryAcquire()mengembalikanfalse— pool menolak tanpa memblokir. Versipinjam()yang memblokir memakaiacquireUninterruptibly()dan akan menunggu sampai ada pengembalian, persis perilakuconnectionTimeoutpada pool produksi.close()padaKoneksiSimulasitidak menutup apa pun: methodclose()hanya memanggilpool.kembali(this)— inilah trik yang membuattry-with-resources tetap nyaman dipakai di atas pool: kode aplikasi tetap menutup “koneksi” seperti biasa, tetapi efeknya pengembalian ke kolam.objek sama dengan sebelumnya: true: koneksi#3yang dikembalikan tersedia kembali dan pemanggil berikutnya menerima objek yang sama — bukti pemakaian ulang, inti penghematan pool.koneksi dibuat : 3,pinjaman dilayani : 4: empat pemakaian hanya membayar biaya pembuatan tiga koneksi. Pada aplikasi nyata dengan puluhan ribu pemakaian, rasionya jauh lebih dramatis.- Urutan pengembalian
#3,#2,#1:try-with-resources menutup resource dalam urutan kebalikan deklarasi, dan pool menerima ketiganya kembali dalam urutan itu.
Parameter Pool yang Perlu Disetel #
| Parameter | Arti | Patokan awal |
|---|---|---|
maximumPoolSize | jumlah koneksi maksimum | kecil: 5-20 tergantung beban |
minimumIdle | koneksi siaga minimum | sama dengan maksimum bila beban stabil |
connectionTimeout | lama menunggu saat kolam kosong | 2-5 detik |
maxLifetime | umur maksimum koneksi | di bawah batas idle server (mis. 30 menit) |
| validasi | pemeriksaan koneksi sebelum dipakai | isValid() JDBC 4 |
Dengan koneksi yang kini murah untuk “dibuka”, fondasi infrastruktur database kita lengkap. Bab-bab berikutnya naik ke lapisan arsitektur kode: bagaimana menata akses data agar tidak menggerus seluruh aplikasi — dimulai dari pola paling klasik, Data Access Object (DAO).
Referensi #
- Oracle: javax.sql.DataSource (JDK 25)
- HikariCP — GitHub
- Oracle: Semaphore (JDK 25)
- dev.java — Learn Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PoolMini.java
Unduh berkasimport java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.Semaphore;
public class PoolMini {
public static void main(String[] args) {
PoolKoneksi pool = new PoolKoneksi(3);
KoneksiSimulasi c = null;
try (KoneksiSimulasi a = pool.pinjam();
KoneksiSimulasi b = pool.pinjam();
KoneksiSimulasi c1 = pool.pinjam()) {
c = c1; // simpan referensi untuk uji pemakaian ulang
System.out.println("memegang koneksi : #" + a.nomor
+ ", #" + b.nomor + ", #" + c1.nomor);
// permintaan keempat saat seluruh koneksi sedang dipakai
KoneksiSimulasi tolak = pool.pinjamBilaAda();
System.out.println("pinjaman keempat : " + tolak);
} // close otomatis: c1, b, a kembali ke pool dalam urutan kebalikan
try (KoneksiSimulasi d = pool.pinjam()) {
System.out.println("pinjaman baru : koneksi #" + d.nomor
+ ", objek sama dengan sebelumnya: " + (d == c));
}
System.out.println("koneksi dibuat : " + pool.getDibuat());
System.out.println("pinjaman dilayani: " + pool.getDilayani());
}
}
/** Pool sederhana: Semaphore menjaga jumlah peminjaman maksimum. */
class PoolKoneksi {
private final Deque<KoneksiSimulasi> bebas = new ArrayDeque<>();
private final Semaphore izin;
private int dibuat = 0;
private int dilayani = 0;
PoolKoneksi(int ukuran) {
this.izin = new Semaphore(ukuran);
for (int i = 1; i <= ukuran; i++) {
bebas.add(new KoneksiSimulasi(this));
dibuat++;
}
System.out.println("[pool] dibuat " + ukuran + " koneksi sejak awal");
}
/** Versi memblokir: menunggu sampai ada koneksi bebas. */
KoneksiSimulasi pinjam() {
izin.acquireUninterruptibly();
KoneksiSimulasi k = bebas.poll();
dilayani++;
System.out.println("[pool] pinjam koneksi #" + k.nomor
+ " (bebas: " + bebas.size() + ")");
return k;
}
/** Versi tidak memblokir: null bila pool sedang habis. */
KoneksiSimulasi pinjamBilaAda() {
if (!izin.tryAcquire()) {
System.out.println("[pool] DITOLAK: seluruh koneksi sedang dipakai");
return null;
}
KoneksiSimulasi k = bebas.poll();
dilayani++;
System.out.println("[pool] pinjam koneksi #" + k.nomor);
return k;
}
void kembali(KoneksiSimulasi k) {
bebas.add(k);
izin.release();
System.out.println("[pool] kembali koneksi #" + k.nomor
+ " (bebas: " + bebas.size() + ")");
}
int getDibuat() {
return dibuat;
}
int getDilayani() {
return dilayani;
}
}
/** Koneksi simulasi: close() mengembalikan ke pool, bukan menutup fisik. */
class KoneksiSimulasi implements AutoCloseable {
final int nomor;
private final PoolKoneksi pool;
private static int penomor = 0;
KoneksiSimulasi(PoolKoneksi pool) {
this.pool = pool;
this.nomor = ++penomor;
}
@Override
public void close() {
pool.kembali(this);
}
}