Pembuatan urutan di Spanner

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_value untuk nama urutan yang akan digunakan dalam aplikasi.
  • Meningkatkan dan memperbarui sel next_value untuk 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:

/**
 * 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;
}

Contoh kode berikut menunjukkan penggunaan fungsi getNext() sinkron 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 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_value untuk nama urutan yang akan digunakan dalam aplikasi.
  • Menyimpan nilai ini secara internal sebagai variabel.
  • Setiap kali nilai urutan baru diminta, meningkatkan variabel next_value tersimpan 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:

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;
}

Contoh kode berikut menunjukkan penggunaan 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 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_value untuk nama urutan yang akan digunakan dalam aplikasi.
    • Menyimpan nilai ini sebagai variabel.
    • Meningkatkan dan memperbarui sel next_value dalam database untuk nama urutan itu.
    • Menyelesaikan transaksi.
  • 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:

/**
 * 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);
}

Contoh kode 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;
            }
          });
}

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:

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 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_value untuk urutan itu.
    • Menyimpan nilai ini secara internal sebagai nilai awal batch baru.
    • Meningkatkan sel next_value dalam 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.

/**
 * 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;
}

Contoh kode 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 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_value untuk nama urutan yang akan digunakan dalam aplikasi.
    • Menyimpan nilai ini secara internal sebagai nilai awal batch baru.
    • Meningkatkan sel next_value dalam 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.

/**
 * 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;
}

Contoh kode berikut menunjukkan penggunaan fungsi getNext() batch asinkron dalam permintaan untuk dua nilai yang akan digunakan 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 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