Kontrol konkurensi

Transaksi Spanner menawarkan dua mode kontrol konkurensi: pesimis dan optimis. Pilihan mode kontrol konkurensi memengaruhi cara transaksi menangani operasi baca dan tulis serentak, sehingga memengaruhi performa, latensi, dan rasio 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 isolasi serialisabel. Anda juga dapat menggunakan konkurensi pesimis dengan isolasi repeatable read.

Konkurensi pesimis dalam isolasi serialisabel

Mode ini mengasumsikan bahwa transaksi serentak mungkin bersaing untuk data yang sama. Mode ini memperoleh kunci secara proaktif pada data saat dibaca atau ditulis dalam transaksi. Mode ini juga memverifikasi bahwa kunci yang diperoleh sebelumnya dalam transaksi tetap dipertahankan dalam pernyataan berikutnya. Saat Spanner mendeteksi konflik kunci, Spanner akan menggunakan algoritma wound-wait untuk menyelesaikan konflik.

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

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

Konkurensi pesimis dalam isolasi repeatable read

Gunakan konkurensi pesimis dalam isolasi repeatable read untuk membuat serial operasi tulis. Dalam mode ini, operasi baca menggunakan snapshot, tetapi kunci eksklusif berlaku untuk data yang dibaca dari FOR UPDATE kueri atau lock_scanned_ranges=exclusive petunjuk, dan data yang ditulis dengan kueri DML.

Manfaat konkurensi pesimis dengan isolasi serialisabel

Manfaat utama menggunakan konkurensi pesimis dengan isolasi serialisabel adalah, dalam workload yang sangat bersaing, konkurensi ini membantu transaksi membuat progres. Spanner memprioritaskan transaksi yang lebih lama daripada transaksi yang lebih baru selama konflik, sehingga memastikan transaksi akhirnya selesai sekaligus mengurangi jumlah transaksi yang berulang kali dibatalkan.

Manfaat konkurensi pesimis dengan isolasi repeatable read

Dengan isolasi repeatable read, transaksi yang memperoleh kunci mungkin masih dibatalkan pada waktu commit jika data yang dibaca sebagai bagian dari kueri dengan FOR UPDATE atau sebagai bagian dari kueri DML, diubah oleh transaksi serentak sebelum transaksi di-commit. Namun, setelah kunci diperoleh, kunci tersebut mencegah update serentak lebih lanjut hingga transaksi di-commit, sehingga membuat serial operasi tulis.

Risiko konkurensi pesimis

Konkurensi pesimis dengan isolasi serialisabel memiliki risiko berikut:

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

Kasus penggunaan untuk konkurensi pesimis dengan isolasi serialisabel

Konkurensi pesimis cocok untuk workload dengan persaingan baca-tulis dan tulis-tulis yang tinggi. Konkurensi ini juga sesuai jika pembatalan dan percobaan ulang transaksi mahal. Gunakan mode default ini kecuali jika workload Anda memiliki penundaan kunci yang sangat lama, atau sangat terpengaruh oleh konflik kunci.

Kasus penggunaan untuk konkurensi pesimis dengan isolasi repeatable read

Gunakan konkurensi pesimis dengan repeatable read untuk workload yang memerlukan klausa FOR UPDATE atau kueri DML untuk memperoleh kunci. Pendekatan ini sangat berguna untuk workload yang dimigrasikan ke Spanner dari database lain yang memperoleh kunci untuk pernyataan ini.

Kontrol konkurensi optimis

Spanner juga menyediakan kontrol konkurensi optimis. Saat Anda menggunakan isolasi repeatable read, mode defaultnya adalah kontrol konkurensi optimis. Anda juga dapat mengonfigurasi isolasi serialisabel 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, operasi baca divalidasi pada waktu commit. Hal ini memastikan bahwa tidak ada transaksi lain yang di-commit secara serentak yang mengubah data yang sebelumnya dibaca oleh transaksi. Jika Anda menggunakan isolasi repeatable read, operasi baca dengan petunjuk FOR UPDATE atau lock_scanned_ranges=exclusive akan divalidasi pada waktu commit. Jika Spanner mendeteksi konflik, Spanner akan membatalkan transaksi.

Cara kerja konkurensi optimis

Konkurensi optimis mengubah cara Spanner menjalankan operasi baca, kueri, dan transaksi commit. Konkurensi ini melakukan eksekusi tanpa kunci selama fase baca dan memvalidasi konsistensi pada commit.

Untuk operasi baca dan kueri

Operasi baca dan kueri tidak memiliki kunci. Semua operasi baca dan kueri dalam transaksi optimis dieksekusi pada satu stempel waktu snapshot. Spanner memilih stempel waktu ini saat operasi baca atau kueri pertama dieksekusi. Hal ini memastikan bahwa semua operasi baca dan kueri berikutnya dalam transaksi melihat operasi tulis yang di-commit 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 di-commit hanya jika tidak ada konflik yang terdeteksi dan kondisi berikut terpenuhi:

  • Tidak ada operasi tulis yang di-commit secara serentak yang berkonflik dengan data yang dibaca oleh transaksi ini; yaitu, tidak ada operasi tulis yang di-commit setelah stempel waktu baca, tetapi sebelum transaksi ini meng-commit operasi tulisnya sendiri.
  • Skema tidak diubah sejak stempel waktu baca.

Tingkat isolasi menentukan kumpulan operasi baca yang divalidasi. Dengan isolasi serialisabel, semua operasi baca divalidasi. Dengan isolasi repeatable read, operasi baca dengan petunjuk FOR UPDATE atau lock_scanned_ranges=exclusive akan divalidasi pada waktu commit.

Dalam persaingan yang tinggi, transaksi optimis mungkin berulang kali dibatalkan. Sebaliknya, transaksi pesimis menyelesaikan konflik baca-tulis dengan mengizinkan transaksi yang lebih lama untuk di-commit dan mencoba kembali transaksi yang lebih baru.

Manfaat konkurensi optimis

Konkurensi optimis 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.
  • Latensi commit yang lebih rendah 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 commit 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 isolasi serialisabel. Pahami risiko ini sebelum Anda menggunakan kontrol konkurensi optimis dengan isolasi serialisabel untuk workload Anda.

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

Kasus penggunaan untuk konkurensi optimis

Konkurensi optimis cocok untuk workload transaksional dengan persaingan baca-tulis yang rendah. Untuk transaksi serialisabel, konkurensi ini juga bermanfaat bagi workload yang dapat menoleransi pembatalan transaksi.

Pertimbangkan konkurensi optimis untuk workload berikut:

  • Workload berprioritas rendah dan toleran terhadap latensi dengan transaksi yang berjalan lama: Gunakan konkurensi optimis jika operasi baca atau kueri yang berjalan lama dapat menunda operasi tulis 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 mempertahankan kunci baca untuk banyak baris atau rentang besar.
  • Workload transaksional yang sensitif terhadap latensi baca dengan persaingan baca-tulis yang rendah: Dalam konfigurasi multi-region, gunakan konkurensi optimis untuk menayangkan operasi baca secara regional, mengurangi latensi baca, dan menghindari masalah produksi dari traffic baca yang tidak stabil ke pemisahan aktif. Konkurensi ini juga meningkatkan ketersediaan baca selama kelebihan beban atau ketidaktersediaan pemimpin.
  • Workload transaksional yang sebagian besar transaksinya hanya baca: Beralih ke konkurensi optimis akan mengurangi latensi commit untuk transaksi hanya baca umum dalam workload ini. Pastikan persaingan baca-tulis rendah untuk menghindari rasio pembatalan yang tinggi untuk transaksi baca-tulis.

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

Mengonfigurasi kontrol konkurensi

Anda dapat menggunakan library klien Spanner, REST, dan RPC API untuk menentukan mode konkurensi 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();
    }
}

C++

void ReadLockModeSetting(std::string const& project_id,
                         std::string const& instance_id,
                         std::string const& database_id) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::Options;
  using ::google::cloud::StatusOr;

  auto db = spanner::Database(project_id, instance_id, database_id);

  // The read lock mode specified at the client-level will be applied
  // to all RW transactions.
  auto options = Options{}.set<spanner::TransactionReadLockModeOption>(
      spanner::Transaction::ReadLockMode::kOptimistic);
  auto client = spanner::Client(spanner::MakeConnection(db, options));

  auto commit = client.Commit(
      [&client](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        // Read an AlbumTitle.
        auto sql = spanner::SqlStatement(
            "SELECT AlbumTitle from Albums WHERE SingerId = @SingerId and "
            "AlbumId = @AlbumId",
            {{"SingerId", spanner::Value(2)}, {"AlbumId", spanner::Value(1)}});
        auto rows = client.ExecuteQuery(txn, std::move(sql));
        for (auto const& row :
             spanner::StreamOf<std::tuple<std::string>>(rows)) {
          if (!row) return row.status();
          std::cout << "Current Album Title: " << std::get<0>(*row) << "\n";
        }

        // Update the title.
        auto update_sql = spanner::SqlStatement(
            "UPDATE Albums "
            "SET AlbumTitle = @AlbumTitle "
            "WHERE SingerId = @SingerId and AlbumId = @AlbumId",
            {{"AlbumTitle", spanner::Value("A New Title")},
             {"SingerId", spanner::Value(2)},
             {"AlbumId", spanner::Value(1)}});
        auto result = client.ExecuteDml(txn, std::move(update_sql));
        if (!result) return result.status();
        std::cout << result->RowsModified() << " record(s) updated.\n";

        return spanner::Mutations{};
      },
      // The read lock mode specified at the transaction-level takes
      // precedence over the read lock mode configured at the client-level.
      // kPessimistic is used here to demonstrate overriding the client-level
      // setting.
      Options{}.set<spanner::TransactionReadLockModeOption>(
          spanner::Transaction::ReadLockMode::kPessimistic));

  if (!commit) throw std::move(commit).status();
  std::cout << "Update was successful [spanner_read_lock_mode]\n";
}

REST

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

RPC

Spanner Transactionoptions RPC API menyediakan enum ReadLockMode dalam pesan ReadWrite yang memungkinkan Anda memilih mode kunci 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