Pembuatan urutan di Spanner

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 unik yang sederhana, misalnya, nomor karyawan atau nomor invoice. Database relasional konvensional sering menyertakan fitur untuk membuat urutan angka yang unik dan meningkat secara monoton. Urutan ini digunakan untuk menghasilkan ID unik (kunci baris) untuk objek yang disimpan dalam database.

Namun, penggunaan nilai yang meningkat (atau menurun) secara monoton sebagai kunci baris mungkin tidak mengikuti praktik terbaik di Spanner karena menciptakan hotspot di database, sehingga menyebabkan pengurangan performa. Dokumen ini mengusulkan mekanisme untuk mengimplementasikan generator urutan menggunakan tabel database Spanner dan logika lapisan aplikasi.

Atau, Spanner mendukung generator urutan terbalik 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:

/**
 * Returns the next value from this sequence.
 *
 * <p>Should only be called once per transaction.
 */
long getNext(TransactionContext txn) {
  Struct result =
      txn.readRow(
          SEQUENCES_TABLE, Key.of(sequenceName), Collections.singletonList(NEXT_VALUE_COLUMN));
  if (result == null) {
    throw new NoSuchElementException(
        "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
  }
  long value = result.getLong(0);
  txn.buffer(
      Mutation.newUpdateBuilder(SEQUENCES_TABLE)
          .set(SEQUENCE_NAME_COLUMN)
          .to(sequenceName)
          .set(NEXT_VALUE_COLUMN)
          .to(value + 1)
          .build());
  return value;
}

Kode contoh berikut menunjukkan cara fungsi getNext() sinkron digunakan dalam transaksi:

// Simple Sequence generator created outside transaction, eg as field.
private SimpleSequenceGenerator simpleSequence = new SimpleSequenceGenerator("my Sequence");

public void usingSimpleSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Get a sequence value
              long nextValue = simpleSequence.getNext(txn);
              // Use nextValue in the transaction
              // ...
              return null;
            }
          });
}

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:

private final TransactionContext txn;
@Nullable private Long nextValue;

/** Creates a sequence generator for this transaction. */
public SynchronousSequenceGenerator(String sequenceName, TransactionContext txn) {
  super(sequenceName);
  this.txn = txn;
}

/**
 * Returns the next value from this sequence.
 *
 * <p>Can be called multiple times in a transaction.
 */
public long getNext() {
  if (nextValue == null) {
    // nextValue is unknown - read it.
    Struct result =
        txn.readRow(
            SEQUENCES_TABLE, Key.of(sequenceName), Collections.singletonList(NEXT_VALUE_COLUMN));
    if (result == null) {
      throw new NoSuchElementException(
          "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
    }
    nextValue = result.getLong(0);
  }
  long value = nextValue;
  // increment and write nextValue to the database.
  nextValue++;
  txn.buffer(
      Mutation.newUpdateBuilder(SEQUENCES_TABLE)
          .set(SEQUENCE_NAME_COLUMN)
          .to(sequenceName)
          .set(NEXT_VALUE_COLUMN)
          .to(nextValue)
          .build());
  return value;
}

Kode contoh berikut menunjukkan cara menggunakan fungsi getNext() sinkron dalam permintaan untuk dua nilai urutan:

public void usingSynchronousSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Create the sequence generator object within the transaction
              SynchronousSequenceGenerator syncSequence =
                  new SynchronousSequenceGenerator("my_sequence", txn);
              // Get two sequence values
              long key1 = syncSequence.getNext();
              long key2 = syncSequence.getNext();
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

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.
  • 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:

/**
 * Gets the next sequence value from the database, and increments the database value by the amount
 * specified in a single transaction.
 */
protected Long getAndIncrementNextValueInDB(long increment) {
  return dbClient
      .readWriteTransaction()
      .run(
          txn -> {
            Struct result =
                txn.readRow(
                    SEQUENCES_TABLE,
                    Key.of(sequenceName),
                    Collections.singletonList(NEXT_VALUE_COLUMN));
            if (result == null) {
              throw new NoSuchElementException(
                  "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
            }
            long value = result.getLong(0);
            txn.buffer(
                Mutation.newUpdateBuilder(SEQUENCES_TABLE)
                    .set(SEQUENCE_NAME_COLUMN)
                    .to(sequenceName)
                    .set(NEXT_VALUE_COLUMN)
                    .to(value + increment)
                    .build());
            return value;
          });
}

Anda dapat menggunakan fungsi ini dengan mudah untuk mengambil satu nilai urutan baru, seperti yang ditunjukkan dalam implementasi fungsi getNext() asinkron berikut:

/**
 * Returns the next value from this sequence.
 *
 * Uses a separate transaction so must be used <strong>outside</strong>any other transactions.
 * See {@link #getNextInBackground()} for an alternative version that uses a background thread
 */
public long getNext() throws SpannerException {
  return getAndIncrementNextValueInDB(1);
}

Kode contoh berikut menunjukkan cara menggunakan fungsi getNext() asinkron dalam permintaan untuk dua nilai urutan:

// Async Sequence generator created outside transaction as a long-lived object.
private AsynchronousSequenceGenerator myAsyncSequence =
    new AsynchronousSequenceGenerator("my Sequence", dbClient);

public void usingAsynchronousSequenceGenerator() {
  // Get two sequence values
  final long key1 = myAsyncSequence.getNext();
  final long key2 = myAsyncSequence.getNext();
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

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:

protected static final ExecutorService executor = Executors.newCachedThreadPool();

/**
 * Gets the next value using a background thread - to be used when inside a transaction to avoid
 * Nested Transaction errors.
 */
public long getNextInBackground() throws Exception {
  return executor.submit(this::getNext).get();
}

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.

/**
 * Gets a new batch of sequence values from the database.
 *
 * <p>Reads next_value, increments it by batch size, then writes the updated next_value back.
 */
private synchronized void getBatch() throws SpannerException {
  if (next_value <= last_value_in_batch) {
    // already have some values left in the batch - maybe this has been refreshed by another
    // thread.
    return;
  }
  next_value = getAndIncrementNextValueInDB(batchSize);
  last_value_in_batch = next_value + batchSize - 1;
}

/**
 * Returns the next value from this sequence, getting a new batch of values if necessary.
 *
 * When getting a new batch, it creates a separate transaction, so this must be called
 * <strong>outside</strong> any other transactions. See {@link #getNextInBackground()} for an
 * alternative version that uses a background thread
 */

public synchronized long getNext() throws SpannerException {
  if (next_value > last_value_in_batch) {
    getBatch();
  }
  long value = next_value;
  next_value++;
  return value;
}

Kode contoh berikut menunjukkan cara menggunakan fungsi getNext() asinkron dalam permintaan untuk dua nilai urutan:

// Batch Sequence generator created outside transaction, as a long-lived object.
private BatchSequenceGenerator myBatchSequence =
    new BatchSequenceGenerator("my Sequence", /* batchSize= */ 100, dbClient);

public void usingBatchSequenceGenerator() {
  // Get two sequence values
  final long key1 = myBatchSequence.getNext();
  final long key2 = myBatchSequence.getNext();
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

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.

/**
 * Gets a new batch of sequence values from the database.
 *
 * <p>Reads nextValue, increments it by batch size, then writes the updated nextValue back.
 * Stores the resulting value in  nextBatchStartValue, ready for when the existing pool of values
 * is exhausted.
 */
private Long readNextBatchFromDB() {
  return getAndIncrementNextValueInDB(batchSize);
}

/**
 * Returns the next value from this sequence.
 *
 * If the number of remaining values is below the low watermark, this triggers a background
 * request for new batch of values if necessary. Once the current batch is exhausted, then a the
 * new batch is used.
 */
public synchronized long getNext() throws SpannerException {
  // Check if a batch refresh is required and is not already running.
  if (nextValue >= (lastValueInBatch - lowWaterMarkForRefresh) && pendingNextBatchStart == null) {
    // Request a new batch in the background.
    pendingNextBatchStart = executor.submit(this::readNextBatchFromDB);
  }

  if (nextValue > lastValueInBatch) {
    // batch is exhausted, we should have received a new batch by now.
    try {
      // This will block if the transaction to get the next value has not completed.
      long nextBatchStart = pendingNextBatchStart.get();
      lastValueInBatch = nextBatchStart + batchSize - 1;
      nextValue = nextBatchStart;
    } catch (InterruptedException | ExecutionException e) {
      if (e.getCause() instanceof SpannerException) {
        throw (SpannerException) e.getCause();
      }
      throw new RuntimeException("Failed to retrieve new batch in background", e);
    } finally {
      pendingNextBatchStart = null;
    }
  }
  // return next value.
  long value = nextValue;
  nextValue++;
  return value;
}

Kode contoh berikut menunjukkan cara fungsi getNext() batch asinkron digunakan dalam permintaan untuk menggunakan dua nilai dalam transaksi:

// Async Batch Sequence generator created outside transaction, as a long-lived object.
private AsyncBatchSequenceGenerator myAsyncBatchSequence =
    new AsyncBatchSequenceGenerator("my Sequence", /* batchSize= */ 1000, 200, dbClient);

public void usingAsyncBatchSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Get two sequence values
              final long key1 = myBatchSequence.getNext();
              final long key2 = myBatchSequence.getNext();
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

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
Namun, dengan beban dan ukuran tumpukan yang cukup kecil, nilai akan berdekatan satu sama lain
Tidak
Namun, dengan beban dan ukuran tumpukan 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 tumpukan 200, LT 50)
10 512 18 20 30
ASYNC BATCH
(ukuran tumpukan 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