Ringkasan transaksi

Halaman ini menjelaskan transaksi di Spanner dan memperkenalkan antarmuka transaksi DML terpartisi, hanya baca, dan baca-tulis Spanner.

Transaksi di Spanner adalah serangkaian operasi baca dan tulis yang dieksekusi secara atomik pada satu titik waktu logis di seluruh kolom, baris, dan tabel dalam database.

Sesi digunakan untuk melakukan transaksi di database Spanner. Sesi mewakili saluran komunikasi logis dengan layanan database Spanner. Sesi dapat menjalankan satu atau beberapa transaksi dalam satu waktu. Untuk mengetahui informasi selengkapnya, lihat Sesi.

Jenis transaksi

Spanner mendukung jenis transaksi berikut, yang masing-masing dirancang untuk pola interaksi data tertentu:

  • Baca-tulis: transaksi ini menggunakan penguncian pesimistis dan, jika diperlukan commit dua fase. Permintaan ini mungkin gagal dan memerlukan percobaan ulang. Meskipun terbatas pada satu database, pengguna dapat mengubah data di beberapa tabel dalam database tersebut.

  • Hanya baca: transaksi ini menjamin konsistensi data di beberapa operasi baca, tetapi tidak mengizinkan modifikasi data. Eksekusi dilakukan pada stempel waktu yang ditentukan sistem agar konsisten, atau pada stempel waktu sebelumnya yang dikonfigurasi pengguna. Tidak seperti transaksi baca-tulis, transaksi ini tidak memerlukan operasi commit atau penguncian, meskipun transaksi ini mungkin dijeda untuk menunggu operasi tulis yang sedang berlangsung selesai.

  • DML Terpartisi: jenis transaksi ini menjalankan pernyataan DML sebagai operasi DML terpartisi. API ini dioptimalkan untuk update dan penghapusan data skala besar, seperti pembersihan data atau penyisipan data massal. Untuk banyak operasi tulis yang tidak memerlukan transaksi atomik, pertimbangkan untuk menggunakan operasi tulis batch. Lihat Mengubah data menggunakan operasi tulis batch untuk mengetahui detailnya.

Transaksi baca-tulis

Gunakan transaksi baca-tulis yang mengunci untuk membaca, mengubah, dan menulis data secara atomik di mana pun dalam database. Jenis transaksi ini konsisten secara eksternal.

Minimalkan waktu transaksi aktif. Durasi transaksi yang lebih singkat meningkatkan kemungkinan commit yang berhasil dan mengurangi pertentangan. Spanner mencoba mempertahankan kunci baca tetap aktif selama transaksi terus melakukan pembacaan dan transaksi belum dihentikan melalui operasi sessions.commit atau sessions.rollback. Jika klien tetap tidak aktif dalam jangka waktu yang lama, Spanner dapat melepaskan kunci transaksi dan membatalkan transaksi.

Secara konseptual, transaksi baca-tulis terdiri dari nol atau beberapa operasi baca atau pernyataan SQL yang diikuti dengan sessions.commit. Kapan saja sebelum sessions.commit, klien dapat mengirim permintaan sessions.rollback untuk membatalkan transaksi.

Untuk melakukan operasi tulis yang bergantung pada satu atau beberapa operasi baca, gunakan transaksi baca-tulis yang mengunci:

  • Jika Anda harus melakukan satu atau beberapa operasi tulis secara atomik, lakukan penulisan tersebut dalam transaksi baca-tulis yang sama. Misalnya, jika Anda mentransfer $200 dari akun A ke akun B, lakukan kedua operasi tulis (mengurangi akun A sebesar $200 dan menambah akun B sebesar $200) dan pembacaan saldo akun awal dalam transaksi yang sama.
  • Jika Anda ingin menggandakan saldo akun A, lakukan operasi baca dan tulis dalam transaksi yang sama. Hal ini memastikan bahwa sistem membaca saldo sebelum sistem menggandakannya, lalu memperbaruinya.
  • Jika Anda mungkin melakukan satu atau beberapa operasi tulis yang bergantung pada hasil dari satu atau beberapa operasi baca, lakukan penulisan dan pembacaan tersebut dalam transaksi baca-tulis yang sama, meskipun operasi tulis tidak dijalankan. Misalnya, jika Anda ingin mentransfer $200 dari akun A ke akun B hanya jika saldo saat ini akun A lebih besar dari $500, sertakan pembacaan saldo akun A dan operasi penulisan bersyarat dalam transaksi yang sama, meskipun transfer tidak terjadi.

Untuk melakukan operasi baca, gunakan metode baca tunggal atau transaksi hanya baca:

  • Jika Anda hanya melakukan operasi baca, dan Anda dapat mengekspresikan operasi baca menggunakan satu metode baca, gunakan satu metode baca tersebut atau transaksi hanya baca. Tidak seperti transaksi baca-tulis, bacaan tunggal tidak memperoleh kunci.

Antarmuka

Library klien Spanner menyediakan antarmuka untuk mengeksekusi serangkaian tugas dalam transaksi baca-tulis, dengan percobaan ulang untuk pembatalan transaksi. Transaksi Spanner mungkin memerlukan beberapa kali percobaan ulang sebelum di-commit.

Beberapa situasi dapat menyebabkan pembatalan transaksi. Misalnya, jika dua transaksi mencoba mengubah data secara bersamaan, kebuntuan dapat terjadi. Dalam kasus seperti itu, Spanner akan membatalkan satu transaksi agar transaksi lainnya dapat dilanjutkan. Lebih jarang, peristiwa sementara dalam Spanner juga dapat menyebabkan pembatalan transaksi.

Karena transaksi bersifat atomik, transaksi yang dibatalkan tidak memengaruhi database. Coba lagi transaksi dalam sesi yang sama untuk meningkatkan tingkat keberhasilan. Setiap percobaan ulang yang menghasilkan error ABORTED akan meningkatkan prioritas penguncian transaksi.

Saat menggunakan transaksi di library klien Spanner, Anda menentukan isi transaksi sebagai objek fungsi. Fungsi ini merangkum pembacaan dan penulisan yang dilakukan pada satu atau beberapa tabel database. Library klien Spanner menjalankan fungsi ini berulang kali hingga transaksi berhasil di-commit atau mengalami error yang tidak dapat dicoba ulang.

Contoh

Anggaplah Anda memiliki kolom MarketingBudget dalam tabel Albums:

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  MarketingBudget INT64
) PRIMARY KEY (SingerId, AlbumId);

Departemen pemasaran Anda meminta Anda untuk memindahkan Rp2.000.000.000 dari anggaran Albums (2, 2) ke Albums (1, 1), tetapi hanya jika dana tersedia dalam anggaran album tersebut. Anda harus menggunakan transaksi baca-tulis yang mengunci untuk operasi ini, karena transaksi dapat melakukan penulisan, bergantung pada hasil pembacaan.

Berikut cara menjalankan transaksi baca-tulis:

C++

void ReadWriteTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::StatusOr;

  // A helper to read a single album MarketingBudget.
  auto get_current_budget =
      [](spanner::Client client, spanner::Transaction txn,
         std::int64_t singer_id,
         std::int64_t album_id) -> StatusOr<std::int64_t> {
    auto key = spanner::KeySet().AddKey(spanner::MakeKey(singer_id, album_id));
    auto rows = client.Read(std::move(txn), "Albums", std::move(key),
                            {"MarketingBudget"});
    using RowType = std::tuple<std::int64_t>;
    auto row = spanner::GetSingularRow(spanner::StreamOf<RowType>(rows));
    if (!row) return std::move(row).status();
    return std::get<0>(*std::move(row));
  };

  auto commit = client.Commit(
      [&client, &get_current_budget](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        auto b1 = get_current_budget(client, txn, 1, 1);
        if (!b1) return std::move(b1).status();
        auto b2 = get_current_budget(client, txn, 2, 2);
        if (!b2) return std::move(b2).status();
        std::int64_t transfer_amount = 200000;

        return spanner::Mutations{
            spanner::UpdateMutationBuilder(
                "Albums", {"SingerId", "AlbumId", "MarketingBudget"})
                .EmplaceRow(1, 1, *b1 + transfer_amount)
                .EmplaceRow(2, 2, *b2 - transfer_amount)
                .Build()};
      });

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

C#


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

public class ReadWriteWithTransactionAsyncSample
{
    public async Task<int> ReadWriteWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        // This sample transfers 200,000 from the MarketingBudget
        // field of the second Album to the first Album. Make sure to run
        // the Add Column and Write Data To New Column samples first,
        // in that order.

        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        decimal transferAmount = 200000;
        decimal secondBudget = 0;
        decimal firstBudget = 0;

        using var connection = new SpannerConnection(connectionString);
        using var cmdLookup1 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");

        using (var reader = await cmdLookup1.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                // Read the second album's budget.
                secondBudget = reader.GetFieldValue<decimal>("MarketingBudget");
                // Confirm second Album's budget is sufficient and
                // if not raise an exception. Raising an exception
                // will automatically roll back the transaction.
                if (secondBudget < transferAmount)
                {
                    throw new Exception($"The second album's budget {secondBudget} is less than the amount to transfer.");
                }
            }
        }

        // Read the first album's budget.
        using var cmdLookup2 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
        using (var reader = await cmdLookup2.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                firstBudget = reader.GetFieldValue<decimal>("MarketingBudget");
            }
        }

        // Specify update command parameters.
        using var cmdUpdate = connection.CreateUpdateCommand("Albums", new SpannerParameterCollection
        {
            { "SingerId", SpannerDbType.Int64 },
            { "AlbumId", SpannerDbType.Int64 },
            { "MarketingBudget", SpannerDbType.Int64 },
        });

        // Update second album to remove the transfer amount.
        secondBudget -= transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 2;
        cmdUpdate.Parameters["AlbumId"].Value = 2;
        cmdUpdate.Parameters["MarketingBudget"].Value = secondBudget;
        var rowCount = await cmdUpdate.ExecuteNonQueryAsync();

        // Update first album to add the transfer amount.
        firstBudget += transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 1;
        cmdUpdate.Parameters["AlbumId"].Value = 1;
        cmdUpdate.Parameters["MarketingBudget"].Value = firstBudget;
        rowCount += await cmdUpdate.ExecuteNonQueryAsync();
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func writeWithTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		getBudget := func(key spanner.Key) (int64, error) {
			row, err := txn.ReadRow(ctx, "Albums", key, []string{"MarketingBudget"})
			if err != nil {
				return 0, err
			}
			var budget int64
			if err := row.Column(0, &budget); err != nil {
				return 0, err
			}
			return budget, nil
		}
		album2Budget, err := getBudget(spanner.Key{2, 2})
		if err != nil {
			return err
		}
		const transferAmt = 200000
		if album2Budget >= transferAmt {
			album1Budget, err := getBudget(spanner.Key{1, 1})
			if err != nil {
				return err
			}
			album1Budget += transferAmt
			album2Budget -= transferAmt
			cols := []string{"SingerId", "AlbumId", "MarketingBudget"}
			txn.BufferWrite([]*spanner.Mutation{
				spanner.Update("Albums", cols, []interface{}{1, 1, album1Budget}),
				spanner.Update("Albums", cols, []interface{}{2, 2, album2Budget}),
			})
			fmt.Fprintf(w, "Moved %d from Album2's MarketingBudget to Album1's.", transferAmt)
		}
		return nil
	})
	return err
}

Java

static void writeWithTransaction(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        Struct row =
            transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
        long album2Budget = row.getLong(0);
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          long album1Budget =
              transaction
                  .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                  .getLong(0);
          album1Budget += transfer;
          album2Budget -= transfer;
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(1)
                  .set("AlbumId")
                  .to(1)
                  .set("MarketingBudget")
                  .to(album1Budget)
                  .build());
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(2)
                  .set("AlbumId")
                  .to(2)
                  .set("MarketingBudget")
                  .to(album2Budget)
                  .build());
        }
        return null;
      });
}

Node.js

// This sample transfers 200,000 from the MarketingBudget field
// of the second Album to the first Album, as long as the second
// Album has enough money in its budget. Make sure to run the
// addColumn and updateData samples first (in that order).

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

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

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const transferAmount = 200000;

database.runTransaction(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  let firstBudget, secondBudget;
  const queryOne = {
    columns: ['MarketingBudget'],
    keys: [[2, 2]], // SingerId: 2, AlbumId: 2
  };

  const queryTwo = {
    columns: ['MarketingBudget'],
    keys: [[1, 1]], // SingerId: 1, AlbumId: 1
  };

  Promise.all([
    // Reads the second album's budget
    transaction.read('Albums', queryOne).then(results => {
      // Gets second album's budget
      const rows = results[0].map(row => row.toJSON());
      secondBudget = rows[0].MarketingBudget;
      console.log(`The second album's marketing budget: ${secondBudget}`);

      // Makes sure the second album's budget is large enough
      if (secondBudget < transferAmount) {
        throw new Error(
          `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`,
        );
      }
    }),

    // Reads the first album's budget
    transaction.read('Albums', queryTwo).then(results => {
      // Gets first album's budget
      const rows = results[0].map(row => row.toJSON());
      firstBudget = rows[0].MarketingBudget;
      console.log(`The first album's marketing budget: ${firstBudget}`);
    }),
  ])
    .then(() => {
      console.log(firstBudget, secondBudget);
      // Transfers the budgets between the albums
      firstBudget += transferAmount;
      secondBudget -= transferAmount;

      console.log(firstBudget, secondBudget);

      // Updates the database
      // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
      // must be converted (back) to strings before being inserted as INT64s.
      transaction.update('Albums', [
        {
          SingerId: '1',
          AlbumId: '1',
          MarketingBudget: firstBudget.toString(),
        },
        {
          SingerId: '2',
          AlbumId: '2',
          MarketingBudget: secondBudget.toString(),
        },
      ]);
    })
    .then(() => {
      // Commits the transaction and send the changes to the database
      return transaction.commit();
    })
    .then(() => {
      console.log(
        `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`,
      );
    })
    .catch(err => {
      console.error('ERROR:', err);
    })
    .then(() => {
      transaction.end();
      // Closes the database when finished
      return database.close();
    });
});

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use UnexpectedValueException;

/**
 * Performs a read-write transaction to update two sample records in the
 * database.
 *
 * This will transfer 200,000 from the `MarketingBudget` field for the second
 * Album to the first Album. If the `MarketingBudget` for the second Album is
 * too low, it will raise an exception.
 *
 * Before running this sample, you will need to run the `update_data` sample
 * to populate the fields.
 * Example:
 * ```
 * read_write_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_write_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) use ($spanner) {
        $transferAmount = 200000;

        // Read the second album's budget.
        $secondAlbumKey = [2, 2];
        $secondAlbumKeySet = $spanner->keySet(['keys' => [$secondAlbumKey]]);
        $secondAlbumResult = $t->read(
            'Albums',
            $secondAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        $firstRow = $secondAlbumResult->rows()->current();
        $secondAlbumBudget = $firstRow['MarketingBudget'];
        if ($secondAlbumBudget < $transferAmount) {
            // Throwing an exception will automatically roll back the transaction.
            throw new UnexpectedValueException(
                'The second album\'s budget is lower than the transfer amount: ' . $transferAmount
            );
        }

        $firstAlbumKey = [1, 1];
        $firstAlbumKeySet = $spanner->keySet(['keys' => [$firstAlbumKey]]);
        $firstAlbumResult = $t->read(
            'Albums',
            $firstAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        // Read the first album's budget.
        $firstRow = $firstAlbumResult->rows()->current();
        $firstAlbumBudget = $firstRow['MarketingBudget'];

        // Update the budgets.
        $secondAlbumBudget -= $transferAmount;
        $firstAlbumBudget += $transferAmount;
        printf('Setting first album\'s budget to %s and the second album\'s ' .
            'budget to %s.' . PHP_EOL, $firstAlbumBudget, $secondAlbumBudget);

        // Update the rows.
        $t->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => $firstAlbumBudget],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => $secondAlbumBudget],
        ]);

        // Commit the transaction!
        $t->commit();

        print('Transaction complete.' . PHP_EOL);
    });
}

Python

def read_write_transaction(instance_id, database_id):
    """Performs a read-write transaction to update two sample records in the
    database.

    This will transfer 200,000 from the `MarketingBudget` field for the second
    Album to the first Album. If the `MarketingBudget` is too low, it will
    raise an exception.

    Before running this sample, you will need to run the `update_data` sample
    to populate the fields.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    def update_albums(transaction):
        # Read the second album budget.
        second_album_keyset = spanner.KeySet(keys=[(2, 2)])
        second_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=second_album_keyset,
            limit=1,
        )
        second_album_row = list(second_album_result)[0]
        second_album_budget = second_album_row[0]

        transfer_amount = 200000

        if second_album_budget < transfer_amount:
            # Raising an exception will automatically roll back the
            # transaction.
            raise ValueError("The second album doesn't have enough funds to transfer")

        # Read the first album's budget.
        first_album_keyset = spanner.KeySet(keys=[(1, 1)])
        first_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=first_album_keyset,
            limit=1,
        )
        first_album_row = list(first_album_result)[0]
        first_album_budget = first_album_row[0]

        # Update the budgets.
        second_album_budget -= transfer_amount
        first_album_budget += transfer_amount
        print(
            "Setting first album's budget to {} and the second album's "
            "budget to {}.".format(first_album_budget, second_album_budget)
        )

        # Update the rows.
        transaction.update(
            table="Albums",
            columns=("SingerId", "AlbumId", "MarketingBudget"),
            values=[(1, 1, first_album_budget), (2, 2, second_album_budget)],
        )

    database.run_in_transaction(update_albums)

    print("Transaction complete.")

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner         = Google::Cloud::Spanner.new project: project_id
client          = spanner.client instance_id, database_id
transfer_amount = 200_000

client.transaction do |transaction|
  first_album  = transaction.read("Albums", [:MarketingBudget], keys: [[1, 1]]).rows.first
  second_album = transaction.read("Albums", [:MarketingBudget], keys: [[2, 2]]).rows.first

  raise "The second album does not have enough funds to transfer" if second_album[:MarketingBudget] < transfer_amount

  new_first_album_budget  = first_album[:MarketingBudget] + transfer_amount
  new_second_album_budget = second_album[:MarketingBudget] - transfer_amount

  transaction.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: new_first_album_budget  },
    { SingerId: 2, AlbumId: 2, MarketingBudget: new_second_album_budget }
  ]
end

puts "Transaction complete"

Semantik

Bagian ini menjelaskan semantik untuk transaksi baca-tulis di Spanner.

Properti

Transaksi baca-tulis di Spanner menjalankan serangkaian operasi baca dan tulis secara atomik. Stempel waktu saat transaksi baca-tulis dieksekusi cocok dengan waktu yang telah berlalu. Urutan serialisasi cocok dengan urutan stempel waktu ini.

Transaksi baca-tulis menyediakan properti ACID database relasional. Transaksi baca-tulis Spanner menawarkan properti yang lebih kuat daripada ACID biasa.

Karena properti ini, sebagai developer aplikasi, Anda dapat berfokus pada kebenaran setiap transaksi dengan sendirinya, tanpa mengkhawatirkan cara melindungi eksekusinya dari transaksi lain yang mungkin dieksekusi pada saat yang sama.

Isolasi untuk transaksi baca-tulis

Setelah berhasil melakukan transaksi yang berisi serangkaian operasi baca dan tulis, Anda akan melihat hal berikut:

  • Transaksi menampilkan nilai yang mencerminkan snapshot yang konsisten pada stempel waktu commit transaksi.
  • Baris atau rentang kosong akan tetap kosong pada waktu penerapan.
  • Transaksi meng-commit semua operasi tulis pada stempel waktu commit transaksi.
  • Tidak ada transaksi yang dapat melihat penulisan hingga setelah transaksi di-commit.

Driver klien Spanner mencakup logika percobaan ulang transaksi yang menyembunyikan error sementara dengan menjalankan kembali transaksi dan memvalidasi data yang diamati klien.

Efeknya adalah semua operasi baca dan tulis tampak terjadi pada satu titik waktu, baik dari perspektif transaksi itu sendiri maupun dari perspektif pembaca dan penulis lain ke database Spanner. Artinya, operasi baca dan tulis terjadi pada stempel waktu yang sama. Sebagai contoh, lihat Serializability dan konsistensi eksternal.

Isolasi untuk transaksi baca

Saat transaksi baca-tulis hanya melakukan operasi baca, transaksi tersebut memberikan jaminan konsistensi yang serupa dengan transaksi hanya baca. Semua operasi baca dalam transaksi menampilkan data dari stempel waktu yang konsisten, termasuk konfirmasi baris yang tidak ada.

Salah satu perbedaannya adalah saat transaksi baca-tulis di-commit tanpa menjalankan operasi tulis. Dalam skenario ini, tidak ada jaminan bahwa data yang dibaca dalam transaksi tetap tidak berubah di database antara operasi baca dan commit transaksi.

Untuk memastikan keaktualan data dan memvalidasi bahwa data tidak dimodifikasi sejak pengambilan terakhir, pembacaan berikutnya diperlukan. Pembacaan ulang ini dapat dilakukan dalam transaksi baca-tulis lain atau dengan pembacaan yang kuat.

Untuk efisiensi yang optimal, jika transaksi hanya melakukan operasi baca, gunakan transaksi hanya baca, bukan transaksi baca-tulis.

Atomisitas, konsistensi, durabilitas

Selain isolasi, Spanner memberikan jaminan properti ACID lainnya:

  • Atomisitas. Transaksi dianggap atomik jika semua operasinya berhasil diselesaikan, atau tidak ada sama sekali. Jika ada operasi dalam transaksi yang gagal, seluruh transaksi akan di-roll back ke status aslinya, sehingga memastikan integritas data.
  • Konsistensi. Transaksi harus menjaga integritas aturan dan batasan database. Setelah transaksi selesai, database harus dalam keadaan valid, mematuhi aturan yang telah ditentukan.
  • Ketahanan. Setelah transaksi di-commit, perubahannya akan disimpan secara permanen di database dan tetap ada jika terjadi kegagalan sistem, pemadaman listrik, atau gangguan lainnya.

Serialisabilitas dan konsistensi eksternal

Spanner menawarkan jaminan transaksional yang kuat, termasuk serialisabilitas dan konsistensi eksternal. Properti ini memastikan bahwa data tetap konsisten dan operasi terjadi dalam urutan yang dapat diprediksi, bahkan dalam lingkungan terdistribusi.

Serialisasi memastikan bahwa semua transaksi tampak dieksekusi satu demi satu dalam urutan tunggal dan berurutan, meskipun diproses secara bersamaan. Spanner mencapai hal ini dengan menetapkan stempel waktu commit ke transaksi, yang mencerminkan urutan commitnya.

Spanner memberikan jaminan yang lebih kuat yang dikenal sebagai konsistensi eksternal. Artinya, tidak hanya transaksi yang di-commit dalam urutan yang tercermin dalam stempel waktu commit-nya, tetapi stempel waktu ini juga selaras dengan waktu dunia nyata. Hal ini memungkinkan Anda membandingkan stempel waktu penerapan dengan waktu real-time, sehingga memberikan tampilan data yang konsisten dan diurutkan secara global.

Pada dasarnya, jika transaksi Txn1 di-commit sebelum transaksi Txn2 secara real time, maka stempel waktu commit Txn1 lebih awal daripada stempel waktu commit Txn2.

Perhatikan contoh berikut:

Linimasa yang menunjukkan eksekusi dua transaksi yang membaca data yang sama

Dalam skenario ini, selama linimasa t:

  • Transaksi Txn1 membaca data A, melakukan penahapan penulisan ke A, lalu berhasil di-commit.
  • Transaksi Txn2 dimulai setelah Txn1 dimulai. Membaca data B dan kemudian membaca data A.

Meskipun Txn2 dimulai sebelum Txn1 selesai, Txn2 mengamati perubahan yang dilakukan oleh Txn1 pada A. Hal ini karena Txn2 membaca A setelah Txn1 melakukan operasi tulisnya ke A.

Meskipun Txn1 dan Txn2 mungkin tumpang-tindih dalam waktu eksekusinya, stempel waktu commit, c1 dan c2 masing-masing, menerapkan urutan transaksi linear. Artinya:

  • Semua operasi baca dan tulis dalam Txn1 tampaknya terjadi pada satu titik waktu, c1.
  • Semua operasi baca dan tulis dalam Txn2 tampaknya terjadi pada satu titik waktu, c2.
  • Yang penting, c1 lebih awal dari c2 untuk penulisan yang dilakukan, meskipun penulisan terjadi di mesin yang berbeda. Jika Txn2 hanya melakukan pembacaan, c1 lebih awal atau pada saat yang sama dengan c2.

Pengurutan yang kuat ini berarti bahwa jika operasi baca berikutnya mengamati efek Txn2, operasi tersebut juga mengamati efek Txn1. Properti ini bernilai benar (true) untuk semua transaksi yang berhasil di-commit.

Jaminan baca dan tulis saat transaksi gagal

Jika panggilan untuk mengeksekusi transaksi gagal, jaminan baca dan tulis yang Anda miliki bergantung pada error yang menyebabkan panggilan commit yang mendasarinya gagal.

Misalnya, error seperti "Baris Tidak Ditemukan" atau "Baris Sudah Ada" berarti penulisan mutasi yang di-buffer mengalami beberapa error, misalnya, baris yang coba diupdate oleh klien tidak ada. Dalam hal ini, operasi baca dijamin konsisten, operasi tulis tidak diterapkan, dan tidak adanya baris dijamin konsisten dengan operasi baca juga.

Jaminan baca dan tulis saat transaksi gagal

Jika transaksi Spanner gagal, jaminan yang Anda terima untuk operasi baca dan tulis bergantung pada error spesifik yang terjadi selama operasi commit.

Misalnya, pesan error seperti "Baris Tidak Ditemukan" atau "Baris Sudah Ada" menunjukkan masalah selama penulisan mutasi yang di-buffer. Hal ini dapat terjadi jika, misalnya, baris yang coba diupdate oleh klien tidak ada. Dalam skenario ini:

  • Pembacaan konsisten: Setiap data yang dibaca selama transaksi dijamin konsisten hingga titik error.
  • Penulisan tidak diterapkan: Mutasi yang dicoba oleh transaksi tidak di-commit ke database.
  • Konsistensi baris: Tidak adanya (atau status yang ada) baris yang memicu error konsisten dengan pembacaan yang dilakukan dalam transaksi.

Anda dapat membatalkan operasi baca asinkron di Spanner kapan saja tanpa memengaruhi operasi lain yang sedang berlangsung dalam transaksi yang sama. Fleksibilitas ini berguna jika operasi tingkat yang lebih tinggi dibatalkan, atau jika Anda memutuskan untuk membatalkan pembacaan berdasarkan hasil awal.

Namun, penting untuk dipahami bahwa permintaan pembatalan pembacaan tidak menjamin penghentiannya secara langsung. Setelah permintaan pembatalan, operasi baca mungkin masih:

  • Berhasil diselesaikan: pembacaan mungkin selesai diproses dan menampilkan hasil sebelum pembatalan berlaku.
  • Gagal karena alasan lain: pembacaan dapat dihentikan karena kesalahan lain, seperti pembatalan.
  • Menampilkan hasil yang tidak lengkap: pembacaan dapat menampilkan hasil parsial, yang kemudian divalidasi sebagai bagian dari proses commit transaksi.

Perlu diperhatikan juga perbedaan dengan operasi commit transaksi: membatalkan commit akan membatalkan seluruh transaksi, kecuali jika transaksi telah di-commit atau gagal karena alasan lain.

Performa

Bagian ini menjelaskan masalah yang memengaruhi performa transaksi baca-tulis.

Kontrol konkurensi penguncian

Spanner mengizinkan beberapa klien berinteraksi dengan database yang sama secara bersamaan. Untuk menjaga konsistensi data di seluruh transaksi serentak ini, Spanner memiliki mekanisme penguncian yang menggunakan kunci bersama dan eksklusif.

Saat transaksi melakukan operasi baca, Spanner akan memperoleh kunci baca bersama pada data yang relevan. Kunci bersama ini memungkinkan operasi baca serentak lainnya mengakses data yang sama. Konkurensi ini dipertahankan hingga transaksi Anda bersiap untuk meng-commit perubahannya.

Selama fase commit, saat penulisan diterapkan, transaksi akan mencoba mengupgrade kuncinya menjadi kunci eksklusif. Untuk melakukannya, alat ini akan melakukan hal berikut:

  • Memblokir permintaan kunci baca bersama baru pada data yang terpengaruh.
  • Menunggu semua kunci baca bersama yang ada pada data tersebut dilepaskan.
  • Setelah semua kunci baca bersama dihapus, kunci eksklusif akan ditempatkan, sehingga memberikan akses tunggal ke data selama penulisan.

Catatan tentang kunci:

  • Granularitas: Spanner menerapkan penguncian pada granularitas baris dan kolom. Artinya, jika transaksi T1 memegang kunci pada kolom A baris albumid, transaksi T2 masih dapat menulis secara bersamaan ke kolom B dari baris albumid yang sama tanpa konflik.
  • Penulisan tanpa pembacaan: Untuk penulisan tanpa pembacaan, Spanner tidak memerlukan kunci eksklusif. Sebagai gantinya, ia menggunakan kunci bersama penulis. Hal ini karena urutan penerapan untuk penulisan tanpa pembacaan ditentukan oleh stempel waktu penerapan, sehingga memungkinkan beberapa penulis beroperasi pada item yang sama secara bersamaan tanpa konflik. Penguncian eksklusif hanya diperlukan jika transaksi Anda pertama-tama membaca data yang akan ditulisnya.
  • Indeks sekunder untuk pencarian baris: saat melakukan pencarian baris dalam transaksi baca-tulis, penggunaan indeks sekunder dapat meningkatkan performa secara signifikan. Dengan menggunakan indeks sekunder untuk membatasi baris yang dipindai ke rentang yang lebih kecil, Spanner mengunci lebih sedikit baris dalam tabel, sehingga memungkinkan modifikasi baris yang lebih bersamaan di luar rentang tertentu tersebut.
  • Akses eksklusif resource eksternal: Kunci internal Spanner dirancang untuk konsistensi data dalam database Spanner itu sendiri. Jangan menggunakannya untuk menjamin akses eksklusif ke resource di luar Spanner. Spanner dapat membatalkan transaksi karena berbagai alasan, termasuk pengoptimalan sistem internal seperti pemindahan data di seluruh resource komputasi. Jika transaksi dicoba lagi (baik secara eksplisit oleh kode aplikasi Anda atau secara implisit oleh library klien seperti driver JDBC Spanner), kunci hanya dijamin telah dipegang selama upaya commit yang berhasil.
  • Statistik kunci: untuk mendiagnosis dan menyelidiki konflik kunci dalam database Anda, Anda dapat menggunakan alat introspeksi Statistik kunci.

Deteksi kebuntuan

Spanner mendeteksi saat beberapa transaksi mungkin mengalami kebuntuan dan memaksa semua transaksi kecuali satu transaksi untuk dibatalkan. Pertimbangkan skenario ini: Txn1 memegang kunci pada rekaman A dan menunggu kunci pada rekaman B, sementara Txn2 memegang kunci pada rekaman B dan menunggu kunci pada rekaman A. Untuk mengatasi hal ini, salah satu transaksi harus dibatalkan, melepaskan kuncinya, dan memungkinkan transaksi lainnya dilanjutkan.

Spanner menggunakan algoritma wound-wait standar untuk deteksi kebuntuan. Di balik layar, Spanner melacak usia setiap transaksi yang meminta kunci yang bertentangan. Hal ini memungkinkan transaksi lama membatalkan transaksi yang lebih baru. Transaksi lama adalah transaksi yang pembacaan, kueri, atau commit paling awalnya terjadi lebih awal.

Dengan memprioritaskan transaksi yang lebih lama, Spanner memastikan bahwa setiap transaksi pada akhirnya akan mendapatkan kunci setelah cukup lama untuk memiliki prioritas yang lebih tinggi. Misalnya, transaksi lama yang memerlukan kunci bersama penulis dapat membatalkan transaksi yang lebih baru yang memegang kunci bersama pembaca.

Eksekusi terdistribusi

Spanner dapat mengeksekusi transaksi pada data yang mencakup beberapa server, meskipun kemampuan ini memiliki biaya performa dibandingkan dengan transaksi server tunggal.

Jenis transaksi apa yang dapat didistribusikan? Spanner dapat mendistribusikan tanggung jawab untuk baris database di banyak server. Biasanya, baris dan baris tabel yang disisipkan yang sesuai dilayani oleh server yang sama, seperti dua baris dalam tabel yang sama dengan kunci yang berdekatan. Spanner dapat melakukan transaksi di seluruh baris pada server yang berbeda. Namun, sebagai aturan umum, transaksi yang memengaruhi banyak baris yang ditempatkan bersama lebih cepat dan lebih murah daripada transaksi yang memengaruhi banyak baris yang tersebar di seluruh database atau tabel besar.

Transaksi yang paling efisien di Spanner hanya mencakup operasi baca dan tulis yang harus diterapkan secara atomik. Transaksi akan berjalan paling cepat jika semua pembacaan dan penulisan mengakses data di bagian ruang kunci yang sama.

Transaksi hanya baca

Selain mengunci transaksi baca-tulis, Spanner menawarkan transaksi hanya baca.

Gunakan transaksi hanya baca saat Anda perlu menjalankan lebih dari satu operasi baca pada stempel waktu yang sama. Jika Anda dapat mengekspresikan pembacaan menggunakan salah satu metode pembacaan tunggal Spanner, Anda harus menggunakan metode pembacaan tunggal tersebut. Performa penggunaan satu panggilan baca tersebut seharusnya sebanding dengan performa satu baca yang dilakukan dalam transaksi baca saja.

Jika Anda membaca data dalam jumlah besar, pertimbangkan untuk menggunakan partisi guna membaca data secara paralel.

Karena transaksi hanya baca tidak menulis, transaksi tersebut tidak memegang kunci dan tidak memblokir transaksi lain. Transaksi hanya baca mengamati awalan yang konsisten dari histori commit transaksi, sehingga aplikasi Anda selalu mendapatkan data yang konsisten.

Antarmuka

Spanner menyediakan antarmuka untuk mengeksekusi serangkaian tugas dalam konteks transaksi hanya baca, dengan percobaan ulang untuk pembatalan transaksi.

Contoh

Contoh berikut menunjukkan cara menggunakan transaksi hanya baca untuk mendapatkan data yang konsisten untuk dua pembacaan pada stempel waktu yang sama:

C++

void ReadOnlyTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto read_only = spanner::MakeReadOnlyTransaction();

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
  using RowType = std::tuple<std::int64_t, std::int64_t, std::string>;

  // Read#1.
  auto rows1 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 1 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows1)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
  // Read#2. Even if changes occur in-between the reads the transaction ensures
  // that Read #1 and Read #2 return the same data.
  auto rows2 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 2 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows2)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Transactions;

public class QueryDataWithTransactionAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        using var connection = new SpannerConnection(connectionString);

        // Opens the connection so that the Spanner transaction included in the TransactionScope
        // is read-only TimestampBound.Strong.
        await connection.OpenAsync(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: default);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : " + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : " + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : " + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }

        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readOnlyTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	ro := client.ReadOnlyTransaction()
	defer ro.Close()
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := ro.Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}

	iter = ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"SingerId", "AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}
}

Java

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    try (ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
      while (queryResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
      }
    } // queryResultSet.close() is automatically called here
    try (ResultSet readResultSet =
        transaction.read(
          "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    } // readResultSet.close() is automatically called here
  } // transaction.close() is automatically called here
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

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

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Gets a transaction object that captures the database state
// at a specific point in time
database.getSnapshot(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

  try {
    // Read #1, using SQL
    const [qOneRows] = await transaction.run(queryOne);

    qOneRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    const queryTwo = {
      columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
    };

    // Read #2, using the `read` method. Even if changes occur
    // in-between the reads, the transaction ensures that both
    // return the same data.
    const [qTwoRows] = await transaction.read('Albums', queryTwo);

    qTwoRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    console.log('Successfully executed read-only transaction.');
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    transaction.end();
    // Close the database when finished.
    await database.close();
  }
});

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads data inside of a read-only transaction.
 *
 * Within the read-only transaction, or "snapshot", the application sees
 * consistent view of the database at a particular timestamp.
 * Example:
 * ```
 * read_only_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_only_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $snapshot = $database->snapshot();
    $results = $snapshot->execute(
        'SELECT SingerId, AlbumId, AlbumTitle FROM Albums'
    );
    print('Results from the first read:' . PHP_EOL);
    foreach ($results as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }

    // Perform another read using the `read` method. Even if the data
    // is updated in-between the reads, the snapshot ensures that both
    // return the same data.
    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['SingerId', 'AlbumId', 'AlbumTitle']
    );

    print('Results from the second read:' . PHP_EOL);
    foreach ($results->rows() as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_only_transaction(instance_id, database_id):
    """Reads data inside of a read-only transaction.

    Within the read-only transaction, or "snapshot", the application sees
    consistent view of the database at a particular timestamp.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot(multi_use=True) as snapshot:
        # Read using SQL.
        results = snapshot.execute_sql(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums"
        )

        print("Results from first read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

        # Perform another read using the `read` method. Even if the data
        # is updated in-between the reads, the snapshot ensures that both
        # return the same data.
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), keyset=keyset
        )

        print("Results from second read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

client.snapshot do |snapshot|
  snapshot.execute("SELECT SingerId, AlbumId, AlbumTitle FROM Albums").rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end

  # Even if changes occur in-between the reads, the transaction ensures that
  # both return the same data.
  snapshot.read("Albums", [:AlbumId, :AlbumTitle, :SingerId]).rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end
end

Semantik

Bagian ini menjelaskan semantik untuk transaksi hanya baca.

Transaksi hanya baca snapshot

Saat transaksi hanya baca dieksekusi di Spanner, transaksi tersebut melakukan semua pembacaannya pada satu titik waktu logis. Artinya, transaksi hanya baca dan pembaca serta penulis serentak lainnya melihat snapshot database yang konsisten pada saat tertentu.

Transaksi hanya baca snapshot ini menawarkan pendekatan yang lebih sederhana untuk pembacaan yang konsisten dibandingkan dengan transaksi baca-tulis penguncian. Berikut alasannya:

  • Tanpa kunci: transaksi hanya baca tidak memperoleh kunci. Sebagai gantinya, opsi tersebut beroperasi dengan memilih stempel waktu Spanner dan menjalankan semua operasi baca terhadap versi historis data tersebut. Karena tidak menggunakan kunci, batch tidak akan memblokir transaksi baca-tulis serentak.
  • Tidak ada pembatalan: transaksi ini tidak pernah dibatalkan. Meskipun dapat gagal jika stempel waktu baca yang dipilihnya dikumpulkan sampah, kebijakan pengumpulan sampah default Spanner biasanya cukup besar sehingga sebagian besar aplikasi tidak akan mengalami masalah ini.
  • Tidak ada commit atau roll back: transaksi hanya baca tidak memerlukan panggilan ke sessions.commit atau sessions.rollback dan sebenarnya dicegah untuk melakukannya.

Untuk menjalankan transaksi snapshot, klien menentukan batas stempel waktu, yang menginstruksikan Spanner cara memilih stempel waktu baca. Jenis batas stempel waktu mencakup:

  • Pembacaan yang andal: pembacaan ini menjamin bahwa Anda akan melihat efek semua transaksi yang dilakukan sebelum pembacaan dimulai. Semua baris dalam satu pembacaan konsisten. Namun, pembacaan kuat tidak dapat diulang, meskipun pembacaan kuat menampilkan stempel waktu, dan pembacaan lagi pada stempel waktu yang sama dapat diulang. Dua transaksi hanya baca yang kuat secara berurutan dapat menghasilkan hasil yang berbeda karena penulisan serentak. Kueri pada aliran perubahan harus menggunakan batas ini. Untuk mengetahui detail selengkapnya, lihat TransactionOptions.ReadOnly.strong.
  • Keterlambatan yang tepat: opsi ini menjalankan pembacaan pada stempel waktu yang Anda tentukan, baik sebagai stempel waktu absolut atau sebagai durasi keterlambatan relatif terhadap waktu saat ini. Hal ini memastikan Anda mengamati awalan yang konsisten dari histori transaksi global hingga stempel waktu tersebut dan memblokir transaksi yang bertentangan yang mungkin di-commit dengan stempel waktu yang kurang dari atau sama dengan stempel waktu baca. Meskipun sedikit lebih cepat daripada mode keusangan terbatas, mode ini dapat menampilkan data yang lebih lama. Untuk mengetahui detail selengkapnya, lihat TransactionOptions.ReadOnly.read_timestamp dan TransactionOptions.ReadOnly.exact_staleness.
  • Keterlambatan yang dibatasi: Spanner memilih stempel waktu terbaru dalam batas keterlambatan yang ditentukan pengguna, sehingga memungkinkan eksekusi di replika terdekat yang tersedia tanpa pemblokiran. Semua baris yang ditampilkan konsisten. Seperti pembacaan kuat, keusangan terbatas tidak dapat diulang, karena pembacaan yang berbeda mungkin dieksekusi pada stempel waktu yang berbeda meskipun dengan batas yang sama. Operasi baca ini beroperasi dalam dua fase (negosiasi stempel waktu, lalu baca) dan biasanya sedikit lebih lambat daripada keusangan tepat, tetapi sering kali menampilkan hasil yang lebih baru dan lebih mungkin dieksekusi di replika lokal. Mode ini hanya tersedia untuk transaksi hanya baca sekali pakai karena negosiasi stempel waktu mengharuskan mengetahui baris mana yang akan dibaca sebelumnya. Untuk detail selengkapnya, lihat TransactionOptions.ReadOnly.max_staleness dan TransactionOptions.ReadOnly.min_read_timestamp.

Transaksi DML yang dipartisi

Anda dapat menggunakan DML berpartisi untuk mengeksekusi pernyataan UPDATE dan DELETE skala besar tanpa mengalami batas transaksi atau mengunci seluruh tabel. Spanner mencapai hal ini dengan mempartisi ruang kunci dan menjalankan pernyataan DML pada setiap partisi dalam transaksi baca-tulis terpisah.

Untuk menggunakan DML yang tidak dipartisi, Anda menjalankan pernyataan dalam transaksi baca-tulis yang Anda buat secara eksplisit dalam kode Anda. Untuk mengetahui detail selengkapnya, lihat Menggunakan DML.

Antarmuka

Spanner menyediakan antarmuka TransactionOptions.partitionedDml untuk menjalankan satu pernyataan DML terpartisi.

Contoh

Contoh kode berikut mengupdate kolom MarketingBudget dari tabel Albums.

C++

Anda menggunakan fungsi ExecutePartitionedDml() untuk menjalankan pernyataan DML berpartisi.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#

Anda menggunakan metode ExecutePartitionedUpdateAsync() untuk menjalankan pernyataan DML berpartisi.


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

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

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

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go

Anda menggunakan metode PartitionedUpdate() untuk menjalankan pernyataan DML berpartisi.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

Anda menggunakan metode executePartitionedUpdate() untuk menjalankan pernyataan DML berpartisi.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

Anda menggunakan metode runPartitionedUpdate() untuk menjalankan pernyataan DML berpartisi.

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

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

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

Anda menggunakan metode executePartitionedUpdate() untuk menjalankan pernyataan DML berpartisi.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

Anda menggunakan metode execute_partitioned_dml() untuk menjalankan pernyataan DML berpartisi.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

Anda menggunakan metode execute_partitioned_update() untuk menjalankan pernyataan DML berpartisi.

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

Contoh kode berikut menghapus baris dari tabel Singers, berdasarkan kolom SingerId.

C++

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#


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

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

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

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

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

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

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

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."

Semantik

Bagian ini menjelaskan semantik untuk DML yang dipartisi.

Memahami eksekusi DML yang dipartisi

Anda hanya dapat menjalankan satu pernyataan DML yang dipartisi dalam satu waktu, baik Anda menggunakan metode library klien maupun Google Cloud CLI.

Transaksi yang dipartisi tidak mendukung commit atau rollback. Spanner akan segera mengeksekusi dan menerapkan pernyataan DML. Jika Anda membatalkan operasi, atau operasi gagal, Spanner akan membatalkan semua partisi yang sedang dieksekusi dan tidak memulai partisi yang tersisa. Namun, Spanner tidak mengembalikan partisi yang telah dieksekusi.

Strategi perolehan kunci DML yang dipartisi

Untuk mengurangi pertentangan kunci, DML berpartisi hanya memperoleh kunci baca pada baris yang cocok dengan klausa WHERE. Transaksi independen yang lebih kecil yang digunakan untuk setiap partisi juga memegang kunci untuk waktu yang lebih singkat.

Batas transaksi sesi

Setiap sesi di Spanner dapat memiliki satu transaksi aktif dalam satu waktu. Hal ini mencakup pembacaan dan kueri mandiri, yang secara internal menggunakan transaksi dan diperhitungkan dalam batas ini. Setelah transaksi selesai, sesi dapat segera digunakan kembali untuk transaksi berikutnya; sesi baru tidak perlu dibuat untuk setiap transaksi.

Stempel waktu baca lama dan pembersihan sampah memori versi

Spanner melakukan pembersihan sampah memori versi untuk mengumpulkan data yang dihapus atau ditimpa dan mengklaim kembali penyimpanan. Secara default, data yang lebih lama dari satu jam akan diklaim ulang. Spanner tidak dapat melakukan pembacaan pada stempel waktu yang lebih lama dari VERSION_RETENTION_PERIOD yang dikonfigurasi, yang secara default adalah satu jam, tetapi dapat dikonfigurasi hingga satu minggu. Jika pembacaan menjadi terlalu lama selama eksekusi, pembacaan tersebut akan gagal dan menampilkan error FAILED_PRECONDITION.

Kueri pada aliran perubahan

Aliran perubahan adalah objek skema yang dapat Anda konfigurasi untuk memantau modifikasi data di seluruh database, tabel tertentu, atau sekumpulan kolom yang ditentukan dalam database.

Saat Anda membuat aliran data perubahan, Spanner akan menentukan fungsi bernilai tabel (TVF) SQL yang sesuai. Anda dapat menggunakan TVF ini untuk membuat kueri catatan perubahan di aliran perubahan terkait dengan metode sessions.executeStreamingSql. Nama TVF dibuat dari nama aliran perubahannya dan selalu dimulai dengan READ_.

Semua kueri pada TVF aliran perubahan harus dieksekusi menggunakan sessions.executeStreamingSql API dalam transaksi hanya baca sekali pakai dengan timestamp_bound hanya baca yang kuat. TVF aliran perubahan memungkinkan Anda menentukan start_timestamp dan end_timestamp untuk rentang waktu. Semua catatan perubahan dalam periode retensi dapat diakses menggunakan timestamp_bound hanya baca yang kuat ini. Semua TransactionOptions lainnya tidak valid untuk kueri aliran perubahan.

Selain itu, jika TransactionOptions.read_only.return_read_timestamp disetel ke true, pesan Transaction yang menjelaskan transaksi akan menampilkan nilai khusus 2^63 - 2, bukan stempel waktu baca yang valid. Anda harus menghapus nilai khusus ini dan tidak menggunakannya untuk kueri berikutnya.

Untuk mengetahui informasi selengkapnya, lihat Alur kerja kueri aliran perubahan.

Transaksi Tidak Aktif

Transaksi dianggap tidak aktif jika tidak memiliki pembacaan atau kueri SQL yang belum selesai dan belum memulai satu pun dalam 10 detik terakhir. Spanner dapat membatalkan transaksi yang tidak aktif untuk mencegahnya menahan kunci tanpa batas. Jika transaksi yang tidak aktif dibatalkan, commit akan gagal dan menampilkan error ABORTED. Menjalankan kueri kecil secara berkala, seperti SELECT 1, dalam transaksi dapat mencegahnya menjadi tidak aktif.