Dokumen ini menjelaskan metode bagi administrator database dan developer aplikasi untuk membuat urutan numerik unik dalam aplikasi yang menggunakan Spanner.
Pengantar
Sering kali ada situasi saat bisnis memerlukan ID numerik yang sederhana dan unik. misalnya, nomor karyawan atau nomor invoice. Database relasional konvensional sering kali menyertakan fitur untuk menghasilkan urutan angka yang unik dan meningkat secara monoton. Urutan ini digunakan untuk menghasilkan ID unik (kunci baris) untuk objek yang disimpan dalam database.
Namun, menggunakan nilai yang meningkat (atau menurun) secara monoton sebagai kunci baris mungkin tidak mengikuti praktik terbaik di Spanner karena menghasilkan hotspot di database, yang menyebabkan penurunan performa. Dokumen ini mengusulkan mekanisme untuk mengimplementasikan generator urutan menggunakan tabel database Spanner dan logika lapisan aplikasi.
Atau, Spanner mendukung generator urutan yang dibalik bit bawaan. Untuk informasi selengkapnya tentang generator urutan Spanner, lihat Membuat dan mengelola urutan.
Persyaratan untuk generator urutan
Setiap generator urutan harus menghasilkan nilai unik untuk setiap transaksi.
Bergantung pada kasus penggunaannya, generator urutan mungkin juga perlu membuat urutan dengan karakteristik berikut:
- Diurutkan: Nilai lebih rendah dalam urutan tidak boleh dikirimkan setelah nilai yang lebih tinggi.
- Tanpa Jeda: Tidak boleh ada celah dalam urutan.
Generator urutan juga harus menghasilkan nilai pada frekuensi yang diperlukan oleh aplikasi.
Semua persyaratan ini mungkin akan sulit dipenuhi, terutama dalam sistem terdistribusi. Jika perlu untuk memenuhi tujuan performa, Anda dapat membuat kompromi dalam persyaratan agar urutannya ditata dan tanpa jeda.
Mesin database lainnya memiliki cara untuk menangani persyaratan ini. Misalnya, urutan di kolom PostgreSQL dan AUTO_INCREMENT di MySQL dapat menghasilkan nilai unik untuk transaksi terpisah, tetapi tidak dapat menghasilkan nilai tanpa jeda jika transaksi di-roll back. Untuk mengetahui informasi selengkapnya, lihat Catatan dalam dokumentasi PostgreSQL) dan Implikasi Auto_INCREMENT di MySQL).
Generator urutan menggunakan baris tabel database
Aplikasi Anda dapat mengimplementasikan generator urutan menggunakan tabel database untuk menyimpan nama urutan dan nilai berikutnya dalam urutan.
Membaca dan meningkatkan sel next_value
urutan dalam transaksi
database akan menghasilkan nilai unik, tanpa memerlukan
sinkronisasi lebih lanjut antarproses aplikasi.
Pertama, tentukan tabel sebagai berikut:
CREATE TABLE sequences (
name STRING(64) NOT NULL,
next_value INT64 NOT NULL,
) PRIMARY KEY (name)
Anda dapat membuat urutan dengan menyisipkan baris dalam tabel dengan nama
urutan baru dan nilai awal—misalnya, ("invoice_id", 1)
. Namun, karena
sel next_value
bertambah untuk setiap nilai urutan yang dihasilkan, performa
dibatasi oleh seberapa sering baris dapat diperbarui.
Library Klien Spanner menggunakan transaksi yang dapat dicoba ulang untuk menyelesaikan konflik. Jika ada sel (nilai kolom) yang dibaca selama transaksi baca-tulis diubah di tempat lain, transaksi tersebut akan diblokir hingga transaksi lain selesai, lalu akan dibatalkan dan dicoba lagi agar dapat membaca transaksi yang telah diperbarui nilai-nilai. Hal ini meminimalkan durasi kunci tulis, tetapi juga berarti bahwa transaksi dapat dicoba beberapa kali sebelum berhasil di-commit.
Karena hanya satu transaksi yang dapat terjadi di satu baris dalam satu waktu, frekuensi maksimum penerbitan nilai urutan berbanding terbalik dengan total latensi transaksi.
Latensi transaksi total ini bergantung pada beberapa faktor, seperti latensi antara aplikasi klien dan node Spanner, latensi antara node Spanner, dan ketidakpastian TrueTime. Misalnya, konfigurasi multi-egional memiliki latensi transaksi yang lebih tinggi karena harus menunggu kuorum konfirmasi penulisan dari node di region yang berbeda agar dapat diselesaikan.
Misalnya, jika transaksi baca-update pada satu sel (satu kolom dalam satu baris) memiliki latensi 10 milidetik (md), maka frekuensi teoretis maksimum nilai urutan adalah 100 per detik. Maksimum ini berlaku untuk seluruh database, berapa pun jumlah instance aplikasi klien, atau jumlah node dalam database. Hal ini karena satu baris selalu dikelola oleh satu node.
Bagian berikut menjelaskan cara untuk mengatasi keterbatasan ini.
Implementasi sisi aplikasi
Kode aplikasi perlu membaca dan memperbarui sel next_value
dalam
database. Ada beberapa cara untuk melakukan ini, yang masing-masing memiliki karakteristik dan kelemahan performa yang berbeda.
Generator urutan dalam transaksi yang sederhana
Cara paling sederhana untuk menangani pembuatan urutan adalah dengan meningkatkan nilai kolom dalam transaksi setiap kali aplikasi memerlukan nilai berurutan baru.
Dalam satu transaksi, aplikasi akan melakukan hal berikut:
- Membaca sel
next_value
untuk nama urutan yang akan digunakan dalam aplikasi. - Menambahkan dan memperbarui sel
next_value
untuk nama urutan. - Menggunakan nilai yang diambil untuk nilai kolom apa pun yang dibutuhkan aplikasi.
- Menyelesaikan sisa transaksi aplikasi.
Proses ini menghasilkan urutan yang berurutan dan tanpa jeda. Jika tidak ada yang memperbarui sel next_value
dalam database ke nilai yang lebih rendah, urutannya juga akan unik.
Karena nilai urutan diambil sebagai bagian dari transaksi aplikasi yang lebih luas, frekuensi maksimum pembuatan urutan bergantung pada seberapa kompleks keseluruhan transaksi aplikasi. Transaksi yang kompleks akan memiliki latensi yang lebih tinggi, sehingga frekuensi maksimumnya lebih rendah.
Dalam sistem terdistribusi, banyak transaksi dapat dicoba secara bersamaan,
yang menyebabkan pertentangan yang tinggi pada nilai urutan. Karena sel next_value
diupdate dalam transaksi aplikasi, transaksi lain
yang mencoba menambah sel next_value
secara bersamaan akan diblokir
oleh transaksi pertama dan
akan dicoba ulang.
Hal ini menyebabkan peningkatan besar pada waktu yang diperlukan aplikasi untuk
berhasil menyelesaikan transaksi, yang dapat menyebabkan masalah performa.
Kode berikut memberikan contoh generator urutan dalam transaksi sederhana yang hanya menampilkan satu nilai urutan per transaksi. Pembatasan ini ada karena operasi tulis dalam transaksi yang menggunakan Mutation API tidak terlihat hingga transaksi di-commit, bahkan untuk membaca dalam transaksi yang sama. Oleh karena itu, memanggil fungsi ini beberapa kali dalam transaksi yang sama akan selalu menampilkan nilai urutan yang sama.
Kode contoh berikut menunjukkan cara mengimplementasikan fungsi getNext()
sinkron:
Kode contoh berikut menunjukkan cara fungsi getNext()
sinkron
digunakan dalam transaksi:
Generator urutan sinkron dalam transaksi yang ditingkatkan
Anda dapat memodifikasi abstraksi sebelumnya untuk menghasilkan beberapa nilai dalam satu transaksi dengan melacak nilai urutan yang dikeluarkan dalam transaksi.
Dalam satu transaksi, aplikasi akan melakukan hal berikut:
- Membaca sel
next_value
untuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini sebagai variabel secara internal.
- Setiap kali nilai urutan baru diminta, menambah variabel
next_value
yang tersimpan dan melakukan buffering operasi tulis yang menetapkan nilai sel yang diperbarui di database. - Menyelesaikan sisa transaksi aplikasi.
Jika Anda menggunakan abstraksi, objek untuk abstraksi ini harus dibuat
dalam transaksi. Objek ini melakukan pembacaan tunggal saat nilai pertama diminta. Objek ini melacak secara internal sel next_value
, sehingga
lebih dari satu nilai dapat dibuat.
Peringatan yang sama terkait latensi dan pertentangan yang berlaku pada versi sebelumnya juga berlaku untuk versi ini.
Kode contoh berikut menunjukkan cara mengimplementasikan fungsi getNext()
sinkron:
Kode contoh berikut menunjukkan cara menggunakan fungsi getNext()
sinkron dalam permintaan untuk dua nilai urutan:
Generator urutan di luar transaksi (asinkron).
Pada dua implementasi sebelumnya, performa generator bergantung pada latensi transaksi aplikasi. Anda dapat meningkatkan frekuensi maksimum, dengan mengorbankan kesenjangan dalam urutan, dengan menambahkan urutan dalam transaksi terpisah. (Ini adalah pendekatan yang digunakan oleh PostgreSQL.) Anda harus mengambil nilai urutan yang akan digunakan terlebih dahulu sebelum aplikasi memulai transaksinya.
Aplikasi melakukan hal berikut:
- Membuat transaksi pertama untuk mendapatkan dan memperbarui nilai urutan:
- Membaca sel
next_value
untuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini sebagai variabel.
- Menambahkan dan memperbarui sel
next_value
dalam database untuk nama urutan. - Menyelesaikan transaksi.
- Membaca sel
- Menggunakan nilai yang ditampilkan dalam transaksi terpisah.
Latensi transaksi terpisah ini akan mendekati latensi minimum, dengan performa yang mendekati frekuensi teoritis maksimum sebesar 100 nilai per detik (dengan asumsi latensi transaksi 10 md). Karena nilai urutan diambil secara terpisah, latensi transaksi aplikasi itu sendiri tidak berubah, dan pertentangan dapat diminimalkan.
Namun, jika nilai urutan diminta dan tidak digunakan, celah akan dibiarkan dalam urutan karena nilai urutan yang diminta tidak dapat di-roll back. Hal ini dapat terjadi jika aplikasi dibatalkan atau gagal selama transaksi setelah meminta nilai urutan.
Kode contoh berikut menunjukkan cara mengimplementasikan fungsi yang mengambil dan menambahkan sel next_value
di database:
Anda dapat menggunakan fungsi ini dengan mudah untuk mengambil satu nilai urutan baru, seperti yang ditunjukkan dalam implementasi fungsi getNext()
asinkron berikut:
Kode contoh berikut menunjukkan cara menggunakan fungsi getNext()
asinkron dalam permintaan untuk dua nilai urutan:
Pada contoh kode sebelumnya, Anda dapat melihat bahwa nilai urutan diminta di luar transaksi aplikasi. Hal ini karena Cloud Spanner tidak mendukung berjalannya transaksi di dalam transaksi lain di thread yang sama (juga dikenal sebagai transaksi bertingkat).
Anda dapat mengatasi batasan ini dengan meminta nilai urutan menggunakan thread latar belakang dan menunggu hasilnya:
Generator urutan batch
Anda dapat memperoleh peningkatan performa yang signifikan jika juga membatalkan persyaratan bahwa nilai urutan harus berurutan. Hal ini memungkinkan aplikasi mempertahankan batch nilai urutan dan menerbitkannya secara internal. Setiap instance aplikasi memiliki batch nilainya sendiri yang terpisah, sehingga nilai yang dikeluarkan tidak berurutan. Selain itu, instance aplikasi yang tidak menggunakan seluruh batch nilainya (misalnya, jika instance aplikasi dimatikan) akan menyisakan kekurangan nilai yang tidak digunakan dalam urutan.
Aplikasi akan melakukan hal berikut:
- Mempertahankan status internal untuk setiap urutan yang berisi nilai awal dan ukuran batch, serta nilai berikutnya yang tersedia.
- Meminta nilai urutan dari batch.
- Jika tidak ada nilai yang tersisa dalam batch, lakukan langkah berikut:
- Membuat transaksi untuk membaca dan memperbarui nilai urutan.
- Baca sel
next_value
untuk mengetahui urutannya. - Simpan nilai ini secara internal sebagai nilai awal batch baru.
- Tambahkan sel
next_value
dalam database dengan jumlah yang sama dengan ukuran tumpukan. - Menyelesaikan transaksi.
- Tampilkan nilai berikutnya yang tersedia dan tingkatkan status internal.
- Menggunakan nilai yang ditampilkan dalam transaksi.
Dengan metode ini, transaksi yang menggunakan nilai urutan akan mengalami peningkatan latensi hanya jika batch nilai urutan baru perlu dicadangkan.
Keuntungannya adalah dengan meningkatkan ukuran tumpukan, performa dapat ditingkatkan ke tingkat mana pun, karena faktor pembatas menjadi jumlah batch yang dikeluarkan per detik.
Misalnya, dengan ukuran batch 100, dengan asumsi latensi 10 milidetik untuk mendapatkan batch baru, dan oleh karena itu, maksimum 100 batch per detik—10.000 nilai urutan per detik dapat dikirimkan.
Kode contoh berikut menunjukkan cara menerapkan fungsi getNext()
menggunakan batch. Perhatikan bahwa kode tersebut menggunakan kembali fungsi getAndIncrementNextValueInDB()
yang ditentukan sebelumnya untuk mengambil batch nilai urutan baru dari
database.
Kode contoh berikut menunjukkan cara menggunakan fungsi getNext()
asinkron dalam permintaan untuk dua nilai urutan:
Sekali lagi, nilai harus diminta di luar transaksi (atau dengan menggunakan thread latar belakang) karena Spanner tidak mendukung transaksi bertingkat.
Generator urutan batch asinkron
Untuk aplikasi berperforma tinggi yang tidak dapat meningkatkan latensinya, Anda dapat meningkatkan performa generator batch sebelumnya dengan menyiapkan batch nilai baru saat batch nilai saat ini habis.
Anda dapat melakukannya dengan menetapkan nilai minimum yang menunjukkan kapan jumlah nilai urutan yang tersisa dalam batch terlalu rendah. Saat batas telah tercapai, generator urutan akan mulai meminta batch nilai baru di thread latar belakang.
Seperti versi sebelumnya, nilai tidak dikeluarkan secara berurutan, dan akan ada jeda nilai yang tidak digunakan dalam urutan jika transaksi gagal atau jika instance aplikasi dinonaktifkan.
Aplikasi akan melakukan hal berikut:
- Mempertahankan status internal untuk setiap urutan, yang berisi nilai awal batch dan nilai berikutnya yang tersedia.
- Meminta nilai urutan dari batch.
- Jika nilai yang tersisa dalam batch lebih kecil dari nilai minimum, lakukan
langkah berikut di thread latar belakang:
- Membuat transaksi untuk membaca dan memperbarui nilai urutan.
- Membaca sel
next_value
untuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini secara internal sebagai nilai awal batch next.
- Menambahkan sel
next_value
dalam database dengan jumlah yang sama dengan ukuran tumpukan. - Menyelesaikan transaksi.
- Jika tidak ada nilai yang tersisa dalam batch tersebut, ambil nilai awal batch berikutnya dari thread latar belakang (menunggu hingga selesai jika perlu), lalu buat batch baru menggunakan nilai awal yang diambil sebagai nilai berikutnya.
- Menampilkan nilai berikutnya dan menambahkan status internal.
- Menggunakan nilai yang ditampilkan dalam transaksi.
Untuk performa yang optimal, thread latar belakang harus dimulai dan diselesaikan sebelum Anda kehabisan nilai urutan dalam batch saat ini. Jika tidak, aplikasi harus menunggu batch berikutnya, dan latensi akan meningkat. Oleh karena itu, Anda harus menyesuaikan ukuran tumpukan dan nilai minimum yang rendah, bergantung pada frekuensi nilai urutan yang dikeluarkan.
Misalnya, asumsikan waktu transaksi 20 milidetik untuk mengambil batch nilai baru, ukuran tumpukan 1.000, dan frekuensi penerbitan urutan maksimum 500 nilai per detik (satu nilai setiap 2 milidetik). Selama 20 milidetik ketika batch nilai baru diterbitkan, 10 nilai urutan akan diterbitkan. Oleh karena itu, nilai minimum untuk jumlah nilai urutan yang tersisa harus lebih besar dari 10, agar batch berikutnya tersedia saat diperlukan.
Kode contoh berikut menunjukkan cara menerapkan fungsi getNext()
menggunakan batch. Perhatikan bahwa kode tersebut menggunakan fungsi getAndIncrementNextValueInDB()
yang ditentukan sebelumnya untuk mengambil batch nilai urutan menggunakan
thread latar belakang.
Kode contoh berikut menunjukkan cara fungsi getNext()
batch asinkron digunakan dalam permintaan untuk menggunakan dua nilai dalam transaksi:
Perhatikan bahwa dalam kasus ini, nilai dapat diminta di dalam transaksi, karena pengambilan batch nilai baru terjadi di thread latar belakang.
Ringkasan
Tabel berikut membandingkan karakteristik empat jenis generator urutan:
Sinkron | Asinkron | Batch | Batch asinkron | |
---|---|---|---|---|
Nilai unik | Ya | Ya | Ya | Ya |
Nilai diurutkan secara global | Ya | Ya | Tidak ada Tetapi dengan beban yang cukup tinggi dan ukuran batch yang cukup kecil, nilai akan berdekatan satu sama lain |
Tidak ada Tetapi dengan beban yang cukup tinggi dan ukuran batch yang cukup kecil, nilai akan berdekatan satu sama lain |
Tanpa Jeda | Ya | Tidak | Tidak | Tidak |
Performa | 1/latensi transaksi, (~25 nilai per detik) |
50-100 nilai per detik | 50–100 batch nilai per detik | 50–100 batch nilai per detik |
Peningkatan latensi | > 10 md Jauh lebih tinggi dengan pertentangan tinggi (saat transaksi memerlukan waktu lama) |
10 md pada setiap transaksi Jauh lebih tinggi dengan pertentangan tinggi |
10 md, namun hanya ketika batch nilai baru diambil | Nol, jika ukuran tumpukan dan batas rendah ditetapkan ke nilai yang sesuai |
Tabel sebelumnya juga menggambarkan fakta bahwa Anda mungkin perlu memenuhi persyaratan untuk nilai yang diurutkan secara global dan serangkaian nilai tanpa jeda untuk menghasilkan nilai yang unik, sekaligus memenuhi persyaratan performa secara keseluruhan.
Pengujian performa
Anda dapat menggunakan alat pengujian/analisis performa, yang terletak di repositori GitHub yang sama dengan class generator urutan sebelumnya, untuk menguji setiap generator urutan ini dan mendemonstrasikan performa dan karakteristik latensi. Alat ini menyimulasikan latensi transaksi aplikasi selama 10 milidetik dan menjalankan beberapa thread secara bersamaan yang meminta nilai urutan.
Pengujian performa hanya memerlukan instance Spanner satu node untuk diuji karena hanya satu baris yang diubah.
Misalnya, output berikut menunjukkan perbandingan performa versus latensi dalam mode sinkron dengan 10 thread:
$ ITERATIONS=2000
$ MODE=SYNC
$ NUMTHREADS=10
$ java -jar sequence-generator.jar \
$INSTANCE_ID $DATABASE_ID $MODE $ITERATIONS $NUMTHREADS
2000 iterations (10 parallel threads) in 58739 milliseconds: 34.048928 values/s
Latency: 50%ile 27 ms
Latency: 75%ile 31 ms
Latency: 90%ile 1189 ms
Latency: 99%ile 2703 ms
Tabel berikut membandingkan hasil untuk berbagai mode dan jumlah thread paralel, termasuk jumlah nilai yang dapat dihasilkan per detik, dan latensi pada persentil ke-50, ke-90, dan ke-99:
Mode dan parameter | Num threads | Nilai/dtk | Latensi persentil ke-50 (mdtk) | Latensi persentil ke-90 (mdtk) | Latensi persentil ke-99 (mdtk) |
---|---|---|---|---|---|
SYNC | 10 | 34 | 27 | 1189 | 2703 |
SYNC | 50 | 30.6 | 1191 | 3513 | 5982 |
ASYNC | 10 | 66.5 | 28 | 611 | 1460 |
ASYNC | 50 | 78.1 | 29 | 1695 | 3442 |
BATCH (ukuran 200) |
10 | 494 | 18 | 20 | 38 |
BATCH (ukuran tumpukan 200) | 50 | 1195 | 27 | 55 | 168 |
ASYNC BATCH (ukuran batch 200, LT 50) |
10 | 512 | 18 | 20 | 30 |
ASYNC BATCH (ukuran batch 200, LT 50) |
50 | 1622 | 24 | 28 | 30 |
Anda dapat melihat bahwa dalam mode sinkron (SYNC), dengan bertambahnya jumlah thread, akan terjadi peningkatan pertentangan. Hal ini menyebabkan latensi transaksi yang jauh lebih tinggi.
Dalam mode asinkron (ASYNC), karena transaksi untuk mendapatkan urutan lebih kecil dan terpisah dari transaksi aplikasi, pertentangannya lebih sedikit dan frekuensinya lebih tinggi. Namun, pertentangan masih dapat terjadi, yang menyebabkan latensi persentil ke-90 yang lebih tinggi.
Dalam mode batch (BATCH), latensi berkurang secara signifikan—kecuali untuk persentil ke-99, yang sesuai dengan saat generator perlu meminta batch nilai urutan lainnya secara sinkron dari database. Performa jauh lebih tinggi dalam mode BATCH daripada dalam mode ASYNC.
Mode batch 50-thread memiliki latensi yang lebih tinggi karena urutan dikeluarkan begitu cepat sehingga faktor pembatasnya adalah kekuatan instance virtual machine (VM) (dalam hal ini, mesin dengan 4 vCPU berjalan pada tingkat 350% CPU selama pengujian). Menggunakan beberapa mesin dan beberapa proses akan menampilkan hasil keseluruhan yang mirip dengan mode batch 10 thread.
Dalam mode BATCH ASYNC, variasi latensi bersifat minimal dan performanya lebih tinggi—bahkan dengan sejumlah besar thread—karena latensi permintaan batch baru dari database sepenuhnya tidak bergantung pada transaksi aplikasi.
Langkah selanjutnya
- Pelajari praktik terbaik untuk desain skema di Spanner.
- Baca cara memilih kunci dan indeks untuk tabel Spanner.
- Pelajari arsitektur referensi, diagram, dan praktik terbaik tentang Google Cloud. Lihat Cloud Architecture Center kami.