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:

AspekDriverManagerDataSource
PemanggilangetConnection(url, ...) statisdataSource.getConnection() dari instance
URL dan kredensialditulis di kode pemanggiltersembunyi di dalam implementasi
Poolingtidak adaimplementasi pool tinggal pasang
Pemakaian lazimskrip, alat bantu, uji cepataplikasi 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):

java Konfigurasi HikariCP (butuh dependensi hikari-cp)
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:

java PoolMini.java
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:

text
[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: 4

Mari kita telaah perilaku pool pada output di atas:

  1. pinjaman keempat : null: ketiga izin Semaphore sedang terpakai sehingga tryAcquire() mengembalikan false — pool menolak tanpa memblokir. Versi pinjam() yang memblokir memakai acquireUninterruptibly() dan akan menunggu sampai ada pengembalian, persis perilaku connectionTimeout pada pool produksi.
  2. close() pada KoneksiSimulasi tidak menutup apa pun: method close() hanya memanggil pool.kembali(this) — inilah trik yang membuat try-with-resources tetap nyaman dipakai di atas pool: kode aplikasi tetap menutup “koneksi” seperti biasa, tetapi efeknya pengembalian ke kolam.
  3. objek sama dengan sebelumnya: true: koneksi #3 yang dikembalikan tersedia kembali dan pemanggil berikutnya menerima objek yang sama — bukti pemakaian ulang, inti penghematan pool.
  4. 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.
  5. 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 #

ParameterArtiPatokan awal
maximumPoolSizejumlah koneksi maksimumkecil: 5-20 tergantung beban
minimumIdlekoneksi siaga minimumsama dengan maksimum bila beban stabil
connectionTimeoutlama menunggu saat kolam kosong2-5 detik
maxLifetimeumur maksimum koneksidi bawah batas idle server (mis. 30 menit)
validasipemeriksaan koneksi sebelum dipakaiisValid() 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PoolMini.java

Unduh berkas
java
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);
    }
}