Transaktionen – Übersicht

Auf dieser Seite werden Transaktionen in Spanner beschrieben und die nicht schreibgeschützten, schreibgeschützten und partitionierten DML-Transaktionsschnittstellen von Spanner vorgestellt.

Bei einer Transaktion in Spanner handelt es sich um eine Gruppe von Lese- und Schreibvorgängen, die zu einem einzigen logischen Zeitpunkt über Spalten, Zeilen und Tabellen in einer Datenbank atomar ausgeführt wird.

In einer Sitzung werden Transaktionen in einer Spanner-Datenbank ausgeführt. Eine Sitzung stellt einen logischen Kommunikationskanal mit dem Spanner-Datenbankdienst dar. Sitzungen können eine oder mehrere Transaktionen gleichzeitig ausführen. Weitere Informationen finden Sie unter Sitzungen.

Transaktionstypen

Spanner unterstützt die folgenden Transaktionstypen, die jeweils für bestimmte Dateninteraktionsmuster entwickelt wurden:

  • Lesen/Schreiben:Diese Transaktionen verwenden eine pessimistische Sperrung und bei Bedarf einen zweiphasigen Commit. Sie können fehlschlagen und müssen dann wiederholt werden. Sie sind zwar auf eine einzelne Datenbank beschränkt, können aber Daten in mehreren Tabellen innerhalb dieser Datenbank ändern.

  • Schreibgeschützt:Diese Transaktionen garantieren Datenkonsistenz über mehrere Lesevorgänge hinweg, lassen jedoch keine Datenänderungen zu. Sie werden aus Konsistenzgründen mit einem vom System bestimmten Zeitstempel oder mit einem vom Nutzer konfigurierten Zeitstempel aus der Vergangenheit ausgeführt. Im Gegensatz zu Lese-Schreib-Transaktionen erfordern sie keinen Commit-Vorgang oder keine Sperren. Sie können jedoch pausieren, um auf den Abschluss laufender Schreibvorgänge zu warten.

  • Partitionierte DML:Bei diesem Transaktionstyp werden DML-Anweisungen als partitionierte DML-Vorgänge ausgeführt. Sie ist für umfangreiche Datenaktualisierungen und ‑löschungen optimiert, z. B. für die Bereinigung von Daten oder das Einfügen von Massendaten. Wenn Sie zahlreiche Schreibvorgänge ausführen müssen, für die keine atomare Transaktion erforderlich ist, sollten Sie Batch-Schreibvorgänge verwenden. Weitere Informationen finden Sie unter Daten mit Batch-Schreibvorgängen ändern.

Lese-/Schreibtransaktionen

Mit sperrenden Lese-/Schreibtransaktionen können Sie Daten an beliebiger Stelle in einer Datenbank atomar lesen, ändern und schreiben. Diese Art von Transaktion ist extern konsistent.

Minimieren Sie die Zeit, in der eine Transaktion aktiv ist. Kürzere Transaktionsdauern erhöhen die Wahrscheinlichkeit eines erfolgreichen Commits und verringern die Konflikte. Spanner versucht, Lesesperren so lange aktiv zu halten, wie die Transaktion weiterhin Lesevorgänge ausführt und die Transaktion nicht durch sessions.commit- oder sessions.rollback-Vorgänge beendet wurde. Wenn der Client über einen längeren Zeitraum inaktiv bleibt, kann es sein, dass Spanner die Sperren der Transaktion freigibt und die Transaktion abbricht.

Konzeptionell besteht eine Lese-Schreib-Transaktion aus null oder mehr Lese- oder SQL-Anweisungen, gefolgt von sessions.commit. Der Client kann jederzeit vor sessions.commit eine sessions.rollback-Anfrage senden, um die Transaktion abzubrechen.

Wenn Sie einen Schreibvorgang ausführen möchten, der von einem oder mehreren Lesevorgängen abhängt, verwenden Sie eine sperrende Lese-Schreib-Transaktion:

  • Wenn Sie einen oder mehrere Schreibvorgänge atomar ausführen müssen, führen Sie diese Schreibvorgänge in derselben Lese-Schreib-Transaktion aus. Wenn Sie beispielsweise 200 $ von Konto A auf Konto B überweisen, führen Sie beide Schreibvorgänge (Verringern von Konto A um 200 $und Erhöhen von Konto B um 200 $) und das Lesen der ursprünglichen Kontostände in derselben Transaktion aus.
  • Wenn Sie den Kontostand von Konto A verdoppeln möchten, führen Sie die Lese- und Schreibvorgänge in derselben Transaktion aus. So wird sichergestellt, dass das System das Guthaben liest, bevor es verdoppelt und dann aktualisiert wird.
  • Wenn Sie einen oder mehrere Schreibvorgänge ausführen könnten, die von den Ergebnissen eines oder mehrerer Lesevorgänge abhängen, sollten Sie diese Schreib- und Lesevorgänge in derselben Lese-Schreib-Transaktion ausführen, auch wenn die Schreibvorgänge nicht ausgeführt werden. Wenn Sie beispielsweise 200 $von Konto A auf Konto B überweisen möchten, aber nur, wenn der aktuelle Kontostand von A mehr als 500 $beträgt, sollten Sie das Lesen des Kontostands von A und die bedingten Schreibvorgänge in derselben Transaktion ausführen, auch wenn die Überweisung nicht erfolgt.

Verwenden Sie für Lesevorgänge eine einzelne Lesemethode oder eine schreibgeschützte Transaktion:

  • Wenn Sie nur Lesevorgänge ausführen und den Lesevorgang mithilfe einer einzelnen Lesemethode ausdrücken können, verwenden Sie diese einzelne Lesemethode oder eine schreibgeschützte Transaktion. Im Gegensatz zu Lese-Schreib-Transaktionen werden bei einzelnen Lesevorgängen keine Sperren abgerufen.

Schnittstelle

Die Spanner-Clientbibliotheken bieten eine Schnittstelle zum Ausführen eines Arbeitsablaufs im Rahmen einer Lese-Schreib-Transaktion mit Wiederholungen für Transaktionsabbrüche. Für eine Spanner-Transaktion sind möglicherweise mehrere Wiederholungsversuche erforderlich, bevor sie mit Commit ausgeführt wird.

Transaktionen können aus verschiedenen Gründen abgebrochen werden. Wenn beispielsweise zwei Transaktionen versuchen, Daten gleichzeitig zu ändern, kann es zu einem Deadlock kommen. In solchen Fällen bricht Spanner eine Transaktion ab, damit die andere fortgesetzt werden kann. Weniger häufig können vorübergehende Ereignisse in Spanner auch zu Transaktionsabbrüchen führen.

Da Transaktionen atomar sind, hat eine abgebrochene Transaktion keine Auswirkungen auf die Datenbank. Versuchen Sie es noch einmal mit der Transaktion in derselben Sitzung, um die Erfolgsraten zu verbessern. Bei jedem Wiederholungsversuch, der zu einem ABORTED-Fehler führt, erhöht sich die Sperrpriorität der Transaktion.

Wenn Sie eine Transaktion in einer Spanner-Clientbibliothek verwenden, definieren Sie den Ablauf der Transaktion als Funktionsobjekt. Diese Funktion kapselt die Lese- und Schreibvorgänge, die für eine oder mehrere Datenbanktabellen ausgeführt werden. Die Spanner-Clientbibliothek führt diese Funktion wiederholt aus, bis die Transaktion entweder erfolgreich übergeben wird oder ein Fehler auftritt, der nicht wiederholt werden kann.

Beispiel

Angenommen, Sie haben in der Tabelle Albums die Spalte MarketingBudget:

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

Ihre Marketingabteilung bittet Sie, 200.000 $aus dem Budget von Albums (2, 2) in das Budget von Albums (1, 1) zu verschieben, aber nur, wenn das Geld im Budget dieses Albums verfügbar ist. Sie sollten für diesen Vorgang eine sperrende Lese-Schreib-Transaktion verwenden, da die Transaktion je nach Ergebnis eines Lesevorgangs Schreibvorgänge ausführen könnte.

Das folgende Beispiel zeigt, wie Sie eine Lese-Schreib-Transaktion ausführen:

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

In diesem Abschnitt wird die Semantik für Lese-/Schreibtransaktionen in Spanner beschrieben.

Eigenschaften

Eine Lese-Schreib-Transaktion in Spanner führt eine Reihe von Lese- und Schreibvorgängen atomar aus. Der Zeitstempel, zu dem Lese-/Schreibtransaktionen ausgeführt werden, entspricht der verstrichenen Zeit. Die Reihenfolge der Serialisierung entspricht dieser Zeitstempelreihenfolge.

Lese-/Schreibtransaktionen bieten die ACID-Attribute von relationalen Datenbanken. Lese-Schreib-Transaktionen in Spanner bieten stärkere Attribute als typische ACID-Transaktionen.

Aufgrund dieser Attribute können Sie sich als Anwendungsentwickler auf die Genauigkeit jeder einzelnen Transaktion konzentrieren, ohne sich um den Schutz der ausgeführten Transaktion vor anderen Transaktionen, die möglicherweise zur gleichen Zeit ausgeführt werden könnten, kümmern zu müssen.

Isolation für Lese-/Schreibtransaktionen

Nachdem Sie eine Transaktion mit einer Reihe von Lese- und Schreibvorgängen erfolgreich committet haben, sehen Sie Folgendes:

  • Die Transaktion gibt Werte zurück, die einen konsistenten Snapshot zum Commit-Zeitstempel der Transaktion widerspiegeln.
  • Leere Zeilen oder Bereiche bleiben beim Commit leer.
  • Bei der Transaktion werden alle Schreibvorgänge zum Commit-Zeitstempel der Transaktion committet.
  • Keine Transaktion kann die Schreibvorgänge sehen, bis die Transaktion übergeben wurde.

Spanner-Clienttreiber enthalten eine Logik für das Wiederholen von Transaktionen, die vorübergehende Fehler maskiert, indem die Transaktion noch einmal ausgeführt und die vom Client beobachteten Daten validiert werden.

Der Effekt besteht darin, dass alle Lese- und Schreibvorgänge zu einem bestimmten Zeitpunkt stattgefunden haben, sowohl aus der Sicht der Transaktion selbst als auch aus der Sicht anderer Leser und Autoren in der Spanner-Datenbank. Das bedeutet, dass die Lese- und Schreibvorgänge zum selben Zeitpunkt erfolgen. Ein Beispiel finden Sie unter Serialisierbarkeit und externe Konsistenz.

Isolation für Lesetransaktionen

Wenn eine Lese-Schreib-Transaktion nur Lesevorgänge ausführt, bietet sie ähnliche Konsistenzgarantien wie eine schreibgeschützte Transaktion. Alle Lesevorgänge innerhalb der Transaktion geben Daten aus einem konsistenten Zeitstempel zurück, einschließlich der Bestätigung nicht vorhandener Zeilen.

Ein Unterschied besteht darin, wenn eine Lese-/Schreibtransaktion ohne Ausführung eines Schreibvorgangs committet wird. In diesem Szenario gibt es keine Garantie dafür, dass die innerhalb der Transaktion gelesenen Daten in der Datenbank zwischen dem Lesevorgang und dem Commit der Transaktion unverändert geblieben sind.

Um die Aktualität der Daten zu gewährleisten und zu prüfen, ob die Daten seit dem letzten Abruf geändert wurden, ist ein nachfolgender Lesevorgang erforderlich. Dieser erneute Lesevorgang kann entweder innerhalb einer anderen Lese-Schreib-Transaktion oder mit einem starken Lesevorgang erfolgen.

Wenn eine Transaktion ausschließlich Lesevorgänge ausführt, sollten Sie für optimale Effizienz eine schreibgeschützte Transaktion anstelle einer Lese-Schreib-Transaktion verwenden.

Atomarität, Konsistenz, Langlebigkeit

Zusätzlich zur Isolation bietet Spanner die anderen ACID-Eigenschaften:

  • Atomarität: Eine Transaktion gilt als atomar, wenn alle ihre Vorgänge erfolgreich abgeschlossen werden oder keiner von ihnen. Wenn ein Vorgang innerhalb einer Transaktion fehlschlägt, wird die gesamte Transaktion auf ihren ursprünglichen Zustand zurückgesetzt, um die Datenintegrität zu gewährleisten.
  • Konsistenz. Eine Transaktion muss die Integrität der Regeln und Einschränkungen der Datenbank aufrechterhalten. Nach Abschluss einer Transaktion sollte sich die Datenbank in einem gültigen Zustand befinden, der den vordefinierten Regeln entspricht.
  • Langlebigkeit. Nachdem eine Transaktion übernommen wurde, werden ihre Änderungen dauerhaft in der Datenbank gespeichert und bleiben auch bei Systemausfällen, Stromausfällen oder anderen Störungen erhalten.

Serialisierbarkeit und externe Konsistenz

Spanner bietet starke Transaktionsgarantien, einschließlich Serialisierbarkeit und externer Konsistenz. Diese Eigenschaften sorgen dafür, dass die Daten konsistent bleiben und Vorgänge in einer vorhersehbaren Reihenfolge ausgeführt werden, auch in einer verteilten Umgebung.

Die Serialisierbarkeit sorgt dafür, dass alle Transaktionen so erscheinen, als würden sie nacheinander in einer einzigen sequenziellen Reihenfolge ausgeführt, auch wenn sie gleichzeitig verarbeitet werden. Spanner erreicht dies, indem Commit-Zeitstempel für Transaktionen zugewiesen werden, die die Reihenfolge widerspiegeln, in der sie übernommen wurden.

Spanner bietet eine noch stärkere Garantie, die als externe Konsistenz bezeichnet wird. Das bedeutet, dass Transaktionen nicht nur in einer Reihenfolge übernommen werden, die von ihren Commit-Zeitstempeln widergespiegelt wird, sondern dass diese Zeitstempel auch mit der realen Zeit übereinstimmen. So können Sie Commit-Zeitstempel mit der Echtzeit vergleichen und erhalten eine einheitliche und global geordnete Ansicht Ihrer Daten.

Wenn eine Transaktion Txn1 vor einer anderen Transaktion Txn2 in Echtzeit festgeschrieben wird, ist der Commit-Zeitstempel von Txn1 früher als der Commit-Zeitstempel von Txn2.

Dazu ein Beispiel:

Zeitachse, die die Ausführung von zwei Transaktionen anzeigt, die dieselben Daten lesen

In diesem Szenario gilt für den Zeitraum t:

  • Bei Transaktion Txn1 werden Daten A gelesen, ein Schreibvorgang für A wird vorbereitet und dann erfolgreich übergeben.
  • Transaktion Txn2 beginnt nach dem Start von Txn1. Zuerst werden Daten B und dann Daten A gelesen.

Obwohl Txn2 vor Abschluss von Txn1 gestartet wurde, werden die von Txn1 an A vorgenommenen Änderungen in Txn2 berücksichtigt. Das liegt daran, dass Txn2 A liest, nachdem Txn1 seinen Schreibvorgang für A committet hat.

Die Ausführungszeiten von Txn1 und Txn2 können sich zwar überschneiden, aber die Commit-Zeitstempel c1 und c2 erzwingen eine lineare Transaktionsreihenfolge. Das bedeutet:

  • Alle Lese- und Schreibvorgänge in Txn1 scheinen zu einem einzigen Zeitpunkt, c1, erfolgt zu sein.
  • Alle Lese- und Schreibvorgänge in Txn2 scheinen zu einem einzigen Zeitpunkt, c2, erfolgt zu sein.
  • Wichtig ist, dass c1 für übergebene Schreibvorgänge vor c2 liegt, auch wenn die Schreibvorgänge auf verschiedenen Geräten ausgeführt wurden. Wenn Txn2 nur Lesevorgänge ausführt, ist c1 früher oder gleichzeitig mit c2.

Diese starke Reihenfolge bedeutet, dass ein nachfolgender Lesevorgang, der die Auswirkungen von Txn2 beobachtet, auch die Auswirkungen von Txn1 beobachtet. Dieses Attribut ist für alle erfolgreich ausgeführten Transaktionen „true“.

Lese- und Schreibgarantien bei Transaktionsfehlern

Wenn ein Aufruf zum Ausführen einer Transaktion fehlschlägt, hängen Ihre Lese- und Schreibgarantien davon ab, welcher Fehler beim zugrunde liegenden Commit-Aufruf für das Fehlschlagen verantwortlich war.

Zum Beispiel bedeutet das Auftreten eines Fehlers der Art „Zeile nicht gefunden“ oder „Zeile existiert bereits“, dass beim Schreiben der gepufferten Mutationen ein Fehler aufgetreten ist, z. B. dass eine Zeile, die der Client zu aktualisieren versucht, nicht vorhanden ist. In diesem Fall sind die Lesevorgänge garantiert konsistent, die Schreibvorgänge werden nicht angewendet und das Nicht-Vorhandensein einer Zeile ist ebenfalls mit den Lesevorgängen garantiert konsistent.

Lese- und Schreibgarantien bei Transaktionsfehlern

Wenn eine Spanner-Transaktion fehlschlägt, hängen die Garantien, die Sie für Lese- und Schreibvorgänge erhalten, vom jeweiligen Fehler ab, der während des commit-Vorgangs aufgetreten ist.

Eine Fehlermeldung wie „Zeile nicht gefunden“ oder „Zeile existiert bereits“ weist beispielsweise auf ein Problem beim Schreiben gepufferter Mutationen hin. Das kann beispielsweise passieren, wenn eine Zeile, die der Client zu aktualisieren versucht, nicht vorhanden ist. In diesen Szenarien:

  • Lesevorgänge sind konsistent:Alle während der Transaktion gelesenen Daten sind garantiert bis zum Zeitpunkt des Fehlers konsistent.
  • Schreibvorgänge werden nicht angewendet:Die Mutationen, die in der Transaktion versucht wurden, werden nicht in der Datenbank gespeichert.
  • Zeilenkonsistenz:Die Nichtexistenz (oder der vorhandene Status) der Zeile, die den Fehler ausgelöst hat, stimmt mit den Lesevorgängen überein, die innerhalb der Transaktion ausgeführt wurden.

Sie können asynchrone Leseoperationen in Spanner jederzeit abbrechen, ohne andere laufende Operationen innerhalb derselben Transaktion zu beeinträchtigen. Diese Flexibilität ist nützlich, wenn eine Operation auf höherer Ebene abgebrochen wird oder Sie entscheiden, einen Lesevorgang basierend auf den ersten Ergebnissen abzubrechen.

Wenn Sie jedoch das Abbrechen eines Lesevorgangs anfordern, wird dieser nicht unbedingt sofort beendet. Nach einer Stornierungsanfrage kann der Lesevorgang weiterhin:

  • Erfolgreich abgeschlossen:Die Verarbeitung des Lesevorgangs wird möglicherweise abgeschlossen und es werden Ergebnisse zurückgegeben, bevor die Abbrechen-Anfrage wirksam wird.
  • Fehler aus einem anderen Grund:Der Lesevorgang konnte aufgrund eines anderen Fehlers beendet werden, z. B. durch einen Abbruch.
  • Unvollständige Ergebnisse zurückgeben:Der Lesevorgang kann Teilergebnisse zurückgeben, die dann im Rahmen des Transaktions-Commits validiert werden.

Es ist auch wichtig, den Unterschied zu commit-Vorgängen zu beachten: Wenn Sie einen commit-Vorgang abbrechen, wird die gesamte Transaktion abgebrochen, es sei denn, die Transaktion wurde bereits übergeben oder ist aus einem anderen Grund fehlgeschlagen.

Leistung

In diesem Abschnitt werden Probleme beschrieben, die sich auf die Leistung von Lese-/Schreibtransaktionen auswirken.

Sperren der Nebenläufigkeitserkennung

Mit Spanner können mehrere Clients gleichzeitig mit derselben Datenbank interagieren. Um die Datenkonsistenz bei diesen gleichzeitigen Transaktionen aufrechtzuerhalten, verwendet Spanner einen Sperrmechanismus, der sowohl gemeinsame als auch exklusive Sperren nutzt.

Wenn eine Transaktion einen Lesevorgang ausführt, ruft Spanner gemeinsam genutzte Lesesperren für die relevanten Daten ab. Mit diesen freigegebenen Sperren können andere gleichzeitige Lesevorgänge auf dieselben Daten zugreifen. Diese Parallelität wird beibehalten, bis die Transaktion ihre Änderungen committet.

Während der Commit-Phase, wenn Schreibvorgänge angewendet werden, versucht die Transaktion, ihre Sperren auf exklusive Sperren zu aktualisieren. Dazu wird Folgendes getan:

  • Blockiert alle neuen Anfragen für gemeinsam genutzte Lesesperren für die betroffenen Daten.
  • Wartet, bis alle vorhandenen gemeinsam genutzten Lesesperren für diese Daten freigegeben werden.
  • Nachdem alle gemeinsam genutzten Lesesperren aufgehoben wurden, wird eine exklusive Sperre gesetzt, die für die Dauer des Schreibvorgangs den alleinigen Zugriff auf die Daten ermöglicht.

Hinweise zu Sperren:

  • Granularität:In Spanner werden Sperren auf Zeilen- und Spaltenebene angewendet. Wenn die Transaktion T1 eine Sperre für die Spalte A der Zeile albumid enthält, kann die Transaktion T2 weiterhin gleichzeitig in die Spalte B derselben Zeile albumid schreiben, ohne dass es zu Konflikten kommt.
  • Schreibvorgänge ohne Lesevorgänge:Für Schreibvorgänge ohne Lesevorgänge ist in Spanner keine exklusive Sperre erforderlich. Stattdessen wird eine gemeinsam genutzte Sperre verwendet. Das liegt daran, dass die Reihenfolge der Anwendung für Schreibvorgänge ohne Lesevorgänge durch ihre Commit-Zeitstempel bestimmt wird. So können mehrere Writer gleichzeitig ohne Konflikt auf dasselbe Element zugreifen. Eine exklusive Sperre ist nur erforderlich, wenn Ihre Transaktion zuerst die Daten liest, die sie schreiben möchte.
  • Sekundäre Indexe für Zeilensuchen:Wenn Sie Zeilensuchen innerhalb einer Lese-/Schreibtransaktion ausführen, kann die Verwendung sekundärer Indexe die Leistung erheblich verbessern. Wenn Sie sekundäre Indexe verwenden, um die gescannten Zeilen auf einen kleineren Bereich zu beschränken, sperrt Spanner weniger Zeilen in der Tabelle. Dadurch können mehr Zeilen außerhalb dieses bestimmten Bereichs gleichzeitig geändert werden.
  • Exklusiver Zugriff auf externe Ressourcen:Die internen Sperren von Spanner sind für die Datenkonsistenz innerhalb der Spanner-Datenbank selbst konzipiert. Verwenden Sie sie nicht, um den exklusiven Zugriff auf Ressourcen außerhalb von Spanner zu garantieren. Spanner kann Transaktionen aus verschiedenen Gründen abbrechen, z. B. aufgrund interner Systemoptimierungen wie dem Verschieben von Daten zwischen Rechenressourcen. Wenn eine Transaktion wiederholt wird (entweder explizit durch Ihren Anwendungscode oder implizit durch Clientbibliotheken wie den Spanner-JDBC-Treiber), wird nur garantiert, dass die Sperren während des erfolgreichen Commit-Versuchs bestanden haben.
  • Sperrstatistiken:Mit dem Introspektionstool Sperrstatistiken können Sie Sperrkonflikte in Ihrer Datenbank diagnostizieren und untersuchen.

Deadlock-Erkennung

Spanner erkennt, wenn mehrere Transaktionen blockiert werden, und zwingt alle bis auf eine der Transaktionen zum Abbrechen. Betrachten Sie das folgende Szenario: Txn1 enthält eine Sperre für Datensatz A und wartet auf eine Sperre für Datensatz B, während Txn2 eine Sperre für Datensatz B enthält und auf eine Sperre für Datensatz A wartet. Um dieses Problem zu beheben, muss eine der Transaktionen abgebrochen werden, damit die Sperre freigegeben wird und die andere Transaktion fortgesetzt werden kann.

Spanner verwendet den Standardalgorithmus „wound-wait“ für die Deadlock-Erkennung. Spanner verfolgt das Alter jeder Transaktion, die in Konflikt stehende Sperren anfordert. Es ermöglicht älteren Transaktionen, jüngere Transaktionen abzubrechen. Eine ältere Transaktion ist eine Transaktion, deren frühester Lese-, Abfrage- oder Commit-Vorgang früher stattgefunden hat.

Durch die Priorisierung älterer Transaktionen sorgt Spanner dafür, dass jede Transaktion schließlich Sperren erhält, nachdem sie alt genug ist, um eine höhere Priorität zu haben. So kann beispielsweise eine ältere Transaktion, die eine vom Autor freigegebene Sperre benötigt, eine jüngere Transaktion abbrechen, die eine vom Leser freigegebene Sperre enthält.

Verteilte Ausführung

Spanner kann Transaktionen für Daten ausführen, die sich über mehrere Server erstrecken. Diese Funktion ist jedoch mit Leistungskosten verbunden, die mit denen von Transaktionen auf nur einem Server zu vergleichen sind.

Welche Arten von Transaktionen können verteilt sein? Spanner kann die Verantwortung für Datenbankzeilen auf viele Server verteilen. Normalerweise werden eine Zeile und die entsprechenden Zeilen in verschränkten Tabellen vom selben Server verarbeitet, ebenso wie zwei Zeilen in derselben Tabelle mit ähnlichen Schlüsseln. Spanner kann Transaktionen mit Zeilen über mehrere Server ausführen. Allerdings gilt als Faustregel, dass Transaktionen, die viele Zeilen an einem Standort betreffen, schneller und günstiger sind als Transaktionen, die viele in der Datenbank oder in einer großen Tabelle verteilte Zeilen betreffen.

Zu den effizientesten Transaktionen in Spanner gehören nur Lese- und Schreibvorgänge, die atomar angewendet werden sollen. Transaktionen sind am schnellsten, wenn alle Lese- und Schreibzugriffe auf Daten im selben Teil des Schlüsselbereichs erfolgen.

Schreibgeschützte Transaktionen

Zusätzlich zu sperrenden Lese-Schreib-Transaktionen bietet Spanner schreibgeschützte Transaktionen.

Verwenden Sie eine schreibgeschützte Transaktion, wenn Sie mehr als einen Lesevorgang mit demselben Zeitstempel ausführen müssen. Wenn Sie Ihren Lesevorgang mithilfe einer einzelnen Lesemethode von Spanner ausdrücken können, sollten Sie stattdessen diese einzelne Lesemethode verwenden. Die Leistung bei der Verwendung eines solchen einzelnen Leseaufrufs sollte mit der Leistung eines einzelnen Lesevorgangs vergleichbar sein, der in einer schreibgeschützten Transaktion ausgeführt wird.

Wenn Sie eine große Datenmenge lesen, sollten Sie Partitionen verwenden, um die Daten parallel zu lesen.

Da schreibgeschützte Transaktionen keine Schreibvorgänge ausführen, haben sie keine Sperren und blockieren andere Transaktionen nicht. Schreibgeschützte Transaktionen erkennen ein konsistentes Präfix des Commit-Verlaufs der Transaktion, damit Ihre Anwendung immer konsistente Daten erhält.

Schnittstelle

Spanner bietet eine Schnittstelle zum Ausführen eines Arbeitsablaufs im Rahmen einer schreibgeschützten Transaktion mit Wiederholungen für Transaktionsabbrüche.

Beispiel

Im folgenden Beispiel wird gezeigt, wie eine schreibgeschützte Transaktion verwendet werden kann, um konsistente Daten für zwei Lesevorgänge zum selben Zeitstempel zu erhalten:

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

In diesem Abschnitt wird die Semantik für schreibgeschützte Transaktionen beschrieben.

Schreibgeschützte Snapshot-Transaktionen

Wenn eine schreibgeschützte Transaktion in Spanner ausgeführt wird, werden alle Lesevorgänge zu einem einzigen logischen Zeitpunkt ausgeführt. Das bedeutet, dass sowohl die schreibgeschützte Transaktion als auch alle anderen gleichzeitigen Leser und Autoren zu diesem bestimmten Zeitpunkt einen konsistenten Snapshot der Datenbank sehen.

Diese schreibgeschützten Snapshot-Transaktionen bieten im Vergleich zu sperrenden Lese-Schreib-Transaktionen einen einfacheren Ansatz für konsistente Lesevorgänge. Das kann folgende Gründe haben:

  • Keine Sperren:Schreibgeschützte Transaktionen erhalten keine Sperren. Stattdessen wird ein Spanner-Zeitstempel ausgewählt und alle Lesevorgänge werden für diese historische Version der Daten ausgeführt. Da sie keine Sperren verwenden, blockieren sie keine gleichzeitigen Lese- und Schreibtransaktionen.
  • Keine Abbrüche:Diese Transaktionen werden nie abgebrochen. Sie können fehlschlagen, wenn der ausgewählte Lesetime-Stempel bereinigt wird. Die standardmäßige Richtlinie für die automatische Speicherbereinigung von Spanner ist in der Regel jedoch großzügig genug, sodass die meisten Anwendungen dieses Problem nicht haben.
  • Keine Commits oder Rollbacks:Für schreibgeschützte Transaktionen sind keine Aufrufe von sessions.commit oder sessions.rollback erforderlich und sie werden sogar verhindert.

Zum Ausführen einer Snapshot-Transaktion definiert der Client eine Zeitstempelgrenze, die Spanner anweist, wie ein Lesezeitstempel ausgewählt werden soll. Es gibt folgende Arten von Zeitstempelgrenzen:

  • Starke Lesevorgänge:Bei diesen Lesevorgängen wird garantiert, dass Sie die Auswirkungen aller Transaktionen sehen, die vor Beginn des Lesevorgangs durchgeführt wurden. Alle Zeilen in einem einzelnen Lesevorgang sind konsistent. Starke Lesevorgänge sind jedoch nicht wiederholbar. Sie geben zwar einen Zeitstempel zurück, aber ein erneuter Lesevorgang mit demselben Zeitstempel ist wiederholbar. Zwei aufeinanderfolgende starke schreibgeschützte Transaktionen können aufgrund gleichzeitiger Schreibvorgänge unterschiedliche Ergebnisse liefern. Für Abfragen für Änderungsstreams muss diese Grenze verwendet werden. Weitere Informationen finden Sie unter TransactionOptions.ReadOnly.strong.
  • Exakte Veralterung:Bei dieser Option werden Lesevorgänge zu einem von Ihnen angegebenen Zeitstempel ausgeführt, entweder als absoluter Zeitstempel oder als Veralterungsdauer relativ zur aktuellen Zeit. So wird sichergestellt, dass Sie bis zu diesem Zeitstempel ein konsistentes Präfix des globalen Transaktionsverlaufs sehen, und es werden in Konflikt stehende Transaktionen blockiert, die mit einem Zeitstempel kleiner oder gleich dem Lesezeitstempel committen könnten. Dieser Modus ist etwas schneller als die Modi mit begrenzter Veraltung, gibt aber möglicherweise ältere Daten zurück. Weitere Informationen finden Sie unter TransactionOptions.ReadOnly.read_timestamp und TransactionOptions.ReadOnly.exact_staleness.
  • Begrenzte Veralterung:Spanner wählt den neuesten Zeitstempel innerhalb eines nutzerdefinierten Veralterungslimits aus, sodass die Ausführung am nächstgelegenen verfügbaren Replikat ohne Blockierung möglich ist. Alle zurückgegebenen Zeilen sind konsistent. Wie bei starken Lesevorgängen ist die begrenzte Veraltung nicht wiederholbar, da verschiedene Lesevorgänge auch bei derselben Grenze zu unterschiedlichen Zeitstempeln ausgeführt werden können. Diese Lesevorgänge werden in zwei Phasen ausgeführt (Zeitstempel-Aushandlung, dann Lesen) und sind in der Regel etwas langsamer als exakt veraltete Lesevorgänge. Sie geben jedoch oft neuere Ergebnisse zurück und werden mit größerer Wahrscheinlichkeit auf einem lokalen Replikat ausgeführt. Dieser Modus ist nur für schreibgeschützte Einmaltransaktionen verfügbar, da für die Zeitstempelvereinbarung im Voraus bekannt sein muss, welche Zeilen gelesen werden. Weitere Informationen finden Sie unter TransactionOptions.ReadOnly.max_staleness und TransactionOptions.ReadOnly.min_read_timestamp.

Partitionierte DML-Transaktionen

Mit partitionierten DML-Anweisungen können Sie umfangreiche Anweisungen des Typs UPDATE und DELETE ausführen, ohne die Transaktionslimits zu überschreiten oder eine ganze Tabelle zu sperren. Spanner erreicht dies, indem der Schlüsselbereich partitioniert und die DML-Anweisungen in jeder Partition in einer separaten Lese-/Schreibtransaktion ausgeführt werden.

Wenn Sie nicht partitionierte DML verwenden möchten, führen Sie Anweisungen in Lese-/Schreibtransaktionen aus, die Sie explizit in Ihrem Code erstellen. Weitere Informationen finden Sie unter DML verwenden.

Schnittstelle

Spanner bietet die Schnittstelle TransactionOptions.partitionedDml zum Ausführen einer einzelnen partitionierten DML-Anweisung.

Beispiele

Mit den folgenden Codebeispielen wird die Spalte MarketingBudget der Tabelle Albums aktualisiert.

C++

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Funktion ExecutePartitionedDml().

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#

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode ExecutePartitionedUpdateAsync().


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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode PartitionedUpdate().


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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode executePartitionedUpdate().

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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode runPartitionedUpdate().

// 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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode executePartitionedUpdate().

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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode execute_partitioned_dml().

# 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

Zum Ausführen einer partitionierten DML-Anweisung verwenden Sie die Methode execute_partitioned_update().

# 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."

Im folgenden Codebeispiel werden Zeilen aus der Tabelle Singers anhand der Spalte SingerId gelöscht.

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

In diesem Abschnitt wird die Semantik für partitionierte DML beschrieben.

Ausführung partitionierter DML-Anweisungen

Sie können jeweils nur eine partitionierte DML-Anweisung ausführen, unabhängig davon, ob Sie eine Clientbibliotheksmethode oder die Google Cloud CLI verwenden.

Partitionierte Transaktionen unterstützen keine Commits oder Rollbacks. Spanner führt die DML-Anweisung sofort aus und wendet sie an. Wenn Sie den Vorgang abbrechen oder der Vorgang fehlschlägt, bricht Spanner alle ausgeführten Partitionen ab und startet keine verbleibenden Partitionen. Cloud Spanner führt für bereits ausgeführte Partitionen jedoch kein Rollback aus.

Strategie zum Abrufen von Sperren für partitionierte DML

Um Konflikte aufgrund von Sperren zu reduzieren, werden bei partitionierter DML nur Lesesperren für Zeilen übernommen, die der WHERE-Klausel entsprechen. Kleinere, unabhängige Transaktionen, die für jede Partition verwendet werden, halten Sperren auch kürzer.

Transaktionslimits für Sitzungen

In jeder Sitzung in Spanner kann jeweils nur eine aktive Transaktion vorhanden sein. Dazu gehören eigenständige Lesevorgänge und Abfragen, die intern eine Transaktion verwenden und auf dieses Limit angerechnet werden. Nach Abschluss einer Transaktion kann die Sitzung sofort für die nächste Transaktion wiederverwendet werden. Es ist nicht erforderlich, für jede Transaktion eine neue Sitzung zu erstellen.

Alte Zeitstempel für Lesezugriffe und automatische Speicherbereinigung von Versionen

In Spanner wird die automatische Speicherbereinigung für Versionen durchgeführt, um gelöschte oder überschriebene Daten zu erfassen und Speicherplatz zurückzugewinnen. Standardmäßig werden Daten, die älter als eine Stunde sind, zurückgefordert. Spanner kann keine Lesevorgänge mit Zeitstempeln ausführen, die älter als das konfigurierte VERSION_RETENTION_PERIOD sind. Der Standardwert ist eine Stunde, kann aber auf bis zu eine Woche konfiguriert werden. Wenn Lesevorgänge während der Ausführung zu alt werden, schlagen sie fehl und geben den Fehler FAILED_PRECONDITION zurück.

Abfragen für Änderungsstreams

Ein Änderungsstream ist ein Schemaobjekt, das Sie so konfigurieren können, dass Datenänderungen in einer gesamten Datenbank, in bestimmten Tabellen oder in einer definierten Gruppe von Spalten in einer Datenbank überwacht werden.

Wenn Sie einen Änderungsstream erstellen, definiert Spanner eine entsprechende SQL-Tabellenwertfunktion (Table-Value-Funktion, TVF). Mit dieser TVF können Sie die Änderungsdatensätze im zugehörigen Änderungsstream mit der Methode sessions.executeStreamingSql abfragen. Der Name der TVF wird aus dem Namen des Änderungsstreams generiert und beginnt immer mit READ_.

Alle Abfragen für Change Stream-TVFs müssen mit der sessions.executeStreamingSql-API in einer einmaligen schreibgeschützten Transaktion mit einem starken schreibgeschützten timestamp_bound ausgeführt werden. Mit der TVF für Änderungsstreams können Sie start_timestamp und end_timestamp für den Zeitraum angeben. Alle Änderungsdatensätze innerhalb des Aufbewahrungszeitraums sind über diese starke schreibgeschützte timestamp_bound zugänglich. Alle anderen TransactionOptions sind für Change Stream-Abfragen ungültig.

Wenn TransactionOptions.read_only.return_read_timestamp auf true gesetzt ist, gibt die Transaction-Nachricht, die die Transaktion beschreibt, den Sonderwert 2^63 - 2 anstelle eines gültigen Lesezeitstempels zurück. Sie sollten diesen Sonderwert verwerfen und nicht für nachfolgende Anfragen verwenden.

Weitere Informationen finden Sie unter Workflow für Change Streams-Abfragen.

Inaktive Transaktionen

Eine Transaktion gilt als inaktiv, wenn keine ausstehenden Lese- oder SQL-Abfragen vorhanden sind und in den letzten 10 Sekunden keine gestartet wurde. Spanner kann inaktive Transaktionen abbrechen, um zu verhindern, dass sie Sperren unbegrenzt lange halten. Wenn eine inaktive Transaktion abgebrochen wird, schlägt der Commit fehl und es wird ein ABORTED-Fehler zurückgegeben. Wenn Sie in der Transaktion regelmäßig eine kleine Abfrage wie SELECT 1 ausführen, kann verhindert werden, dass sie inaktiv wird.