Panoramica delle transazioni

Questa pagina descrive le transazioni in Spanner e introduce le interfacce delle transazioni DML partizionate, di lettura-scrittura e di sola lettura di Spanner.

Una transazione in Spanner è un insieme di letture e scritture che vengono eseguite in modo atomico in un unico punto logico nel tempo su colonne, righe e tabelle di un database.

Una sessione viene utilizzata per eseguire transazioni in un database Spanner. Una sessione rappresenta un canale di comunicazione logico con il servizio di database Spanner. Le sessioni possono eseguire una o più transazioni alla volta. Per ulteriori informazioni, consulta la sezione Sessioni.

Tipi di transazioni

Spanner supporta i seguenti tipi di transazione, ognuno progettato per pattern di interazione con i dati specifici:

  • Lettura/scrittura:queste transazioni utilizzano il blocco pessimistico e, se necessario, un commit in due fasi. Potrebbero non riuscire e richiedere dei tentativi. Sebbene siano limitati a un singolo database, possono modificare i dati in più tabelle all'interno di quel database.

  • Sola lettura:queste transazioni garantiscono la coerenza dei dati in più operazioni di lettura, ma non consentono modifiche ai dati. Vengono eseguiti in corrispondenza di un timestamp determinato dal sistema per garantire la coerenza oppure in corrispondenza di un timestamp passato configurato dall'utente. A differenza delle transazioni di lettura/scrittura, non richiedono un'operazione di commit o blocchi, anche se potrebbero essere messe in pausa per attendere il completamento delle operazioni di scrittura in corso.

  • DML partizionato:questo tipo di transazione esegue istruzioni DML come operazioni DML partizionate. È ottimizzato per aggiornamenti ed eliminazioni di dati su larga scala, come la pulizia dei dati o l'inserimento bulk di dati. Per numerose operazioni di scrittura che non richiedono una transazione atomica, valuta l'utilizzo di operazioni di scrittura in batch. Per maggiori dettagli, consulta Modificare i dati utilizzando le scritture batch.

Transazioni di lettura/scrittura

Utilizza le transazioni di lettura/scrittura con blocco per leggere, modificare e scrivere dati in modo atomico in qualsiasi punto di un database. Questo tipo di transazione è coerente esternamente.

Ridurre al minimo il tempo in cui una transazione è attiva. Durate delle transazioni più brevi aumentano la probabilità di un commit riuscito e riducono la contesa. Spanner tenta di mantenere attivi i blocchi di lettura finché la transazione continua a eseguire letture e non è terminata tramite operazioni sessions.commit o sessions.rollback. Se il client rimane inattivo per lunghi periodi, Spanner potrebbe rilasciare i blocchi della transazione e interromperla.

A livello concettuale, una transazione di lettura/scrittura è costituita da zero o più letture o istruzioni SQL seguite da sessions.commit. In qualsiasi momento prima del giorno sessions.commit, il cliente può inviare una richiesta sessions.rollback per interrompere la transazione.

Per eseguire un'operazione di scrittura che dipende da una o più operazioni di lettura, utilizza una transazione di lettura/scrittura con blocco:

  • Se devi eseguire una o più operazioni di scrittura in modo atomico, esegui queste scritture all'interno della stessa transazione di lettura/scrittura. Ad esempio, se trasferisci 200 $ dall'account A all'account B, esegui entrambe le operazioni di scrittura (riduzione dell'account A di 200 $e aumento dell'account B di 200 $) e le letture dei saldi iniziali degli account all'interno della stessa transazione.
  • Se vuoi raddoppiare il saldo dell'account A, esegui le operazioni di lettura e scrittura all'interno della stessa transazione. In questo modo, il sistema legge il saldo prima di raddoppiarlo e poi aggiornarlo.
  • Se potresti eseguire una o più operazioni di scrittura che dipendono dai risultati di una o più operazioni di lettura, esegui queste operazioni di scrittura e lettura nella stessa transazione di lettura/scrittura, anche se le operazioni di scrittura non vengono eseguite. Ad esempio, se vuoi trasferire 200 $dal conto A al conto B solo se il saldo attuale del conto A è superiore a 500 $, includi la lettura del saldo del conto A e le operazioni di scrittura condizionali nella stessa transazione, anche se il trasferimento non avviene.

Per eseguire operazioni di lettura, utilizza un singolo metodo di lettura o una transazione di sola lettura:

  • Se esegui solo operazioni di lettura e puoi esprimere l'operazione di lettura utilizzando un singolo metodo di lettura, utilizza questo singolo metodo di lettura o una transazione di sola lettura. A differenza delle transazioni di lettura/scrittura, le singole letture non acquisiscono blocchi.

Interfaccia

Le librerie client Spanner forniscono un'interfaccia per l'esecuzione di un corpo di lavoro all'interno di una transazione di lettura/scrittura, con nuovi tentativi per gli interruzioni della transazione. Una transazione Spanner potrebbe richiedere più tentativi prima di essere eseguita.

L'interruzione delle transazioni può essere causata da diverse situazioni. Ad esempio, se due transazioni tentano di modificare i dati contemporaneamente, potrebbe verificarsi un deadlock. In questi casi, Spanner interrompe una transazione per consentire all'altra di procedere. Meno frequentemente, anche gli eventi temporanei all'interno di Spanner possono causare interruzioni delle transazioni.

Poiché le transazioni sono atomiche, una transazione interrotta non influisce sul database. Riprova la transazione nella stessa sessione per migliorare i tassi di successo. Ogni nuovo tentativo che genera un errore ABORTED aumenta la priorità di blocco della transazione.

Quando utilizzi una transazione in una libreria client Spanner, definisci il corpo della transazione come oggetto funzione. Questa funzione incapsula le letture e le scritture eseguite su una o più tabelle del database. La libreria client Spanner esegue questa funzione ripetutamente finché la transazione non viene eseguita correttamente o non si verifica un errore che non può essere riprovato.

Esempio

Supponiamo di avere una colonna MarketingBudget nella tabella Albums:

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

Il tuo reparto marketing ti chiede di trasferire 200.000 € dal budget di Albums (2, 2) a Albums (1, 1), ma solo se i fondi sono disponibili nel budget dell'album. Per questa operazione devi utilizzare una transazione di lettura/scrittura con blocco, perché la transazione potrebbe eseguire scritture a seconda del risultato di una lettura.

Di seguito viene mostrato come eseguire una transazione di lettura/scrittura:

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"

Semantica

Questa sezione descrive la semantica delle transazioni di lettura/scrittura in Spanner.

Proprietà

Una transazione di lettura-scrittura in Spanner esegue un insieme di letture e scritture in modo atomico. Il timestamp in cui vengono eseguite le transazioni di lettura/scrittura corrisponde al tempo trascorso. L'ordine di serializzazione corrisponde a questo ordine di timestamp.

Le transazioni di lettura/scrittura forniscono le proprietà ACID dei database relazionali. Le transazioni di lettura/scrittura di Spanner offrono proprietà più solide rispetto alle transazioni ACID tipiche.

Grazie a queste proprietà, in qualità di sviluppatore di applicazioni, puoi concentrarti sulla correttezza di ogni transazione singolarmente, senza preoccuparti di come proteggere la sua esecuzione da altre transazioni che potrebbero essere eseguite contemporaneamente.

Isolamento per le transazioni di lettura/scrittura

Dopo aver eseguito correttamente il commit di una transazione che contiene una serie di letture e scritture, visualizzi quanto segue:

  • La transazione restituisce valori che riflettono uno snapshot coerente all'ora del commit della transazione.
  • Le righe o gli intervalli vuoti rimangono vuoti al momento del commit.
  • La transazione esegue il commit di tutte le scritture al timestamp di commit della transazione.
  • Nessuna transazione può vedere le scritture fino a quando non viene eseguito il commit.

I driver client Spanner includono una logica di ripetizione dei tentativi di transazione che maschera gli errori temporanei eseguendo nuovamente la transazione e convalidando i dati osservati dal client.

L'effetto è che tutte le letture e le scritture sembrano essere avvenute in un unico momento, sia dal punto di vista della transazione stessa sia dal punto di vista di altri lettori e autori del database Spanner. Ciò significa che le letture e le scritture si verificano nello stesso timestamp. Per un esempio, consulta Serializzabilità e coerenza esterna.

Isolamento per le transazioni di lettura

Quando una transazione di lettura/scrittura esegue solo operazioni di lettura, fornisce garanzie di coerenza simili a una transazione di sola lettura. Tutte le letture all'interno della transazione restituiscono dati da un timestamp coerente, inclusa la conferma delle righe inesistenti.

Una differenza si verifica quando una transazione di lettura/scrittura viene eseguita senza eseguire un'operazione di scrittura. In questo scenario, non è garantito che i dati letti all'interno della transazione siano rimasti invariati nel database tra l'operazione di lettura e il commit della transazione.

Per garantire l'aggiornamento dei dati e verificare che non siano stati modificati dall'ultimo recupero, è necessaria una lettura successiva. Questa rilettura può essere eseguita all'interno di un'altra transazione di lettura/scrittura o con una lettura coerente.

Per un'efficienza ottimale, se una transazione esegue esclusivamente letture, utilizza una transazione di sola lettura anziché una transazione di lettura/scrittura.

Atomicità, coerenza, durabilità

Oltre all'isolamento, Spanner fornisce le altre garanzie delle proprietà ACID:

  • Atomicità. Una transazione viene considerata atomica se tutte le sue operazioni vengono completate correttamente o nessuna. Se un'operazione all'interno di una transazione non va a buon fine, l'intera transazione viene ripristinata allo stato originale, garantendo l'integrità dei dati.
  • Coerenza. Una transazione deve mantenere l'integrità delle regole e dei vincoli del database. Al termine di una transazione, il database deve trovarsi in uno stato valido, rispettando le regole predefinite.
  • Durabilità. Una volta eseguito il commit di una transazione, le modifiche vengono archiviate in modo permanente nel database e persistono in caso di errori di sistema, interruzioni di corrente o altri problemi.

Serializzabilità e coerenza esterna

Spanner offre solide garanzie transazionali, tra cui serializzabilità e coerenza esterna. Queste proprietà garantiscono che i dati rimangano coerenti e che le operazioni avvengano in un ordine prevedibile, anche in un ambiente distribuito.

La serializzabilità garantisce che tutte le transazioni vengano eseguite una dopo l'altra in un unico ordine sequenziale, anche se vengono elaborate contemporaneamente. Spanner lo fa assegnando timestamp di commit alle transazioni, riflettendo l'ordine in cui sono state eseguite.

Spanner offre una garanzia ancora più efficace nota come coerenza esterna. Ciò significa che non solo le transazioni vengono eseguite in un ordine riflesso dai timestamp di commit, ma questi timestamp sono anche allineati all'ora del mondo reale. In questo modo puoi confrontare i timestamp dei commit con il tempo reale, fornendo una visualizzazione coerente e ordinata a livello globale dei tuoi dati.

In sostanza, se una transazione Txn1 viene eseguita prima di un'altra transazione Txn2 in tempo reale, il timestamp di commit di Txn1 è precedente a quello di Txn2.

Considera l'esempio seguente:

Timeline che mostra l&#39;esecuzione di due transazioni che leggono gli stessi dati

In questo scenario, durante la sequenza temporale t:

  • La transazione Txn1 legge i dati A, esegue la preparazione della scrittura in A e poi viene eseguita correttamente.
  • La transazione Txn2 inizia dopo l'avvio di Txn1. Legge i dati B e poi legge i dati A.

Anche se Txn2 è iniziato prima del completamento di Txn1, Txn2 osserva le modifiche apportate da Txn1 a A. Questo perché Txn2 legge A dopo che Txn1 ha eseguito la scrittura in A.

Sebbene Txn1 e Txn2 possano sovrapporsi nel tempo di esecuzione, i timestamp di commit, c1 e c2 rispettivamente, impongono un ordine lineare delle transazioni. Ciò significa:

  • Tutte le letture e le scritture all'interno di Txn1 sembrano essere avvenute in un unico momento, c1.
  • Tutte le letture e le scritture all'interno di Txn2 sembrano essere avvenute in un unico momento, c2.
  • Fondamentalmente, c1 è precedente a c2 per le scritture di commit, anche se le scritture sono avvenute su macchine diverse. Se Txn2 esegue solo letture, c1 è precedente o contemporaneo a c2.

Questo ordinamento rigido significa che se un'operazione di lettura successiva osserva gli effetti di Txn2, osserva anche gli effetti di Txn1. Questa proprietà è vera per tutte le transazioni di cui è stato eseguito il commit.

Garanzie di lettura e scrittura in caso di errore della transazione

Se una chiamata per eseguire una transazione non va a buon fine, le garanzie di lettura e scrittura che hai dipendono dall'errore con cui non è andata a buon fine la chiamata di commit sottostante.

Ad esempio, un errore come "Riga non trovata" o "Riga già esistente" significa che la scrittura delle mutazioni memorizzate nel buffer ha riscontrato un errore, ad esempio una riga che il client sta tentando di aggiornare non esiste. In questo caso, le letture sono garantite coerenti, le scritture non vengono applicate e la non esistenza della riga è garantita per essere coerente anche con le letture.

Garanzie di lettura e scrittura in caso di errore della transazione

Quando una transazione Spanner non va a buon fine, le garanzie che ricevi per letture e scritture dipendono dall'errore specifico riscontrato durante l'operazione commit.

Ad esempio, un messaggio di errore come "Riga non trovata" o "Riga già esistente" indica un problema durante la scrittura delle mutazioni memorizzate nel buffer. Ciò può verificarsi se, ad esempio, una riga che il client sta tentando di aggiornare non esiste. In questi scenari:

  • Le letture sono coerenti: tutti i dati letti durante la transazione sono garantiti come coerenti fino al punto dell'errore.
  • Le scritture non vengono applicate:le mutazioni tentate dalla transazione non vengono commitate nel database.
  • Coerenza delle righe:l'inesistenza (o lo stato esistente) della riga che ha attivato l'errore è coerente con le letture eseguite all'interno della transazione.

Puoi annullare le operazioni di lettura asincrone in Spanner in qualsiasi momento senza influire su altre operazioni in corso all'interno della stessa transazione. Questa flessibilità è utile se un'operazione di livello superiore viene annullata o se decidi di interrompere una lettura in base ai risultati iniziali.

Tuttavia, è importante capire che la richiesta di annullamento di una lettura non ne garantisce l'interruzione immediata. Dopo una richiesta di annullamento, l'operazione di lettura potrebbe ancora:

  • Completamento riuscito:la lettura potrebbe terminare l'elaborazione e restituire i risultati prima che l'annullamento diventi effettivo.
  • Errore per un altro motivo:la lettura potrebbe terminare a causa di un errore diverso, ad esempio un'interruzione.
  • Restituisci risultati incompleti:la lettura potrebbe restituire risultati parziali, che vengono poi convalidati nell'ambito della procedura di commit della transazione.

È inoltre importante notare la distinzione con le operazioni di transazione commit: l'annullamento di una commit interrompe l'intera transazione, a meno che non sia già stata eseguita o non sia andata a buon fine per un altro motivo.

Prestazioni

Questa sezione descrive i problemi che influiscono sulle prestazioni delle transazioni di lettura/scrittura.

Controllo della contemporaneità basato su blocchi

Spanner consente a più client di interagire contemporaneamente con lo stesso database. Per mantenere la coerenza dei dati in queste transazioni simultanee, Spanner dispone di un meccanismo di blocco che utilizza blocchi condivisi ed esclusivi.

Quando una transazione esegue un'operazione di lettura, Spanner acquisisce blocchi di lettura condivisi sui dati pertinenti. Queste serrature condivise consentono ad altre operazioni di lettura simultanee di accedere agli stessi dati. Questa concorrenza viene mantenuta finché la transazione non si prepara a eseguire il commit delle modifiche.

Durante la fase di commit, man mano che vengono applicate le scritture, la transazione tenta di eseguire l'upgrade dei blocchi a blocchi esclusivi. Per farlo, esegue le seguenti operazioni:

  • Blocca tutte le nuove richieste di blocco in lettura condiviso sui dati interessati.
  • Attende il rilascio di tutti i blocchi di lettura condivisi esistenti sui dati.
  • Dopo che tutti i blocchi di lettura condivisi sono stati eliminati, viene inserito un blocco esclusivo, che concede l'accesso esclusivo ai dati per la durata della scrittura.

Note sulle serrature:

  • Granularità:Spanner applica i blocchi a livello di riga e colonna. Ciò significa che se la transazione T1 blocca la colonna A della riga albumid, la transazione T2 può comunque scrivere contemporaneamente nella colonna B della stessa riga albumid senza conflitti.
  • Scritture senza letture: per le scritture senza letture, Spanner non richiede un blocco esclusivo. Utilizza invece un blocco condiviso di scrittura. Questo perché l'ordine di applicazione per le scritture senza letture è determinato dai timestamp di commit, consentendo a più autori di operare sullo stesso elemento contemporaneamente senza conflitti. Un blocco esclusivo è necessario solo se la transazione legge prima i dati che intende scrivere.
  • Indici secondari per le ricerche di righe: quando esegui ricerche di righe all'interno di una transazione di lettura/scrittura, l'utilizzo di indici secondari può migliorare significativamente le prestazioni. Utilizzando gli indici secondari per limitare le righe scansionate a un intervallo più piccolo, Spanner blocca meno righe nella tabella, consentendo così una maggiore modifica simultanea delle righe al di fuori di questo intervallo specifico.
  • Accesso esclusivo alle risorse esterne:i blocchi interni di Spanner sono progettati per la coerenza dei dati all'interno del database Spanner stesso. Non utilizzarli per garantire l'accesso esclusivo alle risorse al di fuori di Spanner. Spanner può interrompere le transazioni per vari motivi, tra cui ottimizzazioni interne del sistema come lo spostamento dei dati tra le risorse di calcolo. Se viene eseguito un nuovo tentativo di transazione (in modo esplicito dal codice dell'applicazione o implicito dalle librerie client come il driver JDBC di Spanner), è garantito che i blocchi siano stati mantenuti solo durante il tentativo di commit riuscito.
  • Statistiche sui blocchi:per diagnosticare e analizzare i conflitti di blocco all'interno del database, puoi utilizzare lo strumento di introspezione Statistiche sui blocchi.

Rilevamento deadlock

Spanner rileva quando più transazioni potrebbero essere in deadlock e forza l'interruzione di tutte le transazioni tranne una. Considera questo scenario: Txn1 mantiene un blocco sul record A ed è in attesa di un blocco sul record B, mentre Txn2 mantiene un blocco sul record B ed è in attesa di un blocco sul record A. Per risolvere il problema, una delle transazioni deve essere interrotta, rilasciando il blocco e consentendo all'altra di procedere.

Spanner utilizza l'algoritmo wound-wait standard per il rilevamento del deadlock. Dietro le quinte, Spanner tiene traccia dell'età di ogni transazione che richiede blocchi in conflitto. Consente alle transazioni meno recenti di interrompere quelle più recenti. Una transazione meno recente è quella la cui prima lettura, query o commit è avvenuta prima.

Dando la priorità alle transazioni meno recenti, Spanner garantisce che ogni transazione acquisisca i blocchi dopo che è diventata abbastanza vecchia da avere una priorità più elevata. Ad esempio, una transazione meno recente che necessita di un blocco condiviso con l'autore può interrompere una transazione più recente che detiene un blocco condiviso con il lettore.

Esecuzione distribuita

Spanner può eseguire transazioni su dati che si estendono su più server, anche se questa funzionalità comporta un costo in termini di prestazioni rispetto alle transazioni su un singolo server.

Quali tipi di transazioni potrebbero essere distribuiti? Spanner può distribuire la responsabilità delle righe del database su più server. In genere, una riga e le righe della tabella interleaved corrispondenti vengono servite dallo stesso server, così come due righe nella stessa tabella con chiavi vicine. Spanner può eseguire transazioni tra righe su server diversi. Tuttavia, in generale, le transazioni che interessano molte righe collocate nella stessa posizione sono più veloci ed economiche di quelle che interessano molte righe sparse nel database o una tabella di grandi dimensioni.

Le transazioni più efficienti in Spanner includono solo le letture e le scritture che devono essere applicate in modo atomico. Le transazioni sono più veloci quando tutte le letture e le scritture accedono ai dati nella stessa parte dello spazio delle chiavi.

Transazioni di sola lettura

Oltre a bloccare le transazioni di lettura/scrittura, Spanner offre transazioni di sola lettura.

Utilizza una transazione di sola lettura quando devi eseguire più di una lettura allo stesso timestamp. Se puoi esprimere la lettura utilizzando uno dei metodi di lettura singola di Spanner, devi utilizzare questo metodo. Le prestazioni dell'utilizzo di una singola chiamata di lettura dovrebbero essere paragonabili a quelle di una singola lettura eseguita in una transazione di sola lettura.

Se stai leggendo una grande quantità di dati, valuta la possibilità di utilizzare le partizioni per leggere i dati in parallelo.

Poiché le transazioni di sola lettura non scrivono, non mantengono blocchi e non bloccano altre transazioni. Le transazioni di sola lettura osservano un prefisso coerente della cronologia dei commit delle transazioni, in modo che la tua applicazione riceva sempre dati coerenti.

Interfaccia

Spanner fornisce un'interfaccia per l'esecuzione di un insieme di operazioni nel contesto di una transazione di sola lettura, con nuovi tentativi in caso di interruzione della transazione.

Esempio

Il seguente esempio mostra come utilizzare una transazione di sola lettura per ottenere dati coerenti per due letture con lo stesso timestamp:

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

Semantica

Questa sezione descrive la semantica per le transazioni di sola lettura.

Transazioni di sola lettura dello snapshot

Quando una transazione di sola lettura viene eseguita in Spanner, esegue tutte le letture in un unico punto logico nel tempo. Ciò significa che sia la transazione di sola lettura sia qualsiasi altro lettore e writer simultaneo vedono un'istantanea coerente del database in quel momento specifico.

Queste transazioni di sola lettura dello snapshot offrono un approccio più semplice per letture coerenti rispetto al blocco delle transazioni di lettura/scrittura. Ecco perché:

  • Nessun blocco:le transazioni di sola lettura non acquisiscono blocchi. Funzionano invece selezionando un timestamp Spanner ed eseguendo tutte le letture rispetto a quella versione storica dei dati. Poiché non utilizzano blocchi, non bloccano le transazioni di lettura/scrittura simultanee.
  • Nessun annullamento:queste transazioni non vengono mai annullate. Anche se potrebbero non riuscire se il timestamp di lettura scelto viene sottoposto a garbage collection, la normale policy di garbage collection di Spanner è in genere abbastanza generosa da non causare questo problema nella maggior parte delle applicazioni.
  • Nessun commit o rollback:le transazioni di sola lettura non richiedono chiamate a sessions.commit o sessions.rollback e viene impedito di farlo.

Per eseguire una transazione snapshot, il client definisce un limite di timestamp, che indica a Spanner come selezionare un timestamp di lettura. I tipi di limiti del timestamp includono:

  • Letture coerenti:queste letture garantiscono che vedrai gli effetti di tutte le transazioni di cui è stato eseguito il commit prima dell'inizio della lettura. Tutte le righe all'interno di una singola lettura sono coerenti. Tuttavia, le letture coerenti non sono ripetibili, anche se restituiscono un timestamp e la lettura di nuovo allo stesso timestamp è ripetibile. Due transazioni di sola lettura consecutive potrebbero produrre risultati diversi a causa di scritture simultanee. Le query sui modifiche in tempo reale devono utilizzare questo limite. Per maggiori dettagli, vedi TransactionOptions.ReadOnly.strong.
  • Stalezza esatta:questa opzione esegue le letture in un timestamp specificato, come timestamp assoluto o come durata di stalezza relativa all'ora corrente. Garantisce di osservare un prefisso coerente della cronologia delle transazioni globali fino a quel timestamp e blocca le transazioni in conflitto che potrebbero essere eseguite con un timestamp inferiore o uguale al timestamp di lettura. Sebbene leggermente più veloce delle modalità di obsolescenza limitata, potrebbe restituire dati meno recenti. Per maggiori dettagli, vedi TransactionOptions.ReadOnly.read_timestamp e TransactionOptions.ReadOnly.exact_staleness.
  • Bounded staleness: Spanner seleziona il timestamp più recente entro un limite di non aggiornamento definito dall'utente, consentendo l'esecuzione nella replica disponibile più vicina senza blocchi. Tutte le righe restituite sono coerenti. Come le letture coerenti, la non aggiornatezza vincolata non è ripetibile, poiché letture diverse potrebbero essere eseguite in timestamp diversi anche con lo stesso limite. Queste letture operano in due fasi (negoziazione del timestamp, quindi lettura) e di solito sono leggermente più lente della non aggiornamento esatto, ma spesso restituiscono risultati più recenti e hanno maggiori probabilità di essere eseguite in una replica locale. Questa modalità è disponibile solo per le transazioni di sola lettura monouso perché la negoziazione del timestamp richiede di sapere in anticipo quali righe verranno lette. Per maggiori dettagli, vediTransactionOptions.ReadOnly.max_staleness e TransactionOptions.ReadOnly.min_read_timestamp.

Transazioni DML partizionate

Puoi utilizzare DML partizionato per eseguire istruzioni UPDATE e DELETE su larga scala senza riscontrare limiti di transazione o bloccare un'intera tabella. Spanner lo fa partizionando lo spazio delle chiavi ed eseguendo le istruzioni DML su ogni partizione all'interno di una transazione di lettura/scrittura separata.

Per utilizzare il DML non partizionato, esegui istruzioni all'interno di transazioni di lettura/scrittura che crei esplicitamente nel codice. Per maggiori dettagli, vedi Utilizzo di DML.

Interfaccia

Spanner fornisce l'interfaccia TransactionOptions.partitionedDml per l'esecuzione di una singola istruzione DML partizionata.

Esempi

Il seguente esempio di codice aggiorna la colonna MarketingBudget della tabella Albums.

C++

Utilizzi la funzione ExecutePartitionedDml() per eseguire un'istruzione DML partizionata.

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#

Utilizzi il metodo ExecutePartitionedUpdateAsync() per eseguire un'istruzione DML partizionata.


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;
    }
}

Vai

Utilizzi il metodo PartitionedUpdate() per eseguire un'istruzione DML partizionata.


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

Utilizzi il metodo executePartitionedUpdate() per eseguire un'istruzione DML partizionata.

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

Utilizzi il metodo runPartitionedUpdate() per eseguire un'istruzione DML partizionata.

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

Utilizzi il metodo executePartitionedUpdate() per eseguire un'istruzione DML partizionata.

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

Utilizzi il metodo execute_partitioned_dml() per eseguire un'istruzione DML partizionata.

# 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

Utilizzi il metodo execute_partitioned_update() per eseguire un'istruzione DML partizionata.

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

Il seguente esempio di codice elimina le righe dalla tabella Singers in base alla colonna SingerId.

C++

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

C#


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

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

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

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

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

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

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

Java

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

Node.js

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

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

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

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

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

PHP

use Google\Cloud\Spanner\SpannerClient;

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

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

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

Python

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

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

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

Ruby

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

require "google/cloud/spanner"

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

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

puts "#{row_count} records deleted."

Semantica

Questa sezione descrive la semantica per DML partizionato.

Informazioni sull'esecuzione di DML partizionato

Puoi eseguire una sola istruzione DML partizionata alla volta, sia che utilizzi un metodo della libreria client sia Google Cloud CLI.

Le transazioni partizionate non supportano i commit o i rollback. Spanner esegue e applica immediatamente l'istruzione DML. Se annulli l'operazione o l'operazione non va a buon fine, Spanner annulla tutte le partizioni in esecuzione e non avvia quelle rimanenti. Tuttavia, Spanner non esegue il rollback delle partizioni già eseguite.

Strategia di acquisizione dei blocchi DML partizionati

Per ridurre il conflitto di blocco, DML partizionato acquisisce blocchi di lettura solo sulle righe che corrispondono alla clausola WHERE. Anche le transazioni più piccole e indipendenti utilizzate per ogni partizione mantengono i blocchi per un periodo di tempo inferiore.

Limiti delle transazioni per sessione

Ogni sessione in Spanner può avere una sola transazione attiva alla volta. Sono incluse letture e query autonome, che internamente utilizzano una transazione e vengono conteggiate ai fini di questo limite. Una volta completata una transazione, la sessione può essere riutilizzata immediatamente per la transazione successiva. Non è necessario creare una nuova sessione per ogni transazione.

Timestamp di lettura precedenti e garbage collection delle versioni

Spanner esegue la garbage collection delle versioni per raccogliere i dati eliminati o sovrascritti e recuperare spazio di archiviazione. Per impostazione predefinita, i dati precedenti a un'ora vengono recuperati. Spanner non può eseguire letture in corrispondenza di timestamp precedenti al VERSION_RETENTION_PERIOD configurato, che per impostazione predefinita è di un'ora, ma può essere configurato fino a una settimana. Quando le letture diventano troppo vecchie durante l'esecuzione, non vanno a buon fine e restituiscono l'errore FAILED_PRECONDITION.

Query sui modifiche in tempo reale

Uno stream di modifiche è un oggetto schema che puoi configurare per monitorare le modifiche ai dati in un intero database, in tabelle specifiche o in un insieme definito di colonne all'interno di un database.

Quando crei un flusso di modifiche, Spanner definisce una funzione TVF (Table-Valued Function) SQL corrispondente. Puoi utilizzare questa TVF per eseguire query sui record di modifica nel flusso di modifiche associato con il metodo sessions.executeStreamingSql. Il nome della TVF viene generato dal nome dello stream di modifiche e inizia sempre con READ_.

Tutte le query sulle funzioni TVF di flusso di modifiche devono essere eseguite utilizzando l'API sessions.executeStreamingSql all'interno di una transazione di sola lettura monouso con un timestamp_bound di sola lettura avanzato. Il TVF del flusso di modifiche consente di specificare start_timestamp e end_timestamp per l'intervallo di tempo. Tutti i record di modifica nel periodo di conservazione sono accessibili utilizzando questo timestamp_bound di sola lettura sicuro. Tutti gli altri TransactionOptions non sono validi per le query di modifica in tempo reale.

Inoltre, se TransactionOptions.read_only.return_read_timestamp è impostato su true, il messaggio Transaction che descrive la transazione restituisce un valore speciale di 2^63 - 2 anziché un timestamp di lettura valido. Devi ignorare questo valore speciale e non utilizzarlo per query successive.

Per saperne di più, vedi Flusso di lavoro delle query sugli stream di modifiche.

Transazioni inattive

Una transazione viene considerata inattiva se non ha letture o query SQL in sospeso e non ne ha avviata una negli ultimi 10 secondi. Spanner può interrompere le transazioni inattive per impedire loro di mantenere i blocchi a tempo indeterminato. Se una transazione inattiva viene interrotta, il commit non va a buon fine e viene restituito un errore ABORTED. L'esecuzione periodica di una piccola query, ad esempio SELECT 1, all'interno della transazione può impedirne l'inattività.