Cloud Spanner 使用入门 (C#)

目标

本教程将引导您使用适用于 C# 的 Cloud Spanner 客户端库完成以下步骤:

  • 创建 Cloud Spanner 实例和数据库。
  • 写入、读取数据库中的数据和对数据执行 SQL 查询。
  • 更新数据库架构。
  • 使用读写事务更新数据。
  • 向数据库添加二级索引。
  • 使用索引来读取数据和对数据执行 SQL 查询。
  • 使用只读事务检索数据。

费用

本教程使用 Cloud Spanner,它是 Google Cloud 的收费组件。如需了解使用 Cloud Spanner 的费用,请参阅价格

准备工作

完成设置中介绍的步骤,包括创建和设置默认 Google Cloud 项目、启用结算功能、启用 Cloud Spanner API 以及设置 OAuth 2.0 来获取身份验证凭据以使用 Cloud Spanner API。

尤其要确保运行 gcloud auth application-default login,以便使用身份验证凭据设置本地开发环境。

准备本地 C# 环境

  1. GOOGLE_PROJECT_ID 环境变量设置为您的 Google Cloud 项目 ID

    1. 首先,为当前 PowerShell 会话设置 GOOGLE_PROJECT_ID

      $env:GOOGLE_PROJECT_ID = "MY_PROJECT_ID"
    2. 然后,为在此命令之后创建的所有进程设置 GOOGLE_PROJECT_ID

      [Environment]::SetEnvironmentVariable("GOOGLE_PROJECT_ID", "MY_PROJECT_ID", "User")
  2. 下载凭据。

    1. 转到 Google Cloud Console 中的凭据页面。

      转到“凭据”页面

    2. 点击创建凭据并选择服务帐号密钥

    3. 在“服务帐号”下,选择 Compute Engine 默认服务帐号,并使“密钥类型”下方的 JSON 处于选中状态。点击创建。您的计算机将下载一个 JSON 文件。

  3. 设置凭据。对于位于 C 盘上 CURRENT_USER 的 Downloads 目录中名为 FILENAME.json 的文件,运行以下命令来将 GOOGLE_APPLICATION_CREDENTIALS 设置为指向 JSON 密钥:

    1. 首先,为此 PowerShell 会话设置 GOOGLE_APPLICATION_CREDENTIALS

      $env:GOOGLE_APPLICATION_CREDENTIALS = "C:\Users\CURRENT_USER\Downloads\FILENAME.json"
    2. 然后,为在此命令之后创建的所有进程设置 GOOGLE_APPLICATION_CREDENTIALS

      [Environment]::SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "C:\Users\CURRENT_USER\Downloads\FILENAME.json", "User")
  4. 将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/dotnet-docs-samples
    

    或者,您也可以以 zip 文件的形式下载该示例并将其解压缩。

  5. 使用 Visual Studio 2017 或更高版本打开 Spanner.sln(位于下载的代码库的 dotnet-docs-samples\spanner\api 目录中),然后对其进行构建。

  6. 切换到下载的代码库中包含已编译的应用的目录。例如:

    cd dotnet-docs-samples\spanner\api\Spanner
    

创建实例

在首次使用 Cloud Spanner 时,必须创建一个实例,实例是 Cloud Spanner 数据库使用的资源分配单位。创建实例时,请选择一个实例配置(决定数据的存储位置),同时选择要使用的节点数(决定实例中服务资源和存储资源的数量)。

执行以下命令,在区域 us-central1 中创建具有 1 个节点的 Cloud Spanner 实例:

gcloud spanner instances create test-instance --config=regional-us-central1 `
    --description="Test Instance" --nodes=1

请注意,此命令将创建一个具有以下特征的实例:

  • 实例 ID 为 test-instance
  • 显示名为 Test Instance
  • 实例配置为 regional-us-central1(单区域配置将数据存储在单个区域中,而多区域配置则将数据分布在多个区域中。如需了解详情,请参阅实例。)
  • 节点数为 1(node_count 对应于实例中数据库可用的服务资源和存储资源的数量。如需了解详情,请参阅节点数。)

应显示如下消息:

Creating instance...done.

浏览示例文件

示例代码库包含一个示例,其展示了如何在 C# 中使用 Cloud Spanner。

请浏览 spanner/api/Spanner/Program.cs 文件,其中说明了如何创建数据库和修改数据库架构。该数据使用架构和数据模型页面中显示的示例架构。

创建数据库

通过在命令行运行以下命令,在名为 test-instance 的实例中创建名为 example-db 的数据库。

dotnet run createSampleDatabase $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Created sample database example-db on instance test-instance

您刚刚创建了一个 Cloud Spanner 数据库。以下是创建数据库的代码。

// Initialize request connection string for database creation.
string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    string createStatement = $"CREATE DATABASE `{databaseId}`";
    var cmd = connection.CreateDdlCommand(createStatement);
    await cmd.ExecuteNonQueryAsync();
}
// Update connection string with Database ID for table creation.
connectionString = connectionString + $"/databases/{databaseId}";
using (var connection = new SpannerConnection(connectionString))
{
    // Define create table statement for table #1.
    string createTableStatement =
   @"CREATE TABLE Singers (
         SingerId INT64 NOT NULL,
         FirstName    STRING(1024),
         LastName STRING(1024),
         ComposerInfo   BYTES(MAX)
     ) PRIMARY KEY (SingerId)";
    // Make the request.
    var cmd = connection.CreateDdlCommand(createTableStatement);
    await cmd.ExecuteNonQueryAsync();
    // Define create table statement for table #2.
    createTableStatement =
    @"CREATE TABLE Albums (
         SingerId     INT64 NOT NULL,
         AlbumId      INT64 NOT NULL,
         AlbumTitle   STRING(MAX)
     ) PRIMARY KEY (SingerId, AlbumId),
     INTERLEAVE IN PARENT Singers ON DELETE CASCADE";
    // Make the request.
    cmd = connection.CreateDdlCommand(createTableStatement);
    await cmd.ExecuteNonQueryAsync();
}

该代码还为一个基本的音乐应用定义了两个表:SingersAlbums。本页面会一直用到这两个表。请查看示例架构(如果您还没有查看)。

下一步是将数据写入数据库。

创建数据库客户端

您必须先创建一个 Spanner​Connection,然后才能执行读写操作:


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

namespace GoogleCloudSamples.Spanner
{
    public class QuickStart
    {
        static async Task MainAsync()
        {
            string projectId = "YOUR-PROJECT-ID";
            string instanceId = "my-instance";
            string databaseId = "my-database";
            string connectionString =
                $"Data Source=projects/{projectId}/instances/{instanceId}/"
                + $"databases/{databaseId}";
            // Create connection to Cloud Spanner.
            using (var connection = new SpannerConnection(connectionString))
            {
                // Execute a simple SQL statement.
                var cmd = connection.CreateSelectCommand(
                    @"SELECT ""Hello World"" as test");
                using (var reader = await cmd.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        Console.WriteLine(
                            reader.GetFieldValue<string>("test"));
                    }
                }
            }
        }
        public static void Main(string[] args)
        {
            MainAsync().Wait();
        }
    }
}

您可以将 Spanner​Connection 视为数据库连接:您与 Cloud Spanner 的所有交互都必须通过 Spanner​Connection 进行。

如需了解详情,请参阅 Spanner​Connection 参考文档。

使用 DML 写入数据

您可以在读写事务中使用数据操纵语言 (DML) 插入数据。

使用 ExecuteNonQueryAsync() 方法来执行 DML 语句。

public static async Task WriteUsingDmlCoreAsync(
    string projectId,
    string instanceId,
    string databaseId)
{
    string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";

    // Create connection to Cloud Spanner.
    using (var connection =
        new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        SpannerCommand cmd = connection.CreateDmlCommand(
            "INSERT Singers (SingerId, FirstName, LastName) VALUES "
               + "(12, 'Melissa', 'Garcia'), "
               + "(13, 'Russell', 'Morales'), "
               + "(14, 'Jacqueline', 'Long'), "
               + "(15, 'Dylan', 'Shaw')");
        int rowCount = await cmd.ExecuteNonQueryAsync();
        Console.WriteLine($"{rowCount} row(s) inserted...");
    }
}

使用 writeUsingDml 参数运行示例。

dotnet run writeUsingDml $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

4 row(s) inserted...

使用变更写入数据

您还可以使用变更插入数据。

您可以使用 connection.CreateInsertCommand() 方法插入数据,此方法会创建一个新的 SpannerCommand 以将行插入表中。SpannerCommand.ExecuteNonQueryAsync() 方法将向表中添加新行。

以下代码演示了如何使用变更插入数据:

public class Singer
{
    public int SingerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Album
{
    public int SingerId { get; set; }
    public int AlbumId { get; set; }
    public string AlbumTitle { get; set; }
}
public static async Task InsertSampleDataAsync(
    string projectId, string instanceId, string databaseId)
{
    const int firstSingerId = 1;
    const int secondSingerId = 2;
    string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}"
    + $"/databases/{databaseId}";
    List<Singer> singers = new List<Singer>
    {
        new Singer { SingerId = firstSingerId, FirstName = "Marc",
            LastName = "Richards" },
        new Singer { SingerId = secondSingerId, FirstName = "Catalina",
            LastName = "Smith" },
        new Singer { SingerId = 3, FirstName = "Alice",
            LastName = "Trentor" },
        new Singer { SingerId = 4, FirstName = "Lea",
            LastName = "Martin" },
        new Singer { SingerId = 5, FirstName = "David",
            LastName = "Lomond" },
    };
    List<Album> albums = new List<Album>
    {
        new Album { SingerId = firstSingerId, AlbumId = 1,
            AlbumTitle = "Total Junk" },
        new Album { SingerId = firstSingerId, AlbumId = 2,
            AlbumTitle = "Go, Go, Go" },
        new Album { SingerId = secondSingerId, AlbumId = 1,
            AlbumTitle = "Green" },
        new Album { SingerId = secondSingerId, AlbumId = 2,
            AlbumTitle = "Forever Hold your Peace" },
        new Album { SingerId = secondSingerId, AlbumId = 3,
            AlbumTitle = "Terrified" },
    };
    // Create connection to Cloud Spanner.
    using (var connection = new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        // Insert rows into the Singers table.
        var cmd = connection.CreateInsertCommand("Singers",
            new SpannerParameterCollection
            {
                { "SingerId", SpannerDbType.Int64 },
                { "FirstName", SpannerDbType.String },
                { "LastName", SpannerDbType.String }
            });
        await Task.WhenAll(singers.Select(singer =>
        {
            cmd.Parameters["SingerId"].Value = singer.SingerId;
            cmd.Parameters["FirstName"].Value = singer.FirstName;
            cmd.Parameters["LastName"].Value = singer.LastName;
            return cmd.ExecuteNonQueryAsync();
        }));

        // Insert rows into the Albums table.
        cmd = connection.CreateInsertCommand("Albums",
            new SpannerParameterCollection
            {
                { "SingerId", SpannerDbType.Int64 },
                { "AlbumId", SpannerDbType.Int64 },
                { "AlbumTitle", SpannerDbType.String }
            });
        await Task.WhenAll(albums.Select(album =>
        {
            cmd.Parameters["SingerId"].Value = album.SingerId;
            cmd.Parameters["AlbumId"].Value = album.AlbumId;
            cmd.Parameters["AlbumTitle"].Value = album.AlbumTitle;
            return cmd.ExecuteNonQueryAsync();
        }));
        Console.WriteLine("Inserted data.");
    }
}

使用 insertSampleData 参数运行示例。

dotnet run insertSampleData $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Inserted data.

使用 SQL 查询数据

Cloud Spanner 支持使用原生 SQL 接口读取数据,您可以使用 gcloud 命令行工具在命令行中使用该接口,也可以通过适用于 C# 的 Cloud Spanner 客户端库以编程方式使用该接口。

在命令行中

执行以下 SQL 语句,读取 Albums 表中所有列的值:

gcloud spanner databases execute-sql example-db --instance=test-instance `
    --sql='SELECT SingerId, AlbumId, AlbumTitle FROM Albums'

结果应为:

SingerId AlbumId AlbumTitle
1        1       Total Junk
1        2       Go, Go, Go
2        1       Green
2        2       Forever Hold Your Peace
2        3       Terrified

使用适用于 C# 的 Cloud Spanner 客户端库

除了在命令行上执行 SQL 语句外,还可以使用适用于 C# 的 Cloud Spanner 客户端库以编程方式发出相同的 SQL 语句。

使用 ExecuteReaderAsync() 运行 SQL 查询。

string connectionString =
$"Data Source=projects/{projectId}/instances/"
+ $"{instanceId}/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
    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"));
        }
    }
}

以下是发出查询并访问数据的方法:

dotnet run querySampleData $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到以下结果:

SingerId: 1 AlbumId: 1 AlbumTitle: Total Junk
SingerId: 1 AlbumId: 2 AlbumTitle: Go, Go, Go
SingerId: 2 AlbumId: 1 AlbumTitle: Green
SingerId: 2 AlbumId: 2 AlbumTitle: Forever Hold your Peace
SingerId: 2 AlbumId: 3 AlbumTitle: Terrified

使用 SQL 参数进行查询

您可以使用支持的 SQL 类型在 SQL 语句中添加自定义值。

以下示例演示了如何在 WHERE 子句中使用 @lastName 作为参数来查询包含特定 LastName 值的记录。

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT SingerId, FirstName, LastName FROM Singers "
        + $"WHERE LastName = @lastName",
        new SpannerParameterCollection {
            {"lastName", SpannerDbType.String}});
    cmd.Parameters["lastName"].Value = "Garcia";
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " FirstName : "
            + reader.GetFieldValue<string>("FirstName")
            + " LastName : "
            + reader.GetFieldValue<string>("LastName"));
        }
    }
}

下面展示了如何使用参数发出查询并访问数据:

dotnet run queryWithParameter $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到以下结果:

SingerId : 12 FirstName : Melissa LastName : Garcia

更新数据库架构

假设您需要将名为 MarketingBudget 的新列添加到 Albums 表。向现有表添加新列需要更新数据库架构。Cloud Spanner 支持在数据库继续处理流量的同时,对数据库进行架构更新。架构更新不需要使数据库离线,并且不会锁定整个表或列;在架构更新期间,您可以继续将数据写入数据库。如需详细了解支持的架构更新和架构更改性能,请参阅架构更新

添加列

您可以使用 gcloud 命令行工具在命令行中添加列,也可以使用适用于 C# 的 Cloud Spanner 客户端库以编程方式添加列。

在命令行中

使用以下 ALTER TABLE 命令向表添加新列:

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'

您应该会看到:

Schema updating...done.

使用适用于 C# 的 Cloud Spanner 客户端库

使用 CreateDdlCommand() 修改架构:

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string alterStatement =
    "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var updateCmd = connection.CreateDdlCommand(alterStatement);
    await updateCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the MarketingBudget column.");

使用 addColumn 命令运行示例。

dotnet run addColumn $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Added the MarketingBudget column.

将数据写入新列

以下代码可将数据写入新列。对于键分别为 Albums(1, 1)Albums(2, 2) 的行,该代码会将 MarketingBudget 分别设置为 100000500000

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateUpdateCommand("Albums",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"AlbumId", SpannerDbType.Int64},
            {"MarketingBudget", SpannerDbType.Int64},
        });
    var cmdLookup =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmdLookup.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            if (reader.GetFieldValue<int>("SingerId") == 1
                && reader.GetFieldValue<int>("AlbumId") == 1)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 100000;
                await cmd.ExecuteNonQueryAsync();
            }
            if (reader.GetInt64(0) == 2 && reader.GetInt64(1) == 2)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 500000;
                await cmd.ExecuteNonQueryAsync();
            }
        }
    }
}
Console.WriteLine("Updated data.");

使用 writeDataToNewColumn 命令运行示例。

dotnet run writeDataToNewColumn $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Updated data.

您也可以执行 SQL 查询来获取刚刚写入的值。

以下是执行查询的代码:

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            string budget = string.Empty;
            if (reader["MarketingBudget"] != DBNull.Value)
            {
                budget = reader.GetFieldValue<string>("MarketingBudget");
            }
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + $" MarketingBudget : {budget}");
        }
    }
}

如需执行此查询,请使用 queryNewColumn 参数运行示例。

dotnet run queryNewColumn $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

SingerId : 1 AlbumId : 1 MarketingBudget : 100000
SingerId : 1 AlbumId : 2 MarketingBudget :
SingerId : 2 AlbumId : 1 MarketingBudget :
SingerId : 2 AlbumId : 2 MarketingBudget : 500000
SingerId : 2 AlbumId : 3 MarketingBudget :

更新数据

您可以在读写事务中使用 DML 来更新数据。

使用 ExecuteNonQueryAsync() 方法来执行 DML 语句。

public static async Task WriteWithTransactionUsingDmlCoreAsync(
    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 addColumn and writeDataToNewColumn samples first,
    // in that order.
    string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";

    decimal transferAmount = 200000;
    decimal secondBudget = 0;
    decimal firstBudget = 0;

    // Create connection to Cloud Spanner.
    using (var connection =
        new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        // Create a readwrite transaction that we'll assign
        // to each SpannerCommand.
        using (var transaction =
                await connection.BeginTransactionAsync())
        {
            // Create statement to select the second album's data.
            var cmdLookup = connection.CreateSelectCommand(
             "SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");
            cmdLookup.Transaction = transaction;
            // Excecute the select query.
            using (var reader = await cmdLookup.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 first album's "
                                + $"budget {secondBudget} "
                                + "is less than the "
                                + "amount to transfer.");
                    }
                }
            }
            // Read the first album's budget.
            cmdLookup = connection.CreateSelectCommand(
             "SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
            cmdLookup.Transaction = transaction;
            using (var reader = await cmdLookup.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    firstBudget =
                      reader.GetFieldValue<decimal>("MarketingBudget");
                }
            }

            // Update second album to remove the transfer amount.
            secondBudget -= transferAmount;
            SpannerCommand cmd = connection.CreateDmlCommand(
                "UPDATE Albums SET MarketingBudget = @MarketingBudget "
                + "WHERE SingerId = 2 and AlbumId = 2");
            cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, secondBudget);
            cmd.Transaction = transaction;
            await cmd.ExecuteNonQueryAsync();
            // Update first album to add the transfer amount.
            firstBudget += transferAmount;
            cmd = connection.CreateDmlCommand(
                "UPDATE Albums SET MarketingBudget = @MarketingBudget "
                + "WHERE SingerId = 1 and AlbumId = 1");
            cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, firstBudget);
            cmd.Transaction = transaction;
            await cmd.ExecuteNonQueryAsync();

            await transaction.CommitAsync();
        }
        Console.WriteLine("Transaction complete.");
    }
}

使用 writeWithTransactionUsingDml 参数运行示例。

dotnet run writeWithTransactionUsingDml $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Transaction complete.

对读写事务执行重试

Cloud Spanner 会为每个网络调用执行重试,并且能够从网络故障恢复。但是,压力过大时可能会发生死锁,这会导致 Cloud Spanner 事务抛出“中止的”SpannerException。要处理此异常,必须使用“重试”方法(如下所示)重试整个事务。

首先,定义一个在需要重试事务时调用的方法。以下示例定义了名为 RetryRobot 的方法。

public class RetryRobot
{
    public TimeSpan FirstRetryDelay { get; set; } = TimeSpan.FromSeconds(1000);
    public float DelayMultiplier { get; set; } = 2;
    public int MaxTryCount { get; set; } = 7;
    public Func<Exception, bool> ShouldRetry { get; set; }

    /// <summary>
    /// Retry action when assertion fails.
    /// </summary>
    /// <param name="func"></param>
    public T Eventually<T>(Func<T> func)
    {
        TimeSpan delay = FirstRetryDelay;
        for (int i = 0; ; ++i)
        {
            try
            {
                return func();
            }
            catch (Exception e)
            when (ShouldCatch(e) && i < MaxTryCount)
            {
                Thread.Sleep(delay);
                delay *= (int)DelayMultiplier;
            }
        }
    }

    private bool ShouldCatch(Exception e)
    {
        return ShouldRetry != null && ShouldRetry(e);
    }
}

接下来,创建 RetryRobot 方法的实例(名为 retryRobot),将 IsTransientSpannerFault() 指定为应重试的错误条件。然后使用 retryRobot.Eventually 执行整个事务。

以下是执行重试的代码:

var retryRobot = new RetryRobot
{
    MaxTryCount = 3,
    DelayMultiplier = 2,
    ShouldRetry = (e) => e.IsTransientSpannerFault()
};

await retryRobot.Eventually(() =>
    ReadWriteWithTransactionAsync(
        projectId, instanceId, databaseId));

使用二级索引

假设您想要提取 Albums 表中 AlbumTitle 值在特定范围内的所有行。您可以使用 SQL 语句或读取调用读取 AlbumTitle 列中的所有值,然后舍弃不符合条件的行。不过,执行全表扫描费用高昂,特别是对内含大量行的表来说更是如此。相反,如果对表创建二级索引,按非主键列进行搜索,则可以提高行检索速度。

向现有表添加二级索引需要更新架构。与其他架构更新一样,Cloud Spanner 支持在数据库继续处理流量的同时添加索引。Cloud Spanner 会使用您的现有数据自动回填索引。回填可能需要几分钟时间才能完成,但在此过程中,您无需使数据库离线,也无需避免写入已编入索引的表。如需了解详情,请参阅索引回填

添加二级索引后,Cloud Spanner 会自动使用该索引进行 SQL 查询,使用该索引后,查询运行速度可能会提高。如果使用读取接口,则必须指定要使用的索引。

添加二级索引

您可以使用 gcloud 命令行工具在命令行中添加索引,也可以使用适用于 C# 的 Cloud Spanner 客户端库以编程方式添加索引。

在命令行中

使用以下 CREATE INDEX 命令向数据库添加索引:

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)'

您应该会看到:

Schema updating...done.

使用适用于 C# 的 Cloud Spanner 客户端库

使用 CreateDdlCommand() 添加索引:

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string createStatement =
    "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var createCmd = connection.CreateDdlCommand(createStatement);
    await createCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the AlbumsByAlbumTitle index.");

使用 addIndex 命令运行示例。

  dotnet run addIndex $env:GOOGLE_PROJECT_ID test-instance example-db

添加索引可能需要几分钟时间。添加索引后,您应该看到:

  Added the AlbumsByAlbumTitle index.

添加带 STORING 子句的索引

您可能已经注意到,上面的读取示例不包括读取 MarketingBudget 列。这是因为,Cloud Spanner 的读取接口不支持将索引与数据表联接起来查找未存储在索引中的值。

请创建 AlbumsByAlbumTitle 的备用定义,用于将 MarketingBudget 的副本存储到索引中。

在命令行中

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)'

添加索引可能需要几分钟时间。添加索引后,您应该会看到:

Schema updating...done.

使用适用于 C# 的 Cloud Spanner 客户端库

使用 CreateDdlCommand() 并附加 STORING 子句以添加索引:

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string createStatement =
    "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
    + "STORING (MarketingBudget)";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var createCmd = connection.CreateDdlCommand(createStatement);
    await createCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the AlbumsByAlbumTitle2 index.");

使用 addStoringIndex 命令运行示例。

dotnet run addStoringIndex $env:GOOGLE_PROJECT_ID test-instance example-db

您应该会看到:

Added the AlbumsByAlbumTitle2 index.

现在,您可以执行从 AlbumsByAlbumTitle2 索引中提取所有 AlbumIdAlbumTitleMarketingBudget 列的读取操作:

通过执行一个明确指定您创建的存储索引的查询,使用该索引读取数据:

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
        + "{FORCE_INDEX=AlbumsByAlbumTitle2} "
        + $"WHERE AlbumTitle >= @startTitle "
        + $"AND AlbumTitle < @endTitle",
        new SpannerParameterCollection {
            {"startTitle", SpannerDbType.String},
            {"endTitle", SpannerDbType.String} });
    cmd.Parameters["startTitle"].Value = startTitle;
    cmd.Parameters["endTitle"].Value = endTitle;
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine("AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + " AlbumTitle : "
            + reader.GetFieldValue<string>("AlbumTitle")
            + " MarketingBudget : "
            + reader.GetFieldValue<string>("MarketingBudget"));
        }
    }
}

使用 queryDataWithStoringIndex 命令运行示例。

dotnet run queryDataWithStoringIndex $env:GOOGLE_PROJECT_ID test-instance example-db

您看到的输出结果应该类似于以下内容:

AlbumId : 2 AlbumTitle : Forever Hold your Peace MarketingBudget : 300000
AlbumId : 2 AlbumTitle : Go, Go, Go MarketingBudget : 300000

使用只读事务检索数据

假设您要在同一时间戳执行多个读取操作。只读事务会观察一致的事务提交历史记录前缀,因此您的应用始终可获得一致的数据。使用 .NET Framework 的 TransactionScope() 以及 OpenAsReadOnlyAsync() 来执行只读事务。

下面演示了如何运行查询并在同一只读事务中执行读取操作:

.NET Standard 2.0

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Gets a transaction object that captures the database state
// at a specific point in time.
using (TransactionScope scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled))
{
    // Create connection to Cloud Spanner.
    using (var connection = new SpannerConnection(connectionString))
    {
        // Open the connection, making the implicitly created
        // transaction read only when it connects to the outer
        // transaction scope.
        await connection.OpenAsReadOnlyAsync()
            .ConfigureAwait(false);
        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())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
    }
    scope.Complete();
    Console.WriteLine("Transaction complete.");
}

.NET Standard 1.5

string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}"
    + $"/databases/{databaseId}";

// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    await connection.OpenAsync();

    // Open a new read only transaction.
    using (var transaction =
        await connection.BeginReadOnlyTransactionAsync())
    {
        var cmd = connection.CreateSelectCommand(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
        cmd.Transaction = transaction;

        // 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())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
    }
}
Console.WriteLine("Transaction complete.");

使用 queryDataWithTransaction 命令运行示例。

dotnet run queryDataWithTransaction $env:GOOGLE_PROJECT_ID test-instance example-db

您看到的输出结果应该类似于以下内容:

SingerId : 2 AlbumId : 2 AlbumTitle : Forever Hold your Peace
SingerId : 1 AlbumId : 2 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 1 AlbumTitle : Total Junk
SingerId : 2 AlbumId : 2 AlbumTitle : Forever Hold your Peace
SingerId : 1 AlbumId : 2 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 1 AlbumTitle : Total Junk

清理

为避免因本教程中使用的资源导致您的 Cloud Billing 帐号产生额外费用,请删除数据库和您创建的实例。

删除数据库

如果您删除一个实例,则该实例中的所有数据库都会自动删除。 本步骤演示了如何在不删除实例的情况下删除数据库(您仍需为该实例付费)。

在命令行中

gcloud spanner databases delete example-db --instance=test-instance

使用 Cloud Console

  1. 转到 Google Cloud Console 中的 Cloud Spanner 实例页面。

    转到“实例”页面

  2. 点击实例。

  3. 点击您想删除的数据库。

  4. 数据库详细信息页面中,点击删除

  5. 确认您要删除数据库并点击删除

删除实例

删除实例会自动删除在该实例中创建的所有数据库。

在命令行中

gcloud spanner instances delete test-instance

使用 Cloud Console

  1. 转到 Google Cloud Console 中的 Cloud Spanner 实例页面。

    转到“实例”页面

  2. 点击您的实例。

  3. 点击删除

  4. 确认您要删除实例并点击删除

后续步骤

  • 在虚拟机实例中访问 Cloud Spanner:创建一个可访问 Cloud Spanner 数据库的虚拟机实例。
  • 身份验证入门中了解授权和身份验证凭据。
  • 了解更多 Cloud Spanner 概念