۱. مقدمه
کلود اسپنر یک سرویس پایگاه داده رابطهای، توزیعشده در سطح جهانی و مقیاسپذیر افقی است که تراکنشهای ACID و مفاهیم SQL را بدون از دست دادن عملکرد و دسترسیپذیری بالا ارائه میدهد.
این ویژگیها، اسپنر را به گزینهای عالی برای معماری بازیهایی تبدیل میکند که میخواهند پایگاه بازیکنان جهانی داشته باشند یا نگران ثبات دادهها هستند.
در این تمرین، شما دو سرویس Go ایجاد خواهید کرد که با یک پایگاه داده منطقهای Spanner تعامل دارند تا بازیکنان بتوانند اقلام و پول ( item-service ) را به دست آورند و سپس اقلام را در فروشگاه برای خرید سایر بازیکنان فهرست کنند ( tradepost-service ).
این آزمایشگاه برای تولید بازیکنان و بازیها با استفاده از profile-service و matchmaking-service به آزمایشگاه کد Cloud Spanner Getting Started with Games Development وابسته است.

در مرحله بعد، با استفاده از چارچوب بارگذاری پایتون Locust.io، دادههایی را تولید خواهید کرد تا بازیکنان را در حال کسب پول و اقلام در طول "بازی" شبیهسازی کنید. سپس بازیکنان میتوانند اقلام را برای فروش در یک فروشگاه فهرست کنند، جایی که سایر بازیکنان با پول کافی میتوانند آن اقلام را خریداری کنند.
همچنین از Spanner درخواست خواهید کرد تا موجودی حساب بازیکنان و تعداد اقلام و برخی آمار مربوط به سفارشات تجاری باز یا پر شده را تعیین کند.
در نهایت، منابعی را که در این آزمایشگاه ایجاد شدهاند، پاکسازی خواهید کرد.
آنچه خواهید ساخت
به عنوان بخشی از این آزمایشگاه، شما:
- از نمونه Spanner از Cloud Spanner Getting Started with Games Development دوباره استفاده کنید.
- یک سرویس آیتم نوشته شده با زبان Go برای مدیریت بازیکنانی که آیتم و پول به دست میآورند، مستقر کنید.
- یک سرویس Trading Post نوشته شده با زبان Go را برای شبیهسازی فهرست کردن اقلام توسط بازیکنان و خرید آن اقلام توسط سایر بازیکنان، مستقر کنید.
آنچه یاد خواهید گرفت
- نحوه استفاده از تراکنشهای خواندن-نوشتن برای اطمینان از سازگاری تغییرات دادهها
- نحوه استفاده از جهشهای DML و Spanner برای تغییر دادهها
آنچه نیاز دارید
- یک پروژه گوگل کلود که به یک حساب صورتحساب متصل است.
- یک مرورگر وب، مانند کروم یا فایرفاکس .
- قبلاً Cloud Spanner Getting Started with Games Development را بدون مرحله پاکسازی تکمیل کردهام.
۲. تنظیمات و الزامات
شروع کار با توسعه بازیها در codelab، مراحل Cloud Spanner را کامل کنید
کد آزمایشگاه شروع کار با توسعه بازیها در Cloud Spanner را تکمیل کنید. این کد برای دریافت مجموعه دادهای از بازیکنان و بازیها لازم است. بازیکنان و بازیها برای به دست آوردن اقلام و پول مورد نیاز هستند که به نوبه خود برای فهرست کردن اقلام برای فروش و خرید اقلام از پست معاملاتی استفاده میشود.
پیکربندی متغیرهای محیطی در Cloud Shell
با کلیک روی فعال کردن Cloud Shell، Cloud Shell را باز کنید.
(از آنجایی که قبلاً این کار را انجام دادهاید، آمادهسازی و اتصال به محیط فقط چند لحظه طول میکشد.)


پس از اتصال به Cloud Shell، باید ببینید که از قبل احراز هویت شدهاید و پروژه از قبل روی PROJECT_ID شما تنظیم شده است.
متغیرهای محیطی SPANNER را در Cloud Shell تنظیم کنید
export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game
طرحواره را ایجاد کنید
اکنون که پایگاه داده شما ایجاد شده است، میتوانید طرحواره را در پایگاه داده sample-game تعریف کنید.
این آزمایش چهار جدول جدید ایجاد خواهد کرد: game_items ، player_items ، player_ledger_entries و trade_orders .

روابط آیتمهای بازیکن

روابط سفارش تجاری
آیتمهای بازی در جدول game_items اضافه میشوند و سپس میتوانند توسط بازیکنان خریداری شوند. جدول player_items دارای کلیدهای خارجی برای itemUUID و playerUUID است تا اطمینان حاصل شود که بازیکنان فقط آیتمهای معتبر را خریداری میکنند.
جدول player_ledger_entries هرگونه تغییر مالی در موجودی حساب بازیکن را ثبت میکند. این تغییرات میتواند از طریق غنیمت یا فروش اقلام در فروشگاه باشد.
و در نهایت، جدول trade_orders برای مدیریت ارسال سفارشات فروش و انجام این سفارشات توسط خریداران استفاده میشود.
برای ایجاد طرحواره، روی دکمهی Write DDL در کنسول ابری کلیک کنید:

در اینجا، تعریف طرحواره را از فایل schema/trading.sql وارد خواهید کرد:
CREATE TABLE game_items
(
itemUUID STRING(36) NOT NULL,
item_name STRING(MAX) NOT NULL,
item_value NUMERIC NOT NULL,
available_time TIMESTAMP NOT NULL,
duration int64
)PRIMARY KEY (itemUUID);
CREATE TABLE player_items
(
playerItemUUID STRING(36) NOT NULL,
playerUUID STRING(36) NOT NULL,
itemUUID STRING(36) NOT NULL,
price NUMERIC NOT NULL,
source STRING(MAX) NOT NULL,
game_session STRING(36) NOT NULL,
acquire_time TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()),
expires_time TIMESTAMP,
visible BOOL NOT NULL DEFAULT(true),
FOREIGN KEY (itemUUID) REFERENCES game_items (itemUUID),
FOREIGN KEY (game_session) REFERENCES games (gameUUID)
) PRIMARY KEY (playerUUID, playerItemUUID),
INTERLEAVE IN PARENT players ON DELETE CASCADE;
CREATE TABLE player_ledger_entries (
playerUUID STRING(36) NOT NULL,
source STRING(MAX) NOT NULL,
game_session STRING(36) NOT NULL,
amount NUMERIC NOT NULL,
entryDate TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
FOREIGN KEY (game_session) REFERENCES games (gameUUID)
) PRIMARY KEY (playerUUID, entryDate DESC),
INTERLEAVE IN PARENT players ON DELETE CASCADE;
CREATE TABLE trade_orders
(
orderUUID STRING(36) NOT NULL,
lister STRING(36) NOT NULL,
buyer STRING(36),
playerItemUUID STRING(36) NOT NULL,
trade_type STRING(5) NOT NULL,
list_price NUMERIC NOT NULL,
created TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()),
ended TIMESTAMP,
expires TIMESTAMP NOT NULL DEFAULT (TIMESTAMP_ADD(CURRENT_TIMESTAMP(), interval 24 HOUR)),
active BOOL NOT NULL DEFAULT (true),
cancelled BOOL NOT NULL DEFAULT (false),
filled BOOL NOT NULL DEFAULT (false),
expired BOOL NOT NULL DEFAULT (false),
FOREIGN KEY (playerItemUUID) REFERENCES player_items (playerItemUUID)
) PRIMARY KEY (orderUUID);
CREATE INDEX TradeItem ON trade_orders(playerItemUUID, active);
برای تغییر طرحواره، روی دکمه « Submit » کلیک کنید و منتظر بمانید تا بهروزرسانی طرحواره تکمیل شود:

بعدی
در مرحله بعد، سرویس آیتم را مستقر خواهید کرد.
۳. سرویس آیتم را مستقر کنید
نمای کلی خدمات
سرویس آیتم یک API REST است که با زبان Go نوشته شده و از چارچوب جین بهره میبرد. در این API، بازیکنانی که در بازیهای آزاد شرکت میکنند، پول و آیتم به دست میآورند.

فایل ./src/golang/item-service/main.go نقاط پایانی زیر را برای کار با آیتمهای بازی و بازیکنانی که این آیتمها را به دست میآورند پیکربندی میکند. علاوه بر این، یک نقطه پایانی برای بازیکنان وجود دارد تا پول به دست آورند.
func main() {
configuration, _ := config.NewConfig()
router := gin.Default()
router.SetTrustedProxies(nil)
router.Use(setSpannerConnection(configuration))
router.GET("/items", getItemUUIDs)
router.POST("/items", createItem)
router.GET("/items/:id", getItem)
router.PUT("/players/balance", updatePlayerBalance)
router.GET("/players", getPlayer)
router.POST("/players/items", addPlayerItem)
router.Run(configuration.Server.URL())
}
پیکربندی و استفاده از اتصالات Spanner دقیقاً مانند profile-service و matchmaking-service از codelab قبلی انجام میشود.
سرویس آیتم با GameItem، Player، PlayerLedger و PlayerItem با تعاریف زیر کار میکند:
// models/game_items.go
type GameItem struct {
ItemUUID string `json:"itemUUID"`
Item_name string `json:"item_name"`
Item_value big.Rat `json:"item_value"`
Available_time time.Time `json:"available_time"`
Duration int64 `json:"duration"`
}
// models/players.go
type Player struct {
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
Updated time.Time `json:"updated"`
Account_balance big.Rat `json:"account_balance"`
Current_game string `json:"current_game"`
}
type PlayerLedger struct {
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
Amount big.Rat `json:"amount"`
Game_session string `json:"game_session"`
Source string `json:"source"`
}
// models/player_items.go
type PlayerItem struct {
PlayerItemUUID string `json:"playerItemUUID" binding:"omitempty,uuid4"`
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
ItemUUID string `json:"itemUUID" binding:"required,uuid4"`
Source string `json:"source" binding:"required"`
Game_session string `json:"game_session" binding:"omitempty,uuid4"`
Price big.Rat `json:"price"`
AcquireTime time.Time `json:"acquire_time"`
ExpiresTime spanner.NullTime `json:"expires_time"`
Visible bool `json:"visible"`
}
ابتدا، بازی باید تعدادی آیتم ایجاد کرده باشد. برای انجام این کار، یک درخواست POST به نقطه پایانی /items فراخوانی میشود. این یک درج DML بسیار ساده در جدول game_items است.
// main.go
func createItem(c *gin.Context) {
var item models.GameItem
if err := c.BindJSON(&item); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
ctx, client := getSpannerConnection(c)
err := item.Create(ctx, client)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.IndentedJSON(http.StatusCreated, item.ItemUUID)
}
// models/game_items.go
func (i *GameItem) Create(ctx context.Context, client spanner.Client) error {
// Initialize item values
i.ItemUUID = generateUUID()
if i.Available_time.IsZero() {
i.Available_time = time.Now()
}
// insert into spanner
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
stmt := spanner.Statement{
SQL: `INSERT game_items (itemUUID, item_name, item_value, available_time, duration)
VALUES (@itemUUID, @itemName, @itemValue, @availableTime, @duration)
`,
Params: map[string]interface{}{
"itemUUID": i.ItemUUID,
"itemName": i.Item_name,
"itemValue": i.Item_value,
"availableTime": i.Available_time,
"duration": i.Duration,
},
}
_, err := txn.Update(ctx, stmt)
return err
})
if err != nil {
return err
}
// return empty error on success
return nil
}
برای به دست آوردن یک آیتم، یک درخواست POST به نقطه پایانی /players/items فراخوانی میشود. منطق این نقطه پایانی، بازیابی مقدار فعلی یک آیتم بازی و جلسه بازی فعلی بازیکن است. سپس اطلاعات مناسب را در جدول player_items که منبع و زمان به دست آوردن آیتم را نشان میدهد، وارد کنید.
این به توابع زیر نگاشت میشود:
// main.go
func addPlayerItem(c *gin.Context) {
var playerItem models.PlayerItem
if err := c.BindJSON(&playerItem); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
ctx, client := getSpannerConnection(c)
err := playerItem.Add(ctx, client)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.IndentedJSON(http.StatusCreated, playerItem)
}
// models/player_items.go
func (pi *PlayerItem) Add(ctx context.Context, client spanner.Client) error {
// insert into spanner
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// Get item price at time of transaction
price, err := GetItemPrice(ctx, txn, pi.ItemUUID)
if err != nil {
return err
}
pi.Price = price
// Get Game session
session, err := GetPlayerSession(ctx, txn, pi.PlayerUUID)
if err != nil {
return err
}
pi.Game_session = session
pi.PlayerItemUUID = generateUUID()
// Insert
cols := []string{"playerItemUUID", "playerUUID", "itemUUID", "price", "source", "game_session"}
txn.BufferWrite([]*spanner.Mutation{
spanner.Insert("player_items", cols,
[]interface{}{pi.PlayerItemUUID, pi.PlayerUUID, pi.ItemUUID, pi.Price, pi.Source, pi.Game_session}),
})
return nil
})
if err != nil {
return err
}
// return empty error on success
return nil
}
برای اینکه یک بازیکن پول به دست آورد، یک درخواست PUT به نقطه پایانی /players/updatebalance فراخوانی میشود.
منطق این نقطه پایانی، بهروزرسانی موجودی بازیکن پس از اعمال amount و همچنین بهروزرسانی جدول player_ledger_entries با سابقهای از خرید است. موجودی حساب بازیکن اصلاح میشود تا به فراخواننده بازگردانده شود. DML برای اصلاح هر دو مورد players و player_ledger_entries استفاده میشود.
این به توابع زیر نگاشت میشود:
// main.go
func updatePlayerBalance(c *gin.Context) {
var player models.Player
var ledger models.PlayerLedger
if err := c.BindJSON(&ledger); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
ctx, client := getSpannerConnection(c)
err := ledger.UpdateBalance(ctx, client, &player)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
type PlayerBalance struct {
PlayerUUID, AccountBalance string
}
balance := PlayerBalance{PlayerUUID: player.PlayerUUID, AccountBalance: player.Account_balance.FloatString(2)}
c.IndentedJSON(http.StatusOK, balance)
}
// models/players.go
func (l *PlayerLedger) UpdateBalance(ctx context.Context, client spanner.Client, p *Player) error {
// Update balance with new amount
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
p.PlayerUUID = l.PlayerUUID
stmt := spanner.Statement{
SQL: `UPDATE players SET account_balance = (account_balance + @amount) WHERE playerUUID = @playerUUID`,
Params: map[string]interface{}{
"amount": l.Amount,
"playerUUID": p.PlayerUUID,
},
}
numRows, err := txn.Update(ctx, stmt)
if err != nil {
return err
}
// No rows modified. That's an error
if numRows == 0 {
errorMsg := fmt.Sprintf("Account balance for player '%s' could not be updated", p.PlayerUUID)
return errors.New(errorMsg)
}
// Get player's new balance (read after write)
stmt = spanner.Statement{
SQL: `SELECT account_balance, current_game FROM players WHERE playerUUID = @playerUUID`,
Params: map[string]interface{}{
"playerUUID": p.PlayerUUID,
},
}
iter := txn.Query(ctx, stmt)
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
var accountBalance big.Rat
var gameSession string
if err := row.Columns(&accountBalance, &gameSession); err != nil {
return err
}
p.Account_balance = accountBalance
l.Game_session = gameSession
}
stmt = spanner.Statement{
SQL: `INSERT INTO player_ledger_entries (playerUUID, amount, game_session, source, entryDate)
VALUES (@playerUUID, @amount, @game, @source, PENDING_COMMIT_TIMESTAMP())`,
Params: map[string]interface{}{
"playerUUID": l.PlayerUUID,
"amount": l.Amount,
"game": l.Game_session,
"source": l.Source,
},
}
numRows, err = txn.Update(ctx, stmt)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
به طور پیشفرض، سرویس با استفاده از متغیرهای محیطی پیکربندی میشود. به بخش مربوطه در فایل ./src/golang/item-service/config/config.go مراجعه کنید.
func NewConfig() (Config, error) {
*snip*
// Server defaults
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.port", 8082)
// Bind environment variable override
viper.BindEnv("server.host", "SERVICE_HOST")
viper.BindEnv("server.port", "SERVICE_PORT")
viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")
*snip*
return c, nil
}
همانطور که میبینید، رفتار پیشفرض، اجرای سرویس روی localhost:8082 است.
با این اطلاعات، زمان اجرای سرویس فرا رسیده است.
سرویس را اجرا کنید
اجرای سرویس، وابستگیها را دانلود کرده و سرویس را روی پورت ۸۰۸۲ راهاندازی میکند:
cd ~/spanner-gaming-sample/src/golang/item-service
go run . &
خروجی دستور:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /items --> main.getItemUUIDs (4 handlers)
[GIN-debug] POST /items --> main.createItem (4 handlers)
[GIN-debug] GET /items/:id --> main.getItem (4 handlers)
[GIN-debug] PUT /players/balance --> main.updatePlayerBalance (4 handlers)
[GIN-debug] GET /players --> main.getPlayer (4 handlers)
[GIN-debug] POST /players/items --> main.addPlayerItem (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8082
سرویس را با صدور دستور curl برای ایجاد یک آیتم آزمایش کنید:
curl http://localhost:8082/items \
--include \
--header "Content-Type: application/json" \
--request "POST" \
--data '{"item_name": "test_item","item_value": "3.14"}'
خروجی دستور:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <Date>
Content-Length: 38
"aecde380-0a79-48c0-ab5d-0da675d3412c"
در مرحله بعد، میخواهید یک بازیکن این آیتم را به دست آورد. برای انجام این کار، به ItemUUID و PlayerUUID نیاز دارید. ItemUUID خروجی دستور قبلی است. در این مثال، عبارت است از: aecde380-0a79-48c0-ab5d-0da675d3412c .
برای دریافت PlayerUUID، نقطه پایانی GET /players را فراخوانی کنید:
curl http://localhost:8082/players
خروجی دستور:
{
"playerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352",
"updated": "0001-01-01T00:00:00Z",
"account_balance": {},
"current_game": "7b97fa85-5658-4ded-a962-4c09269a0a79"
}
برای اینکه بازیکن بتواند آیتم را بدست آورد، یک درخواست به نقطه پایانی POST /players/items ارسال کنید:
curl http://localhost:8082/players/items \
--include \
--header "Content-Type: application/json" \
--request "POST" \
--data '{"playerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352","itemUUID": "109ec745-9906-402b-9d03-ca7153a10312", "source": "loot"}'
خروجی دستور:
Content-Type: application/json; charset=utf-8
Date: <Date>
Content-Length: 369
{
"playerItemUUID": "a42b1899-4509-4fce-9958-265d2a2838a0",
"playerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352",
"itemUUID": "109ec745-9906-402b-9d03-ca7153a10312",
"source": "loot",
"game_session": "7b97fa85-5658-4ded-a962-4c09269a0a79",
"price": {},
"acquire_time": "0001-01-01T00:00:00Z",
"expires_time": null,
"visible": false
}
خلاصه
در این مرحله، شما سرویس آیتم را که امکان ایجاد آیتمهای بازی را فراهم میکند، مستقر کردید و بازیکنانی را که برای باز کردن بازیها تعیین شدهاند، قادر به کسب پول و آیتمهای بازی خواهید کرد.
مراحل بعدی
در مرحله بعد، سرویس tradepost را مستقر خواهید کرد.
۴. سرویس tradepost را مستقر کنید
نمای کلی خدمات
سرویس tradepost یک API REST است که با زبان Go نوشته شده و از چارچوب gin بهره میبرد. در این API، اقلام بازیکنان برای فروش قرار داده میشوند. سپس بازیکنان بازیها میتوانند معاملات آزاد داشته باشند و در صورت داشتن پول کافی، میتوانند کالا را خریداری کنند.

فایل ./src/golang/tradepost-service/main.go برای سرویس tradepost از تنظیمات و کدهای مشابهی مانند سایر سرویسها پیروی میکند، بنابراین در اینجا تکرار نمیشود. این سرویس چندین نقطه پایانی به شرح زیر ارائه میدهد:
func main() {
configuration, _ := config.NewConfig()
router := gin.Default()
router.SetTrustedProxies(nil)
router.Use(setSpannerConnection(configuration))
router.GET("/trades/player_items", getPlayerItem)
router.POST("/trades/sell", createOrder)
router.GET("/trades/open", getOpenOrder)
router.PUT("/trades/buy", purchaseOrder)
router.Run(configuration.Server.URL())
}
این سرویس یک ساختار TradeOrder و همچنین ساختارهای مورد نیاز برای GameItem، PlayerItem، Player و PlayerLedger را ارائه میدهد:
type TradeOrder struct {
OrderUUID string `json:"orderUUID" binding:"omitempty,uuid4"`
Lister string `json:"lister" binding:"omitempty,uuid4"`
Buyer string `json:"buyer" binding:"omitempty,uuid4"`
PlayerItemUUID string `json:"playerItemUUID" binding:"omitempty,uuid4"`
TradeType string `json:"trade_type"`
ListPrice big.Rat `json:"list_price" spanner:"list_price"`
Created time.Time `json:"created"`
Ended spanner.NullTime `json:"ended"`
Expires time.Time `json:"expires"`
Active bool `json:"active"`
Cancelled bool `json:"cancelled"`
Filled bool `json:"filled"`
Expired bool `json:"expired"`
}
type GameItem struct {
ItemUUID string `json:"itemUUID"`
ItemName string `json:"item_name"`
ItemValue big.Rat `json:"item_value"`
AvailableTime time.Time `json:"available_time"`
Duration int64 `json:"duration"`
}
type PlayerItem struct {
PlayerItemUUID string `json:"playerItemUUID" binding:"omitempty,uuid4"`
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
ItemUUID string `json:"itemUUID" binding:"required,uuid4"`
Source string `json:"source"`
GameSession string `json:"game_session" binding:"omitempty,uuid4"`
Price big.Rat `json:"price"`
AcquireTime time.Time `json:"acquire_time" spanner:"acquire_time"`
ExpiresTime spanner.NullTime `json:"expires_time" spanner:"expires_time"`
Visible bool `json:"visible"`
}
type Player struct {
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
Updated time.Time `json:"updated"`
AccountBalance big.Rat `json:"account_balance" spanner:"account_balance"`
CurrentGame string `json:"current_game" binding:"omitempty,uuid4" spanner:"current_game"`
}
type PlayerLedger struct {
PlayerUUID string `json:"playerUUID" binding:"required,uuid4"`
Amount big.Rat `json:"amount"`
GameSession string `json:"game_session" spanner:"game_session"`
Source string `json:"source"`
}
برای ایجاد یک سفارش معاملاتی، یک درخواست POST به نقطه پایانی API /trades/sell ارسال میشود. اطلاعات مورد نیاز شامل playerItemUUID مربوط به player_item مورد نظر برای فروش، فروشنده و list_price است.
جهشهای آچار برای ایجاد ترتیب معامله و علامتگذاری player_item به عنوان «غیرقابل مشاهده» انتخاب میشوند. انجام این کار مانع از ارسال اقلام تکراری توسط فروشنده برای فروش میشود.
func (o *TradeOrder) Create(ctx context.Context, client spanner.Client) error {
// insert into spanner
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// get the Item to be listed
pi, err := GetPlayerItem(ctx, txn, o.Lister, o.PlayerItemUUID)
if err != nil {
return err
}
// Set expires to 1 day by default
if o.Expires.IsZero() {
currentTime := time.Now()
o.Expires = currentTime.Add(time.Hour * 24)
}
// Item is not visible or expired, so it can't be listed. That's an error
if !validateSellOrder(pi) {
errorMsg := fmt.Sprintf("Item (%s, %s) cannot be listed.", o.Lister, o.PlayerItemUUID)
return errors.New(errorMsg)
}
// Initialize order values
o.OrderUUID = generateUUID()
o.Active = true // TODO: Have to set this by default since testing with emulator does not support 'DEFAULT' schema option
// Insert the order
var m []*spanner.Mutation
cols := []string{"orderUUID", "playerItemUUID", "lister", "list_price", "trade_type", "expires", "active"}
m = append(m, spanner.Insert("trade_orders", cols, []interface{}{o.OrderUUID, o.PlayerItemUUID, o.Lister, o.ListPrice, "sell", o.Expires, o.Active}))
// Mark the item as invisible
cols = []string{"playerUUID", "playerItemUUID", "visible"}
m = append(m, spanner.Update("player_items", cols, []interface{}{o.Lister, o.PlayerItemUUID, false}))
txn.BufferWrite(m)
return nil
})
if err != nil {
return err
}
// return empty error on success
return nil
}
قبل از ایجاد سفارش، آیتم بازیکن اعتبارسنجی میشود تا از امکان فروش آن اطمینان حاصل شود. در درجه اول این بدان معناست که آیتم بازیکن برای بازیکن قابل مشاهده است و منقضی نشده است.
// Validate that the order can be placed: Item is visible and not expired
func validateSellOrder(pi PlayerItem) bool {
// Item is not visible, can't be listed
if !pi.Visible {
return false
}
// item is expired. can't be listed
if !pi.ExpiresTime.IsNull() && pi.ExpiresTime.Time.Before(time.Now()) {
return false
}
// All validation passed. Item can be listed
return true
}
خرید با یک درخواست PUT به نقطه پایانی /trades/buy انجام میشود. اطلاعات مورد نیاز، orderUUID و buyer است که UUID بازیکنی است که خرید را انجام میدهد.
به دلیل این پیچیدگی و میزان تغییرات، جهشها دوباره برای خرید سفارش انتخاب میشوند. عملیات زیر در یک تراکنش خواندن-نوشتن انجام میشود:
- تأیید کنید که سفارش میتواند تکمیل شود زیرا قبلاً تکمیل نشده و منقضی نشده است.
// Validate that the order can be filled: Order is active and not expired
func validatePurchase(o TradeOrder) bool {
// Order is not active
if !o.Active {
return false
}
// order is expired. can't be filled
if !o.Expires.IsZero() && o.Expires.Before(time.Now()) {
return false
}
// All validation passed. Order can be filled
return true
}
- اطلاعات خریدار را بازیابی کنید و تأیید کنید که آنها میتوانند کالا را خریداری کنند. این بدان معناست که خریدار نمیتواند همان فروشنده باشد و پول کافی هم دارد.
// Validate that a buyer can buy this item.
func validateBuyer(b Player, o TradeOrder) bool {
// Lister can't be the same as buyer
if b.PlayerUUID == o.Lister {
return false
}
// Big.rat returns -1 if Account_balance is less than price
if b.AccountBalance.Cmp(&o.ListPrice) == -1 {
return false
}
return true
}
- قیمت لیست سفارش را به موجودی حساب فروشنده اضافه کنید، به همراه یک ورودی دفتر کل مطابق با آن.
// models/trade_order.go
// Buy an order
func (o *TradeOrder) Buy(ctx context.Context, client spanner.Client) error {
*snip*
// Update seller's account balance
lister.UpdateBalance(ctx, txn, o.ListPrice)
*snip*
}
// models/players.go
// Update a player's balance, and add an entry into the player ledger
func (p *Player) UpdateBalance(ctx context.Context, txn *spanner.ReadWriteTransaction, newAmount big.Rat) error {
// This modifies player's AccountBalance, which is used to update the player entry
p.AccountBalance.Add(&p.AccountBalance, &newAmount)
txn.BufferWrite([]*spanner.Mutation{
spanner.Update("players", []string{"playerUUID", "account_balance"}, []interface{}{p.PlayerUUID, p.AccountBalance}),
spanner.Insert("player_ledger_entries", []string{"playerUUID", "amount", "game_session", "source", "entryDate"},
[]interface{}{p.PlayerUUID, newAmount, p.CurrentGame, "tradepost", spanner.CommitTimestamp}),
})
return nil
}
- قیمت لیست سفارش را از موجودی حساب خریدار ، با یک ورودی دفتر کل منطبق، کم کنید.
// Update buyer's account balance
negAmount := o.ListPrice.Neg(&o.ListPrice)
buyer.UpdateBalance(ctx, txn, *negAmount)
- با وارد کردن یک نمونه جدید از آیتم بازی به همراه جزئیات بازی و خریدار در جدول PlayerItems ، آیتم player_item را به بازیکن جدید منتقل کنید و نمونه آیتم لیستکننده را حذف کنید.
// models/player_items.go
// Move an item to a new player, removes the item entry from the old player
func (pi *PlayerItem) MoveItem(ctx context.Context, txn *spanner.ReadWriteTransaction, toPlayer string) error {
fmt.Printf("Buyer: %s", toPlayer)
txn.BufferWrite([]*spanner.Mutation{
spanner.Insert("player_items", []string{"playerItemUUID", "playerUUID", "itemUUID", "price", "source", "game_session"},
[]interface{}{pi.PlayerItemUUID, toPlayer, pi.ItemUUID, pi.Price, pi.Source, pi.GameSession}),
spanner.Delete("player_items", spanner.Key{pi.PlayerUUID, pi.PlayerItemUUID}),
})
return nil
}
- ورودی سفارشات را بهروزرسانی میکند تا نشان دهد که کالا تکمیل شده و دیگر فعال نیست.
روی هم رفته، تابع خرید به شکل زیر است:
// Buy an order
func (o *TradeOrder) Buy(ctx context.Context, client spanner.Client) error {
// Fulfil the order
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// Get Order information
err := o.getOrderDetails(ctx, txn)
if err != nil {
return err
}
// Validate order can be filled
if !validatePurchase(*o) {
errorMsg := fmt.Sprintf("Order (%s) cannot be filled.", o.OrderUUID)
return errors.New(errorMsg)
}
// Validate buyer has the money
buyer := Player{PlayerUUID: o.Buyer}
err = buyer.GetBalance(ctx, txn)
if err != nil {
return err
}
if !validateBuyer(buyer, *o) {
errorMsg := fmt.Sprintf("Buyer (%s) cannot purchase order (%s).", buyer.PlayerUUID, o.OrderUUID)
return errors.New(errorMsg)
}
// Move money from buyer to seller (which includes ledger entries)
var m []*spanner.Mutation
lister := Player{PlayerUUID: o.Lister}
err = lister.GetBalance(ctx, txn)
if err != nil {
return err
}
// Update seller's account balance
lister.UpdateBalance(ctx, txn, o.ListPrice)
// Update buyer's account balance
negAmount := o.ListPrice.Neg(&o.ListPrice)
buyer.UpdateBalance(ctx, txn, *negAmount)
// Move item from seller to buyer, mark item as visible.
pi, err := GetPlayerItem(ctx, txn, o.Lister, o.PlayerItemUUID)
if err != nil {
return err
}
pi.GameSession = buyer.CurrentGame
// Moves the item from lister (current pi.PlayerUUID) to buyer
pi.MoveItem(ctx, txn, o.Buyer)
// Update order information
cols := []string{"orderUUID", "active", "filled", "buyer", "ended"}
m = append(m, spanner.Update("trade_orders", cols, []interface{}{o.OrderUUID, false, true, o.Buyer, time.Now()}))
txn.BufferWrite(m)
return nil
})
if err != nil {
return err
}
// return empty error on success
return nil
}
به طور پیشفرض، سرویس با استفاده از متغیرهای محیطی پیکربندی میشود. به بخش مربوطه در فایل ./src/golang/tradepost-service/config/config.go مراجعه کنید.
func NewConfig() (Config, error) {
*snip*
// Server defaults
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.port", 8083)
// Bind environment variable override
viper.BindEnv("server.host", "SERVICE_HOST")
viper.BindEnv("server.port", "SERVICE_PORT")
viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")
*snip*
return c, nil
}
همانطور که میبینید، رفتار پیشفرض، اجرای سرویس روی localhost:8083 است تا از تداخل با سایر سرویسها جلوگیری شود.
با این اطلاعات، اکنون زمان اجرای سرویس tradepost فرا رسیده است.
سرویس را اجرا کنید
اجرای سرویس، سرویسی را که روی پورت ۸۰۸۳ اجرا میشود، ایجاد میکند. این سرویس بسیاری از وابستگیهای مشابه item-service را دارد، بنابراین وابستگیهای جدید دانلود نخواهند شد.
cd ~/spanner-gaming-sample/src/golang/tradepost-service
go run . &
خروجی دستور:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /trades/player_items --> main.getPlayerItem (4 handlers)
[GIN-debug] POST /trades/sell --> main.createOrder (4 handlers)
[GIN-debug] GET /trades/open --> main.getOpenOrder (4 handlers)
[GIN-debug] PUT /trades/buy --> main.purchaseOrder (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8083
یک مورد پست کنید
سرویس را با ارسال یک درخواست GET برای بازیابی یک PlayerItem جهت فروش، آزمایش کنید:
curl http://localhost:8083/trades/player_items
خروجی دستور:
{
"PlayerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352",
"PlayerItemUUID": "a42b1899-4509-4fce-9958-265d2a2838a0",
"Price": "3.14"
}
حالا، بیایید با فراخوانی نقطه پایانی /trades/sel l یک کالا را برای فروش ارسال کنیم.
curl http://localhost:8083/trades/sell \
--include \
--header "Content-Type: application/json" \
--request "POST" \
--data '{"lister": "<PlayerUUID>","playerItemUUID": "<PlayerItemUUID>", "list_price": "<some price higher than item's price>"}'
خروجی دستور:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <Date>
Content-Length: 38
"282ea691-b956-4c4c-95ff-f461d6415651"
خلاصه
در این مرحله، شما سرویس tradepost را برای مدیریت ایجاد سفارشات فروش مستقر کردید. این سرویس همچنین امکان خرید آن سفارشات را نیز فراهم میکند.
مراحل بعدی
حالا که سرویسهای شما در حال اجرا هستند، وقت آن رسیده که بازیکنانی را که در پست معاملاتی خرید و فروش میکنند، شبیهسازی کنید!
۵. شروع به معامله کنید
اکنون که سرویسهای کالا و فروشگاه در حال اجرا هستند، میتوانید با استفاده از مولدهای ملخ ارائه شده، بار تولید کنید.
Locust یک رابط وب برای اجرای ژنراتورها ارائه میدهد، اما در این آزمایش از خط فرمان (گزینه –headless ) استفاده خواهید کرد.
تولید آیتمهای بازی
ابتدا، شما باید آیتمها را تولید کنید. فایل ./generators/item_generator.py شامل وظیفهای برای ایجاد آیتمهای بازی با رشتههای تصادفی برای نامها و مقادیر تصادفی قیمت است:
# Generate random items
class ItemLoad(HttpUser):
def generateItemName(self):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
def generateItemValue(self):
return str(decimal.Decimal(random.randrange(100, 10000))/100)
@task
def createItem(self):
headers = {"Content-Type": "application/json"}
data = {"item_name": self.generateItemName(), "item_value": self.generateItemValue()}
self.client.post("/items", data=json.dumps(data), headers=headers)
دستور زیر فایل item_generator.py را فراخوانی میکند که آیتمهای بازی را به مدت 10 ثانیه ( t=10s) تولید میکند:
cd ~/spanner-gaming-sample
locust -H http://127.0.0.1:8082 -f ./generators/item_generator.py --headless -u=1 -r=1 -t=10s
خروجی دستور:
*snip*
/INFO/locust.main: --run-time limit reached. Stopping Locust
/INFO/locust.main: Shutting down (exit code 0)
Name # reqs # fails | Avg Min Max Median | req/s failures/s
----------------------------------------------------------------------------------------------------------------------------------------------------------------
POST /items 606 0(0.00%) | 16 12 161 15 | 60.61 0.00
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregated 606 0(0.00%) | 16 12 161 15 | 60.61 0.00
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
POST /items 15 16 16 17 18 19 21 34 160 160 160 606
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
None Aggregated 15 16 16 17 18 19 21 34 160 160 160 606
بازیکنان آیتمها و پول به دست میآورند
در مرحله بعد، بیایید از بازیکنان بخواهیم که اقلام و پول به دست آورند تا بتوانند در تجارت مشارکت کنند. برای انجام این کار، فایل ./generators/game_server.py وظایفی را برای بازیابی اقلام بازی برای اختصاص به بازیکنان و همچنین مقادیر تصادفی ارز فراهم میکند.
# Players generate items and money at 5:2 ratio. We don't want to devalue the currency!
class GameLoad(HttpUser):
def on_start(self):
self.getItems()
def getItems(self):
headers = {"Content-Type": "application/json"}
r = requests.get(f"{self.host}/items", headers=headers)
global itemUUIDs
itemUUIDs = json.loads(r.text)
def generateAmount(self):
return str(round(random.uniform(1.01, 49.99), 2))
@task(2)
def acquireMoney(self):
headers = {"Content-Type": "application/json"}
# Get a random player that's part of a game, and update balance
with self.client.get("/players", headers=headers, catch_response=True) as response:
try:
data = {"playerUUID": response.json()["playerUUID"], "amount": self.generateAmount(), "source": "loot"}
self.client.put("/players/balance", data=json.dumps(data), headers=headers)
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'playerUUID'")
@task(5)
def acquireItem(self):
headers = {"Content-Type": "application/json"}
# Get a random player that's part of a game, and add an item
with self.client.get("/players", headers=headers, catch_response=True) as response:
try:
itemUUID = itemUUIDs[random.randint(0, len(itemUUIDs)-1)]
data = {"playerUUID": response.json()["playerUUID"], "itemUUID": itemUUID, "source": "loot"}
self.client.post("/players/items", data=json.dumps(data), headers=headers)
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'playerUUID'")
این دستور به بازیکنان اجازه میدهد تا به مدت ۶۰ ثانیه آیتمها و پول به دست آورند:
locust -H http://127.0.0.1:8082 -f game_server.py --headless -u=1 -r=1 -t=60s
خروجی دستور:
*snip*
dev-machine/INFO/locust.main: --run-time limit reached. Stopping Locust
dev-machine/INFO/locust.main: Shutting down (exit code 0)
Name # reqs # fails | Avg Min Max Median | req/s failures/s
----------------------------------------------------------------------------------------------------------------------------------------------------------------
GET /players 231 0(0.00%) | 14 9 30 13 | 23.16 0.00
PUT /players/balance 53 0(0.00%) | 33 30 39 34 | 5.31 0.00
POST /players/items 178 0(0.00%) | 26 22 75 26 | 17.85 0.00
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregated 462 0(0.00%) | 21 9 75 23 | 46.32 0.00
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
GET /players 13 16 17 17 19 20 21 23 30 30 30 231
PUT /players/balance 34 34 35 35 36 37 38 40 40 40 40 53
POST /players/items 26 27 27 27 28 29 34 53 76 76 76 178
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
None Aggregated 23 26 27 27 32 34 36 37 76 76 76 462
بازیکنانی که در پست معاملاتی خرید و فروش میکنند
حالا که بازیکنان آیتمها و پول لازم برای خرید آیتمها را دارند، میتوانند از پست تجاری استفاده کنند!
فایل مولد ./generators/trading_server.py وظایفی را برای ایجاد سفارشات فروش و انجام آن سفارشات فراهم میکند.
# Players can sell and buy items
class TradeLoad(HttpUser):
def itemMarkup(self, value):
f = float(value)
return str(f*1.5)
@task
def sellItem(self):
headers = {"Content-Type": "application/json"}
# Get a random item
with self.client.get("/trades/player_items", headers=headers, catch_response=True) as response:
try:
playerUUID = response.json()["PlayerUUID"]
playerItemUUID = response.json()["PlayerItemUUID"]
list_price = self.itemMarkup(response.json()["Price"])
# Currently don't have any items that can be sold, retry
if playerItemUUID == "":
raise RescheduleTask()
data = {"lister": playerUUID, "playerItemUUID": playerItemUUID, "list_price": list_price}
self.client.post("/trades/sell", data=json.dumps(data), headers=headers)
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'playerUUID'")
@task
def buyItem(self):
headers = {"Content-Type": "application/json"}
# Get a random item
with self.client.get("/trades/open", headers=headers, catch_response=True) as response:
try:
orderUUID = response.json()["OrderUUID"]
buyerUUID = response.json()["BuyerUUID"]
# Currently don't have any buyers that can fill the order, retry
if buyerUUID == "":
raise RescheduleTask()
data = {"orderUUID": orderUUID, "buyer": buyerUUID}
self.client.put("/trades/buy", data=json.dumps(data), headers=headers)
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'playerUUID'")
این دستور به بازیکنان اجازه میدهد تا اقلامی را که برای فروش خریداری کردهاند، فهرست کنند و سایر بازیکنان نیز میتوانند آن اقلام را به مدت 10 ثانیه خریداری کنند:
locust -H http://127.0.0.1:8083 -f ./generators/trading_server.py --headless -u=1 -r=1 -t=10s
خروجی دستور:
*snip*
Name # reqs # fails | Avg Min Max Median | req/s failures/s
----------------------------------------------------------------------------------------------------------------------------------------------------------------
PUT /trades/buy 20 5(25.00%) | 43 10 78 43 | 2.07 0.52
GET /trades/open 20 0(0.00%) | 358 7 971 350 | 2.07 0.00
GET /trades/player_items 20 0(0.00%) | 49 35 113 41 | 2.07 0.00
POST /trades/sell 20 0(0.00%) | 29 21 110 24 | 2.07 0.00
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregated 80 5(6.25%) | 120 7 971 42 | 8.29 0.52
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
PUT /trades/buy 43 45 49 50 71 78 78 78 78 78 78 20
GET /trades/open 360 500 540 550 640 970 970 970 970 970 970 20
GET /trades/player_items 43 55 57 59 72 110 110 110 110 110 110 20
POST /trades/sell 24 25 25 27 50 110 110 110 110 110 110 20
--------|--------------------------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
None Aggregated 42 50 71 110 440 550 640 970 970 970 970 80
خلاصه
در این مرحله، شما ثبتنام بازیکنان برای انجام بازیها را شبیهسازی کردید و سپس شبیهسازیهایی را برای بازیکنان اجرا کردید تا با استفاده از سرویس matchmaking بازی کنند. این شبیهسازیها از چارچوب Locust Python برای ارسال درخواستها به REST API سرویسهای ما استفاده کردند.
میتوانید زمان صرف شده برای ایجاد بازیکنان و انجام بازیها و همچنین تعداد کاربران همزمان ( -u) را تغییر دهید.
مراحل بعدی
پس از شبیهسازی، میتوانید با پرسوجو از Spanner، آمارهای مختلف را بررسی کنید.
۶. بازیابی آمار تجارت
حالا که نحوهی کسب پول و آیتم توسط بازیکنان و سپس فروش آن آیتمها در فروشگاه را شبیهسازی کردهایم، بیایید برخی از آمارها را بررسی کنیم.
برای انجام این کار، از Cloud Console برای ارسال درخواستهای پرسوجو به Spanner استفاده کنید.

بررسی سفارشات معاملاتی باز در مقابل سفارشات انجام شده
وقتی یک سفارش تجاری در دفتر معاملاتی خریداری میشود، فیلد فراداده پر شده بهروزرسانی میشود.
این کوئری به شما کمک میکند تا بررسی کنید که چه تعداد سفارش باز و چه تعداد پر شده است:
-- Open vs Filled Orders
SELECT Type, NumTrades FROM
(SELECT "Open Trades" as Type, count(*) as NumTrades FROM trade_orders WHERE active=true
UNION ALL
SELECT "Filled Trades" as Type, count(*) as NumTrades FROM trade_orders WHERE filled=true
)
نتیجه:
نوع | NumTrades |
معاملات باز | ۱۵۹ |
معاملات پر شده | ۴۵۴ |
بررسی موجودی حساب بازیکن و تعداد اقلام
اگر ستون current_game بازیکن تنظیم شده باشد، او در حال بازی است. در غیر این صورت، او در حال حاضر در حال بازی نیست.
برای رسیدن به 10 بازیکن برتر که در حال حاضر بازیهایی با بیشترین آیتم انجام میدهند، با استفاده از موجودی حساب کاربریشان
، از این کوئری استفاده کنید:
SELECT playerUUID, account_balance, (SELECT COUNT(*) FROM player_items WHERE playerUUID=p.PlayerUUID) AS numItems, current_game
FROM players AS p
WHERE current_game IS NOT NULL
ORDER BY numItems DESC
LIMIT 10;
نتیجه:
| | | |
| | | |
| | | |
| | | |
خلاصه
در این مرحله، شما با استفاده از Cloud Console برای پرس و جو از Spanner، آمارهای مختلف سفارشات بازیکن و معامله را بررسی کردید.
مراحل بعدی
بعدش، وقت تمیزکاریه!
۷. تمیز کردن
بعد از کلی بازی و سرگرمی با Spanner، باید زمین بازیمان را تمیز کنیم. خوشبختانه این مرحله آسانی است، فقط کافی است به بخش Cloud Spanner در Cloud Console بروید و نمونهای را که برای این codelab ایجاد کردهایم، حذف کنید.
۸. تبریک میگویم!
تبریک میگویم، شما با موفقیت یک بازی نمونه را روی Spanner مستقر کردید.
بعدش چی؟
در این آزمایش، شما راهاندازی دو سرویس برای مدیریت تولید آیتمهای بازی و خرید آیتم توسط بازیکنان برای فروش در فروشگاه را به پایان رساندهاید.
این نمونههای کد باید درک بهتری از نحوه عملکرد سازگاری Cloud Spanner در تراکنشها برای جهشهای DML و Spanner به شما ارائه دهند.
برای بررسی مقیاسبندی Spanner، میتوانید از مولدهای ارائه شده استفاده کنید.