Descripción general de las transacciones

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

Una transacción en Spanner es un conjunto de operaciones de lectura y escritura que se ejecutan de manera atómica en un único momento lógico 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. Es posible que fallen y requieran reintentos. Si bien se limitan a una sola base de datos, pueden modificar datos en varias tablas dentro de esa base de datos.

  • Solo lectura: Estas transacciones garantizan la coherencia de los datos en varias operaciones de lectura, pero no permiten modificaciones de datos. Se ejecutan en una marca de tiempo determinada por el sistema para garantizar la coherencia o en una marca de tiempo pasada 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 declaraciones DML como operaciones de DML particionado. Está optimizada para actualizaciones y eliminaciones de datos a gran escala, como la limpieza o la inserción masiva de datos. Para numerosas escrituras que no necesitan una transacción atómica, considera usar escrituras por lotes. Consulta Cómo modificar datos con escrituras por lotes para obtener más detalles.

Transacciones de lectura y escritura

Usa transacciones de bloqueo de lectura y escritura 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 de forma externa.

Minimiza el tiempo durante el que una transacción está activa. Las duraciones de transacción más cortas aumentan la probabilidad de una confirmación exitosa y reducen la contención. Spanner intenta mantener activos los bloqueos de lectura mientras la transacción siga realizando lecturas y no haya finalizado a través de las operaciones sessions.commit o sessions.rollback. Si el cliente permanece inactivo durante períodos prolongados, es posible que Spanner libere los bloqueos de la transacción y la anule.

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

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

  • Si debes confirmar una o más operaciones de escritura de forma atómica, realiza esas escrituras dentro de la misma transacción de lectura y escritura. Por ejemplo, si transfieres USD 200 de la cuenta A a la cuenta B, realiza ambas operaciones de escritura (disminuir la cuenta A en USD 200 y aumentar la cuenta B en USD 200) y las lecturas de los saldos de cuenta iniciales dentro de la misma transacción.
  • Si deseas duplicar el saldo de la cuenta A, realiza las operaciones de lectura y escritura dentro de la misma transacción. Esto garantiza que el sistema lea el saldo antes de duplicarlo y actualizarlo.
  • Si es posible que realices una o más operaciones de escritura que dependan de los resultados de una o más operaciones de lectura, realiza esas escrituras y lecturas en la misma transacción de lectura y escritura, incluso si las operaciones de escritura no se ejecutan. Por ejemplo, si quieres transferir USD 200 de la cuenta A a la cuenta B solo si el saldo actual de A es superior a USD 500, incluye la lectura del saldo de A y las operaciones de escritura condicionales dentro de la misma transacción, incluso si no se realiza la transferencia.

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

  • Si solo realizas operaciones de lectura y puedes expresarlas con un método de lectura única, 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 cliente de Spanner proporcionan una interfaz para ejecutar un cuerpo de trabajo dentro de una transacción de lectura y escritura, con reintentos para anulaciones de transacciones. Una transacción de Spanner puede requerir varios reintentos antes de confirmarse.

Hay varias situaciones que pueden provocar la anulación de transacciones. Por ejemplo, si dos transacciones intentan modificar datos de forma simultánea, es posible que se produzca un interbloqueo. En estos casos, Spanner anula una transacción para permitir que la otra continúe. Con menos frecuencia, los eventos transitorios dentro de Spanner también pueden provocar la anulación de transacciones.

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

Cuando usas una transacción en una biblioteca 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 más tablas de la base de datos. La biblioteca cliente de Spanner ejecuta esta función de manera reiterada hasta que la transacción se confirma correctamente o se produce un error que no se puede volver a intentar.

Ejemplo

Supongamos que tienes 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 USD 200,000 del presupuesto de Albums (2, 2) a Albums (1, 1), pero solo si el dinero está disponible en el presupuesto de ese álbum. Debes usar una transacción de bloqueo de lectura y escritura para esta operación, ya que la transacción puede realizar operaciones de escritura según el 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;

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 operaciones de lectura y escritura 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 este orden de marcas 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 de ACID típicas.

Debido a estas propiedades, como desarrollador de aplicaciones, puedes enfocarte en la precisión de cada transacción, sin preocuparte por proteger su ejecución de otras transacciones que podrían ejecutarse al mismo tiempo.

Aislamiento para 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 rangos 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 después de que se confirma la transacción.

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

El efecto es que todas las operaciones de lectura y escritura parecen haber ocurrido en un momento determinado, desde la perspectiva de la transacción y desde la perspectiva de otros lectores y escritores en la base de datos de Spanner. Esto significa que las lecturas y escrituras ocurren en la misma marca de tiempo. Para ver un ejemplo, consulta Serialización y coherencia externa.

Aislamiento para transacciones de lectura

Cuando una transacción de lectura y escritura solo realiza operaciones de lectura, proporciona garantías de coherencia similares a las de una transacción de solo lectura. Todas las lecturas dentro de la transacción muestran datos de una marca de tiempo coherente, incluida la confirmación de filas inexistentes.

Una diferencia se da cuando una transacción de lectura y escritura se confirma sin ejecutar una operación de escritura. En esta situación, no hay garantía de que los datos leídos dentro de la transacción hayan permanecido sin cambios en la base de datos entre la operación de lectura y la confirmación de la transacción.

Para garantizar la actualización de los datos y validar que no se hayan modificado desde su última recuperación, se requiere una lectura posterior. Esta segunda lectura se puede realizar dentro de otra transacción de lectura y escritura o con una lectura sólida.

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

Atomicidad, coherencia y durabilidad

Además del aislamiento, Spanner proporciona las otras garantías de 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 dentro de una transacción, toda la transacción se revierte a su estado original, lo que garantiza la integridad de los datos.
  • Coherencia: Una transacción debe mantener la integridad de las reglas y restricciones de la base de datos. Después de que se completa una transacción, la base de datos debe estar en un estado válido, de acuerdo con las reglas predefinidas.
  • Durabilidad Después de que se confirma una transacción, sus cambios se almacenan de forma permanente en la base de datos y persisten en caso de fallas del sistema, cortes de energía o cualquier otra interrupción.

Serialización y coherencia externa

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

La serialización garantiza que todas las transacciones parezcan ejecutarse una después de otra en un solo orden secuencial, incluso si se procesan de forma simultánea. Spanner logra esto asignando marcas de tiempo de confirmación a las transacciones, lo que refleja el orden en que se confirmaron.

Spanner proporciona una garantía aún más sólida conocida como coherencia externa. Esto significa que las transacciones no solo se confirman en un orden que se refleja en sus marcas de tiempo de confirmación, sino que estas marcas de tiempo también se alinean con el tiempo del mundo real. Esto te permite comparar las marcas de tiempo de confirmación con el tiempo real, lo que proporciona una vista coherente y ordenada a nivel global 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 marca de tiempo de confirmación de Txn2.

Considera el siguiente ejemplo:

Cronograma en el que se muestra la ejecución de dos transacciones que leen los mismos datos

En este caso, durante la línea de tiempo t, ocurre lo siguiente:

  • La transacción Txn1 lee los datos A, prepara una escritura en A y, luego, la confirma de forma correcta.
  • La transacción Txn2 comienza después de que se inicia Txn1. Lee los datos B y, luego, los datos A.

Aunque Txn2 comenzó antes de que se completara Txn1, Txn2 observa los cambios que Txn1 realizó en A. Esto se debe a que Txn2 lee A después de que Txn1 confirma su escritura en A.

Si bien 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 dentro de Txn1 parecen haber ocurrido en un solo punto en el tiempo, c1.
  • Todas las lecturas y escrituras dentro de Txn2 parecen haber ocurrido en un solo punto en el tiempo, c2.
  • Fundamentalmente, c1 es anterior a c2 para las escrituras confirmadas, incluso si las escrituras se produjeron en diferentes máquinas. Si Txn2 solo realiza lecturas, c1 es anterior o se produce al mismo tiempo que c2.

Este ordenamiento sólido significa que, si una operación de lectura posterior observa los efectos de Txn2, también observa los efectos de Txn1. Esta propiedad es verdadera para todas las transacciones confirmadas correctamente.

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

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

Por ejemplo, un error como “No se encontró la fila” o “La fila ya existe” significa que la escritura de las mutaciones almacenadas en búfer encontró algún error; por ejemplo, no existe una fila que el cliente intenta actualizar. En ese caso, las lecturas tienen coherencia garantizada, las escrituras no se aplican y se garantiza que la inexistencia de la fila también será coherente con las lecturas.

Garantías de lectura y escritura en caso de falla 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 produjo durante la operación commit.

Por ejemplo, un mensaje de error como “No se encontró la fila” o “La fila ya existe” indica un problema durante la escritura de las mutaciones almacenadas en búfer. Esto puede ocurrir si, por ejemplo, no existe una fila que el cliente intenta actualizar. En estos casos:

  • Las lecturas son coherentes: Se garantiza que todos los datos leídos durante la transacción son coherentes hasta el punto del error.
  • No se aplican las escrituras: Las mutaciones que intentó realizar la transacción no se confirman en la base de datos.
  • Coherencia de la fila: La no existencia (o el estado existente) de la fila que activó el error es coherente con las lecturas realizadas dentro de la transacción.

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

Sin embargo, es importante comprender que solicitar la cancelación de una lectura no garantiza su finalización inmediata. Después de una solicitud de cancelación, es posible que la operación de lectura aún haga lo siguiente:

  • Completar correctamente: Es posible que la lectura termine de procesarse y devuelva resultados antes de que la cancelación surta efecto.
  • Falla por otro motivo: La lectura podría finalizar debido a un error diferente, como una anulación.
  • Devuelve resultados incompletos: La lectura puede devolver resultados parciales, que luego se validan como parte del proceso de confirmación de la transacción.

También vale la pena destacar la distinción con las operaciones de transacción commit: cancelar una commit anula 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 el 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 de forma simultánea. 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 lograrlo, hace lo siguiente:

  • Bloquea cualquier solicitud nueva de bloqueo de lectura compartido en los datos afectados.
  • Espera a que se liberen todos los bloqueos de lectura compartidos existentes en esos datos.
  • Después de que se borran todos los bloqueos de lectura compartidos, se coloca un bloqueo exclusivo, lo que le otorga acceso exclusivo a los datos durante la escritura.

Notas sobre los bloqueos:

  • Nivel de detalle: Spanner aplica bloqueos a nivel de detalle de la fila y la columna. Esto significa que, si la transacción T1 mantiene un bloqueo en la columna A de la fila albumid, la transacción T2 puede escribir de forma simultánea en la columna B de la misma fila albumid sin 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 para las escrituras sin lecturas se determina según sus marcas de tiempo de confirmación, lo que permite que varios escritores operen en el mismo elemento de forma simultánea 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: Cuando realices búsquedas de filas dentro de una transacción de lectura y escritura, el uso de índices secundarios puede mejorar significativamente el rendimiento. Si usas índices secundarios para limitar las filas analizadas a un rango más pequeño, Spanner bloqueará menos filas en la tabla, lo que permitirá una mayor modificación simultánea de filas fuera de ese rango específico.
  • Acceso exclusivo a recursos externos: Los bloqueos internos de Spanner están diseñados para garantizar la coherencia de los datos dentro de la propia base de datos de Spanner. No los uses para garantizar el acceso exclusivo a recursos fuera de Spanner. Spanner puede anular transacciones por varios motivos, incluidas las optimizaciones internas del sistema, como el movimiento de datos entre recursos de procesamiento. Si se vuelve a intentar una transacción (ya sea de forma explícita por el código de tu aplicación o de forma implícita por bibliotecas cliente como el controlador JDBC de Spanner), se garantiza que las exclusiones solo se mantuvieron durante el intento de confirmación exitoso.
  • 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 interbloqueo

Spanner detecta cuándo varias transacciones podrían estar interbloqueadas y fuerza la anulación de todas excepto una. Considera esta situación: Txn1 mantiene un bloqueo en el registro A y espera un bloqueo en el registro B, mientras que Txn2 mantiene un bloqueo en el registro B y espera un bloqueo en el registro A. Para resolver este problema, una de las transacciones debe anularse, liberar su bloqueo y permitir que la otra continúe.

Spanner usa el algoritmo estándar de wound-wait para la detección de interbloqueos. De forma interna, Spanner realiza un seguimiento de la antigüedad de cada transacción que solicita bloqueos en conflicto. Permite que las transacciones más antiguas anulen las más recientes. Una transacción más antigua es aquella cuya lectura, consulta o confirmación más temprana ocurrió antes.

Al priorizar las transacciones más antiguas, Spanner garantiza que todas las transacciones adquieran bloqueos eventualmente después de que tengan la antigüedad suficiente para tener mayor prioridad. Por ejemplo, una transacción más antigua que necesita un bloqueo compartido de escritura puede anular una transacción más reciente que contenga un bloqueo compartido de lectura.

Ejecución distribuida

Spanner puede ejecutar transacciones con datos que se distribuyen en varios servidores, aunque esta capacidad tiene un costo 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 en muchos servidores. Por lo general, el mismo servidor entrega una fila y las filas correspondientes de una tabla intercalada; lo mismo sucede con dos filas de una misma tabla con claves cercanas. Spanner puede realizar transacciones entre filas en servidores diferentes. Sin embargo, como regla general, las transacciones que afectan muchas filas en la misma ubicación son más rápidas y económicas que las que afectan muchas filas dispersas en la base de datos o en una tabla grande.

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

Transacciones de solo lectura

Además de las transacciones de bloqueo 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 con uno de los métodos de lectura única de Spanner, debes usar ese método. El rendimiento del uso de una llamada de lectura única debe ser similar al rendimiento de una lectura única realizada en una transacción de solo lectura.

Si lees una gran cantidad de datos, considera usar particiones para leer los datos en paralelo.

Debido a que 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 confirmación de transacciones, por lo que la aplicación siempre obtiene datos coherentes.

Interfaz

Spanner proporciona una interfaz para ejecutar un cuerpo de trabajo en el contexto de una transacción de solo lectura, con reintentos para anulaciones de transacciones.

Ejemplo

En el siguiente ejemplo, se muestra cómo usar una transacción de solo lectura para obtener datos coherentes para dos lecturas en 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 instantáneas

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

Estas transacciones de solo lectura de instantáneas ofrecen un enfoque más simple para las lecturas coherentes en comparación con las transacciones de lectura y escritura de bloqueo. Esto se debe a los siguientes 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 simultáneas de lectura y escritura.
  • Sin anulaciones: Estas transacciones nunca se anulan. Si bien pueden fallar si la marca de tiempo de lectura elegida se recolecta como basura, la política de recolección 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 o sessions.rollback y, de hecho, se les impide hacerlo.

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

  • Lecturas sólidas: Estas lecturas garantizan que verás los efectos de todas las transacciones confirmadas antes de que comenzara la lectura. Todas las filas dentro de una sola lectura son coherentes. Sin embargo, las lecturas sólidas no se pueden repetir, aunque sí muestran una marca de tiempo, y volver a leer en esa misma marca de tiempo sí se puede repetir. Dos transacciones consecutivas de solo lectura sólida podrían producir resultados diferentes debido a escrituras simultáneas. Las consultas en los flujos de cambios deben usar este límite. Para obtener más detalles, consulta TransactionOptions.ReadOnly.strong.
  • Inactividad exacta: Esta opción ejecuta lecturas en una marca de tiempo que especificas, ya sea como una marca de tiempo absoluta o como una duración de inactividad relativa a la hora actual. Garantiza que observes un prefijo coherente del historial de transacciones global hasta esa marca de tiempo y bloquea las transacciones conflictivas que podrían confirmarse con una marca de tiempo inferior o igual a la marca de tiempo de lectura. Si bien es un poco más rápido que los modos de obsolescencia limitada, es posible que devuelva datos más antiguos. Para obtener más detalles, consulta TransactionOptions.ReadOnly.read_timestamp y TransactionOptions.ReadOnly.exact_staleness.
  • Inactividad limitada: Spanner selecciona la marca de tiempo más reciente dentro de un límite de inactividad definido por el usuario, lo que permite la ejecución en la réplica disponible más cercana sin bloquear. Todas las filas que se devuelven son coherentes. Al igual que las lecturas sólidas, la inactividad limitada no se puede repetir, ya que las diferentes lecturas pueden ejecutarse en diferentes marcas de tiempo, incluso con el mismo límite. Estas lecturas operan en dos fases (negociación de la marca de tiempo y, luego, lectura) y suelen ser un poco más lentas que la inactividad exacta, pero a menudo muestran 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 marcas de tiempo requiere saber de antemano qué filas se leerán. Para obtener más detalles, consulta TransactionOptions.ReadOnly.max_staleness y TransactionOptions.ReadOnly.min_read_timestamp.

Transacciones de DML particionado

Puedes usar el DML particionado para ejecutar declaraciones UPDATE y DELETE a gran escala sin encontrar límites de transacciones ni bloquear una tabla completa. Spanner logra esto 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, ejecuta declaraciones dentro de transacciones de lectura y escritura que creas de forma explícita en tu código. Para obtener más detalles, consulta Usa DML.

Interfaz

Spanner proporciona la interfaz TransactionOptions.partitionedDml para ejecutar una sola declaración DML particionada.

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 declaración DML particionada.

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 declaració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 declaració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 declaració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 declaració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 declaració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 declaració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 declaració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 borran las filas de la tabla Singers según 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 del DML particionado.

Información sobre la ejecución de DML particionado

Solo puedes ejecutar una declaración DML particionada a la vez, ya sea que uses un método de biblioteca cliente o Google Cloud CLI.

Las transacciones particionadas no admiten confirmaciones ni reversiones. Spanner ejecuta y aplica la declaración DML de inmediato. 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 ninguna partición que ya se haya ejecutado.

Estrategia de adquisición de bloqueos de DML particionado

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

Límites de transacciones por sesión

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

Marcas de tiempo de lectura antiguas y recolección de elementos no utilizados de versiones

Spanner realiza la recolección de elementos no utilizados de versiones para recopilar los datos borrados o reemplazados y recuperar el almacenamiento. De forma predeterminada, se recuperan los datos con más de una hora de antigüedad. 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. Cuando las lecturas se vuelven demasiado antiguas durante la ejecución, fallan y muestran el error FAILED_PRECONDITION.

Consultas en flujos de cambios

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

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

Todas las consultas en las TVF de flujo de cambios se deben ejecutar con la API de sessions.executeStreamingSql dentro de una transacción de solo lectura de un solo uso con un timestamp_bound de solo lectura sólido. La TVF de la transmisión de cambios te permite especificar start_timestamp y end_timestamp para el intervalo de tiempo. Se puede acceder a todos los registros de cambios dentro del período de retención con este timestamp_bound de solo lectura sólido. Todos los demás TransactionOptions no son válidos para las consultas de transmisiones de cambios.

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

Para obtener más información, consulta Flujo de trabajo de consultas de flujos de cambios.

Transacciones inactivas

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