Resumen de transacciones

En esta página se describen las transacciones de Spanner y se presentan las interfaces de transacciones de lectura y escritura, de solo lectura y de DML con particiones de Spanner.

Una transacción en Spanner es un conjunto de lecturas y escrituras que se ejecutan de forma atómica en un único punto lógico en el tiempo en columnas, filas y tablas de una base de datos.

Una sesión se usa para realizar transacciones en una base de datos de Spanner. Una sesión representa un canal de comunicación lógico con el servicio de base de datos de Spanner. Las sesiones pueden ejecutar una o varias transacciones a la vez. Para obtener más información, consulta Sesiones.

Tipos de transacciones

Spanner admite los siguientes tipos de transacciones, cada uno diseñado para patrones de interacción de datos específicos:

  • Lectura y escritura: estas transacciones usan el bloqueo pesimista y, si es necesario, una confirmación en dos fases. Puede que fallen y requieran reintentos. Aunque están limitadas a una sola base de datos, pueden modificar datos en varias tablas de esa base de datos.

  • Solo lectura: estas transacciones garantizan la coherencia de los datos en varias operaciones de lectura, pero no permiten modificar los datos. Se ejecutan en una marca de tiempo determinada por el sistema para mantener la coherencia o en una marca de tiempo anterior configurada por el usuario. A diferencia de las transacciones de lectura y escritura, no requieren una operación de confirmación ni bloqueos, aunque pueden pausarse para esperar a que finalicen las operaciones de escritura en curso.

  • DML particionado: este tipo de transacción ejecuta instrucciones de DML como operaciones de DML particionado. Está optimizada para actualizaciones y eliminaciones de datos a gran escala, como la limpieza de datos o la inserción de datos en bloque. Si tienes que hacer muchas escrituras que no necesitan una transacción atómica, te recomendamos que uses escrituras por lotes. Para obtener más información, consulta Modificar datos mediante escrituras por lotes.

Transacciones de lectura y escritura

Usa transacciones de lectura y escritura con bloqueo para leer, modificar y escribir datos de forma atómica en cualquier parte de una base de datos. Este tipo de transacción es coherente externamente.

Minimiza el tiempo que una transacción está activa. Las transacciones de menor duración aumentan la probabilidad de que se completen correctamente y reducen la contención. Spanner intenta mantener activas las exclusivas de lectura mientras la transacción siga realizando lecturas y no haya finalizado mediante las operaciones sessions.commit u sessions.rollback. Si el cliente permanece inactivo durante largos periodos, Spanner podría liberar los bloqueos de la transacción y abortarla.

Conceptualmente, una transacción de lectura y escritura consta de cero o más lecturas o instrucciones SQL seguidas de sessions.commit. En cualquier momento antes del sessions.commit, el cliente puede enviar una solicitud sessions.rollback para cancelar la transacción.

Para realizar una operación de escritura que dependa de una o varias operaciones de lectura, usa una transacción de lectura y escritura con bloqueo:

  • Si debes confirmar una o varias operaciones de escritura de forma atómica, realiza esas escrituras en la misma transacción de lectura y escritura. Por ejemplo, si transfiere 200 $ de la cuenta A a la cuenta B, realice las dos operaciones de escritura (disminuir 200 $en la cuenta A y aumentar 200 $en la cuenta B) y las lecturas de los saldos iniciales de las cuentas en la misma transacción.
  • Si quieres duplicar el saldo de la cuenta A, realiza las operaciones de lectura y escritura en la misma transacción. De esta forma, el sistema lee el saldo antes de duplicarlo y, a continuación, lo actualiza.
  • Si vas a realizar una o varias operaciones de escritura que dependen de los resultados de una o varias operaciones de lectura, realiza esas escrituras y lecturas en la misma transacción de lectura y escritura, aunque las operaciones de escritura no se ejecuten. Por ejemplo, si quieres transferir 200 $de la cuenta A a la cuenta B solo si el saldo actual de A es superior a 500 $, incluye la lectura del saldo de A y las operaciones de escritura condicionales en la misma transacción, aunque no se produzca la transferencia.

Para realizar operaciones de lectura, usa un único método de lectura o una transacción de solo lectura:

  • Si solo vas a realizar operaciones de lectura y puedes expresar la operación de lectura con un único método de lectura, usa ese método o una transacción de solo lectura. A diferencia de las transacciones de lectura y escritura, las lecturas únicas no adquieren bloqueos.

Interfaz

Las bibliotecas de cliente de Spanner proporcionan una interfaz para ejecutar un cuerpo de trabajo en una transacción de lectura y escritura, con reintentos para las anulaciones de transacciones. Una transacción de Spanner puede requerir varios reintentos antes de confirmarse.

Las transacciones se pueden anular por varios motivos. Por ejemplo, si dos transacciones intentan modificar datos simultáneamente, puede producirse un interbloqueo. En estos casos, Spanner aborta una transacción para que la otra pueda continuar. Con menos frecuencia, los eventos transitorios de Spanner también pueden provocar la cancelación de transacciones.

Como las transacciones son atómicas, una transacción anulada no afecta a la base de datos. Vuelve a intentar la transacción en la misma sesión para aumentar las tasas de éxito. Cada reintento que da como resultado un error ABORTED aumenta la prioridad de bloqueo de la transacción.

Cuando usas una transacción en una biblioteca de cliente de Spanner, defines el cuerpo de la transacción como un objeto de función. Esta función encapsula las lecturas y escrituras realizadas en una o varias tablas de la base de datos. La biblioteca de cliente de Spanner ejecuta esta función repetidamente hasta que la transacción se confirma correctamente o se produce un error que no se puede volver a intentar.

Ejemplo

Supongamos que tiene una columna MarketingBudget en la tabla Albums:

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

El departamento de marketing te pide que transfieras 200.000 € del presupuesto de Albums (2, 2) a Albums (1, 1), pero solo si hay dinero disponible en el presupuesto de ese álbum. Debes usar una transacción de lectura y escritura con bloqueo para esta operación, ya que la transacción puede realizar escrituras en función del resultado de una lectura.

A continuación, se muestra cómo ejecutar una transacción de lectura y escritura:

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"

Semántica

En esta sección se describe la semántica de las transacciones de lectura y escritura en Spanner.

Propiedades

Una transacción de lectura y escritura en Spanner ejecuta un conjunto de lecturas y escrituras de forma atómica. La marca de tiempo en la que se ejecutan las transacciones de lectura y escritura coincide con el tiempo transcurrido. El orden de serialización coincide con el orden de esta marca de tiempo.

Las transacciones de lectura y escritura proporcionan las propiedades ACID de las bases de datos relacionales. Las transacciones de lectura y escritura de Spanner ofrecen propiedades más sólidas que las transacciones ACID típicas.

Gracias a estas propiedades, como desarrollador de aplicaciones, puedes centrarte en la corrección de cada transacción por separado, sin preocuparte por cómo proteger su ejecución de otras transacciones que puedan ejecutarse al mismo tiempo.

Aislamiento de las transacciones de lectura y escritura

Después de confirmar correctamente una transacción que contiene una serie de lecturas y escrituras, verás lo siguiente:

  • La transacción devuelve valores que reflejan una instantánea coherente en la marca de tiempo de confirmación de la transacción.
  • Las filas o los intervalos vacíos permanecen vacíos en el momento de la confirmación.
  • La transacción confirma todas las escrituras en la marca de tiempo de confirmación de la transacción.
  • Ninguna transacción puede ver las escrituras hasta que se confirme la transacción.

Los controladores de cliente de Spanner incluyen una lógica de reintento de transacciones que oculta los errores transitorios volviendo a ejecutar la transacción y validando los datos que observa el cliente.

El efecto es que todas las lecturas y escrituras parecen haber ocurrido en un único momento, tanto desde la perspectiva de la transacción en sí como desde la perspectiva de otros lectores y escritores de la base de datos de Spanner. Esto significa que las lecturas y las escrituras se producen en la misma marca de tiempo. Para ver un ejemplo, consulta Serialización y coherencia externa.

Aislamiento de las transacciones de lectura

Cuando una transacción de lectura y escritura solo realiza operaciones de lectura, ofrece garantías de coherencia similares a las de una transacción de solo lectura. Todas las lecturas de la transacción devuelven datos de una marca de tiempo coherente, incluida la confirmación de filas no existentes.

Una de las diferencias es que una transacción de lectura y escritura se confirma sin ejecutar una operación de escritura. En este caso, no se garantiza que los datos leídos en la transacción no hayan cambiado en la base de datos entre la operación de lectura y la confirmación de la transacción.

Para asegurarse de que los datos estén actualizados y validar que no se han modificado desde la última vez que se obtuvieron, es necesario realizar una lectura posterior. Esta lectura se puede realizar en otra transacción de lectura y escritura o con una lectura fuerte.

Para obtener una eficiencia óptima, si una transacción solo realiza lecturas, usa una transacción de solo lectura en lugar de una transacción de lectura y escritura.

Atomicidad, coherencia y durabilidad

Además del aislamiento, Spanner ofrece las demás garantías de las propiedades ACID:

  • Atomicidad. Una transacción se considera atómica si todas sus operaciones se completan correctamente o si no se completa ninguna. Si falla alguna operación de una transacción, se revierte toda la transacción a su estado original, lo que garantiza la integridad de los datos.
  • Coherencia. Una transacción debe mantener la integridad de las reglas y las restricciones de la base de datos. Una vez completada una transacción, la base de datos debe estar en un estado válido y cumplir las reglas predefinidas.
  • Durabilidad. Una vez que se confirma una transacción, sus cambios se almacenan de forma permanente en la base de datos y se conservan en caso de que se produzcan fallos en el sistema, cortes de suministro eléctrico u otras interrupciones.

Serialización y coherencia externa

Spanner ofrece sólidas garantías transaccionales, como la serialización y la coherencia externa. Estas propiedades aseguran que los datos sigan siendo coherentes y que las operaciones se realicen en un orden predecible, incluso en un entorno distribuido.

La serializabilidad asegura que todas las transacciones se ejecuten una tras otra en un orden único y secuencial, aunque se procesen simultáneamente. Spanner lo consigue asignando marcas de tiempo de confirmación a las transacciones, lo que refleja el orden en el que se han confirmado.

Spanner ofrece una garantía aún mayor, conocida como coherencia externa. Esto significa que las transacciones no solo se completan en un orden reflejado por sus marcas de tiempo de confirmación, sino que estas marcas de tiempo también se ajustan a la hora real. De esta forma, puedes comparar las marcas de tiempo de las confirmaciones con la hora real, lo que te proporciona una vista coherente y ordenada a nivel mundial de tus datos.

En esencia, si una transacción Txn1 se confirma antes que otra transacción Txn2 en tiempo real, la marca de tiempo de confirmación de Txn1 es anterior a la de Txn2.

Veamos un ejemplo:

Cronología que muestra la ejecución de dos transacciones que leen los mismos datos

En este caso, durante la cronología t:

  • La transacción Txn1 lee los datos A, prepara una escritura en A y, a continuación, se confirma correctamente.
  • La transacción Txn2 comienza después de que se inicie Txn1. Lee los datos B y, a continuación, los datos A.

Aunque Txn2 haya empezado antes de que se completara Txn1, Txn2 observa los cambios que Txn1 ha hecho en A. Esto se debe a que Txn2 lee A después de que Txn1 haya confirmado su escritura en A.

Aunque Txn1 y Txn2 pueden superponerse en su tiempo de ejecución, sus marcas de tiempo de confirmación, c1 y c2 respectivamente, imponen un orden de transacción lineal. Esto significa lo siguiente:

  • Todas las lecturas y escrituras de Txn1 parecen haber ocurrido en un solo momento, c1.
  • Todas las lecturas y escrituras de Txn2 parecen haber ocurrido en un solo momento, c2.
  • Es fundamental que c1 sea anterior a c2 en las escrituras confirmadas, aunque las escrituras se hayan producido en máquinas diferentes. Si Txn2 solo realiza lecturas, c1 es anterior o se produce al mismo tiempo que c2.

Este orden estricto significa que, si una operación de lectura posterior observa los efectos de Txn2, también observa los efectos de Txn1. Esta propiedad tiene el valor true en todas las transacciones confirmadas correctamente.

Garantías de lectura y escritura en caso de fallo de la transacción

Si falla una llamada para ejecutar una transacción, las garantías de lectura y escritura que tengas dependerán del error con el que haya fallado la llamada de confirmación subyacente.

Por ejemplo, un error como "Row Not Found" o "Row Already Exists" significa que se ha producido un error al escribir las mutaciones almacenadas en el búfer. Por ejemplo, una fila que el cliente está intentando actualizar no existe. En ese caso, las lecturas son coherentes, las escrituras no se aplican y la inexistencia de la fila también es coherente con las lecturas.

Garantías de lectura y escritura en caso de fallo de la transacción

Cuando falla una transacción de Spanner, las garantías que recibes para las lecturas y escrituras dependen del error específico que se haya producido durante la operación commit.

Por ejemplo, un mensaje de error como "Row Not Found" (No se ha encontrado la fila) o "Row Already Exists" (La fila ya existe) indica que hay un problema al escribir mutaciones almacenadas en búfer. Esto puede ocurrir si, por ejemplo, no existe una fila que el cliente esté intentando actualizar. En estos casos:

  • Las lecturas son coherentes: se garantiza que los datos leídos durante la transacción son coherentes hasta el momento en que se produce el error.
  • No se aplican las escrituras: las mutaciones que ha intentado la transacción no se han confirmado en la base de datos.
  • Coherencia de las filas: la inexistencia (o el estado) de la fila que ha activado el error es coherente con las lecturas realizadas en la transacción.

Puedes cancelar operaciones de lectura asíncronas en Spanner en cualquier momento sin que afecte a otras operaciones en curso de la misma transacción. Esta flexibilidad es útil si se cancela una operación de nivel superior o si decides abortar una lectura en función de los resultados iniciales.

Sin embargo, es importante tener en cuenta que solicitar la cancelación de una lectura no garantiza que se detenga inmediatamente. Después de una solicitud de cancelación, la operación de lectura puede seguir ocurriendo:

  • Se completa correctamente: la lectura puede terminar de procesarse y devolver resultados antes de que se aplique la cancelación.
  • Fallo por otro motivo: la lectura podría finalizar debido a otro error, como una cancelación.
  • Devuelve resultados incompletos: la lectura puede devolver resultados parciales, que se validan como parte del proceso de confirmación de la transacción.

También es importante tener en cuenta la diferencia con las operaciones de commit de transacción: cancelar una commit aborta toda la transacción, a menos que la transacción ya se haya confirmado o haya fallado por otro motivo.

Rendimiento

En esta sección se describen los problemas que afectan al rendimiento de las transacciones de lectura y escritura.

Control de simultaneidad de bloqueo

Spanner permite que varios clientes interactúen con la misma base de datos simultáneamente. Para mantener la coherencia de los datos en estas transacciones simultáneas, Spanner tiene un mecanismo de bloqueo que usa bloqueos compartidos y exclusivos.

Cuando una transacción realiza una operación de lectura, Spanner adquiere bloqueos de lectura compartidos en los datos pertinentes. Estos bloqueos compartidos permiten que otras operaciones de lectura simultáneas accedan a los mismos datos. Esta simultaneidad se mantiene hasta que tu transacción se prepara para confirmar sus cambios.

Durante la fase de confirmación, a medida que se aplican las escrituras, la transacción intenta actualizar sus bloqueos a bloqueos exclusivos. Para ello, hace lo siguiente:

  • Bloquea cualquier solicitud de bloqueo de lectura compartido nueva en los datos afectados.
  • Espera a que se liberen todos los bloqueos de lectura compartidos de esos datos.
  • Una vez que se han borrado todos los bloqueos de lectura compartidos, se coloca un bloqueo exclusivo, lo que le da acceso exclusivo a los datos durante la escritura.

Notas sobre los bloqueos:

  • Granularidad: Spanner aplica bloqueos a nivel de fila y columna. Esto significa que, si la transacción T1 tiene un bloqueo en la columna A de la fila albumid, la transacción T2 puede seguir escribiendo simultáneamente en la columna B de la misma fila albumid sin que haya ningún conflicto.
  • Escrituras sin lecturas: para las escrituras sin lecturas, Spanner no requiere un bloqueo exclusivo. En su lugar, usa un bloqueo compartido de escritura. Esto se debe a que el orden de aplicación de las escrituras sin lecturas se determina por sus marcas de tiempo de confirmación, lo que permite que varios escritores operen en el mismo elemento simultáneamente sin conflictos. Un bloqueo exclusivo solo es necesario si tu transacción primero lee los datos que pretende escribir.
  • Índices secundarios para búsquedas de filas: al realizar búsquedas de filas en una transacción de lectura y escritura, el uso de índices secundarios puede mejorar significativamente el rendimiento. Al usar índices secundarios para limitar las filas analizadas a un intervalo más pequeño, Spanner bloquea menos filas en la tabla, lo que permite una mayor modificación simultánea de las filas fuera de ese intervalo específico.
  • Acceso exclusivo a recursos externos: los bloqueos internos de Spanner se han diseñado para mantener la coherencia de los datos en la propia base de datos de Spanner. No los utilice para garantizar el acceso exclusivo a recursos fuera de Spanner. Spanner puede anular transacciones por varios motivos, como optimizaciones internas del sistema, como el movimiento de datos entre recursos de computación. Si se vuelve a intentar una transacción (ya sea de forma explícita mediante el código de tu aplicación o de forma implícita mediante bibliotecas de cliente como el controlador JDBC de Spanner), se garantiza que los bloqueos se han mantenido solo durante el intento de confirmación correcto.
  • Estadísticas de bloqueo: para diagnosticar e investigar conflictos de bloqueo en tu base de datos, puedes usar la herramienta de introspección Estadísticas de bloqueo.

Detección de interbloqueos

Spanner detecta cuándo se pueden producir interbloqueos en varias transacciones y fuerza la cancelación de todas las transacciones menos una. Imagina esta situación: Txn1 tiene un bloqueo en el registro A y está esperando un bloqueo en el registro B, mientras que Txn2 tiene un bloqueo en el registro B y está esperando un bloqueo en el registro A. Para resolver este problema, una de las transacciones debe abortar, liberando su bloqueo y permitiendo que la otra continúe.

Spanner usa el algoritmo estándar wound-wait para detectar interbloqueos. En segundo plano, Spanner monitoriza la antigüedad de cada transacción que solicita bloqueos en conflicto. Permite que las transacciones más antiguas aborten las más recientes. Una transacción más antigua es aquella cuya lectura, consulta o confirmación más antigua se produjo antes.

Al priorizar las transacciones más antiguas, Spanner se asegura de que todas las transacciones acaben adquiriendo bloqueos cuando tengan la antigüedad suficiente para tener una prioridad más alta. Por ejemplo, una transacción más antigua que necesite un bloqueo compartido de escritura puede anular una transacción más reciente que tenga un bloqueo compartido de lectura.

Ejecución distribuida

Spanner puede ejecutar transacciones en datos que abarcan varios servidores, aunque esta función conlleva un coste de rendimiento en comparación con las transacciones de un solo servidor.

¿Qué tipos de transacciones se pueden distribuir? Spanner puede distribuir la responsabilidad de las filas de la base de datos entre muchos servidores. Normalmente, el mismo servidor proporciona una fila y las filas de la tabla intercalada correspondientes, así como dos filas de la misma tabla con claves cercanas. Spanner puede realizar transacciones entre filas de diferentes servidores. Sin embargo, por lo general, las transacciones que afectan a muchas filas ubicadas en el mismo lugar son más rápidas y económicas que las que afectan a muchas filas dispersas por toda la base de datos o una tabla grande.

Las transacciones más eficientes de Spanner solo incluyen las lecturas y escrituras que se deben aplicar de forma atómica. Las transacciones son más rápidas cuando todas las lecturas y escrituras acceden a los datos en la misma parte del espacio de claves.

Transacciones de solo lectura

Además de bloquear las transacciones de lectura y escritura, Spanner ofrece transacciones de solo lectura.

Usa una transacción de solo lectura cuando necesites ejecutar más de una lectura en la misma marca de tiempo. Si puedes expresar tu lectura mediante uno de los métodos de lectura única de Spanner, debes usar ese método en su lugar. El rendimiento de usar una sola llamada de lectura de este tipo debería ser comparable al de una sola lectura realizada en una transacción de solo lectura.

Si vas a leer una gran cantidad de datos, te recomendamos que uses particiones para leer los datos en paralelo.

Como las transacciones de solo lectura no escriben, no mantienen bloqueos y no bloquean otras transacciones. Las transacciones de solo lectura observan un prefijo coherente del historial de confirmaciones de transacciones, por lo que tu aplicación siempre obtiene datos coherentes.

Interfaz

Spanner proporciona una interfaz para ejecutar un conjunto de tareas en el contexto de una transacción de solo lectura, con reintentos para las transacciones anuladas.

Ejemplo

En el siguiente ejemplo se muestra cómo usar una transacción de solo lectura para obtener datos coherentes de dos lecturas con la misma marca de tiempo:

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

Semántica

En esta sección se describe la semántica de las transacciones de solo lectura.

Transacciones de solo lectura de la copia

Cuando se ejecuta una transacción de solo lectura en Spanner, realiza todas sus lecturas en un único punto lógico en el tiempo. Esto significa que tanto la transacción de solo lectura como cualquier otro lector y escritor simultáneo ven una instantánea coherente de la base de datos en ese momento concreto.

Estas transacciones de solo lectura de la instantánea ofrecen un enfoque más sencillo para las lecturas coherentes en comparación con las transacciones de lectura y escritura con bloqueo. Estos son los motivos:

  • Sin bloqueos: las transacciones de solo lectura no adquieren bloqueos. En su lugar, funcionan seleccionando una marca de tiempo de Spanner y ejecutando todas las lecturas en esa versión histórica de los datos. Como no usan bloqueos, no bloquearán las transacciones de lectura y escritura simultáneas.
  • Sin cancelaciones: estas transacciones nunca se cancelan. Aunque pueden fallar si la marca de tiempo de lectura elegida se recoge como elemento no utilizado, la política de recogida de elementos no utilizados predeterminada de Spanner suele ser lo suficientemente generosa como para que la mayoría de las aplicaciones no tengan este problema.
  • Sin confirmaciones ni reversiones: las transacciones de solo lectura no requieren llamadas a sessions.commit ni a sessions.rollback y, de hecho, no pueden hacerlo.

Para ejecutar una transacción de instantánea, el cliente define un límite de marca de tiempo, que indica a Spanner cómo seleccionar una marca de tiempo de lectura. Entre los tipos de límites de marca de tiempo se incluyen los siguientes:

  • Lecturas sólidas: estas lecturas garantizan que verás los efectos de todas las transacciones confirmadas antes de que empezara la lectura. Todas las filas de una misma lectura son coherentes. Sin embargo, las lecturas sólidas no se pueden repetir, aunque sí devuelven una marca de tiempo, y la lectura de nuevo en esa misma marca de tiempo se puede repetir. Dos transacciones de solo lectura consecutivas pueden producir resultados diferentes debido a escrituras simultáneas. Las consultas en flujos de cambios deben usar este límite. Para obtener más información, consulta TransactionOptions.ReadOnly.strong.
  • Obsoleto exacto: esta opción ejecuta lecturas en una marca de tiempo que especifiques, ya sea como una marca de tiempo absoluta o como una duración de obsolescencia relativa a la hora actual. De esta forma, se asegura de que se observe un prefijo coherente del historial de transacciones globales hasta esa marca de tiempo y se bloquean las transacciones conflictivas que puedan confirmarse con una marca de tiempo inferior o igual a la marca de tiempo de lectura. Aunque es ligeramente más rápido que los modos de obsolescencia limitada, puede devolver datos antiguos. Para obtener más información, consulta TransactionOptions.ReadOnly.read_timestamp y TransactionOptions.ReadOnly.exact_staleness.
  • Obsolecencia limitada: Spanner selecciona la marca de tiempo más reciente dentro de un límite de obsolescencia definido por el usuario, lo que permite la ejecución en la réplica disponible más cercana sin bloquearse. Todas las filas devueltas son coherentes. Al igual que las lecturas sólidas, la obsolescencia limitada no se puede repetir, ya que las diferentes lecturas pueden ejecutarse en marcas de tiempo diferentes, incluso con el mismo límite. Estas lecturas se realizan en dos fases (negociación de la marca de tiempo y, a continuación, lectura) y suelen ser ligeramente más lentas que la obsolescencia exacta, pero a menudo devuelven resultados más recientes y es más probable que se ejecuten en una réplica local. Este modo solo está disponible para transacciones de solo lectura de un solo uso, ya que la negociación de la marca de tiempo requiere saber qué filas se leerán de antemano. Para obtener más información, consulta TransactionOptions.ReadOnly.max_staleness y TransactionOptions.ReadOnly.min_read_timestamp.

Transacciones de DML particionado

Puedes usar DML con particiones para ejecutar instrucciones UPDATE y DELETE a gran escala sin superar los límites de las transacciones ni bloquear una tabla completa. Spanner lo consigue particionando el espacio de claves y ejecutando las instrucciones DML en cada partición dentro de una transacción de lectura y escritura independiente.

Para usar DML no particionado, debes ejecutar instrucciones en transacciones de lectura y escritura que crees explícitamente en tu código. Para obtener más información, consulta el artículo sobre cómo usar DML.

Interfaz

Spanner proporciona la interfaz TransactionOptions.partitionedDml para ejecutar una única instrucción de DML particionado.

Ejemplos

En el siguiente ejemplo de código se actualiza la columna MarketingBudget de la tabla Albums.

C++

Usa la función ExecutePartitionedDml() para ejecutar una instrucción DML con particiones.

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#

Usa el método ExecutePartitionedUpdateAsync() para ejecutar una instrucción DML particionada.


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

Usa el método PartitionedUpdate() para ejecutar una instrucción DML particionada.


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

Usa el método executePartitionedUpdate() para ejecutar una instrucción DML particionada.

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

Usa el método runPartitionedUpdate() para ejecutar una instrucción DML particionada.

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

Usa el método executePartitionedUpdate() para ejecutar una instrucción DML particionada.

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

Usa el método execute_partitioned_dml() para ejecutar una instrucción DML particionada.

# 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

Usa el método execute_partitioned_update() para ejecutar una instrucción DML particionada.

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

En el siguiente ejemplo de código se eliminan filas de la tabla Singers en función de la columna 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."

Semántica

En esta sección se describe la semántica de DML particionado.

Información sobre la ejecución de DML particionado

Solo puedes ejecutar una instrucción de DML particionado a la vez, ya sea mediante un método de biblioteca de cliente o la CLI de Google Cloud.

Las transacciones particionadas no admiten confirmaciones ni restauraciones. Spanner ejecuta y aplica la instrucción DML inmediatamente. Si cancelas la operación o esta falla, Spanner cancela todas las particiones en ejecución y no inicia ninguna de las restantes. Sin embargo, Spanner no revierte las particiones que ya se hayan ejecutado.

Estrategia de adquisición de bloqueos de DML particionado

Para reducir la contención de bloqueos, DML particionado adquiere bloqueos de lectura solo en las filas que coinciden con la cláusula WHERE. Las transacciones independientes más pequeñas que se usan para cada partición también mantienen los bloqueos durante menos tiempo.

Límites de las transacciones de sesión

Cada sesión de Spanner puede tener una transacción activa a la vez. Esto incluye lecturas y consultas independientes, que internamente usan una transacción y se tienen en cuenta para este límite. Una vez completada una transacción, la sesión se puede volver a usar inmediatamente para la siguiente. No es necesario crear una sesión nueva para cada transacción.

Marcas de tiempo de lectura antiguas y recolección de elementos obsoletos de versiones

Spanner realiza la recogida de elementos obsoletos de versiones para recoger los datos eliminados o sobrescritos y recuperar el almacenamiento. De forma predeterminada, los datos de más de una hora se recuperan. Spanner no puede realizar lecturas en marcas de tiempo anteriores al VERSION_RETENTION_PERIOD configurado, que tiene un valor predeterminado de una hora, pero se puede configurar hasta una semana. Si las lecturas son demasiado antiguas durante la ejecución, fallarán y devolverán el error FAILED_PRECONDITION.

Consultas en flujos de cambios

Un flujo de cambios es un objeto de esquema que puedes configurar para monitorizar las modificaciones de datos en toda una base de datos, en tablas específicas o en un conjunto definido de columnas de una base de datos.

Cuando creas un flujo de cambios, Spanner define una función con valores de tabla (TVF) de SQL correspondiente. Puede usar esta TVF para consultar los registros de cambios de la secuencia de cambios asociada con el método sessions.executeStreamingSql. El nombre de la función de valor de tabla se genera a partir del nombre del flujo de cambios y siempre empieza por READ_.

Todas las consultas en las funciones de valor de tabla de flujo de cambios deben ejecutarse mediante la API sessions.executeStreamingSql en una transacción de solo lectura de un solo uso con una timestamp_bound de solo lectura sólida. La función de valor de tabla de flujo de cambios te permite especificar start_timestamp y end_timestamp para el intervalo de tiempo. Se puede acceder a todos los registros de cambios que se encuentren dentro del periodo de conservación mediante esta lectura de solo lectura segura timestamp_bound. Todos los demás TransactionOptions no son válidos para las consultas de flujo de cambios.

Además, si TransactionOptions.read_only.return_read_timestamp se define como true, el mensaje Transaction que describe la transacción devuelve el valor especial 2^63 - 2 en lugar de una marca de tiempo de lectura válida. Debe descartar este valor especial y no usarlo en ninguna consulta posterior.

Para obtener más información, consulta el artículo Flujo de trabajo de consulta de secuencias de cambios.

Transacciones inactivas

Una transacción se considera inactiva si no tiene lecturas ni consultas de SQL pendientes y no ha iniciado ninguna en los últimos 10 segundos. Spanner puede anular las transacciones inactivas para evitar que mantengan bloqueos indefinidamente. Si se aborta una transacción inactiva, la confirmación falla y devuelve un error ABORTED. Ejecutar periódicamente una consulta pequeña, como SELECT 1, dentro de la transacción puede evitar que se quede inactiva.