Transaktionsübersicht

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

Eine Transaktion in Spanner ist eine Gruppe von Lese- und Schreibvorgängen. Alle Vorgänge in einer Transaktion sind atomar. Das bedeutet, dass sie entweder alle erfolgreich sind oder alle fehlschlagen.

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 Muster der Dateninteraktion konzipiert sind:

  • Lese-/Schreibvorgänge:Diese Transaktionen werden für Lese- und Schreibvorgänge verwendet, gefolgt von einem Commit. Sie rufen möglicherweise Sperren ab. Wenn sie fehlschlagen, sind Wiederholungsversuche erforderlich. 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-/Schreibtransaktionen ist für sie kein Commit-Vorgang oder keine Sperren erforderlich. Sie werden jedoch möglicherweise pausiert, 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 die Ausführung von DML-Anweisungen im großen Maßstab optimiert, unterliegt jedoch Einschränkungen, um sicherzustellen, dass die Anweisung idempotent und partitionierbar ist, sodass sie unabhängig von anderen Partitionen ausgeführt werden kann. Wenn Sie viele 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

Eine Lese-/Schreibtransaktion besteht aus null oder mehr Lese- oder Abfrageanweisungen, gefolgt von einer Commit-Anfrage. Der Client kann jederzeit vor der Commit-Anfrage eine Rollback-Anfrage senden, um die Transaktion abzubrechen.

Serialisierbare Isolation

Bei der standardmäßigen serialisierbaren Isolationsebene werden Daten in Lese-Schreib-Transaktionen atomar gelesen, geändert und geschrieben. Diese Art von Transaktion ist extern konsistent.

Wenn Sie Lese-/Schreibtransaktionen verwenden, empfehlen wir, die Zeit zu minimieren, in der eine Transaktion aktiv ist. Kürzere Transaktionsdauern führen dazu, dass Sperren weniger lange gehalten werden. Dadurch steigt die Wahrscheinlichkeit eines erfolgreichen Commits und die Konflikte werden reduziert. Das liegt daran, dass lange gehaltene Sperren zu Deadlocks und abgebrochenen Transaktionen führen können. Spanner versucht, Lesesperren so lange aktiv zu halten, wie die Transaktion weiterhin Lesevorgänge ausführt und die Transaktion nicht durch Commit oder Rollback beendet wurde. Wenn der Client über einen längeren Zeitraum inaktiv bleibt, kann Spanner die Sperren der Transaktion aufheben und die Transaktion abbrechen.

Wenn Sie einen Schreibvorgang ausführen möchten, der von einem oder mehreren Lesevorgängen abhängt, verwenden Sie eine 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 das Guthaben vor der Verdopplung und Aktualisierung gelesen.
  • Wenn Schreibvorgänge von Lesevorgängen abhängen, führen Sie beide in derselben Lese-Schreib-Transaktion aus, auch wenn die Schreibvorgänge nicht ausgeführt werden. Wenn Sie beispielsweise 200 $von Konto A zu Konto B überweisen möchten, aber nur, wenn der 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 die einzelne Lesemethode oder eine schreibgeschützte Transaktion. Im Gegensatz zu Lese-Schreib-Transaktionen werden bei einzelnen Lesevorgängen keine Sperren abgerufen.

Isolation durch wiederholbare Lesevorgänge

In Spanner wird die Isolation für wiederholtes Lesen mithilfe einer Technik namens Snapshot-Isolation implementiert. Die Isolation vom Typ „Repeatable Read“ sorgt dafür, dass alle Lesevorgänge innerhalb einer Transaktion mit der Datenbank konsistent sind, wie sie zu Beginn der Transaktion vorhanden war. Außerdem wird dadurch sichergestellt, dass gleichzeitige Schreibvorgänge für dieselben Daten nur dann erfolgreich sind, wenn keine Konflikte auftreten.

Beim standardmäßigen optimistischen Sperrverfahren werden keine Sperren bis zum Commit-Zeitpunkt abgerufen, wenn Daten geschrieben werden müssen. Wenn ein Konflikt mit den geschriebenen Daten oder aufgrund von vorübergehenden Ereignissen in Spanner wie einem Serverneustart auftritt, kann Spanner Transaktionen trotzdem abbrechen. Da Lesevorgänge in Lese-Schreib-Transaktionen keine Sperren in der Isolation mit wiederholbarem Lesevorgang erhalten, gibt es keinen Unterschied zwischen der Ausführung schreibgeschützter Vorgänge in einer schreibgeschützten Transaktion oder einer Lese-Schreib-Transaktion.

In den folgenden Szenarien sollten Sie Lese-/Schreibtransaktionen in der Isolationsebene „Wiederholbares Lesen“ verwenden:

  • Die Arbeitslast ist leselastig und es gibt nur wenige Schreibkonflikte.
  • In der Anwendung treten Leistungsengpässe aufgrund von Verzögerungen durch Sperrenkonflikte und Transaktionsabbrüche auf, die dadurch verursacht werden, dass ältere Transaktionen mit höherer Priorität neuere Transaktionen mit niedrigerer Priorität abbrechen, um potenzielle Deadlocks zu verhindern (Wound-Wait).
  • Die Anwendung erfordert nicht die strengeren Garantien, die von der Isolationsebene „Serializable“ bereitgestellt werden.

Wenn Sie einen Schreibvorgang ausführen, der von einem oder mehreren Lesevorgängen abhängt, ist bei der Isolation mit wiederholbarem Lesen eine Schreibabweichung möglich. Schreibabweichungen entstehen durch eine bestimmte Art von gleichzeitiger Aktualisierung, bei der jede Aktualisierung unabhängig akzeptiert wird, ihre kombinierte Wirkung jedoch die Datenintegrität der Anwendung verletzt. Führen Sie daher Lesevorgänge, die Teil des kritischen Abschnitts einer Transaktion sind, entweder mit einer FOR UPDATE-Klausel oder einem lock_scanned_ranges=exclusive-Hinweis aus, um Schreibabweichungen zu vermeiden. Weitere Informationen finden Sie unter Lese-/Schreibkonflikte und Richtigkeit und im Beispiel unter Lese-/Schreibsemantik.

Schnittstelle

Die Spanner-Clientbibliotheken bieten eine Schnittstelle zum Ausführen eines Arbeitsablaufs im Rahmen einer Lese-/Schreibtransaktion mit Wiederholungen für Transaktionsabbrüche. Eine Transaktion muss möglicherweise mehrmals wiederholt werden, 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. Seltener können vorübergehende Ereignisse in Spanner auch zu Transaktionsabbrüchen führen.

Alle Lese-/Schreibtransaktionen bieten die ACID-Attribute von relationalen Datenbanken. Da Transaktionen atomar sind, hat eine abgebrochene Transaktion keine Auswirkungen auf die Datenbank. Spanner-Clientbibliotheken wiederholen solche Transaktionen automatisch. Wenn Sie die Clientbibliotheken jedoch nicht verwenden, wiederholen Sie die 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. Außerdem enthalten Spanner-Clienttreiber eine interne Logik für Transaktionswiederholungen, die vorübergehende Fehler durch erneutes Ausführen der Transaktion maskiert.

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 übertragen 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, 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.

Die folgenden Clientbibliotheksbeispiele zeigen, wie eine Lese-/Schreibtransaktion mit der standardmäßigen serialisierbaren Isolationsstufe ausgeführt wird:

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;

// Note: the `runTransaction()` method is non blocking and returns "void".
// For sequential execution of the transaction use `runTransactionAsync()` method which returns a promise.
// For example: await database.runTransactionAsync(async (err, transaction) => { ... })
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"

Beispiele für die Ausführung einer Lese-/Schreibtransaktion mit der Isolationsebene „Wiederholbarer Lesevorgang“ finden Sie unter Isolationsebene „Wiederholbarer Lesevorgang“ verwenden.

Semantik

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

Eigenschaften

Die serialisierbare Isolation ist die Standardisolationsebene in Spanner. Bei serialisierbarer Isolation bietet Spanner Kunden die besten Garantien der Gleichzeitigkeitserkennung für Transaktionen, die externe Konsistenz. Bei einer Lese-Schreib-Transaktion wird eine Reihe von Lese- und Schreibvorgängen atomar ausgeführt. Schreibvorgänge können ohne Blockierung durch schreibgeschützte Transaktionen erfolgen. Der Zeitstempel, zu dem Lese-/Schreibtransaktionen ausgeführt werden, entspricht der verstrichenen Zeit. Die Serialisierungsreihenfolge entspricht dieser Zeitstempelreihenfolge.

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.

Sie können Ihre Lese-Schreib-Transaktionen auch mit der Isolationsstufe „Repeatable Read“ ausführen. Die Isolationsebene „Wiederholbares Lesen“ sorgt dafür, dass bei allen Lesevorgängen innerhalb einer Transaktion ein konsistenter starker Snapshot der Datenbank angezeigt wird, wie er zu Beginn der Transaktion vorhanden war. Weitere Informationen finden Sie unter Isolation vom Typ „Repeatable Read“.

Lese-/Schreibtransaktionen mit serialisierbarer Isolation

Nachdem eine Transaktion, die eine Reihe von Lese- und Schreibvorgängen in der standardmäßigen serialisierbaren Isolation enthält, erfolgreich committet wurde, gilt 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 mit Commit gespeichert.
  • 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.

Lese-/Schreibtransaktionen mit der Isolationsebene „Repeatable Read“

Nachdem eine Transaktion mit der Isolationsstufe „Wiederholbarer Lesevorgang“ erfolgreich mit Commit ausgeführt wurde, gilt Folgendes:

  • Die Transaktion gibt Werte zurück, die einen konsistenten Snapshot der Datenbank widerspiegeln. Der Snapshot wird in der Regel während des ersten Transaktionsvorgangs erstellt, der möglicherweise nicht mit dem Commit-Zeitstempel übereinstimmt.
  • Da „Repeatable Read“ mit Snapshot-Isolation implementiert wird, werden alle Schreibvorgänge der Transaktion nur dann zum Commit-Zeitstempel der Transaktion committet, wenn sich die Schreibmenge zwischen dem Zeitstempel des Transaktions-Snapshots und dem Commit-Zeitstempel nicht geändert hat.
  • Andere Transaktionen sehen die Schreibvorgänge erst nach dem Commit der Transaktion.

Isolation für Lese-/Schreibtransaktionen mit Nur-Lese-Vorgängen

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 bei einer Transaktion ausschließlich Lesevorgänge ausgeführt werden, sollten Sie für optimale Effizienz eine schreibgeschützte Transaktion anstelle einer Lese-Schreib-Transaktion verwenden, insbesondere bei Verwendung der serialisierbaren Isolation.

Unterschied zwischen Serialisierbarkeit und externer Konsistenz und wiederholbarem Lesen

Standardmäßig bietet Spanner 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 konsistente 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:

  • Transaktion Txn1 liest Daten A, führt einen Schreibvorgang in A aus und wird 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 seine Schreibvorgänge in A übernommen 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 Computern 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“.

Wenn Sie dagegen die Isolationsstufe „Wiederholbares Lesen“ verwenden, tritt für dieselben Transaktionen das folgende Szenario ein:

  • Txn1 beginnt mit dem Lesen von Daten A und erstellt einen eigenen Snapshot der Datenbank zu diesem Zeitpunkt.
  • Txn2 beginnt dann mit dem Lesen von Daten B und erstellt einen eigenen Snapshot.
  • Als Nächstes ändert Txn1 die Daten A und führt die Änderungen erfolgreich aus.
  • Txn2 Versuche, Daten zu lesen A. Da Txn2 auf einem früheren Snapshot basiert, wird die Aktualisierung Txn1, die gerade für A vorgenommen wurde, nicht berücksichtigt. Txn2 liest den älteren Wert.
  • Mit Txn2 werden Daten B geändert und übernommen.

In diesem Szenario wird jede Transaktion für einen eigenen konsistenten Snapshot der Datenbank ausgeführt, der zum Zeitpunkt des Transaktionsbeginns erstellt wurde. Diese Sequenz kann zu einer Anomalie bei der Schreibabweichung führen, wenn der Schreibvorgang für B durch Txn2 logisch vom Wert abhängig war, der aus A gelesen wurde. Im Wesentlichen hat Txn2 seine Updates auf Grundlage veralteter Informationen vorgenommen und der nachfolgende Schreibvorgang verstößt möglicherweise gegen eine Invariante auf Anwendungsebene. Um dieses Szenario zu vermeiden, sollten Sie entweder SELECT...FOR UPDATE für die Isolation von wiederholbaren Lesevorgängen verwenden oder Prüfeinschränkungen in Ihrem Schema erstellen.

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.

Spanner führt die Vorgänge einer Transaktion möglicherweise intern mehrmals aus. Wenn ein Ausführungsversuch fehlschlägt, gibt der zurückgegebene Fehler die aufgetretenen Bedingungen an und damit, welche Garantien Sie erhalten. Wenn Spanner Ihre Transaktion jedoch wiederholt, können alle Nebeneffekte der zugehörigen Vorgänge (z. B. Änderungen an externen Systemen oder an einem Systemstatus außerhalb einer Spanner-Datenbank) mehrmals auftreten.

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:Das Nichtvorhandensein (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 einen Lesevorgang basierend auf den ersten Ergebnissen abbrechen möchten.

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 Stornierung 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.

Durch das Abbrechen eines Commit-Vorgangs wird die gesamte Transaktion abgebrochen, es sei denn, die Transaktion wurde bereits übergeben oder ist aus einem anderen Grund fehlgeschlagen.

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 wahren. Nach Abschluss einer Transaktion sollte sich die Datenbank in einem gültigen Zustand befinden, der den vordefinierten Regeln entspricht.
  • Beständigkeit: Nachdem eine Transaktion committet wurde, werden ihre Änderungen dauerhaft in der Datenbank gespeichert und bleiben auch bei Systemausfällen, Stromausfällen oder anderen Störungen erhalten.

Leistung

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

Sperren der Gleichzeitigkeitserkennung

Standardmäßig erlaubt Spanner mehreren Clients, gleichzeitig mit derselben Datenbank zu interagieren, und zwar mit der standardmäßigen serialisierbaren Isolationsebene. Damit die Datenkonsistenz bei diesen gleichzeitigen Transaktionen gewährleistet wird, verwendet Spanner einen Sperrmechanismus, der sowohl gemeinsame als auch exklusive Sperren nutzt. Diese Lesesperren werden nur für serialisierbare Transaktionen, nicht aber für Transaktionen mit wiederholbarer Leseisolation abgerufen.

Wenn eine serialisierbare Transaktion einen Lesevorgang ausführt, ruft Spanner gemeinsame Lesesperren für die relevanten Daten ab. Diese gemeinsamen Sperren ermöglichen es anderen gleichzeitigen Lesevorgängen, auf dieselben Daten zuzugreifen. Diese Parallelität wird beibehalten, bis die Transaktion ihre Änderungen committet.

Bei der serialisierbaren Isolation versucht die Transaktion während der Commit-Phase, wenn Schreibvorgänge angewendet werden, ihre Sperren auf exklusive Sperren zu aktualisieren. Dazu geht Spanner so vor:

  • 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.

Beim Committen einer Transaktion mit der Isolationsebene „Repeatable Read“ werden exklusive Sperren für die geschriebenen Daten abgerufen. Die Transaktion muss möglicherweise auf Sperren warten, wenn eine gleichzeitige Transaktion auch Schreibvorgänge für dieselben Daten committet.

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 ein Konflikt auftritt.
  • Schreibvorgänge ohne Lesevorgänge:

    • Wenn die Transaktion keine Lesevorgänge enthält, ist für Schreibvorgänge ohne Lesevorgänge möglicherweise keine exklusive Sperre erforderlich. Stattdessen wird möglicherweise 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 am selben Element arbeiten. Eine exklusive Sperre ist nur erforderlich, wenn Ihre Transaktion zuerst die Daten liest, die sie schreiben möchte.
    • Bei der Isolationsebene „Repeatable Read“ erhalten Transaktionen in der Regel zum Zeitpunkt des Commits exklusive Sperren für geschriebene Zellen.
  • Sekundäre Indexe für Zeilensuchen:Bei der serialisierbaren Isolation kann die Verwendung sekundärer Indexe die Leistung erheblich verbessern, wenn Lesevorgänge innerhalb einer Lese-/Schreibtransaktion ausgeführt werden. Wenn Sie sekundäre Indexe verwenden, um die Anzahl der durchsuchten 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 Leistungseinbußen 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 einen einfacheren Ansatz für konsistente Lesevorgänge als sperrende Lese-Schreib-Transaktionen. 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 stamp 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:Mit dieser Option werden Lesevorgänge mit 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 höherer Wahrscheinlichkeit auf einem lokalen Replikat ausgeführt. Dieser Modus ist nur für schreibgeschützte Einmaltransaktionen verfügbar, da für die Zeitstempelvereinbarung vorab 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 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 weniger lange aufrecht.

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 von Änderungsstreams

Ein Änderungsstream ist ein Schemaobjekt, das Sie so konfigurieren können, dass Datenänderungen in einer gesamten Datenbank, in bestimmten Tabellen oder in einem definierten Satz 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 Tabellenwertfunktion 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, einen speziellen Wert von 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.

Nächste Schritte