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) 并创建一个新项目。
如果您已经有一个项目,请点击控制台左上方的项目选择下拉菜单:
然后在出现的对话框中点击“新建项目”按钮以创建一个新项目:
如果您还没有项目,则应该看到一个类似这样的对话框来创建您的第一个项目:
随后的项目创建对话框可让您输入新项目的详细信息:
请记住项目 ID,该 ID 在所有 Google Cloud 项目中都是唯一的名称(上面的名称已经被使用,对您不起作用,对不起!)。此 Codelab 稍后将在 PROJECT_ID
中引用它。
接下来,如果尚未执行此操作,则需要在 Developers Console 中启用结算功能,以便使用 Google Cloud 资源并启用 Cloud Spanner API。
在此 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 上进行)。
- 如需从 Cloud Console 激活 Cloud Shell,只需点击激活 Cloud Shell(预配和连接到环境仅需花费一些时间)。
在连接到 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:
默认情况下,Cloud Shell 还会设置一些环境变量,这对您日后运行命令可能会很有用。
echo $GOOGLE_CLOUD_PROJECT
命令输出
<PROJECT_ID>
- 最后,设置默认可用区和项目配置。
gcloud config set compute/zone us-central1-f
您可以选择各种不同的可用区。如需了解详情,请参阅区域和可用区。
摘要
在此步骤中,您将设置环境。
下一步
接下来,您将设置 Cloud Spanner 实例。
在此步骤中,我们会为此 Codelab 设置 Cloud Spanner 实例。搜索左上角汉堡式菜单中的 Spanner 条目 ,也可以按“/”并输入“Spanner”以搜索 Spanner。
接下来,点击 并填写表单,方法是为您的实例输入实例名称 cloudspanner-leaderboard,选择配置(选择区域实例),并设置节点数,对于此 Codelab,我们仅需要一个节点。对于生产实例并符合 Cloud Spanner SLA 的资格,您将需要在 Cloud Spanner 实例中运行 3 个或更多节点。
最后但并非最不重要的一点是,点击“创建”,然后在几秒钟内就可以使用 Cloud Spanner 实例。
在下一步中,我们将使用 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:
在 ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab 文件夹中创建名为“leaderboard.go”的文件。
- 首先请确保您已在 Cloud Shell Editor 的文件夹列表中选择了“Codelab”文件夹。
- 然后在 Cloud Shell Editor 的“文件”菜单下选择“新文件”。
- 输入“leaderboard.go”作为新文件的名称。
这是应用的主文件,将包含我们的应用代码和参考,以包括所有依赖项。
如需创建 leaderboard
数据库以及 Players
和 Scores
表,请复制 (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 部分中,您应该在左侧菜单中看到新的数据库和表。
在下一步中,我们将更新应用以将一些数据加载到新数据库中。
现在,我们有一个名为 leaderboard
的数据库,包含两个表:Players
和 Scores
。现在,我们使用 Go 客户端库在 Players
表中填充玩家人数,在 Scores
表中填入每个玩家的随机得分。
如果 Cloud Shell Editor 尚未打开,请点击下面突出显示的图标来打开它:
接下来,在 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
文件查看添加添加代码以启用 insertplayers
和 insertscores
命令后,leaderboard.go
文件应如何显示的示例。
现在,让我们构建并运行应用,以确认新 insertplayers
和 insertscores
命令已包含在应用的可能命令列表中。运行以下命令以构建应用:
go build leaderboard.go
输入以下命令,在 Cloud Shell 中运行生成的应用:
./leaderboard
您应该看到 insertplayers
和 insertscores
命令现在包含在应用的默认输出中:
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
标签页。您应该看到表的 PlayerId
和 PlayerName
列中有数据。
接下来,通过点击 Scores
表并选择 Data
标签页,验证“得分”表中是否也包含数据。您应该看到表的 PlayerId
、Timestamp
和 Score
列中都有数据。
太棒了! 让我们更新我们的应用,以运行一些可用于创建游戏排行榜的查询。
现在我们已经建立了数据库并将信息加载到表中,让我们使用此数据创建一个排行榜。为此,我们需要回答以下四个问题:
- 哪些玩家属于“十大热门”游戏?
- 今年哪些国家/地区的“十大”玩家?
- 当月哪些玩家为“十大”?
- 本周哪些玩家达到“十大”?
我们将更新应用以运行 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, ×tamp); 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, ×tamp); 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
文件查看添加添加代码以启用 query
和 querywithtimespan
命令后,leaderboard.go
文件应如何显示的示例。
现在,让我们构建并运行应用,以确认新 query
和 querywithtimespan
命令已包含在应用的可能命令列表中。
在 Cloud Shell 中运行以下命令以构建应用:
go build leaderboard.go
输入以下命令,在 Cloud Shell 中运行生成的应用:
./leaderboard
您应该将应用的默认输出中现在包含的 query
和 querywithtimespan
命令作为新的命令选项包括在内:
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
命令使我们可以根据 Scores
表 Timestamp
列中的值来指定用于过滤记录的小时数。
让我们使用运行 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 客户端库查询数据中的“十大”结果
后续步骤:
- 阅读 Spanner CAP 白皮书
- 了解架构设计和查询最佳做法
- 详细了解 Cloud Spanner 提交时间戳
向我们提供反馈
- 请抽出一些时间填写这份简短的调查问卷