Utiliser l'isolation de lecture répétable

Cette page explique comment utiliser l'isolation de lecture reproductible dans Spanner.

La lecture reproductible est un niveau d'isolation qui garantit que toutes les opérations de lecture d'une transaction voient un instantané cohérent de la base de données tel qu'il existait au début de la transaction. Dans Spanner, ce niveau d'isolation est implémenté à l'aide d'une technique également appelée isolation d'instantané. Cette approche est utile dans les scénarios de concurrence lecture-écriture élevée, où de nombreuses transactions lisent des données que d'autres transactions peuvent modifier. En utilisant un instantané fixe, la lecture reproductible évite les impacts sur les performances du niveau d'isolation sérialisable plus rigoureux. Avec sa concurrence optimiste par défaut, les lectures peuvent s'exécuter sans acquérir de verrous et sans bloquer les écritures simultanées, ce qui peut entraîner moins de transactions abandonnées qui pourraient devoir être réessayées en raison de conflits de sérialisation. Avec la concurrence pessimiste, les opérations de lecture utilisent des instantanés, mais des verrous exclusifs s'appliquent aux données lues à partir de requêtes FOR UPDATE ou d'indices lock_scanned_ranges=exclusive, ainsi qu'aux données écrites avec des requêtes LMD. La simultanéité pessimiste réduit également la probabilité de conflits d'écriture. Pour en savoir plus, consultez Présentation du niveau d'isolation et Contrôle de simultanéité.

Définir le niveau d'isolation

Vous pouvez définir le niveau d'isolation des transactions en lecture-écriture au niveau du client de base de données ou au niveau de la transaction à l'aide des méthodes suivantes :

Bibliothèques clientes

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	pb "cloud.google.com/go/spanner/apiv1/spannerpb"
)

func writeWithTransactionUsingIsolationLevel(w io.Writer, db string) error {
	ctx := context.Background()

	// The isolation level specified at the client-level will be applied
	// to all RW transactions.
	cfg := spanner.ClientConfig{
		TransactionOptions: spanner.TransactionOptions{
			IsolationLevel: pb.TransactionOptions_SERIALIZABLE,
		},
	}
	client, err := spanner.NewClientWithConfig(ctx, db, cfg)
	if err != nil {
		return fmt.Errorf("failed to create client: %w", err)
	}
	defer client.Close()

	// The isolation level specified at the transaction-level takes
	// precedence over the isolation level configured at the client-level.
	// REPEATABLE_READ is used here to demonstrate overriding the client-level setting.
	txnOpts := spanner.TransactionOptions{
		IsolationLevel: pb.TransactionOptions_REPEATABLE_READ,
	}

	_, err = client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		// Read the current album title
		key := spanner.Key{1, 1}
		row, err := txn.ReadRow(ctx, "Albums", key, []string{"AlbumTitle"})
		if err != nil {
			return fmt.Errorf("failed to read album: %v", err)
		}
		var title string
		if err := row.Column(0, &title); err != nil {
			return fmt.Errorf("failed to get album title: %v", err)
		}
		fmt.Fprintf(w, "Current album title: %s\n", title)

		// Update the album title
		stmt := spanner.Statement{
			SQL: `UPDATE Albums
				SET AlbumTitle = @AlbumTitle
				WHERE SingerId = @SingerId AND AlbumId = @AlbumId`,
			Params: map[string]interface{}{
				"SingerId":   1,
				"AlbumId":    1,
				"AlbumTitle": "New Album Title",
			},
		}
		count, err := txn.Update(ctx, stmt)
		if err != nil {
			return fmt.Errorf("failed to update album: %v", err)
		}
		fmt.Fprintf(w, "Updated %d record(s).\n", count)
		return nil
	}, txnOpts)

	if err != nil {
		return fmt.Errorf("transaction failed: %v", err)
	}
	return nil
}

Java

static void isolationLevelSetting(DatabaseId db) {
  // The isolation level specified at the client-level will be applied to all
  // RW transactions.
  DefaultReadWriteTransactionOptions transactionOptions =
      DefaultReadWriteTransactionOptions.newBuilder()
          .setIsolationLevel(IsolationLevel.SERIALIZABLE)
          .build();
  SpannerOptions options =
      SpannerOptions.newBuilder()
          .setDefaultTransactionOptions(transactionOptions)
          .build();
  Spanner spanner = options.getService();
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbClient
      // The isolation level specified at the transaction-level takes precedence
      // over the isolation level configured at the client-level.
      .readWriteTransaction(Options.isolationLevel(IsolationLevel.REPEATABLE_READ))
      .run(transaction -> {
        // Read an AlbumTitle.
        String selectSql =
            "SELECT AlbumTitle from Albums WHERE SingerId = 1 and AlbumId = 1";
        String title = null;
        try (ResultSet resultSet = transaction.executeQuery(Statement.of(selectSql))) {
          if (resultSet.next()) {
            title = resultSet.getString("AlbumTitle");
          }
        }
        System.out.printf("Current album title: %s\n", title);

        // Update the title.
        String updateSql =
            "UPDATE Albums "
                + "SET AlbumTitle = 'New Album Title' "
                + "WHERE SingerId = 1 and AlbumId = 1";
        long rowCount = transaction.executeUpdate(Statement.of(updateSql));
        System.out.printf("%d record updated.\n", rowCount);
        return null;
      });
}

Node.js

// Imports the Google Cloud Spanner client library
const {Spanner, protos} = require('@google-cloud/spanner');
// The isolation level specified at the client-level will be applied
// to all RW transactions.
const defaultTransactionOptions = {
  isolationLevel:
    protos.google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE,
};

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

function runTransactionWithIsolationLevel() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);
  // The isolation level specified at the request level takes precedence over the isolation level configured at the client level.
  const isolationOptionsForTransaction = {
    isolationLevel:
      protos.google.spanner.v1.TransactionOptions.IsolationLevel
        .REPEATABLE_READ,
  };

  database.runTransaction(
    isolationOptionsForTransaction,
    async (err, transaction) => {
      if (err) {
        console.error(err);
        return;
      }
      try {
        const query =
          'SELECT AlbumTitle FROM Albums WHERE SingerId = 1 AND AlbumId = 1';
        const results = await transaction.run(query);
        // Gets first album's title
        const rows = results[0].map(row => row.toJSON());
        const albumTitle = rows[0].AlbumTitle;
        console.log(`previous album title ${albumTitle}`);

        const update =
          "UPDATE Albums SET AlbumTitle = 'New Album Title' WHERE SingerId = 1 AND AlbumId = 1";
        const [rowCount] = await transaction.runUpdate(update);
        console.log(
          `Successfully updated ${rowCount} record in Albums table.`,
        );
        await transaction.commit();
        console.log(
          'Successfully executed read-write transaction with isolationLevel option.',
        );
      } catch (err) {
        console.error('ERROR:', err);
        transaction.end();
      } finally {
        // Close the database when finished.
        await database.close();
      }
    },
  );
}
runTransactionWithIsolationLevel();

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
from google.cloud.spanner_v1 import TransactionOptions, DefaultTransactionOptions

# The isolation level specified at the client-level will be applied to all RW transactions.
isolation_options_for_client = TransactionOptions.IsolationLevel.SERIALIZABLE

spanner_client = spanner.Client(
    default_transaction_options=DefaultTransactionOptions(
        isolation_level=isolation_options_for_client
    )
)
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

# The isolation level specified at the request level takes precedence over the isolation level configured at the client level.
isolation_options_for_transaction = (
    TransactionOptions.IsolationLevel.REPEATABLE_READ
)

def update_albums_with_isolation(transaction):
    # Read an AlbumTitle.
    results = transaction.execute_sql(
        "SELECT AlbumTitle from Albums WHERE SingerId = 1 and AlbumId = 1"
    )
    for result in results:
        print("Current Album Title: {}".format(*result))

    # Update the AlbumTitle.
    row_ct = transaction.execute_update(
        "UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 1 and AlbumId = 1"
    )

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

database.run_in_transaction(
    update_albums_with_isolation, isolation_level=isolation_options_for_transaction
)

C++

void IsolationLevelSetting(std::string const& project_id,
                           std::string const& instance_id,
                           std::string const& database_id) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::Options;
  using ::google::cloud::StatusOr;

  auto db = spanner::Database(project_id, instance_id, database_id);

  // The isolation level specified at the client-level will be applied
  // to all RW transactions.
  auto options = Options{}.set<spanner::TransactionIsolationLevelOption>(
      spanner::Transaction::IsolationLevel::kSerializable);
  auto client = spanner::Client(spanner::MakeConnection(db, options));

  auto commit = client.Commit(
      [&client](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        // Read an AlbumTitle.
        auto sql = spanner::SqlStatement(
            "SELECT AlbumTitle from Albums WHERE SingerId = @SingerId and "
            "AlbumId = @AlbumId",
            {{"SingerId", spanner::Value(1)}, {"AlbumId", spanner::Value(1)}});
        auto rows = client.ExecuteQuery(txn, std::move(sql));
        for (auto const& row :
             spanner::StreamOf<std::tuple<std::string>>(rows)) {
          if (!row) return row.status();
          std::cout << "Current album title: " << std::get<0>(*row) << "\n";
        }

        // Update the title.
        auto update_sql = spanner::SqlStatement(
            "UPDATE Albums "
            "SET AlbumTitle = @AlbumTitle "
            "WHERE SingerId = @SingerId and AlbumId = @AlbumId",
            {{"AlbumTitle", spanner::Value("New Album Title")},
             {"SingerId", spanner::Value(1)},
             {"AlbumId", spanner::Value(1)}});
        auto result = client.ExecuteDml(txn, std::move(update_sql));
        if (!result) return result.status();
        std::cout << result->RowsModified() << " record updated.\n";

        return spanner::Mutations{};
      },
      // The isolation level specified at the transaction-level takes
      // precedence over the isolation level configured at the client-level.
      // kRepeatableRead is used here to demonstrate overriding the client-level
      // setting.
      Options{}.set<spanner::TransactionIsolationLevelOption>(
          spanner::Transaction::IsolationLevel::kRepeatableRead));

  if (!commit) throw std::move(commit).status();
  std::cout << "Update was successful [spanner_isolation_level_setting]\n";
}

C#


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

public class IsolationLevelAsyncSample
{
    public async Task IsolationLevelAsync(string projectId, string instanceId, string databaseId)
    {
        // Create client with IsolationLevel=Serializable.
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId};IsolationLevel=Serializable";

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

        // Create transaction options overriding IsolationLevel to RepeatableRead.
        var transactionOptions = SpannerTransactionCreationOptions.ReadWrite
            .WithIsolationLevel(IsolationLevel.RepeatableRead);

        using var transaction = await connection.BeginTransactionAsync(transactionOptions, null, CancellationToken.None);

        using var cmd = connection.CreateSelectCommand("SELECT AlbumTitle FROM Albums WHERE SingerId = 1 AND AlbumId = 1");
        cmd.Transaction = transaction;
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine($"AlbumTitle: {reader.GetFieldValue<string>("AlbumTitle")}");
            }
        }

        using var updateCmd = connection.CreateDmlCommand("UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 1 AND AlbumId = 1");
        updateCmd.Transaction = transaction;
        var rowCount = await updateCmd.ExecuteNonQueryAsync();
        Console.WriteLine($"{rowCount} records updated.");

        await transaction.CommitAsync();
    }
}

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;

/**
 * Shows how to run a Read Write transaction with isolation level options.
 *
 * Example:
 * ```
 * isolation_level($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function isolation_level(string $instanceId, string $databaseId): void
{
    // The isolation level specified at the client-level will be applied to all
    // RW transactions.
    $spanner = new SpannerClient([
        'isolationLevel' => IsolationLevel::SERIALIZABLE
    ]);
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    // The isolation level specified at the request level takes precedence over
    // the isolation level configured at the client level.
    $database->runTransaction(function (Transaction $t) {
        // Read an AlbumTitle.
        $results = $t->execute('SELECT AlbumTitle from Albums WHERE SingerId = 1 and AlbumId = 1');
        foreach ($results as $row) {
            printf('Current Album Title: %s' . PHP_EOL, $row['AlbumTitle']);
        }

        // Update the AlbumTitle.
        $rowCount = $t->executeUpdate('UPDATE Albums SET AlbumTitle = \'A New Title\' WHERE SingerId = 1 and AlbumId = 1');

        // Commit the transaction!
        $t->commit();

        printf('%d record(s) updated.' . PHP_EOL, $rowCount);
    }, [
        'transactionOptions' => [
            'isolationLevel' => IsolationLevel::REPEATABLE_READ
        ]
    ]);
}

Ruby

require "google/cloud/spanner"

def spanner_isolation_level project_id:, instance_id:, database_id:
  # Instantiates a client with isolation_level: :SERIALIZABLE
  spanner = Google::Cloud::Spanner.new project: project_id
  client = spanner.client instance_id, database_id, isolation_level: :SERIALIZABLE

  # Overrides isolation_level to :REPEATABLE_READ at transaction level
  client.transaction isolation_level: :REPEATABLE_READ do |tx|
    results = tx.execute_query "SELECT AlbumTitle FROM Albums WHERE SingerId = 1 AND AlbumId = 1"

    results.rows.each do |row|
      puts "AlbumTitle: #{row[:AlbumTitle]}"
    end

    row_count = tx.execute_update "UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 1 AND AlbumId = 1"

    puts "#{row_count} records updated."
  end
end

REST

Vous pouvez utiliser l'API REST TransactionOptions.isolation_level pour définir le niveau d'isolation des transactions en lecture/écriture et en lecture seule au niveau de la transaction. Les options valides sont TransactionOptions.SERIALIZABLE et TransactionOptions.REPEATABLE_READ. Par défaut, Spanner définit le niveau d'isolation sur l'isolation sérialisable.

Vous pouvez utiliser les pilotes Spanner pour définir le niveau d'isolation et le mode de verrouillage en lecture en tant que paramètre de connexion au niveau de la connexion ou en tant qu'option d'instruction SET au niveau de la transaction. Pour en savoir plus sur chaque pilote, consultez Présentation des pilotes.

Vous pouvez également configurer la concurrence du verrouillage pour chaque niveau d'isolation. Pour en savoir plus, consultez Contrôle de la simultanéité.

Cas d'utilisation non compatibles

  • Vous ne pouvez pas définir l'isolation de lecture reproductible sur les transactions LMD partitionnées.
  • Toutes les transactions en lecture seule fonctionnent déjà avec un instantané fixe et ne nécessitent pas de verrous. Par conséquent, la définition de l'isolation de lecture reproductible dans ce type de transaction ne modifie aucun comportement.
  • Vous ne pouvez pas définir l'isolation de lecture reproductible sur les opérations de lecture seule, à usage unique et de partition à l'aide des bibliothèques clientes Spanner. Les bibliothèques clientes Spanner ne permettent pas de définir l'isolation de lecture reproductible sur les opérations de requête de lecture seule, à usage unique et de partition.

Étapes suivantes