Kontrol konkurensi

Transaksi Spanner menawarkan dua mode kontrol konkurensi: pesimis dan optimis. Pilihan mode kontrol serentak memengaruhi cara transaksi menangani pembacaan dan penulisan serentak, yang memengaruhi performa, latensi, dan tingkat pembatalan transaksi. Pilih mode yang paling sesuai dengan persyaratan performa dan konsistensi aplikasi Anda.

Perilaku default bergantung pada tingkat isolasi yang digunakan transaksi Anda:

Kontrol konkurensi pesimis

Secara default, Spanner menggunakan konkurensi pesimis dengan serializable isolation. Mode ini mengasumsikan bahwa transaksi serentak mungkin bersaing untuk data yang sama. Fitur ini secara proaktif mendapatkan kunci pada data saat dibaca atau ditulis dalam transaksi. Hal ini juga memverifikasi bahwa kunci yang diperoleh sebelumnya dalam transaksi tetap dipegang dalam pernyataan selanjutnya. Saat mendeteksi konflik kunci, Spanner menggunakan algoritma wound-wait untuk menyelesaikan konflik.

Dalam konkurensi pesimis, transaksi memperoleh kunci pada data selama fase eksekusi dan commit transaksi.

  • Untuk pembacaan: Saat membaca data, transaksi akan mendapatkan kunci baca bersama (ReaderShared) selama fase eksekusi. Kunci ini dipegang hingga transaksi dilakukan.
  • Untuk DML dan penulisan:
    • Selama eksekusi, untuk data yang diubah oleh DML atau operasi tulis, transaksi mungkin mendapatkan kunci baca pada keberadaan baris.
    • Pada waktu commit, transaksi mencoba mendapatkan kunci tulis atau eksklusif untuk data yang ditulis. Kunci tulis memblokir pembacaan serentak, tetapi mungkin tidak memblokir penulisan serentak, terutama saat keduanya menggunakan kunci tulis. Artinya, beberapa transaksi dapat dilanjutkan ke commit, dan konflik tulis-tulis diselesaikan pada waktu commit menggunakan algoritma wound-wait. Semua kunci dipegang hingga transaksi dilakukan.

Manfaat konkurensi pesimistis dengan isolasi serialisabel

Manfaat utama menggunakan konkurensi pesimis dengan serializable isolation adalah, dalam workload yang sangat bersaing, hal ini membantu transaksi berjalan. Spanner memprioritaskan transaksi lama daripada transaksi baru selama terjadi konflik, sehingga memastikan bahwa transaksi akhirnya selesai sekaligus mengurangi jumlah transaksi yang dibatalkan berulang kali.

Risiko serentak pesimis

Konkurensi pesimistis dengan isolasi serialisabel menimbulkan risiko berikut:

  • Operasi baca yang berjalan lama dapat memblokir operasi tulis yang sensitif terhadap latensi.
  • Transaksi yang melibatkan interaksi pengguna sebelum selesai dapat menyebabkan kunci dipertahankan dalam waktu yang lama, sehingga berpotensi memblokir operasi lain.

Kasus penggunaan untuk konkurensi pesimis

Konkurensi pesimistis cocok untuk beban kerja dengan persaingan baca-tulis dan tulis-tulis yang tinggi. Hal ini juga tepat saat transaksi dibatalkan dan percobaan ulang mahal. Gunakan mode default ini kecuali jika workload Anda memiliki penundaan penguncian panjang yang berlebihan, atau sangat terpengaruh oleh konflik penguncian.

Kontrol konkurensi optimis

Spanner juga menyediakan kontrol serentak optimis. Saat Anda menggunakan isolasi baca yang dapat diulang, mode default adalah kontrol konkurensi optimis. Anda juga dapat mengonfigurasi isolasi yang dapat diserialisasi untuk menggunakan kontrol konkurensi optimis.

Kontrol konkurensi optimis mengasumsikan bahwa konflik jarang terjadi. Operasi baca dan kueri, bahkan dalam transaksi baca-tulis, akan dilanjutkan tanpa memperoleh kunci. Dengan isolasi serialisabel default Spanner, pembacaan divalidasi pada waktu commit. Hal ini memastikan bahwa tidak ada transaksi lain yang dilakukan secara serentak yang mengubah data yang sebelumnya dibaca oleh transaksi. Jika Anda menggunakan isolasi baca yang dapat diulang, baca dengan petunjuk FOR UPDATE atau lock_scanned_ranges=exclusive divalidasi pada waktu commit. Jika Spanner mendeteksi konflik, Spanner akan membatalkan transaksi.

Cara kerja konkurensi optimis

Konkurensi optimis mengubah cara Spanner menjalankan pembacaan, kueri, dan transaksi commit. Layanan ini melakukan eksekusi bebas kunci selama fase baca dan memvalidasi konsistensi saat melakukan commit.

Untuk operasi baca dan kueri

Operasi baca dan kueri tidak memerlukan penguncian. Semua pembacaan dan kueri dalam transaksi optimis dieksekusi pada satu stempel waktu snapshot. Spanner memilih stempel waktu ini saat operasi baca atau kueri pertama dijalankan. Hal ini memastikan bahwa semua operasi baca dan kueri berikutnya dalam transaksi melihat operasi tulis yang dilakukan sebelum operasi baca atau kueri pertama.

Untuk operasi baca dan tulis

Untuk transaksi optimis dengan operasi baca dan tulis, Spanner melakukan langkah validasi pada waktu commit. Transaksi berhasil dilakukan hanya jika tidak ada konflik yang terdeteksi dan kondisi berikut terpenuhi:

  • Tidak ada penulisan yang di-commit secara serentak yang bertentangan dengan data yang dibaca oleh transaksi ini; artinya, tidak ada penulisan yang di-commit setelah stempel waktu baca, tetapi sebelum transaksi ini meng-commit penulisannya sendiri.
  • Skema tidak diubah sejak stempel waktu baca.

Tingkat isolasi menentukan kumpulan pembacaan yang divalidasi. Dengan isolasi yang dapat diserialisasi, semua operasi baca divalidasi. Dengan isolasi baca yang dapat diulang, baca dengan petunjuk FOR UPDATE atau lock_scanned_ranges=exclusive divalidasi pada waktu commit.

Dalam pertentangan yang tinggi, transaksi optimistis mungkin berulang kali dibatalkan. Sebaliknya, transaksi pesimistis menyelesaikan konflik baca-tulis dengan mengizinkan transaksi yang lebih lama di-commit dan mencoba kembali transaksi yang lebih baru.

Manfaat serentak optimis

Optimistic concurrency menawarkan manfaat berikut:

  • Operasi baca tidak memperoleh kunci: Transaksi optimis tidak memperoleh kunci untuk operasi baca, sehingga operasi baca yang berjalan lama tidak memblokir operasi tulis yang sensitif terhadap latensi.
  • Mengurangi latensi penerapan untuk transaksi hanya baca: Karena semua operasi baca dalam transaksi optimis didasarkan pada stempel waktu snapshot yang sama, tidak perlu memverifikasi konsistensi selama eksekusi atau penerapan untuk operasi baca ini, yang secara signifikan mengurangi latensi.

Risiko konkurensi optimis

Konkurensi optimis menimbulkan risiko, terutama dalam persaingan baca-tulis yang tinggi saat digunakan dengan serializable isolation. Pahami risiko ini sebelum Anda menggunakan kontrol konkurensi optimis dengan isolasi yang dapat diserialkan untuk beban kerja Anda.

  • Dalam pertentangan baca-tulis yang tinggi, transaksi optimis mungkin mengalami tingkat pembatalan yang tinggi, karena operasi tulis serentak dapat membatalkan pembacaan transaksi optimis.
  • Dengan pertentangan tinggi yang persisten, transaksi dapat dibatalkan berulang kali dan tidak pernah dilakukan karena kekurangan transaksi.

Kasus penggunaan untuk konkurensi optimis

Konkurensi optimis cocok untuk workload transaksional dengan persaingan baca-tulis yang rendah. Untuk transaksi yang dapat diserialisasi, fitur ini juga bermanfaat bagi workload yang dapat mentoleransi pembatalan transaksi.

Pertimbangkan konkurensi optimis untuk workload berikut:

  • Beban kerja berlatensi toleran dan berprioritas rendah dengan transaksi yang berjalan lama: Gunakan konkurensi optimis jika operasi baca atau kueri yang berjalan lama dapat menunda penulisan yang sensitif terhadap latensi. Hal ini menghindari penundaan yang disebabkan oleh kunci baca. Misalnya, transaksi di klien seluler dengan koneksi lambat, atau transaksi SLA rendah yang menahan kunci baca untuk banyak baris atau rentang besar.
  • Beban kerja transaksional yang sensitif terhadap latensi baca dengan persaingan baca-tulis yang rendah: Dalam konfigurasi multi-region, gunakan konkurensi optimis untuk melayani bacaan secara regional, mengurangi latensi baca, dan menghindari masalah produksi dari traffic baca yang melonjak ke pemisahan panas. Hal ini juga meningkatkan ketersediaan baca selama kelebihan beban atau tidak tersedianya pemimpin.
  • Workload transaksional dengan sebagian besar transaksi bersifat hanya baca: Beralih ke konkurensi optimis akan mengurangi latensi commit untuk transaksi hanya baca umum dalam workload ini. Pastikan pertentangan baca-tulis rendah untuk menghindari tingkat pembatalan yang tinggi untuk transaksi baca-tulis.

Hindari penggunaan konkurensi optimis untuk beban kerja transaksional yang sensitif terhadap latensi dan sering terjadi konflik baca-tulis.

Mengonfigurasi kontrol konkurensi

Anda dapat menggunakan library klien, REST, dan RPC API Spanner untuk menentukan mode serentak untuk transaksi baca-tulis.

Library klien

Java

static void readLockModeSetting(DatabaseId db) {
  // The read lock mode specified at the client-level will be applied to all
  // RW transactions.
  DefaultReadWriteTransactionOptions transactionOptions =
      DefaultReadWriteTransactionOptions.newBuilder()
          .setReadLockMode(ReadLockMode.OPTIMISTIC)
          .build();
  SpannerOptions options =
      SpannerOptions.newBuilder()
          .setDefaultTransactionOptions(transactionOptions)
          .build();
  Spanner spanner = options.getService();
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbClient
      // The read lock mode specified at the transaction-level takes precedence
      // over the read lock mode configured at the client-level.
      .readWriteTransaction(Options.readLockMode(ReadLockMode.PESSIMISTIC))
      .run(transaction -> {
        // Read an AlbumTitle.
        String selectSql =
            "SELECT AlbumTitle from Albums WHERE SingerId = 1 and AlbumId = 1";
        String title = null;
        try (ResultSet resultSet = transaction.executeQuery(Statement.of(selectSql))) {
          if (resultSet.next()) {
            title = resultSet.getString("AlbumTitle");
          }
        }
        System.out.printf("Current album title: %s\n", title);

        // Update the title.
        String updateSql =
            "UPDATE Albums "
                + "SET AlbumTitle = 'New Album Title' "
                + "WHERE SingerId = 1 and AlbumId = 1";
        long rowCount = transaction.executeUpdate(Statement.of(updateSql));
        System.out.printf("%d record updated.\n", rowCount);
        return null;
      });
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	pb "cloud.google.com/go/spanner/apiv1/spannerpb"
)

// writeWithTransactionUsingReadLockMode sets the ReadLockMode globally
// by using ClientConfig and shows how to override it for a specific
// transaction. ReadLockMode determines the locking strategy used during
// transaction execution.
func writeWithTransactionUsingReadLockMode(w io.Writer, db string) error {
	ctx := context.Background()

	// Client-level configuration: Applies to all read-write transactions
	// for this client. OPTIMISTIC mode avoids locks during reads and
	// verifies changes during the commit phase.
	cfg := spanner.ClientConfig{
		TransactionOptions: spanner.TransactionOptions{
			ReadLockMode: pb.TransactionOptions_ReadWrite_OPTIMISTIC,
		},
	}
	client, err := spanner.NewClientWithConfig(ctx, db, cfg)
	if err != nil {
		return fmt.Errorf("failed to create client: %w", err)
	}
	defer client.Close()

	// Transaction-level options take precedence over client-level
	// configuration. PESSIMISTIC mode is used here to override the
	// client-level setting and ensure immediate locking during reads.
	txnOpts := spanner.TransactionOptions{
		ReadLockMode: pb.TransactionOptions_ReadWrite_PESSIMISTIC,
	}

	_, err = client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		// In PESSIMISTIC mode with SERIALIZABLE isolation, the transaction
		// acquires a shared lock during this read.
		key := spanner.Key{1, 2}
		row, err := txn.ReadRow(ctx, "Albums", key, []string{"AlbumTitle"})
		if err != nil {
			return fmt.Errorf("failed to read album: %w", err)
		}
		var title string
		if err := row.Column(0, &title); err != nil {
			return fmt.Errorf("failed to get album title: %w", err)
		}
		fmt.Fprintf(w, "Current album title: %s\n", title)

		// Update the album title
		stmt := spanner.Statement{
			SQL: `UPDATE Albums
				SET AlbumTitle = @AlbumTitle
				WHERE SingerId = @SingerId AND AlbumId = @AlbumId`,
			Params: map[string]interface{}{
				"SingerId":   1,
				"AlbumId":    2,
				"AlbumTitle": "New Album Title",
			},
		}
		count, err := txn.Update(ctx, stmt)
		if err != nil {
			return fmt.Errorf("failed to update album: %w", err)
		}
		fmt.Fprintf(w, "Updated %d record(s).\n", count)
		return nil
	}, txnOpts)

	if err != nil {
		return fmt.Errorf("transaction failed: %w", err)
	}
	return nil
}

Node.js

// Imports the Google Cloud Spanner client library
const {Spanner, protos} = require('@google-cloud/spanner');
// The read lock mode specified at the client-level will be applied
// to all RW transactions.
const defaultTransactionOptions = {
  readLockMode:
    protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode
      .OPTIMISTIC,
};

// Instantiates a client with defaultTransactionOptions
const spanner = new Spanner({
  projectId: projectId,
  defaultTransactionOptions,
});

function runTransactionWithReadLockMode() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);
  // The read lock mode specified at the request-level takes precedence over
  // the read lock mode configured at the client-level.
  const readLockModeOptionsForTransaction = {
    readLockMode:
      protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode
        .PESSIMISTIC,
  };

  database.runTransaction(
    readLockModeOptionsForTransaction,
    async (err, transaction) => {
      if (err) {
        console.error(err);
        return;
      }
      try {
        const query =
          'SELECT AlbumTitle FROM Albums WHERE SingerId = 2 AND AlbumId = 1';
        const results = await transaction.run(query);
        // Gets first album's title
        const rows = results[0].map(row => row.toJSON());
        const albumTitle = rows[0].AlbumTitle;
        console.log(`previous album title ${albumTitle}`);

        const update =
          "UPDATE Albums SET AlbumTitle = 'New Album Title' WHERE SingerId = 2 AND AlbumId = 1";
        const [rowCount] = await transaction.runUpdate(update);
        console.log(
          `Successfully updated ${rowCount} record in Albums table.`,
        );
        await transaction.commit();
        console.log(
          'Successfully executed read-write transaction with readLockMode option.',
        );
      } catch (err) {
        console.error('ERROR:', err);
        transaction.end();
      } finally {
        // Close the database when finished.
        await database.close();
      }
    },
  );
}
runTransactionWithReadLockMode();

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
from google.cloud.spanner_v1 import TransactionOptions, DefaultTransactionOptions

# The read lock mode specified at the client-level will be applied to all
# RW transactions.
read_lock_mode_options_for_client = TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC

# Create a client that uses Serializable isolation (default) with
# optimistic locking for read-write transactions.
spanner_client = spanner.Client(
    default_transaction_options=DefaultTransactionOptions(
        read_lock_mode=read_lock_mode_options_for_client
    )
)
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

# The read lock mode specified at the request level takes precedence over
# the read lock mode configured at the client level.
read_lock_mode_options_for_transaction = (
    TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC
)

def update_albums_with_read_lock_mode(transaction):
    # Read an AlbumTitle.
    results = transaction.execute_sql(
        "SELECT AlbumTitle from Albums WHERE SingerId = 2 and AlbumId = 1"
    )
    for result in results:
        print("Current Album Title: {}".format(*result))

    # Update the AlbumTitle.
    row_ct = transaction.execute_update(
        "UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 2 and AlbumId = 1"
    )

    print("{} record(s) updated.".format(row_ct))

database.run_in_transaction(
    update_albums_with_read_lock_mode,
    read_lock_mode=read_lock_mode_options_for_transaction
)

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Threading;
using System.Threading.Tasks;

public class ReadLockModeAsyncSample
{
    public async Task ReadLockModeAsync(string projectId, string instanceId, string databaseId)
    {
        // Create client with ReadLockMode.Optimistic.
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId};ReadLockMode=Optimistic";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        // Create transaction options with ReadLockMode.Pessimistic.
        var transactionOptions = SpannerTransactionCreationOptions.ReadWrite
            .WithReadLockMode(ReadLockMode.Pessimistic);

        using var transaction = await connection.BeginTransactionAsync(transactionOptions, null, CancellationToken.None);

        var cmd = connection.CreateSelectCommand("SELECT AlbumTitle FROM Albums WHERE SingerId = 2 AND AlbumId = 1");
        cmd.Transaction = transaction;
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine($"AlbumTitle: {reader.GetFieldValue<string>("AlbumTitle")}");
            }
        }

        var updateCmd = connection.CreateDmlCommand("UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 2 AND AlbumId = 1");
        updateCmd.Transaction = transaction;
        var rowCount = await updateCmd.ExecuteNonQueryAsync();
        Console.WriteLine($"{rowCount} records updated.");

        await transaction.CommitAsync();
    }
}

REST

Spanner TransactionOptions REST API menyediakan enum ReadLockMode dalam pesan ReadWrite yang memungkinkan Anda memilih mode penguncian PESSIMISTIC atau OPTIMISTIC.

RPC

Spanner Transactionoptions RPC API menyediakan enum ReadLockMode dalam pesan ReadWrite yang memungkinkan Anda memilih mode penguncian PESSIMISTIC atau OPTIMISTIC.

Driver

Anda dapat menggunakan driver Spanner untuk menetapkan read_lock_mode sebagai parameter koneksi di tingkat koneksi atau sebagai opsi pernyataan SET di tingkat transaksi. Untuk mengetahui informasi selengkapnya tentang setiap driver, lihat Ringkasan driver.

Langkah berikutnya