Cloud Spanner:使用 Go 打造游戏排行榜

Google Cloud Spanner 是一种全代管式可横向扩容的关系型数据库服务,可提供 ACID 事务和 SQL 语义,而不会提升性能和高可用性。

在本实验中,您将学习如何设置 Cloud Spanner 实例。我们将逐步说明如何创建可用于游戏排行榜的数据库和架构。首先,创建一个用于存储玩家信息的玩家表和一个用于存储玩家得分的得分表。

接下来,您将使用示例数据填充表。然后,您将通过运行前十名示例查询并最终删除实例以释放资源来结束本实验。

您将学习的内容

  • 如何设置 Cloud Spanner 实例。
  • 如何创建数据库和表。
  • 如何使用提交时间戳列。
  • 如何使用时间戳将数据加载到您的 Cloud Spanner 数据库表中。
  • 如何查询您的 Cloud Spanner 数据库。
  • 如何删除 Cloud Spanner 实例。

所需条件

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

如何评价您使用 Google Cloud Platform 的体验?

新手水平 中等水平 熟练水平

自定进度的环境设置

如果您还没有 Google 帐号(Gmail 或 Google Apps),则必须创建一个。登录 Google Cloud Platform Console (console.cloud.google.com) 并创建一个新项目。

如果您已经有一个项目,请点击控制台左上方的项目选择下拉菜单:

6c9406d9b014760.png

然后在出现的对话框中点击“新建项目”按钮以创建一个新项目:

f708315ae07353d0.png

如果您还没有项目,则应该看到一个类似这样的对话框来创建您的第一个项目:

870a3cbd6541ee86.png

随后的项目创建对话框可让您输入新项目的详细信息:

6a92c57d3250a4b3.png

请记住项目 ID,该 ID 在所有 Google Cloud 项目中都是唯一的名称(上面的名称已经被使用,对您不起作用,对不起!)。此 Codelab 稍后将在 PROJECT_ID 中引用它。

接下来,如果尚未执行此操作,则需要在 Developers Console 中启用结算功能,以便使用 Google Cloud 资源并启用 Cloud Spanner API

15d0ef27a8fbab27.png

在此 Codelab 中运行不会花费您超过几美元,但是如果您决定使用更多的资源或让它们运行(请参阅本文档末尾的“清理”部分),则可能会花费更多。如需了解 Google Cloud Spanner 价格,请参阅此处

Google Cloud Platform 的新用户均有资格获享 $300 赠金,免费试用此 Codelab。

Google Cloud Shell 设置

虽然 Google Cloud 和 Spanner 可以从笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

这个基于 Debian 的虚拟机加载了您所需的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。也就是说,Codelab 只需要一个浏览器(是的,它可在 Chromebook 上进行)。

  1. 如需从 Cloud Console 激活 Cloud Shell,只需点击激活 Cloud ShellgcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A(预配和连接到环境仅需花费一些时间)。

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的 PROJECT_ID

gcloud auth list

命令输出

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果由于某种原因无法设置项目,只需发出以下命令:

gcloud config set project <PROJECT_ID>

正在查找您的 PROJECT_ID?检查您在设置步骤中使用的 ID,或在 Cloud Console 信息中心查找该 ID:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

默认情况下,Cloud Shell 还会设置一些环境变量,这对您日后运行命令可能会很有用。

echo $GOOGLE_CLOUD_PROJECT

命令输出

<PROJECT_ID>
  1. 最后,设置默认可用区和项目配置。
gcloud config set compute/zone us-central1-f

您可以选择各种不同的可用区。如需了解详情,请参阅区域和可用区

摘要

在此步骤中,您将设置环境。

下一步

接下来,您将设置 Cloud Spanner 实例。

在此步骤中,我们会为此 Codelab 设置 Cloud Spanner 实例。搜索左上角汉堡式菜单中的 3129589f7bc9e5ce.png Spanner 条目 1a6580bd3d3e6783.png,也可以按“/”并输入“Spanner”以搜索 Spanner。

36e52f8df8e13b99.png

接下来,点击 95269e75bc8c3e4d.png 并填写表单,方法是为您的实例输入实例名称 cloudspanner-leaderboard,选择配置(选择区域实例),并设置节点数,对于此 Codelab,我们仅需要一个节点。对于生产实例并符合 Cloud Spanner SLA 的资格,您将需要在 Cloud Spanner 实例中运行 3 个或更多节点。

最后但并非最不重要的一点是,点击“创建”,然后在几秒钟内就可以使用 Cloud Spanner 实例。

dceb68e9ed3801e8.png

在下一步中,我们将使用 Go 客户端库在新实例中创建数据库和架构。

在此步骤中,我们将创建示例数据库和架构。

让我们使用 Go 客户端库创建两个表;一个用于玩家信息的玩家表和一个用于存储玩家得分的得分表。为此,我们将逐步介绍在 Cloud Shell 中创建 Go 控制台应用的步骤。

首先,通过在 Cloud Shell 中键入以下命令,从 GitHub 克隆此 Codelab 的示例代码:

go get -u github.com/GoogleCloudPlatform/golang-samples/spanner/...

然后将目录更改为创建应用的“排行榜”目录。

cd gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/spanner_leaderboard

此 Codelab 所需的所有代码都作为一个可运行的 Go 应用位于现有 golang-samples/spanner/spanner_leaderboard/ 目录中,该应用命名为 leaderboard 在您逐步通过 Codelab 时可作为参考。我们将创建一个新目录,并分阶段构建排行榜应用的副本。

为应用创建一个名为“Codelab”的新目录,并使用以下命令将目录切换到该目录:

mkdir codelab && cd $_

现在,让我们更新创建一个名为“排行榜”的基本 Go 应用,该应用使用 Spanner 客户端库创建由两个表组成的页首横幅;游戏玩家和得分。您可以直接在 Cloud Shell Editor 中执行此操作:

通过点击下面突出显示的图标,打开 Cloud Shell Editor:

73cf70e05f653ca.png

在 ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab 文件夹中创建名为“leaderboard.go”的文件。

  • 首先请确保您已在 Cloud Shell Editor 的文件夹列表中选择了“Codelab”文件夹。
  • 然后在 Cloud Shell Editor 的“文件”菜单下选择“新文件”。
  • 输入“leaderboard.go”作为新文件的名称。

这是应用的主文件,将包含我们的应用代码和参考,以包括所有依赖项。

如需创建 leaderboard 数据库以及 PlayersScores 表,请复制 (Ctrl + P) 并将以下 Go 代码粘贴 (Ctrl + V) 到 leaderboard.go 文件中:

package main

import (
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "os"
        "regexp"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error

func createDatabase(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, db string) error {
        matches := regexp.MustCompile("^(.*)/databases/(.*)$").FindStringSubmatch(db)
        if matches == nil || len(matches) != 3 {
                return fmt.Errorf("Invalid database id %s", db)
        }
        op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
                Parent:          matches[1],
                CreateStatement: "CREATE DATABASE `" + matches[2] + "`",
                ExtraStatements: []string{
                        `CREATE TABLE Players(
                            PlayerId INT64 NOT NULL,
                            PlayerName STRING(2048) NOT NULL
                        ) PRIMARY KEY(PlayerId)`,
                        `CREATE TABLE Scores(
                            PlayerId INT64 NOT NULL,
                            Score INT64 NOT NULL,
                            Timestamp TIMESTAMP NOT NULL
                            OPTIONS(allow_commit_timestamp=true)
                        ) PRIMARY KEY(PlayerId, Timestamp),
                        INTERLEAVE IN PARENT Players ON DELETE NO ACTION`,
                },
        })
        if err != nil {
                return err
        }
        if _, err := op.Wait(ctx); err != nil {
                return err
        }
        fmt.Fprintf(w, "Created database [%s]\n", db)
        return nil
}

func createClients(ctx context.Context, db string) (*database.DatabaseAdminClient, *spanner.Client) {
        adminClient, err := database.NewDatabaseAdminClient(ctx)
        if err != nil {
                log.Fatal(err)
        }

        dataClient, err := spanner.NewClient(ctx, db)
        if err != nil {
                log.Fatal(err)
        }

        return adminClient, dataClient
}

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }
        return nil
}

func main() {
        flag.Usage = func() {
                fmt.Fprintf(os.Stderr, `Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
`)
        }

        flag.Parse()
        flagCount := len(flag.Args())
        if flagCount != 2 {
                flag.Usage()
                os.Exit(2)
        }

        cmd, db := flag.Arg(0), flag.Arg(1)
        // Set timespan to zero, as it's not currently being used
        var timespan int = 0

        ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
        defer cancel()
        adminClient, dataClient := createClients(ctx, db)
        if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
                os.Exit(1)
        }
}

在 Cloud Shell Editor 的“文件”菜单下选择“保存”,保存您对 leaderboard.go 文件所做的更改。

您可以使用 golang-samples/spanner/spanner_leaderboard 目录中的 leaderboard.go 文件查看添加代码以启用 createdatabase 命令后 leaderboard.go 文件外观的示例。

如需在 Cloud Shell 中构建应用,请从 leaderboard.go 文件所在的 codelab 目录中运行“go build”:

go build leaderboard.go

成功构建应用后,通过输入以下命令在 Cloud Shell 中运行生成的应用:

./leaderboard

您应该会看到如下所示的输出:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.

从此响应中我们可以看到,Leaderboard 应用当前有一个可能的命令:createdatabase。我们可以看到 createdatabase 命令的预期参数是一个包含特定实例 ID 和数据库 ID 的字符串。

现在运行以下命令。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard createdatabase projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

几秒钟后,您应该看到如下的响应:

Created database [projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard]

在 Cloud Console 的 Cloud Spanner 部分中,您应该在左侧菜单中看到新的数据库和表。

ba9008bb84cb90b0.png

在下一步中,我们将更新应用以将一些数据加载到新数据库中。

现在,我们有一个名为 leaderboard 的数据库,包含两个表:PlayersScores。现在,我们使用 Go 客户端库在 Players 表中填充玩家人数,在 Scores 表中填入每个玩家的随机得分。

如果 Cloud Shell Editor 尚未打开,请点击下面突出显示的图标来打开它:

ef49fcbaaed19024.png

接下来,在 Cloud Shell Editor 中修改 leaderboard.go 文件,添加可用于将 100 个玩家插入 Players 表中的 insertplayers 命令。我们还将添加一个 insertscores 命令,该命令可用于为 Players 表中的每个玩家在 Scores 表中插入 4 个随机得分。

首先更新 leaderboard.go 文件顶部的 imports 部分,替换当前的部分,以便一旦完成,它应如下所示:

import (
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "math/rand"
        "os"
        "regexp"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        "google.golang.org/api/iterator"
        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

接下来,在文件顶部,以“type adminCommand...”开头的行的下方添加新的命令类型以及命令列表,以便一旦完成,它应如下所示:

type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error

type command func(ctx context.Context, w io.Writer, client *spanner.Client) error
var (
        commands = map[string]command{
                "insertplayers": insertPlayers,
                "insertscores":  insertScores,
        }
)

接下来,添加以下 insertPlayers,并在现有 createdatabase() 函数下方插入 insertScores 函数:

func insertPlayers(ctx context.Context, w io.Writer, client *spanner.Client) error {
        // Get number of players to use as an incrementing value for each PlayerName to be inserted
        stmt := spanner.Statement{
                SQL: `SELECT Count(PlayerId) as PlayerCount FROM Players`,
        }
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        row, err := iter.Next()
        if err != nil {
                return err
        }
        var numberOfPlayers int64 = 0
        if err := row.Columns(&numberOfPlayers); err != nil {
                return err
        }
        // Intialize values for random PlayerId
        rand.Seed(time.Now().UnixNano())
        min := 1000000000
        max := 9000000000
        // Insert 100 player records into the Players table
        _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
                stmts := []spanner.Statement{}
                for i := 1; i <= 100; i++ {
                        numberOfPlayers++
                        playerID := rand.Intn(max-min) + min
                        playerName := fmt.Sprintf("Player %d", numberOfPlayers)
                        stmts = append(stmts, spanner.Statement{
                                SQL: `INSERT INTO Players
                                                (PlayerId, PlayerName)
                                                VALUES (@playerID, @playerName)`,
                                Params: map[string]interface{}{
                                        "playerID":   playerID,
                                        "playerName": playerName,
                                },
                        })
                }
                _, err := txn.BatchUpdate(ctx, stmts)
                if err != nil {
                        return err
                }
                return nil
        })
        fmt.Fprintf(w, "Inserted players \n")
        return nil
}

func insertScores(ctx context.Context, w io.Writer, client *spanner.Client) error {
        playerRecordsFound := false
        // Create slice for insert statements
        stmts := []spanner.Statement{}
        // Select all player records
        stmt := spanner.Statement{SQL: `SELECT PlayerId FROM Players`}
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        // Insert 4 score records into the Scores table for each player in the Players table
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        break
                }
                if err != nil {
                        return err
                }
                playerRecordsFound = true
                var playerID int64
                if err := row.ColumnByName("PlayerId", &playerID); err != nil {
                        return err
                }
                // Intialize values for random score and date
                rand.Seed(time.Now().UnixNano())
                min := 1000
                max := 1000000
                for i := 0; i < 4; i++ {
                        // Generate random score between 1,000 and 1,000,000
                        score := rand.Intn(max-min) + min
                        // Generate random day within the past two years
                        now := time.Now()
                        endDate := now.Unix()
                        past := now.AddDate(0, -24, 0)
                        startDate := past.Unix()
                        randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
                        randomDate := time.Unix(randomDateInSeconds, 0)
                        // Add insert statement to stmts slice
                        stmts = append(stmts, spanner.Statement{
                                SQL: `INSERT INTO Scores
                                                (PlayerId, Score, Timestamp)
                                                VALUES (@playerID, @score, @timestamp)`,
                                Params: map[string]interface{}{
                                        "playerID":  playerID,
                                        "score":     score,
                                        "timestamp": randomDate,
                                },
                        })
                }

        }
        if !playerRecordsFound {
                fmt.Fprintln(w, "No player records currently exist. First insert players then insert scores.")
        } else {
                _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
                        // Commit insert statements for all scores to be inserted as a single transaction
                        _, err := txn.BatchUpdate(ctx, stmts)
                        return err
                })
                if err != nil {
                        return err
                }
                fmt.Fprintln(w, "Inserted scores")
        }
        return nil
}

然后,为使 insert 命令起作用,请将以下代码添加到您的应用的“run”函数中的 createdatabase 处理语句下方,以替换 return nil 语句:

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err

完成后,run 函数应如下所示:

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err
}

要为您的应用添加“插入”功能,最后一步是向 flag.Usage() 函数添加“insertplayers”和“insertscores”命令的帮助文本。将以下帮助文本添加到 flag.Usage() 函数,以纳入插入命令的帮助文本:

将两个命令添加到可能的命令列表中:

Command can be one of: createdatabase, insertplayers, insertscores

然后将此附加帮助文本添加到 createdatabase 命令的帮助文本下方。

        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.

在 Cloud Shell Editor 的“文件”菜单下选择“保存”,保存您对 leaderboard.go 文件所做的更改。

您可以使用 golang-samples/spanner/spanner_leaderboard 目录中的 leaderboard.go 文件查看添加添加代码以启用 insertplayersinsertscores 命令后,leaderboard.go 文件应如何显示的示例。

现在,让我们构建并运行应用,以确认新 insertplayersinsertscores 命令已包含在应用的可能命令列表中。运行以下命令以构建应用:

go build leaderboard.go

输入以下命令,在 Cloud Shell 中运行生成的应用:

./leaderboard

您应该看到 insertplayersinsertscores 命令现在包含在应用的默认输出中:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase, insertplayers, insertscores

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.

现在,我们使用我们调用 createdatabase 命令时所用的参数值运行 insertplayers 命令。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard insertplayers projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

几秒钟后,您应该看到如下的响应:

Inserted players

现在,让我们使用 Go 客户端库在 Scores 表中填充四个随机得分以及 Players 表中每个玩家的时间戳。

通过以下 SQL 语句,将 Scores 表的 Timestamp 列定义为“提交时间戳”列,该 SQL 语句是在我们先前运行 create 命令时执行的:

CREATE TABLE Scores(
  PlayerId INT64 NOT NULL,
  Score INT64 NOT NULL,
  Timestamp TIMESTAMP NOT NULL OPTIONS(allow_commit_timestamp=true)
) PRIMARY KEY(PlayerId, Timestamp),
    INTERLEAVE IN PARENT Players ON DELETE NO ACTION

请注意 OPTIONS(allow_commit_timestamp=true) 特性。这使得 Timestamp 成为“提交时间戳”列,并且会在指定表行中自动填充 INSERT 和 UPDATE 操作的确切事务时间戳。

您还可以将自己的时间戳值插入“提交时间戳”列中,只要您插入带有过去值的时间戳即可,这是本 Codelab 将要做的。

现在,我们使用我们调用 insertplayers 命令时所用的参数值运行 insertscores 命令。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard insertscores projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

几秒钟后,您应该看到如下的响应:

Inserted scores

运行 insertScores 函数使用以下代码段插入一个随机生成的时间戳,该时间戳具有一个过去发生的日期时间:

now := time.Now()
endDate := now.Unix()
past := now.AddDate(0, -24, 0)
startDate := past.Unix()
randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
randomDate := time.Unix(randomDateInSeconds, 0)
stmts = append(stmts, spanner.Statement{
        SQL: `INSERT INTO Scores
              (PlayerId, Score, Timestamp)
                 VALUES (@playerID, @score, @timestamp)`,
        Params: map[string]interface{}{
                "playerID":  playerID,
                "score":     score,
                "timestamp": randomDate,
        },
})

如需使用“插入”事务发生时的确切时间戳自动填充 Timestamp 列,您可以改为插入 Go 常量 spanner.CommitTimestamp,如以下代码段所示:

...
stmts = append(stmts, spanner.Statement{
        SQL: `INSERT INTO Scores
              (PlayerId, Score, Timestamp)
                 VALUES (@playerID, @score, @timestamp)`,
        Params: map[string]interface{}{
                "playerID":  playerID,
                "score":     score,
                "timestamp": spanner.CommitTimestamp,
        },
})

现在,我们已经完成了数据加载,让我们在 Cloud Console 的 Cloud Spanner 部分中验证刚刚写入新表中的值。首先选择 leaderboard 数据库,然后选择 Players 表。点击 Data 标签页。您应该看到表的 PlayerIdPlayerName 列中有数据。

7bc2c96293c31c49.png

接下来,通过点击 Scores 表并选择 Data 标签页,验证“得分”表中是否也包含数据。您应该看到表的 PlayerIdTimestampScore 列中都有数据。

d8a4ee4f13244c19.png

太棒了! 让我们更新我们的应用,以运行一些可用于创建游戏排行榜的查询。

现在我们已经建立了数据库并将信息加载到表中,让我们使用此数据创建一个排行榜。为此,我们需要回答以下四个问题:

  1. 哪些玩家属于“十大热门”游戏?
  2. 今年哪些国家/地区的“十大”玩家?
  3. 当月哪些玩家为“十大”?
  4. 本周哪些玩家达到“十大”?

我们将更新应用以运行 SQL 查询,以回答这些问题。

我们将添加一个 query 命令和一个 queryWithTimespan 命令,它们将提供一种运行查询来回答将产生排行榜所需信息的问题的方法。

在 Cloud Shell Editor 中修改 leaderboard.go 文件,以更新应用以添加 query 命令和 queryWithTimespan 命令。我们还将添加 formatWithCommas 辅助函数,并用英文逗号对得分进行格式化。

首先更新 leaderboard.go 文件顶部的 imports 部分,替换当前的部分,以便一旦完成,它应如下所示:

import (
        "bytes"
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "math/rand"
        "os"
        "regexp"
        "strconv"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        "google.golang.org/api/iterator"
        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

接下来,将以下两个函数和辅助函数添加到现有 insertScores 方法下方:

func query(ctx context.Context, w io.Writer, client *spanner.Client) error {
        stmt := spanner.Statement{
                SQL: `SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp
                        FROM Players p
                        JOIN Scores s ON p.PlayerId = s.PlayerId
                        ORDER BY s.Score DESC LIMIT 10`}
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        return nil
                }
                if err != nil {
                        return err
                }
                var playerID, score int64
                var playerName string
                var timestamp time.Time
                if err := row.Columns(&playerID, &playerName, &score, &timestamp); err != nil {
                        return err
                }
                fmt.Fprintf(w, "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
                        playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
        }
}

func queryWithTimespan(ctx context.Context, w io.Writer, client *spanner.Client, timespan int) error {
        stmt := spanner.Statement{
                SQL: `SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp
                                FROM Players p
                                JOIN Scores s ON p.PlayerId = s.PlayerId
                                WHERE s.Timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @Timespan HOUR)
                                ORDER BY s.Score DESC LIMIT 10`,
                Params: map[string]interface{}{"Timespan": timespan},
        }
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        return nil
                }
                if err != nil {
                        return err
                }
                var playerID, score int64
                var playerName string
                var timestamp time.Time
                if err := row.Columns(&playerID, &playerName, &score, &timestamp); err != nil {
                        return err
                }
                fmt.Fprintf(w, "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
                        playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
        }
}

func formatWithCommas(n int64) string {
        numberAsString := strconv.FormatInt(n, 10)
        numberLength := len(numberAsString)
        if numberLength < 4 {
                return numberAsString
        }
        var buffer bytes.Buffer
        comma := []rune(",")
        bufferPosition := numberLength % 3
        if (bufferPosition) > 0 {
                bufferPosition = 3 - bufferPosition
        }
        for i := 0; i < numberLength; i++ {
                if bufferPosition == 3 {
                        buffer.WriteRune(comma[0])
                        bufferPosition = 0
                }
                bufferPosition++
                buffer.WriteByte(numberAsString[i])
        }
        return buffer.String()
}

接下来,在 leaderboard.go 文件的顶部,将“查询”作为一个命令选项添加到 commands 变量中,就在 "insertscores": insertScores 选项的下面,以便 commands 变量看起来像这样:

var (
        commands = map[string]command{
                "insertplayers": insertPlayers,
                "insertscores":  insertScores,
                "query":         query,
        }
)

接下来,将“queryWithTimespan”作为命令选项添加到 run 函数中的“createdatabase”命令部分下方和“insert and query”命令处理部分上方:

        // querywithtimespan command
        if cmd == "querywithtimespan" {
                err := queryWithTimespan(ctx, w, dataClient, timespan)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

完成后,run 函数应如下所示:

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // querywithtimespan command
        if cmd == "querywithtimespan" {
                if timespan == 0 {
                        flag.Usage()
                        os.Exit(2)
                }
                err := queryWithTimespan(ctx, w, dataClient, timespan)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err
}

然后,要使 queryWithTimespan 命令起作用,请更新应用的“main”方法中的 flag.Parse() 代码块,使其外观如下所示:

        flag.Parse()
        flagCount := len(flag.Args())
        if flagCount < 2 || flagCount > 3 {
                flag.Usage()
                os.Exit(2)
        }

        cmd, db := flag.Arg(0), flag.Arg(1)
        // If query timespan flag is specified, parse to int
        var timespan int = 0
        if flagCount == 3 {
                parsedTimespan, err := strconv.Atoi(flag.Arg(2))
                if err != nil {
                        fmt.Println(err)
                        os.Exit(2)
                }
                timespan = parsedTimespan
        }

        ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
        defer cancel()
        adminClient, dataClient := createClients(ctx, db)
        if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
                os.Exit(1)
        }

向应用添加“查询”功能的最后一步是向 flag.Usage() 函数添加“query”和“querywithtimespan”命令的帮助文本。将以下代码行添加到 flag.Usage() 函数中,以添加查询命令的帮助文本:

将两个“query”命令添加到可能的命令列表中:

Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan

然后将此附加帮助文本添加到 insertscores 命令的帮助文本下方。

        leaderboard query projects/my-project/instances/my-instance/databases/example-db
                - Query players with top ten scores of all time.
        leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168
                - Query players with top ten scores within a timespan specified in hours.

在 Cloud Shell Editor 的“文件”菜单下选择“保存”,保存您对 leaderboard.go 文件所做的更改。

您可以使用 golang-samples/spanner/spanner_leaderboard 目录中的 leaderboard.go 文件查看添加添加代码以启用 queryquerywithtimespan 命令后,leaderboard.go 文件应如何显示的示例。

现在,让我们构建并运行应用,以确认新 queryquerywithtimespan 命令已包含在应用的可能命令列表中。

在 Cloud Shell 中运行以下命令以构建应用:

go build leaderboard.go

输入以下命令,在 Cloud Shell 中运行生成的应用:

./leaderboard

您应该将应用的默认输出中现在包含的 queryquerywithtimespan 命令作为新的命令选项包括在内:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.
        leaderboard query projects/my-project/instances/my-instance/databases/example-db
                - Query players with top ten scores of all time.
        leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168
                - Query players with top ten scores within a timespan specified in hours.

您可以从响应中看到,我们可以使用 query 命令来获取有史以来“十大”玩家的列表。我们还可以看到,querywithtimespan 命令使我们可以根据 ScoresTimestamp 列中的值来指定用于过滤记录的小时数。

让我们使用运行 query 命令时使用的相同参数值来运行 create 命令。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard query  projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

您应会看到包含所有“十大”玩家的响应,如下所示:

PlayerId: 4018687297  PlayerName: Player 83  Score: 999,618  Timestamp: 2017-07-01
PlayerId: 4018687297  PlayerName: Player 83  Score: 998,956  Timestamp: 2017-09-02
PlayerId: 4285713246  PlayerName: Player 51  Score: 998,648  Timestamp: 2017-12-01
PlayerId: 5267931774  PlayerName: Player 49  Score: 997,733  Timestamp: 2017-11-09
PlayerId: 1981654448  PlayerName: Player 35  Score: 997,480  Timestamp: 2018-12-06
PlayerId: 4953940705  PlayerName: Player 87  Score: 995,184  Timestamp: 2018-09-14
PlayerId: 2456736905  PlayerName: Player 84  Score: 992,881  Timestamp: 2017-04-14
PlayerId: 8234617611  PlayerName: Player 19  Score: 992,399  Timestamp: 2017-12-27
PlayerId: 1788051688  PlayerName: Player 76  Score: 992,265  Timestamp: 2018-11-22
PlayerId: 7127686505  PlayerName: Player 97  Score: 992,038  Timestamp: 2017-12-02

现在,让我们运行带有必要参数的 querywithtimespan 命令,通过指定等于一年中小时数的“时间范围”(即 8760)来查询年度“十大”玩家。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 8760

您应该会看到一条包含一年中排名前 10 位的玩家的响应,如下所示:

PlayerId: 1981654448  PlayerName: Player 35  Score: 997,480  Timestamp: 2018-12-06
PlayerId: 4953940705  PlayerName: Player 87  Score: 995,184  Timestamp: 2018-09-14
PlayerId: 1788051688  PlayerName: Player 76  Score: 992,265  Timestamp: 2018-11-22
PlayerId: 6862349579  PlayerName: Player 30  Score: 990,877  Timestamp: 2018-09-14
PlayerId: 5529627211  PlayerName: Player 16  Score: 989,142  Timestamp: 2018-03-30
PlayerId: 9743904155  PlayerName: Player 1  Score: 988,765  Timestamp: 2018-05-30
PlayerId: 6809119884  PlayerName: Player 7  Score: 986,673  Timestamp: 2018-05-16
PlayerId: 2132710638  PlayerName: Player 54  Score: 983,108  Timestamp: 2018-09-11
PlayerId: 2320093590  PlayerName: Player 79  Score: 981,373  Timestamp: 2018-05-07
PlayerId: 9554181430  PlayerName: Player 80  Score: 981,087  Timestamp: 2018-06-21

现在,我们运行 querywithtimespan 命令来查询每月的“十大”玩家,方法是指定“时间范围”等于一个月中的小时(共 730 个)。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 730

您应该会看到一条包含当月“前十大玩家”的响应,如下所示:

PlayerId: 3869829195  PlayerName: Player 69  Score: 949,686  Timestamp: 2019-02-19
PlayerId: 7448359883  PlayerName: Player 20  Score: 938,998  Timestamp: 2019-02-07
PlayerId: 1981654448  PlayerName: Player 35  Score: 929,003  Timestamp: 2019-02-22
PlayerId: 9336678658  PlayerName: Player 44  Score: 914,106  Timestamp: 2019-01-27
PlayerId: 6968576389  PlayerName: Player 40  Score: 898,041  Timestamp: 2019-02-21
PlayerId: 5529627211  PlayerName: Player 16  Score: 896,433  Timestamp: 2019-01-29
PlayerId: 9395039625  PlayerName: Player 59  Score: 879,495  Timestamp: 2019-02-09
PlayerId: 2094604854  PlayerName: Player 39  Score: 860,434  Timestamp: 2019-02-01
PlayerId: 9395039625  PlayerName: Player 59  Score: 849,955  Timestamp: 2019-02-21
PlayerId: 4285713246  PlayerName: Player 51  Score: 805,654  Timestamp: 2019-02-02

现在,我们运行 querywithtimespan 命令来查询每周的“十大”玩家,方法是指定“时间范围”等于一周中的小时(共 168 个)。确保my-project 替换为在此 Codelab 开始时创建的项目 ID。

./leaderboard querywithtimespan  projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 168

您应该会看到一条包含本周排名“十大”玩家的响应,如下所示:

PlayerId: 3869829195  PlayerName: Player 69  Score: 949,686  Timestamp: 2019-02-19
PlayerId: 1981654448  PlayerName: Player 35  Score: 929,003  Timestamp: 2019-02-22
PlayerId: 6968576389  PlayerName: Player 40  Score: 898,041  Timestamp: 2019-02-21
PlayerId: 9395039625  PlayerName: Player 59  Score: 849,955  Timestamp: 2019-02-21
PlayerId: 5954045812  PlayerName: Player 8  Score: 795,639  Timestamp: 2019-02-22
PlayerId: 3889939638  PlayerName: Player 71  Score: 775,252  Timestamp: 2019-02-21
PlayerId: 5529627211  PlayerName: Player 16  Score: 604,695  Timestamp: 2019-02-19
PlayerId: 9006728426  PlayerName: Player 3  Score: 457,208  Timestamp: 2019-02-22
PlayerId: 8289497066  PlayerName: Player 58  Score: 227,697  Timestamp: 2019-02-20
PlayerId: 8065482904  PlayerName: Player 99  Score: 198,429  Timestamp: 2019-02-24

进展喜人!

随着您添加记录,Cloud Spanner 会根据您的数据库扩大规模需求。无论您数据库的增长幅度如何,使用 Cloud Spanner 及其 Truetime 技术,游戏排行榜都可以继续保持精准扩容。

在使用 Spanner 完成各种有趣的任务之后,我们需要清理游乐场,从而节省宝贵的资源和金钱。幸运的是,这是一个简单的步骤,您只需转到 Cloud Console 的 Cloud Spanner 部分,然后在 Codelab 步骤中删除名为“设置 Cloud Spanner 实例”的实例。

我们的学习内容

  • 排行榜的 Google Cloud Spanner 实例、数据库和表架构
  • 如何创建 Go 控制台应用
  • 如何使用 Go 客户端库创建 Spanner 数据库和表
  • 如何使用 Go 客户端库将数据加载到 Spanner 数据库中
  • 如何使用 Spanner 提交时间戳和 Go 客户端库查询数据中的“十大”结果

后续步骤:

向我们提供反馈