Meningkatkan performa menggunakan cache dengan server fleksibel Azure Database for PostgreSQL

Saat Anda membangun aplikasi di Azure Database for PostgreSQL Flexible Server, menambahkan lapisan cache adalah salah satu cara paling efektif untuk mempercepat waktu respons, mengurangi beban pada basis data Anda, dan meningkatkan ketahanan. Dengan sering membaca data dari cache dalam memori, aplikasi Anda mengirimkan lebih sedikit kueri ke PostgreSQL. Itu berarti konsumsi CPU dan IOPS yang lebih rendah, sehingga Anda dapat berjalan pada tingkat komputasi yang lebih kecil, menskalakan baca tanpa meningkatkan server, dan menyerap lonjakan lalu lintas. Cache juga dapat menambahkan ketahanan. Jika PostgreSQL mengalami gangguan singkat, permintaan yang mencapai data yang sudah ada di cache dapat terus berhasil, sehingga jalur baca tetap tersedia saat database pulih.

Artikel ini membantu Anda memutuskan kapan penggunaan cache bermanfaat dan pola yang paling sesuai untuk aplikasi Anda. Kemudian menerapkan empat pola caching di Python (cache-aside, prefetching data referensi, write-through, dan event-driven) menggunakan Azure Managed Redis, pustaka redis dan psycopg, serta autentikasi Microsoft Entra ID.

Kapan harus menambahkan cache

PostgreSQL sudah menyimpan halaman data yang sering diakses di cache buffer dan juga mendapat manfaat dari cache file sistem operasi. Cache ini membuat akses berulang lebih cepat, tetapi mereka berbagi memori yang dialokasikan ke server database dengan eksekusi kueri dan proses lainnya. Isinya juga perlu dihangatkan lagi setelah beberapa operasi pemeliharaan dan failover.

Anda bisa mendapatkan lebih banyak kapasitas cache database dengan menskalakan ke opsi komputasi dengan lebih banyak memori. Anda juga dapat menyetel pengaturan memori PostgreSQL, tetapi mengalokasikan lebih banyak memori ke cache buffer menyisakan lebih sedikit untuk eksekusi kueri dan sistem operasi. Uji perubahan memori dengan hati-hati untuk menghindari kondisi kehabisan memori.

Azure Managed Redis melengkapi cache asli ini. Ini menyimpan data aplikasi yang dipilih dan hasil kueri di luar server database, menyediakan akses latensi yang lebih rendah untuk jalur baca yang sensitif terhadap waktu, dan dapat menjaga pembacaan cache tetap tersedia saat PostgreSQL memulihkan atau menghangatkan cache-nya. Gunakan saat manfaat ini membenarkan logika aplikasi tambahan dan layanan lain untuk beroperasi. Ini tidak menggantikan PostgreSQL sebagai sumber kebenaran.

Menambahkan cache eksternal seperti Azure Managed Redis paling membantu ketika beban kerja Anda memiliki karakteristik berikut:

  • Pola akses yang didominasi pembacaan. Baris yang sama dibaca jauh lebih sering daripada yang diubah, seperti katalog produk, profil pengguna, data konfigurasi, atau tabel referensi.
  • Kueri yang mahal atau berulang. Agregasi, gabungan, atau hasil komputasi yang mahal untuk menghasilkan tetapi stabil selama jendela waktu singkat.
  • Titik akhir yang sensitif terhadap latensi. Operasi yang berhadapan langsung dengan pengguna, di mana pembacaan dari memori (kurang dari satu milidetik) lebih disukai daripada akses bolak-balik ke database.
  • Lonjakan yang dapat diprediksi. Trafik musiman atau yang dipicu oleh peristiwa, di mana caching menyerap beban yang jika tidak ada caching akan memaksa Anda meningkatkan kapasitas komputasi.
  • Sensitivitas pemeliharaan dan failover. Membaca jalur yang membutuhkan waktu respons yang stabil saat instans PostgreSQL memulihkan atau menghangatkan cache-nya setelah operasi pemeliharaan atau failover.

Penyimpanan dalam cache kurang membantu untuk beban kerja yang didominasi penulisan, data yang harus selalu konsisten secara transaksional, atau kueri yang sudah cepat dan jarang diulang.

Pola penembolokan

Artikel ini menggunakan toko ritel sebagai contoh yang digunakan sepanjang artikel. Berbagai bagian aplikasi cocok dengan pola cache yang berbeda. Bagian berikut menerapkan empat pola pertama dalam Python. Artikel ini menjelaskan pola offload sesi dan status serta cache multi-region, tetapi tidak menyediakan implementasi kode untuk pola-pola tersebut.

Pattern Cara kerjanya Di etalase ritel
Cache-aside (pemuatan tertunda) Aplikasi memeriksa cache terlebih dahulu. Saat terjadi cache miss, sistem membaca dari PostgreSQL, lalu mengisi cache dengan data tersebut. Katalog produk dan halaman detail, dengan beberapa produk populer menyumbang sebagian besar pembacaan.
Pengambilan awal data referensi Data yang stabil dimuat ke cache sejak awal dan disegarkan saat sumbernya berubah, bukan saat terjadi cache miss. Kategori, merek, dan konfigurasi pengiriman.
Write-through Aplikasi menulis ke cache dan PostgreSQL dalam operasi yang sama, menjaganya tetap konsisten. Pembaruan harga dan inventori yang harus segera terlihat.
Invalidasi berbasis peristiwa Entri cache diperbarui atau tidak valid sebagai respons terhadap peristiwa perubahan data alih-alih pada timer. Status pesanan saat pesanan melalui proses pemenuhan.
Pengalihan beban sesi dan keadaan Status sementara berada di cache alih-alih database. Keranjang belanja dan sesi pengguna.
Penembolokan multi-wilayah Cache di setiap wilayah melayani bacaan lokal, tetap sinkron dengan replikasi geografis aktif. Etalase global yang melayani pembeli di beberapa wilayah.

Prasyarat

Tip

Untuk versi lengkap dan dapat disebarkan dari sampel ini, termasuk infrastruktur sebagai kode dan keempat pola, lihat repositori amr-caching-pattern-samples pada GitHub.

Langkah 1: Instal pustaka klien

Instal pustaka klien Redis dan PostgreSQL, bersama dengan pustaka identitas Azure untuk autentikasi Microsoft Entra.

pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"

Langkah 2: Sambungkan dengan Microsoft Entra ID

Gunakan autentikasi Microsoft Entra ID alih-alih kunci akses. Microsoft Entra ID menghapus kebutuhan untuk menyimpan rahasia di aplikasi Anda dan memungkinkan Anda mengelola akses secara terpusat.

Kode berikut membuat klien Redis dan koneksi PostgreSQL, keduanya diautentikasi dengan identitas terkelola atau kredensial pengembang melalui DefaultAzureCredential. Contoh ini menggunakan klien RedisCluster yang mendukung kluster, yang sesuai dengan kebijakan klastering OSS yang digunakan dalam sampel ini. Jika cache Anda menggunakan kebijakan pengklusteran Enterprise, gunakan klien standar redis.Redis sebagai gantinya.

import os
import json
import redis
from redis.cluster import RedisCluster
import psycopg
from azure.identity import DefaultAzureCredential

REDIS_HOST = os.environ["REDIS_HOST"]  # for example, mycache.eastus.redis.azure.net
REDIS_PORT = 10000
PG_HOST = os.environ["PG_HOST"]        # for example, myserver.postgres.database.azure.com
PG_DATABASE = os.environ["PG_DATABASE"]

credential = DefaultAzureCredential()

# Acquire a token for Azure Managed Redis and use it as the password.
redis_token = credential.get_token("https://redis.azure.com/.default")

# The username is the object ID of the Microsoft Entra identity.
redis_client = RedisCluster(
    host=REDIS_HOST,
    port=REDIS_PORT,
    ssl=True,
    ssl_check_hostname=False,  # cluster nodes are reached by IP; the certificate chain is still validated
    username=os.environ["REDIS_USER_OBJECT_ID"],
    password=redis_token.token,
    decode_responses=True,
)

# Acquire a token for Azure Database for PostgreSQL and use it as the password.
pg_token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default")

pg_conn = psycopg.connect(
    host=PG_HOST,
    dbname=PG_DATABASE,
    user=os.environ["PG_USER"],
    password=pg_token.token,
    sslmode="require",
)

Note

Token akses Microsoft Entra biasanya akan kedaluwarsa setelah sekitar satu jam. Untuk aplikasi yang berjalan lama, segarkan token sebelum masa berlakunya habis dan lakukan koneksi ulang untuk Azure Managed Redis dan Azure Database for PostgreSQL, atau gunakan alat bantu yang secara transparan mendapatkan ulang token. Untuk detailnya, lihat Menggunakan Microsoft Entra ID untuk autentikasi dengan Azure Managed Redis.

Langkah 3: Cache-aside

Cache-aside adalah pola yang paling umum dan digunakan untuk operasi pembacaan data produk dalam contoh ini. Aplikasi memeriksa Redis terlebih dahulu, dan jika terjadi cache miss, aplikasi mengueri PostgreSQL lalu mengisi cache dengan masa berlaku (TTL). Beberapa produk populer menyumbang sebagian besar pembacaan, sehingga rasio hit tinggi.

Karena sebagian besar bacaan disajikan dari memori, cache-aside mengambil beban baca berkelanjutan dari PostgreSQL. Pengurangan ini berarti lebih sedikit koneksi, lebih sedikit perputaran cache buffer, serta penggunaan CPU dan IOPS yang lebih rendah. Anda dapat menyerap lonjakan baca tanpa meningkatkan kapasitas server atau menambahkan replika baca. Anda menjalankan kueri ke PostgreSQL hanya saat terjadi cache miss (akses pertama, atau setelah TTL berakhir). Lihat Menemukan apa yang harus di-cache untuk mengidentifikasi kueri yang layak di-cache.

CACHE_TTL_SECONDS = 300  # 5 minutes

def get_product(product_id: int) -> dict | None:
    cache_key = f"product:{product_id}"

    # 1. Try the cache first.
    cached = redis_client.get(cache_key)
    if cached is not None:
        return json.loads(cached)

    # 2. On a miss, read from PostgreSQL.
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
        row = cursor.fetchone()
    if row is None:
        return None

    product = {"id": row[0], "name": row[1], "price": float(row[2])}

    # 3. Populate the cache with a TTL, then return.
    redis_client.set(cache_key, json.dumps(product), ex=CACHE_TTL_SECONDS)
    return product

Langkah 4: Prapengambilan data referensi

Data stabil yang dibaca terus-menerus tetapi jarang berubah (misalnya, kategori, merek, atau konfigurasi pengiriman) tidak perlu menunggu ketinggalan cache. Muat ke cache di depan dan refresh saat sumber berubah. Dalam skema PostgreSQL, biasanya ini adalah tabel pencarian dan dimensi kecil yang digabungkan ke dalam banyak kueri. Menyajikannya dari memori akan menghapus volume besar gabungan dan pencarian berulang dari database. Tidak seperti cache-aside, tidak ada miss per permintaan dan tidak ada balapan TTL. Anda menyegarkan perubahan, sehingga bacaan selalu hangat.

def prefetch_categories() -> None:
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name FROM categories ORDER BY name")
        categories = [{"id": r[0], "name": r[1]} for r in cursor.fetchall()]
    redis_client.set("ref:categories", json.dumps(categories))  # no TTL; refreshed on change

def get_categories() -> list[dict]:
    cached = redis_client.get("ref:categories")
    return json.loads(cached) if cached else []

Langkah 5: Tulis-tembus

Ketika perubahan harus segera terlihat, tulis PostgreSQL dan cache dalam operasi yang sama alih-alih menunggu TTL kedaluwarsa atau membatalkan kunci. Misalnya, pola ini digunakan untuk memperbarui informasi harga dalam sampel. PostgreSQL tetap menjadi sumber kebenaran. Perubahan diterapkan di sana terlebih dahulu, lalu cache di-refresh, jadi baca setelah tulis mengembalikan nilai baru.

Saat menulis data ke dua sistem yang tidak berada dalam transaksi yang sama, komit database dapat berhasil sementara penyegaran cache gagal, dan tidak ada solusi sederhana untuk mengatasinya. Cuplikan berikut menunjukkan skenario ideal dan tidak mencakup penanganan kegagalan. Dalam produksi, Anda menentukan cara menangani refresh yang gagal, misalnya dengan mencoba lagi jika kegagalannya tampak bersifat sementara, atau menginvalidasi kunci agar pembacaan berikutnya memuat ulang data dari PostgreSQL. Bagaimanapun, PostgreSQL menyimpan nilai yang benar, sehingga entri cache kedaluarsa atau hilang selalu dapat dipulihkan. Ketika pembaruan cache harus benar-benar diterapkan secara andal, sebaiknya picu melalui stream perubahan database (lihat invalidasi berbasis peristiwa).

def update_price(product_id: int, new_price: float) -> None:
    # 1. Write to PostgreSQL, the source of truth.
    with pg_conn.cursor() as cursor:
        cursor.execute("UPDATE products SET price = %s WHERE id = %s", (new_price, product_id))
    pg_conn.commit()

    # 2. Refresh the cached entry so reads see the new price right away.
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
        row = cursor.fetchone()
    if row is not None:
        product = {"id": row[0], "name": row[1], "price": float(row[2])}
        redis_client.set(f"product:{product_id}", json.dumps(product), ex=CACHE_TTL_SECONDS)

Langkah 6: Pembatalan berbasis peristiwa

Pembatalan yang digerakkan oleh peristiwa membuat cache tetap konsisten dengan database dengan bereaksi terhadap peristiwa perubahan data. Ini memperbarui atau membuat entri menjadi tidak valid saat entri berubah, alih-alih menunggu kedaluwarsa berdasarkan pengatur waktu. Penulis menambahkan peristiwa ke aliran Redis yang tahan lama, log khusus tambahan, dan satu atau beberapa konsumen membaca peristiwa tersebut dan memperbarui cache. Karena stream tetap ada, event tidak hilang saat consumer dimulai ulang. Grup konsumen mengirimkan setiap peristiwa kepada satu pekerja, melacak pengakuan sehingga tidak ada yang hilang atau diproses dua kali, dan memungkinkan Anda menskalakan pemrosesan di seluruh pekerja.

Gunakan pola ini saat nilai cache berasal dari data yang berubah di tempat lain (status, proyeksi, atau agregat), di mana TTL akan melayani data kedaluarsa atau memaksa komputasi ulang konstanta. Di etalase toko, pola ini menentukan status pesanan sepanjang proses pemenuhan pesanan. Membuat pesanan menulis data pesanan ke PostgreSQL, menyimpan status awalnya dalam cache, dan menambahkan event placed ke aliran:

ORDER_STREAM = "orders:events"

def place_order(product_id: int, quantity: int) -> int:
    with pg_conn.cursor() as cursor:
        cursor.execute(
            "INSERT INTO orders (product_id, quantity, status) VALUES (%s, %s, 'placed') RETURNING id",
            (product_id, quantity),
        )
        order_id = cursor.fetchone()[0]
    pg_conn.commit()

    redis_client.set(f"order:{order_id}:status", "placed", ex=86400)
    redis_client.xadd(ORDER_STREAM, {"order_id": order_id, "status": "placed"}, maxlen=10000, approximate=True)
    return order_id

Pekerja pemenuhan menjalankan grup konsumen: ia membaca peristiwa baru, memajukan setiap pesanan di PostgreSQL, me-refresh proyeksi cache order:{id}:status , dan mengakui peristiwa tersebut. Halaman pesanan membaca proyeksi tersebut, sehingga pemeriksaan status tetap cepat dan tidak pernah menyentuh database. Nilai tetap benar karena peristiwa tetap terkini.

GROUP = "fulfillment"

def process_orders() -> None:
    try:
        redis_client.xgroup_create(ORDER_STREAM, GROUP, id="0", mkstream=True)
    except redis.exceptions.ResponseError:
        pass  # group already exists

    while True:
        events = redis_client.xreadgroup(GROUP, "worker-1", {ORDER_STREAM: ">"}, count=10, block=5000)
        for _stream, entries in events or []:
            for event_id, fields in entries:
                order_id = int(fields["order_id"])
                with pg_conn.cursor() as cursor:
                    cursor.execute("UPDATE orders SET status = 'shipped' WHERE id = %s", (order_id,))
                pg_conn.commit()
                redis_client.set(f"order:{order_id}:status", "shipped", ex=86400)
                redis_client.xack(ORDER_STREAM, GROUP, event_id)

Sumber kejadian dalam sampel ini adalah aplikasi, yang menulis PostgreSQL dan menambahkan peristiwa di jalur yang sama. PostgreSQL juga dapat memancarkan perubahan itu sendiri: LISTEN/NOTIFY untuk pemberitahuan ringan, atau decoding logis (mengubah pengambilan data) untuk aliran perubahan tingkat baris yang tahan lama. Dengan menjadikan aliran perubahan PostgreSQL itu sendiri sebagai penggerak cache, cache akan bereaksi terhadap setiap perubahan yang sudah dikomit, termasuk operasi tulis yang melewati aplikasi.

Praktik terbaik

Ikuti praktik ini untuk menjaga cache Anda tetap benar, efisien, dan hemat biaya.

  • Atur TTL jika ada risiko data menjadi usang. TTL membatasi tingkat keusangan data jika invalidasi gagal. Sesuaikan TTL dengan tingkat kedaluwarsa maksimum yang masih dapat ditoleransi oleh aplikasi. Gunakan TTL panjang atau tanpa TTL untuk data referensi hanya ketika ada proses invalidasi dan refresh yang andal.
  • Gunakan skema penamaan kunci yang konsisten. Kunci namespace menurut layanan, entitas, dan pengidentifikasi, seperti product:42 atau user:1001:profile. Tambahkan versi saat format kunci atau skema nilai dapat berubah.
  • Simpan dalam cache pada tingkat granularitas yang tepat. Simpan di cache entitas individual atau kumpulan hasil kecil saat keduanya sering digunakan kembali dan mudah diinvalidasi. Jangan cache data yang memiliki sedikit penggunaan kembali. Penggunaan cache yang berlebihan memboroskan memori dan dapat menurunkan hit rate.
  • Tangani kegagalan cache dan gangguan dengan baik. Perlakukan cache sebagai pengoptimalan, bukan sumber kebenaran. Jika Redis tidak tersedia, gunakan fallback terikat ke PostgreSQL. Tambahkan batas waktu, circuit breaker, backoff, dan batas permintaan untuk melindungi PostgreSQL. Jika PostgreSQL mengalami gangguan singkat, Anda dapat tetap melayani operasi baca untuk data yang sudah ada di cache, sementara operasi tulis menunggu basis data pulih.
  • Cegah lonjakan permintaan akibat kedaluwarsanya cache. Ketika kunci populer kedaluwarsa, banyak permintaan dapat mencapai database sekaligus. Gunakan TTL jitter, penggabungan permintaan, stale-while-revalidate, atau kunci terdistribusi berdurasi singkat agar satu permintaan dapat mengisi ulang entri tersebut.
  • Sesuaikan ukuran cache. Pantau rasio hit, penggunaan memori, rasio penggusuran, rasio kedaluwarsa, latensi, kunci yang sering diakses, dan kardinalitas kunci. Laju hit rendah dapat menunjukkan cache yang terlalu kecil, pengeluaran berlebihan, pemilihan kunci yang buruk, atau pola akses yang buruk. Untuk panduan ukuran, lihat panduan pemilihan tingkat Azure Managed Redis.
  • Pilih kebijakan pengeluaran yang sesuai dengan kunci Anda. Untuk database khusus cache, mulailah dengan allkeys-lru atau allkeys-lfu. Gunakan kebijakan volatile-* hanya jika database yang sama berisi kunci cache yang kedaluwarsa dan kunci non-kedaluwarsa yang dilindungi. Kebijakan volatile dapat berhenti melakukan eviksi ketika tidak ada kunci yang memiliki TTL. Pisahkan data cache dari data yang dilindungi jika memungkinkan.
  • Serialisasikan secara efisien. JSON dapat dibaca dan portabel. Untuk jalur throughput tinggi, uji format biner ringkas untuk mengurangi memori dan overhead jaringan. Memori tolok ukur, CPU, latensi, evolusi skema, dan dampak debugging sebelum Anda mengubah format.

Temukan apa yang harus di-cache

Target cache yang paling efektif adalah kueri yang paling sering dijalankan aplikasi Anda terhadap data yang paling sedikit berubah. Alih-alih menebak kueri mana yang sesuai dengan deskripsi ini, bandingkan tampilan historis dan saat ini dalam telemetri kueri yang dapat dikumpulkan Azure Database for PostgreSQL saat Anda mengaktifkan fitur yang relevan:

  • Query Store mempertahankan statistik eksekusi kueri untuk analisis historis. Gunakan jumlah panggilan dan total dan waktu eksekusi rata-rata untuk menemukan kueri yang secara konsisten mendominasi beban database selama periode yang lebih lama. Lihat Memantau kinerja dengan Query Store.
  • Query Performance Insight menampilkan visualisasi data Query Store di portal Azure, sehingga Anda dapat mengidentifikasi kueri yang sering dijalankan dan banyak menggunakan sumber daya serta membandingkan perilakunya seiring waktu. Lihat Query Performance Insight.
  • pg_stat_statements mengekspos statistik per pernyataan kumulatif di dalam database untuk tampilan langsung dari jendela pengamatan saat ini. Statistiknya dapat diatur ulang, jadi gunakan Query Store saat Anda memerlukan riwayat yang dipertahankan di seluruh jendela pengamatan.

Gunakan kedua tampilan sebelum Anda memilih kandidat cache. Lonjakan jangka pendek mungkin tidak mewakili perilaku normal beban kerja, sementara rata-rata historis dapat menyembunyikan regresi saat ini. Prioritaskan kueri yang sering, mahal, dan stabil, yang berarti jumlah panggilan tinggi, total waktu eksekusi yang tinggi, dan hasil yang tidak berubah pada setiap permintaan. Kueri tersebut memberikan tingkat hit cache tertinggi dan penurunan beban database terbesar. Kueri yang berjalan terus-menerus tetapi mengembalikan baris yang sama selama beberapa menit, seperti daftar produk, pohon kategori, atau tabel harga, adalah kandidat yang ideal.