트랜잭션 개요

이 페이지에서는 Spanner의 트랜잭션을 설명하고 트랜잭션 실행을 위한 샘플 코드를 포함합니다.

소개

Spanner의 트랜잭션은 데이터베이스의 열, 행, 테이블 전체에 걸쳐 단일 논리 시점에서 원자적으로 실행되는 읽기 및 쓰기 집합입니다.

Spanner는 다음과 같은 트랜잭션 모드를 지원합니다.

  • 읽기-쓰기 잠금. 이러한 트랜잭션은 최악 잠금과 필요한 경우 2단계 커밋에 의존합니다. 읽기-쓰기 잠금 트랜잭션이 중단되면 애플리케이션을 다시 시도해야 할 수 있습니다.

  • 읽기 전용. 이 트랜잭션 유형은 여러 읽기에서 일관성을 보장하지만 쓰기를 허용하지 않습니다. 기본적으로 읽기 전용 트랜잭션은 외적 일관성을 보장하는 시스템에서 선택한 타임스탬프에서 실행되지만 이전의 타임스탬프에서 읽도록 구성될 수도 있습니다. 읽기 전용 트랜잭션은 커밋할 필요가 없으며 잠금을 사용하지 않습니다. 또한 읽기 전용 트랜잭션은 실행 전에 진행 중인 쓰기가 완료될 때까지 기다릴 수 있습니다.

  • Partitioned DML. 이 트랜잭션 유형은 데이터 조작 언어(DML) 문을 Partitioned DML로 실행합니다. Partitioned DML은 일괄 업데이트 및 삭제, 특히 정기적인 정리 및 백필을 위해 설계되었습니다. 블라인드 쓰기를 대량으로 커밋해야 하지만 원자적 트랜잭션이 필요하지 않은 경우 일괄 쓰기를 사용하여 Spanner 테이블을 일괄 수정할 수 있습니다. 자세한 내용은 일괄 쓰기를 사용하여 데이터 수정을 참조하세요.

이 페이지에서는 Spanner 트랜잭션의 일반적인 속성과 시맨틱스를 설명하고, Spanner의 읽기-쓰기, 읽기 전용, Partitioned DML 트랜잭션 인터페이스를 소개합니다.

읽기-쓰기 트랜잭션

읽기-쓰기 잠금 트랜잭션을 사용해야 하는 상황은 다음과 같습니다.

  • 한 개 이상의 읽기 결과에 따라 쓰기를 수행하는 경우, 동일한 읽기-쓰기 트랜잭션에서 해당 쓰기와 읽기를 수행해야 합니다.
    • 예: 은행 계좌 A의 잔액을 두 배로 늘립니다. A 잔액 읽기는 잔액을 두 배로 대체할 쓰기와 동일한 트랜잭션에 있어야 합니다.

  • 원자적으로 커밋해야 하는 쓰기 작업을 한 개 이상 수행하는 경우, 동일한 읽기-쓰기 트랜잭션으로 해당 쓰기 작업을 수행해야 합니다.
    • 예: A 계좌에서 B 계좌로 200달러를 이체합니다. 초기 계좌 잔액의 쓰기(A를 200달러 줄이는 쓰기와 B를 200달러 늘리는 쓰기)와 읽기 모두 동일한 트랜잭션에서 있어야 합니다.

  • 한 개 이상의 읽기 결과에 따라 쓰기 작업을 한 개 이상 수행할 수 있는 경우, 쓰기를 실행하지 않게 되더라고 동일한 읽기-쓰기 트랜잭션으로 해당 쓰기 및 읽기 작업을 수행해야 합니다.
    • 예: A의 현재 잔액이 500달러 이상인 경우, 은행 계좌 A에서 은행 계좌 B로 200달러를 이체합니다. 이 트랜잭션에는 A 잔액의 읽기와 쓰기가 포함된 조건문이 포함되어야 합니다.

다음은 읽기-쓰기 잠금 트랜잭션을 사용하지 않아야 하는 상황입니다.

  • 읽기만 수행하고 단일 읽기 메서드를 사용하여 읽기를 표현할 수 있는 경우, 해당 단일 읽기 메서드 또는 읽기 전용 트랜잭션을 사용해야 합니다. 단일 읽기는 읽기-쓰기 트랜잭션과 달리 잠기지 않습니다.

속성

Spanner의 읽기-쓰기 트랜잭션은 단일 논리 시점에서 원자적으로 읽기 및 쓰기 집합을 실행합니다. 또한 읽기-쓰기 트랜잭션이 실행되는 타임스탬프는 벽시계 시간과 일치하고, 직렬화 순서는 타임스탬프 순서와 일치합니다.

읽기-쓰기 트랜잭션을 왜 사용할까요? 읽기-쓰기 트랜잭션은 관계형 데이터베이스의 ACID 속성을 제공합니다. 실제로, Spanner 읽기-쓰기 트랜잭션은 기존의 ACID보다 훨씬 강력한 보장을 제공합니다. 아래 시맨틱스 섹션을 참조하세요.

격리

다음은 읽기-쓰기 및 읽기 전용 트랜잭션의 격리 속성입니다.

읽고 쓰는 트랜잭션

다음은 일련의 읽기(또는 쿼리) 및 쓰기가 포함된 트랜잭션을 성공적으로 커밋한 후 제공되는 격리 속성입니다.

  • 트랜잭션 내 모든 읽기는 트랜잭션의 커밋 타임스탬프에서 작성된 일관된 스냅샷을 반영하는 값을 반환했습니다.
  • 커밋 시점에 빈 행이나 범위가 남아 있었습니다.
  • 트랜잭션 내 모든 쓰기는 트랜잭션의 커밋 타임스탬프에서 커밋되었습니다.
  • 쓰기는 트랜잭션이 커밋될 때까지 어떠한 트랜잭션에도 표시되지 않았습니다.

특정 Spanner 클라이언트 드라이버에는 트랜잭션을 다시 실행하고 클라이언트에서 관찰된 데이터를 검증하여 일시적인 오류를 마스킹하는 트랜잭션 재시도 로직이 포함됩니다.

트랜잭션 자체의 관점과 Spanner 데이터베이스에 대한 다른 판독기와 기록기의 관점 모두에서 모든 읽기와 쓰기가 단일 시점에서 발생한 것으로 보이는 효과가 있습니다. 즉, 읽기와 쓰기는 결국 동일한 타임스탬프에서 발생합니다(아래 직렬 가능성 및 외적 일관성 섹션 설명 참조).

읽기 전용 트랜잭션

읽기 전용인 읽기-쓰기 트랜잭션에 대한 보장은 유사합니다. 해당 트랜잭션 내의 모든 읽기는 행이 없는 경우에도 동일한 타임스탬프의 데이터를 반환합니다. 한 가지 차이점은 데이터를 읽고 나중에 쓰기 없이 읽기-쓰기 트랜잭션을 커밋하는 경우 읽기 후와 커밋 전에 데이터가 데이터베이스에서 변경되지 않았다는 보장하지 않는다는 점입니다. 마지막으로 읽은 후 데이터가 변경되었는지 여부를 알고 싶으면 읽기-쓰기 트랜잭션에서 또는 강력한 읽기를 사용하여 데이터를 다시 읽는 것이 가장 좋은 방법입니다. 또한 효율성을 위해 읽기만 하고 쓰지는 않을 것을 미리 알고 있다면 읽기-쓰기 트랜잭션 대신 읽기 전용 트랜잭션을 사용해야 합니다.

원자성, 일관성, 내구성

격리 속성 외에도 Spanner는 원자성(트랜잭션에서 쓰기 중 어느 하나라도 커밋되면 모두 커밋됨), 일관성(트랜잭션 후 데이터베이스가 일관된 상태로 유지됨) 및 내구성(커밋된 데이터가 커밋된 상태로 유지됨)을 제공합니다.

이러한 속성의 이점

이러한 속성으로 인해 애플리케이션 개발자는 동시에 실행될 수 있는 다른 트랜잭션에서 해당 실행을 보호할 방법을 걱정하지 않고 각 트랜잭션의 정확성에만 집중할 수 있습니다.

인터페이스

Spanner 클라이언트 라이브러리는 읽기-쓰기 트랜잭션의 컨텍스트에서 트랜잭션 중단을 위한 재시도와 함께 작업 본문을 실행하기 위한 인터페이스를 제공합니다. 이 점을 설명하기 위한 약간의 컨텍스트가 있습니다. Spanner 트랜잭션은 커밋되기 전에 Spanner 트랜잭션을 여러 번 시도해야 할 수 있습니다. 예를 들어 두 트랜잭션이 교착 상태를 유발할 수 있는 방식으로 동시에 데이터에서 작업을 시도하면 Spanner는 다른 트랜잭션이 진행될 수 있도록 이들 중 하나를 중단합니다. (드물게, Spanner 내의 일시적인 이벤트로 인해 일부 트랜잭션이 중단될 수 있습니다.) 트랜잭션은 원자적이므로, 중단된 트랜잭션은 데이터베이스에 가시적인 영향을 미치지 않습니다. 따라서 트랜잭션이 성공할 때까지 트랜잭션을 다시 시도하여 실행해야 합니다.

Spanner 클라이언트 라이브러리에서 트랜잭션을 사용하는 경우, 함수 객체 형태로 트랜잭션 본문을 정의합니다(즉, 데이터베이스에 있는 한 개 이상의 테이블에서 수행하는 읽기 및 쓰기). 백그라운드에서 Spanner 클라이언트 라이브러리는 트랜잭션이 커밋되거나 다시 시도할 수 없는 오류가 발생할 때까지 함수를 반복적으로 실행합니다.

스키마 및 데이터 모델 페이지에 나와 있는 Albums 테이블MarketingBudget 열을 추가했다고 가정해 보겠습니다.

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

마케팅 부서는 Albums (1, 1)로 키가 지정된 앨범에 대해 마케팅을 추진하기로 결정하고 Albums (2, 2)의 예산에서 $200,000를 전환해달라고 요청했지만 해당 앨범의 예산 내에서만 이 금액을 사용할 수 있습니다. 읽기 결과에 따라 트랜잭션이 쓰기를 수행할 수 있으므로 이 작업에 읽기-쓰기 잠금 트랜잭션을 사용해야 합니다.

다음은 읽기-쓰기 트랜잭션을 실행하는 방법을 보여줍니다.

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"

시맨틱스

직렬 가능성 및 외적 일관성

Spanner는 '직렬 가능성'을 제공합니다. 즉, 고유 트랜잭션의 일부 읽기, 쓰기 및 기타 작업이 실제로 병렬로 발생하더라도 모든 트랜잭션이 순차적으로 실행되는 것처럼 나타난다는 것을 의미합니다. Spanner는 이 속성을 구현하기 위해 커밋된 트랜잭션 순서를 반영하는 커밋 타임스탬프를 할당합니다. 실제로, Spanner는 외적 일관성이라는 직렬 가능성보다 강력한 보장을 제공합니다. 트랜잭션은 커밋 타임스탬프에 반영되는 순서로 커밋되며 이러한 커밋 타임스탬프는 실시간을 반영되므로, 보유하고 있는 시계와 타임스탬프를 비교할 수 있습니다. 트랜잭션에서 읽기는 이 트랜잭션이 커밋되기 전에 커밋된 모든 내용을 확인하고 쓰기는 트랜잭션이 커밋된 후에 시작되는 모든 내용을 통해 확인됩니다.

예를 들어 아래 다이어그램과 같이 트랜잭션 두 개 실행을 고려합니다.

동일한 데이터를 읽는 두 트랜잭션의 실행을 보여주는 타임라인

파란색의 트랜잭션 Txn1은 일부 데이터 A를 읽고 A에 쓰기를 버퍼링한 후 성공적으로 커밋됩니다. 녹색의 트랜잭션 Txn2Txn1 이후에 시작되고 일부 데이터 B를 읽은 후 데이터 A를 읽습니다. Txn2Txn1A에 대한 쓰기를 커밋한 후 A의 값을 읽으므로 Txn1이 완료되기 전에 Txn2가 시작되었더라도 Txn2A에 대한 Txn1의 쓰기 효과를 인식합니다.

Txn1Txn2가 모두 실행되는 시간이 일부 중첩되지만 해당 커밋 타임스탬프 c1c2는 선형적 트랜잭션 순서를 따릅니다. 즉, Txn1의 모든 읽기 및 쓰기 효과는 단일 시점(c1)에 발생한 것으로 나타나고 Txn2의 모든 읽기 및 쓰기 효과는 단일 시점(c2)에 발생한 것으로 나타납니다. 또한 Txn2 전에 Txn1 발생 순서를 따르는 c1 < c2의 관계는 Txn1Txn2 모두를 커밋했으므로 보장되며 쓰기가 서로 다른 머신에서 발생했더라도 마찬가지입니다. 그러나 Txn2가 트랜잭션에서 읽기만 수행한 경우 c1 <= c2 관계가 됩니다.

읽기는 커밋 기록의 프리픽스를 관찰합니다. 읽기가 Txn2의 효과를 인식하면 Txn1의 효과도 인식할 수 있습니다. 성공적으로 커밋된 모든 트랜잭션에는 이 속성이 있습니다.

읽기 및 쓰기 보장

트랜잭션을 실행하기 위한 호출이 실패하면 사용자는 기본 커밋 호출이 실패한 오류에 따라 가질 수 있는 읽기 및 쓰기 보장을 가집니다.

예를 들어, 'Row Not Found(행을 찾을 수 없음)' 또는 'Row Already Exists(행이 이미 있음)'와 같은 오류는 버퍼링된 변형에 일부 오류가 발생했음을 의미합니다. 클라이언트가 업데이트하려고 하는 행이 존재하지 않는 경우를 예로 들 수 있습니다. 이 경우 읽기 일관성이 보장되고 쓰기가 적용되지 않습니다. 그뿐만 아니라, 행이 없으면 읽기 일관성이 보장됩니다.

트랜잭션 작업 취소

사용자는 트랜잭션 내의 다른 기존 작업에 영향을 주지 않으면서 언제든지 비동기 읽기 작업을 취소할 수 있습니다(예: 더 높은 수준의 작업이 취소되거나 읽기에서 받은 초기 결과에 따라 읽기를 중지하려는 경우).

그러나 읽기 취소를 시도하더라도 Spanner는 실제로 읽기가 취소된다고 보장하지 않습니다. 읽기 취소를 요청한 후 일부 다른 이유(예: 중단)로 읽기가 성공적으로 완료되거나 실패할 수 있습니다. 또한 취소된 읽기가 실제로 결과를 반환할 수 있으며, 이러한 불완전할 수 있는 결과는 트랜잭션 커밋의 일부로 확인됩니다.

읽기와 달리 트랜잭션 커밋 작업을 취소하면 트랜잭션이 이미 커밋되었거나 다른 이유로 실패하지 않는 한 트랜잭션이 중단됩니다.

성능

잠금

Spanner를 사용하면 여러 클라이언트가 동시에 동일한 데이터베이스와 상호 작용할 수 있습니다. 여러 동시 트랜잭션의 일관성을 보장하기 위해 Spanner는 공유 잠금과 배타적 잠금을 결합 및 사용하여 데이터 액세스를 제어합니다. 트랜잭션의 일부로 읽기를 수행하면 Spanner는 공유 읽기 잠금을 획득하여 트랜잭션이 커밋될 수 있을 때까지 다른 읽기가 계속해서 데이터에 액세스할 수 있게 합니다. 트랜잭션이 커밋되고 쓰기가 적용되면 트랜잭션은 배타적 잠금으로 업그레이드하려 합니다. 트랜잭션은 데이터에 대한 새로운 공유 읽기 잠금을 차단하고 기존 공유 읽기 잠금이 해제될 때까지 기다린 후 데이터에 배타적으로 액세스하기 위한 배타적 잠금을 설정합니다.

잠금에 대한 참고 사항:

  • 잠금은 행과 열로 세분화되어 수행됩니다. 트랜잭션 T1이 'foo' 행의 'A' 열을 잠갔고 트랜잭션 T2가 'foo' 행의 'B' 열을 쓰려는 경우에는 충돌이 발생하지 않습니다.
  • 기록되는 데이터를 동시에 읽지 않는 데이터 항목에 대한 쓰기('블라인드 쓰기'라고도 함)는 동일한 항목의 다른 블라인드 작성자와 충돌하지 않습니다. 각 쓰기의 커밋 타임스탬프가 데이터베이스에 적용되는 순서를 결정합니다. 결과적으로 사용자가 작성 중인 데이터를 읽은 경우에만 Spanner가 배타적 잠금으로 업그레이드되어야 합니다. 그렇지 않으면 Spanner는 작성자 공유 잠금이라는 공유 잠금을 사용합니다.
  • 읽기-쓰기 트랜잭션 내에서 행 조회를 수행할 때는 검사되는 행을 더 작은 범위로 제한하기 위해 보조 색인을 사용합니다. 그러면 Spanner가 테이블에서 더 적은 행 수를 잠그기 때문에 범위 바깥에서 행 동시 수정이 가능해집니다.
  • Spanner 외부 리소스에 대한 독점 액세스를 보장하는 데 잠금을 사용해서는 안 됩니다. 예를 들어 인스턴스의 컴퓨팅 리소스 간 데이터 이동을 허용하는 경우와 같이 Spanner에서 여러 가지 이유로 트랜잭션을 취소할 수 있습니다. 애플리케이션 코드에서 명시적으로 또는 Spanner JDBC 드라이버와 같은 클라이언트 코드에서 암시적으로 트랜잭션이 재시도되는 경우 실제로 커밋된 시도 중에만 잠금이 보장됩니다.

  • 잠금 통계 점검 도구를 사용하여 데이터베이스의 잠금 충돌을 조사할 수 있습니다.

교착 상태 감지

여러 트랜잭션이 교착 상태에 빠진 경우, Spanner는 이를 감지하고 트랜잭션 중 하나를 제외한 모든 트랜잭션을 강제로 중단합니다. 예를 들어 다음 상황을 생각해 보겠습니다. 트랜잭션 Txn1은 레코드 A에서 잠금을 가지고 있고 레코드 B에서는 잠금을 대기하고 있습니다. 한편, Txn2는 레코드 B에서 잠금을 가지고 있고 레코드 A에서 잠금을 대기하고 있습니다. 이 상황에서 계속 작업을 진행하기 위한 유일한 방법은 트랜잭션 중 하나를 중단하여 잠금을 해제함으로써 다른 트랜잭션이 진행될 수 있게 하는 방법입니다.

Spanner는 교착 상태 감지를 처리하기 위해 표준 'wound-wait' 알고리즘을 사용합니다. Spanner는 충돌하는 잠금을 요청하는 각 트랜잭션의 기간을 계속해서 추적합니다. 또한 더 오래된 트랜잭션이 덜 오래된 트랜잭션을 중단하도록 합니다('더 오래된'은 읽기, 쿼리 또는 커밋이 트랜잭션에서 먼저 발생한 것을 의미함).

오래된 트랜잭션에 우선순위를 부여함으로써 Spanner는 결국 모든 트랜잭션이 다른 트랜잭션보다 우선순위가 높아질 만큼 충분히 오래되었다는 이유로 결국 잠금을 획득할 수 있는 기회를 보장합니다. 예를 들어 판독기 공유 잠금을 획득한 트랜잭션은 작성자 공유 잠금이 필요한 더 오래된 트랜잭션에 의해 중단될 수 있습니다.

배포 실행

Spanner는 여러 서버에 걸쳐진 데이터로 트랜잭션을 실행할 수 있습니다. 이러한 이점은 단일 서버 트랜잭션에 비해 성능을 희생시키면서 얻어집니다.

어떤 유형의 트랜잭션이 배포될 수 있을까요? Spanner는 내부에서 여러 서버 사이에서 데이터베이스의 행에 대한 책임을 나눌 수 있습니다. 인접한 키를 가지고 있는 동일한 테이블의 두 행과 마찬가지로 어떤 행과 인터리브 처리된 테이블의 해당 행은 일반적으로 동일한 서버에서 처리됩니다. Spanner는 서로 다른 서버의 전체 행에서 트랜잭션을 수행할 수 있습니다. 그러나 경험에 비추어 볼 때 같은 위치에 있는 여러 행에 영향을 미치는 트랜잭션이 데이터베이스 전체나 큰 테이블 전체에 분산된 많은 행에 영향을 미치는 트랜잭션보다 빠르고 경제적입니다.

Spanner의 가장 효율적인 트랜잭션에는 원자적으로 적용되어야 하는 읽기 및 쓰기만 포함됩니다. 트랜잭션은 모든 읽기 및 쓰기가 키 공간의 동일한 부분에서 데이터에 액세스할 때 가장 빠릅니다.

읽기 전용 트랜잭션

읽기-쓰기 잠금 트랜잭션 외에도 Spanner는 읽기 전용 트랜잭션을 제공합니다.

동일한 타임스탬프에서 읽기를 두 개 이상 실행해야 하는 경우, 읽기 전용 트랜잭션을 사용합니다. Spanner의 단일 읽기 메서드 중 하나를 사용하여 읽기를 표현할 수 있는 경우, 이 단일 읽기 메서드를 대신 사용해야 합니다. 이러한 단일 읽기 호출 사용 시의 성능과 읽기 전용 트랜잭션에서 수행된 단일 읽기의 성능을 비교해야 합니다.

대량의 데이터 읽기를 수행할 경우에는 파티션을 사용하여 데이터 병렬로 읽기를 수행하는 것이 좋습니다.

읽기 전용 트랜잭션은 쓰지 않으므로 잠금을 보유하지 않으며 다른 트랜잭션을 차단하지 않습니다. 읽기 전용 트랜잭션은 트랜잭션 커밋 기록의 일관된 프리픽스를 관찰하므로 애플리케이션이 항상 일관된 데이터를 가져옵니다.

속성

Spanner 읽기 전용 트랜잭션은 읽기 전용 트랜잭션 자체의 관점과 Spanner 데이터베이스에 대한 다른 판독기 및 작성자의 관점 모두에서 볼 때 단일 논리 시점에서 일련의 읽기를 실행합니다. 즉, 읽기 전용 트랜잭션은 항상 트랜잭션 기록에서 선택한 시점에서 일관된 데이터베이스 상태를 관찰합니다.

인터페이스

Spanner는 읽기 전용 트랜잭션의 컨텍스트에서 트랜잭션 중단을 위한 재시도와 함께 작업 본문을 실행하기 위한 인터페이스를 제공합니다.

다음은 읽기 전용 트랜잭션을 사용하여 동일한 타임스탬프에서 2개의 읽기에 일관된 데이터를 얻는 방법을 보여 줍니다.

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(AmbientTransactionOptions.ForTimestampBoundReadOnly(), 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()) {
    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));
    }
    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));
      }
    }
  }
}

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

Partitioned DML 트랜잭션

파티션을 나눈 데이터 조작 언어(Partitioned DML)를 사용하면 트랜잭션 제한에 걸리거나 전체 테이블을 잠그지 않고 대규모 UPDATEDELETE 문을 실행할 수 있습니다. Spanner는 키 공간을 파티션으로 나누고 각 파티션에서 별도의 읽기-쓰기 트랜잭션으로 DML 문을 실행합니다.

코드에서 명시적으로 만드는 읽기-쓰기 트랜잭션에서 DML 문을 실행하세요. 자세한 내용은 DML 사용을 참조하세요.

속성

클라이언트 라이브러리 메서드 또는 Google Cloud CLI 사용 여부에 관계없이 한 번에 Partitioned DML 문 하나만 실행할 수 있습니다.

파티션을 나눈 트랜잭션은 커밋이나 롤백을 지원하지 않습니다. Spanner는 DML 문을 즉시 실행하고 적용합니다. 작업을 취소하거나 작업이 실패하면 Spanner는 실행 중인 파티션을 모두 취소하며, 나머지 파티션을 시작하지 않습니다. Spanner는 이미 실행된 파티션을 롤백하지 않습니다.

인터페이스

Spanner는 단일 Partitioned DML 문을 실행하기 위한 인터페이스를 제공합니다.

Examples

다음 코드 예시에서는 Albums 테이블의 MarketingBudget 열을 업데이트합니다.

C++

ExecutePartitionedDml() 함수를 사용하여 Partitioned DML 문을 실행합니다.

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#

ExecutePartitionedUpdateAsync() 메서드를 사용하여 Partitioned DML 문을 실행합니다.


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

PartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.


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

executePartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

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

runPartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

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

executePartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

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

execute_partitioned_dml() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

execute_partitioned_update() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

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

다음 코드 예시에서는 SingerId 열을 기준으로 Singers 테이블에서 행을 삭제합니다.

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