Dokumen ini menjelaskan metode yang dapat digunakan administrator database dan developer aplikasi untuk menghasilkan urutan numerik unik dalam aplikasi yang menggunakan Spanner.
Pengantar
Sering kali, terdapat situasi saat bisnis memerlukan ID numerik yang sederhana dan unik—misalnya, untuk nomor induk karyawan atau nomor invoice. Database relasional konvensional biasanya 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 di database.
Namun, penggunaan nilai yang meningkat (atau menurun) secara monoton sebagai kunci baris mungkin tidak mengikuti praktik terbaik di Spanner karena hal itu menghasilkan hotspot di database, yang menyebabkan penurunan performa. Dokumen ini mengusulkan mekanisme untuk mengimplementasikan generator urutan menggunakan tabel database Spanner dan logika lapisan aplikasi.
Spanner juga mendukung generator urutan bit-terbalik bawaan. Untuk mengetahui informasi selengkapnya tentang generator urutan Spanner, baca 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:
- Urut: Nilai yang lebih rendah dalam urutan tidak boleh diterbitkan setelah nilai yang lebih tinggi.
- Tanpa celah: Tidak boleh ada celah dalam urutan.
Generator urutan juga harus menghasilkan nilai pada frekuensi yang diperlukan oleh aplikasi.
Persyaratan di atas mungkin sulit dipenuhi semuanya, terutama dalam sistem terdistribusi. Jika diperlukan untuk memenuhi tujuan performa, Anda dapat membuat kompromi terkait persyaratan bahwa urutan harus urut dan tanpa celah.
Mesin database lain memiliki cara untuk menangani persyaratan tersebut. Misalnya, urutan di kolom PostgreSQL dan AUTO_INCREMENT di MySQL dapat menghasilkan nilai unik untuk transaksi terpisah, tetapi tidak dapat menghasilkan nilai tanpa celah jika transaksi di-roll back. Untuk mengetahui informasi selengkapnya, lihat Catatan dalam dokumentasi PostgreSQL dan Implikasi Auto_INCREMENT di MySQL.
Generator urutan yang menggunakan baris tabel database
Aplikasi Anda dapat mengimplementasikan generator urutan menggunakan tabel database untuk menyimpan nama urutan dan nilai berikutnya dalam urutan itu.
Membaca dan meningkatkan sel next_value urutan di dalam transaksi database akan menghasilkan nilai unik, tanpa perlu sinkronisasi lebih lanjut antar-proses 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 yang berisi nama urutan baru dan nilai awal—misalnya ("invoice_id", 1). Namun, karena sel next_value meningkat untuk setiap nilai urutan yang dihasilkan, performanya dibatasi oleh seberapa sering baris dapat diperbarui.
Library Klien Spanner menggunakan transaksi yang dapat dicoba ulang untuk menyelesaikan konflik. Jika sel (nilai kolom) apa pun yang dibaca selama transaksi baca-tulis diubah di tempat lain, transaksi itu akan diblokir hingga transaksi lainnya selesai, lalu akan dibatalkan dan dicoba lagi agar membaca nilai yang telah diperbarui. Hal ini meminimalkan durasi penguncian operasi tulis, tetapi juga berarti transaksi dapat dicoba berkali-kali sebelum berhasil di-commit.
Karena hanya satu transaksi yang dapat terjadi di satu baris pada satu waktu, frekuensi maksimum penerbitan nilai urutan berbanding terbalik dengan total latensi transaksi.
Total latensi transaksi ini bergantung pada beberapa faktor, seperti latensi antara aplikasi klien dan node Spanner, latensi antar-node Spanner, dan ketidakpastian TrueTime. Misalnya, konfigurasi multi-region memiliki latensi transaksi lebih tinggi karena harus menunggu kuorum konfirmasi operasi tulis dari node di region lain agar dapat diselesaikan.
Misalnya, jika transaksi baca-perbarui di satu sel (satu kolom dalam satu baris) memiliki latensi 10 milidetik (mdtk), frekuensi teoretis maksimum penerbitan nilai urutan adalah 100 per detik. Frekuensi maksimum ini berlaku untuk seluruh database, berapa pun jumlah instance aplikasi klien, atau jumlah node dalam database itu. Hal ini karena satu baris selalu dikelola oleh satu node.
Bagian berikut menjelaskan cara mengatasi keterbatasan ini.
Implementasi sisi aplikasi
Kode aplikasi perlu membaca dan memperbarui sel next_value dalam database. Ada beberapa cara untuk melakukan hal ini, dengan karakteristik performa dan kelemahannya masing-masing.
Generator urutan dalam-transaksi 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 melakukan hal berikut:
- Membaca sel
next_valueuntuk nama urutan yang akan digunakan dalam aplikasi. - Meningkatkan dan memperbarui sel
next_valueuntuk nama urutan. - Menggunakan nilai yang diambil untuk nilai kolom apa pun yang dibutuhkan aplikasi.
- Menyelesaikan transaksi aplikasi selebihnya.
Proses ini menghasilkan urutan yang berurutan dan tanpa celah. Jika tidak ada apa pun yang memperbarui sel next_value dalam database ke nilai yang lebih rendah, urutan tersebut juga akan unik.
Karena nilai urutan diambil sebagai bagian dari transaksi aplikasi yang lebih luas, frekuensi maksimum pembuatan urutan bergantung pada seberapa kompleks transaksi aplikasi tersebut secara keseluruhan. Transaksi yang kompleks akan memiliki latensi lebih tinggi, sehingga frekuensi maksimumnya lebih rendah.
Dalam sistem terdistribusi, banyak transaksi dapat dicoba secara bersamaan, sehingga timbul pertentangan yang tinggi pada nilai urutan. Karena sel next_value diperbarui di dalam transaksi aplikasi, transaksi lain apa pun yang mencoba meningkatkan sel next_value pada waktu bersamaan akan diblokir oleh transaksi pertama dan akan dicoba ulang.
Akibatnya, waktu yang diperlukan aplikasi untuk menyelesaikan transaksi dengan baik meningkat, sehingga dapat menimbulkan masalah performa.
Kode berikut memberikan contoh generator urutan dalam-transaksi sederhana yang hanya menampilkan satu nilai urutan per transaksi. Pembatasan ini berlaku karena operasi tulis di dalam transaksi yang menggunakan Mutation API tidak akan terlihat hingga transaksi di-commit, termasuk oleh operasi baca dalam transaksi yang sama. Oleh karena itu, memanggil fungsi ini berkali-kali dalam transaksi yang sama akan selalu menampilkan nilai urutan yang sama.
Contoh kode berikut menunjukkan cara mengimplementasikan fungsi getNext() sinkron:
Contoh kode berikut menunjukkan penggunaan fungsi getNext() sinkron dalam transaksi:
Generator urutan sinkron dalam-transaksi dan ditingkatkan
Anda dapat memodifikasi abstraksi di atas untuk menghasilkan beberapa nilai dalam satu transaksi dengan melacak nilai urutan yang diterbitkan dalam sebuah transaksi.
Dalam satu transaksi, aplikasi melakukan hal berikut:
- Membaca sel
next_valueuntuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini secara internal sebagai variabel.
- Setiap kali nilai urutan baru diminta, meningkatkan variabel
next_valuetersimpan dan mem-buffer operasi tulis yang menetapkan nilai sel yang diperbarui di database. - Menyelesaikan transaksi aplikasi selebihnya.
Jika Anda menggunakan abstraksi, objek untuk abstraksi ini harus dibuat di dalam transaksi. Objek ini melakukan satu operasi baca saat nilai pertama diminta. Objek ini melacak sel next_value secara internal, sehingga lebih dari satu nilai dapat dihasilkan.
Catatan yang sama terkait latensi dan pertentangan yang berlaku untuk versi sebelumnya juga berlaku untuk versi ini.
Contoh kode berikut menunjukkan cara mengimplementasikan fungsi getNext() sinkron:
Contoh kode berikut menunjukkan penggunaan fungsi getNext() sinkron dalam permintaan untuk dua nilai urutan:
Generator urutan luar-transaksi (asinkron).
Pada dua implementasi sebelumnya, performa generator bergantung pada latensi transaksi aplikasi. Anda dapat meningkatkan frekuensi maksimum—tetapi dengan menoleransi celah dalam urutan—dengan meningkatkan urutan dalam transaksi terpisah. (Pendekatan ini 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_valueuntuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini sebagai variabel.
- Meningkatkan dan memperbarui sel
next_valuedalam database untuk nama urutan itu. - 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 yakni 100 nilai per detik (dengan asumsi latensi transaksi 10 mdtk). Karena nilai urutan diambil terpisah, latensi transaksi aplikasi itu sendiri tidak berubah, dan pertentangan dapat diminimalkan.
Namun, jika nilai urutan diminta dan tidak digunakan, celah akan ada dalam urutan karena nilai urutan yang telah diminta tidak dapat di-roll back. Hal ini dapat terjadi jika aplikasi dibatalkan atau gagal selama transaksi setelah meminta nilai urutan.
Contoh kode berikut menunjukkan cara mengimplementasikan fungsi yang mengambil dan meningkatkan 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:
Contoh kode berikut menunjukkan cara menggunakan fungsi getNext() asinkron dalam permintaan untuk dua nilai urutan:
Dalam contoh kode di atas, Anda dapat melihat bahwa nilai urutan diminta di luar transaksi aplikasi. Hal ini karena Cloud Spanner tidak mendukung pengoperasian sebuah transaksi di dalam transaksi lain di thread yang sama (disebut juga 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 dengan menghilangkan persyaratan bahwa nilai urutan harus urut. Hal ini memungkinkan aplikasi mempertahankan batch nilai urutan dan menerbitkannya secara internal. Setiap instance aplikasi memiliki batch nilainya masing-masing, sehingga nilai yang diterbitkan tidak berurutan. Selain itu, instance aplikasi yang tidak menggunakan seluruh batch nilainya (misalnya, jika instance aplikasi dimatikan) akan meninggalkan celah berupa nilai yang tidak digunakan dalam urutan itu.
Aplikasi akan melakukan hal berikut:
- Mempertahankan status internal setiap urutan yang berisi nilai awal dan ukuran batch, serta nilai berikutnya yang tersedia.
- Meminta nilai urutan dari batch.
- Menjalankan langkah berikut jika tidak ada nilai yang tersisa dalam batch itu:
- Membuat transaksi untuk membaca dan memperbarui nilai urutan.
- Membaca sel
next_valueuntuk urutan itu. - Menyimpan nilai ini secara internal sebagai nilai awal batch baru.
- Meningkatkan sel
next_valuedalam database sebesar ukuran batch. - Menyelesaikan transaksi.
- Menampilkan nilai berikutnya yang tersedia dan meningkatkan 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 yang baru perlu dipertahankan.
Kelebihannya adalah dengan meningkatkan ukuran batch, performa dapat ditingkatkan ke level mana pun, karena faktor pembatasnya menjadi jumlah batch yang diterbitkan per detik.
Misalnya, jika ukuran batch adalah 100—dengan asumsi latensi 10 milidetik untuk mendapatkan batch baru dan, oleh karena itu, frekuensi maksimum 100 batch per detik—10.000 nilai urutan per detik dapat diterbitkan.
Contoh kode berikut menunjukkan cara mengimplementasikan fungsi getNext() menggunakan batch. Perhatikan bahwa kode ini menggunakan kembali fungsi getAndIncrementNextValueInDB() yang ditentukan sebelumnya untuk mengambil batch nilai urutan yang baru dari database.
Contoh kode berikut menunjukkan cara menggunakan fungsi getNext() asinkron dalam permintaan untuk dua nilai urutan:
Sekali lagi, nilai ini 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 menoleransi kenaikan latensi, Anda dapat meningkatkan performa generator batch sebelumnya dengan menyiapkan batch nilai baru saat batch nilai saat ini habis.
Anda dapat melakukannya dengan menetapkan batas yang menunjukkan kapan jumlah nilai urutan yang tersisa dalam sebuah batch terlalu rendah. Saat batas ini tercapai, generator urutan akan mulai meminta batch nilai baru di thread latar belakang.
Seperti pada versi sebelumnya, nilai tidak diterbitkan secara urut, dan akan ada celah berupa nilai yang tidak digunakan dalam urutan jika transaksi gagal, atau jika instance aplikasi dimatikan.
Aplikasi akan melakukan hal berikut:
- Mempertahankan status internal setiap urutan, yang berisi nilai awal batch dan nilai berikutnya yang tersedia.
- Meminta nilai urutan dari batch.
- Melakukan langkah berikut di thread latar belakang jika nilai yang tersisa dalam batch berada di bawah batas:
- Membuat transaksi untuk membaca dan memperbarui nilai urutan.
- Membaca sel
next_valueuntuk nama urutan yang akan digunakan dalam aplikasi. - Menyimpan nilai ini secara internal sebagai nilai awal batch baru.
- Meningkatkan sel
next_valuedalam database sebesar ukuran batch. - Menyelesaikan transaksi.
- Jika tidak ada nilai yang tersisa dalam batch itu, mengambil nilai awal batch berikutnya dari thread latar belakang (menunggu hingga selesai, jika perlu), lalu membuat batch baru menggunakan nilai awal yang diambil tersebut sebagai nilai berikutnya.
- Menampilkan nilai berikutnya dan meningkatkan status internal.
- Menggunakan nilai yang ditampilkan dalam transaksi.
Untuk mencapai performa yang optimal, thread latar belakang harus dimulai dan selesai sebelum Anda kehabisan nilai urutan dalam batch saat ini. Jika tidak, aplikasi harus menunggu batch berikutnya, dan latensi akan meningkat. Oleh karena itu, Anda perlu menyesuaikan ukuran batch dan batas bawah, bergantung pada frekuensi nilai urutan yang diterbitkan.
Misalnya, asumsikan waktu transaksi pengambilan batch nilai baru 20 milidetik, ukuran batch 1.000, dan frekuensi penerbitan urutan maksimum 500 nilai per detik (satu nilai setiap 2 milidetik). Selama 20 milidetik saat batch nilai baru diterbitkan, 10 nilai urutan akan diterbitkan. Oleh karena itu, batas untuk jumlah nilai urutan yang tersisa harus lebih dari 10, agar batch berikutnya tersedia saat diperlukan.
Contoh kode berikut menunjukkan cara mengimplementasikan fungsi getNext() menggunakan batch. Perhatikan bahwa kode ini menggunakan fungsi getAndIncrementNextValueInDB() yang ditentukan sebelumnya untuk mengambil batch nilai urutan menggunakan thread latar belakang.
Contoh kode berikut menunjukkan penggunaan fungsi getNext() batch asinkron dalam permintaan untuk dua nilai yang akan digunakan 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 keempat jenis generator urutan:
| Sinkron | Asinkron | Batch | Batch asinkron | |
|---|---|---|---|---|
| Nilai unik | Ya | Ya | Ya | Ya |
| Nilai berurutan secara global | Ya | Ya | Tidak ada Namun, dengan beban yang cukup tinggi dan ukuran batch yang cukup kecil, nilai akan berdekatan satu sama lain |
Tidak ada Namun, dengan beban yang cukup tinggi dan ukuran batch yang cukup kecil, nilai akan berdekatan satu sama lain |
| Tanpa celah | 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 mdtk Jauh lebih tinggi dengan pertentangan tinggi (saat transaksi memerlukan waktu lama) |
10 mdtk pada setiap transaksi Jauh lebih tinggi dengan pertentangan tinggi |
10 mdtk, tetapi hanya ketika batch nilai baru diambil | Nol, jika ukuran batch dan batas bawah ditetapkan ke nilai yang sesuai |
Tabel di atas juga mengilustrasikan fakta bahwa Anda mungkin perlu mengorbankan persyaratan nilai berurutan secara global dan rangkaian nilai tanpa celah demi menghasilkan nilai yang unik, sekaligus memenuhi persyaratan performa secara keseluruhan.
Pengujian performa
Anda dapat menggunakan alat pengujian/analisis performa, yang tersedia di repositori GitHub yang sama dengan class generator urutan di atas, untuk menguji setiap generator urutan ini dan mendemonstrasikan karakteristik performa dan latensinya. Alat tersebut menyimulasikan latensi transaksi aplikasi 10 milidetik dan menjalankan beberapa thread secara bersamaan untuk meminta nilai urutan.
Pengujian performa hanya memerlukan satu instance Spanner node tunggal 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 diterbitkan per detik, dan latensi pada persentil ke-50, ke-90, dan ke-99:
| Mode dan parameter | Jumlah thread | 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 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 jumlah thread lebih banyak, pertentangan meningkat. Hal ini menimbulkan latensi transaksi yang jauh lebih tinggi.
Dalam mode asinkron (ASYNC), karena transaksi untuk mendapatkan urutan lebih kecil dan terpisah dari transaksi aplikasi, pertentangan lebih sedikit dan frekuensi lebih tinggi. Namun, pertentangan masih dapat terjadi, yang menyebabkan latensi persentil ke-90 lebih tinggi.
Dalam mode batch (BATCH), latensi berkurang signifikan—kecuali untuk persentil ke-99, yang berkaitan dengan saat generator perlu meminta batch nilai urutan lainnya secara sinkron dari database. Performa lebih tinggi beberapa kali lipat dalam mode BATCH daripada dalam mode ASYNC.
Mode batch dengan 50 thread memiliki latensi lebih tinggi karena urutan diterbitkan sedemikian cepatnya sehingga faktor pembatasnya adalah kekuatan instance virtual machine (VM) (dalam hal ini, mesin 4 vCPU berjalan pada tingkat CPU 350% selama pengujian). Penggunaan banyak mesin dan banyak proses akan menampilkan hasil keseluruhan yang mirip dengan mode batch 10 thread.
Dalam mode ASYNC BATCH, variasi latensi tidak seberapa dan performa lebih tinggi—bahkan dengan thread yang sangat banyak—karena latensi permintaan batch baru dari database terpisah sepenuhnya dari transaksi aplikasi.
Langkah berikutnya
- Pelajari praktik terbaik 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.