1. مقدمه
Cloud Spanner یک سرویس پایگاه داده رابطهای با مقیاسپذیر افقی، توزیع شده در سطح جهانی و کاملاً مدیریت شده است که تراکنشهای ACID و معنایی SQL را بدون کاهش عملکرد و در دسترس بودن بالا ارائه میکند.
این ویژگیها باعث میشود تا Spanner در معماری بازیهایی که میخواهند پایگاه جهانی بازیکنان را فعال کنند یا نگران ثبات دادهها هستند، مناسب باشد.
در این آزمایشگاه، شما دو سرویس Go ایجاد میکنید که با یک پایگاه داده منطقهای Spanner تعامل میکنند تا بازیکنان را قادر میسازد اقلام و پول ( item-service
) را به دست آورند و سپس مواردی را در پست معاملاتی فهرست کنید تا بازیکنان دیگر خریداری کنند ( tradepost-service
) .
این آزمایشگاه به Cloud Spanner Getting Started with Games Codelab وابسته است تا بازیکنان و بازیهایی را با استفاده از profile-service
و matchmaking-service
تولید کند.
سپس با استفاده از چارچوب بارگذاری پایتون، Locust.io، دادههایی را برای شبیهسازی بازیکنانی که پول و اقلام را از طریق «بازی بازی» به دست میآورند، تولید میکنید. سپس بازیکنان میتوانند اقلامی را برای فروش در یک پست تجاری فهرست کنند، جایی که سایر بازیکنان با پول کافی میتوانند آن موارد را خریداری کنند.
همچنین برای تعیین موجودی حساب بازیکنان و تعداد آیتمها و برخی آمار درباره سفارشهای تجاری که باز یا تکمیل شدهاند، از Spanner پرس و جو میکنید.
در نهایت، منابعی که در این آزمایشگاه ایجاد شده اند را پاکسازی خواهید کرد.
چیزی که خواهی ساخت
به عنوان بخشی از این آزمایشگاه، شما:
- استفاده مجدد از نمونه آچار در Cloud Spanner شروع با توسعه بازی ها .
- یک سرویس Item نوشته شده در Go را برای رسیدگی به بازیکنانی که اقلام و پول به دست می آورند، مستقر کنید
- یک سرویس Trading Post نوشته شده در Go را برای شبیه سازی بازیکنانی که اقلام را برای فروش فهرست می کنند، و سایر بازیکنانی که آن اقلام را خریداری می کنند، مستقر کنید.
چیزی که یاد خواهید گرفت
- نحوه استفاده از تراکنش های خواندن و نوشتن برای اطمینان از سازگاری برای تغییرات داده ها
- چگونه از جهش های DML و Spanner برای اصلاح داده ها استفاده کنیم
آنچه شما نیاز دارید
- یک پروژه Google Cloud که به یک حساب صورتحساب متصل است.
- یک مرورگر وب، مانند کروم یا فایرفاکس .
- Cloud Spanner که قبلاً با شروع توسعه بازیها تکمیل شده بود، بدون مرحله پاکسازی.
2. راه اندازی و الزامات
Cloud Spanner Getting Started with Games Development Codelab را تکمیل کنید
Cloud Spanner Getting Started with Games Development Codelab را تکمیل کنید. این برای دریافت مجموعه داده ای از بازیکنان و بازی ها لازم است. بازیکنان و بازی ها برای به دست آوردن اقلام و پول مورد نیاز هستند، که به نوبه خود برای فهرست اقلام برای فروش و خرید اقلام از پست تجاری استفاده می شود.
متغیرهای محیطی را در Cloud Shell پیکربندی کنید
Cloud Shell را با کلیک روی Activate 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
دارای کلیدهای خارجی برای هر دو آیتمUUID و playerUUID است تا مطمئن شود بازیکنان فقط آیتم های معتبر را دریافت می کنند.
جدول player_ledger_entries
تغییرات پولی موجودی حساب بازیکن را پیگیری می کند. این می تواند به دست آوردن پول از غارت یا با فروش اقلام در پست تجاری باشد.
و در نهایت، جدول trade_orders
برای رسیدگی به ارسال سفارشات فروش و برای انجام آن سفارشات توسط خریداران استفاده می شود.
برای ایجاد این طرح، روی دکمه Write DDL
در Cloud Console کلیک می کنید:
در اینجا، شما تعریف طرحواره را از فایل 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
» کلیک کنید و منتظر بمانید تا بهروزرسانی طرح کامل شود:
بعدی
در مرحله بعد، سرویس مورد را مستقر خواهید کرد.
3. سرویس مورد را مستقر کنید
نمای کلی خدمات
سرویس آیتم یک 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())
}
پیکربندی و استفاده از اتصالات آچار دقیقاً مانند سرویس پروفایل و سرویس matchmaking از کدهای قبلی انجام می شود.
سرویس آیتم با 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 فراخوانی می شود.
منطق این نقطه پایانی بهروزرسانی موجودی بازیکن پس از اعمال مقدار و همچنین بهروزرسانی جدول player_ledger_entries با رکوردی از کسب است. موجودی حساب بازیکن اصلاح شده است تا به تماس گیرنده برگردانده شود. DML برای اصلاح بازیکنان و 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 است.
با این اطلاعات زمان اجرای سرویس فرا رسیده است.
سرویس را اجرا کنید
اجرای سرویس وابستگی ها را دانلود می کند و سرویسی را که در پورت 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 را مستقر خواهید کرد.
4. سرویس tradepost را مستقر کنید
نمای کلی خدمات
سرویس tradepost یک API REST است که در Go نوشته شده است و از چارچوب جین استفاده می کند. در این 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 بازیکن_آیتم که باید فروخته شود، فهرست و لیست_قیمت.
جهش های آچار برای ایجاد سفارش تجاری و علامت گذاری بازیکن_آیتم به عنوان غیر قابل مشاهده انتخاب می شوند. انجام این کار از ارسال اقلام تکراری برای فروش توسط فروشنده جلوگیری می کند.
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
}
قبل از ایجاد سفارش، PlayerItem اعتبارسنجی می شود تا اطمینان حاصل شود که می تواند برای فروش فهرست شود. در اصل این بدان معناست که PlayerItem برای پخش کننده قابل مشاهده است و منقضی نشده است.
// 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 انجام می شود. اطلاعات مورد نیاز سفارش UUID و خریدار است که 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 به صورت زیر است:
// 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 فرا رسیده است.
سرویس را اجرا کنید
اجرای سرویس، سرویسی را که روی پورت 8083 اجرا می شود، ایجاد می کند.
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 را برای مدیریت ایجاد سفارشهای فروش مستقر کردید. این سرویس توانایی خرید آن سفارش ها را نیز انجام می دهد.
مراحل بعدی
اکنون که خدمات شما در حال اجرا است، زمان شبیه سازی فروش و خرید بازیکنان در پست معاملاتی است!
5. شروع به معامله کنید
اکنون که کالا و خدمات پست تجاری در حال اجرا هستند، می توانید با استفاده از مولدهای ملخ ارائه شده بار تولید کنید.
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'")
این دستور به بازیکنان اجازه می دهد تا به مدت 60 ثانیه آیتم ها و پول خود را بدست آورند:
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
خلاصه
در این مرحله، بازیکنانی را که برای بازی کردن ثبت نام کرده بودند، شبیه سازی کردید و سپس شبیه سازی هایی را برای بازیکنان انجام دادید تا با استفاده از سرویس خواستگاری بازی کنند. این شبیهسازیها از چارچوب Locust Python برای ارسال درخواستها به api REST سرویسهای ما استفاده میکنند.
به راحتی می توانید زمان صرف شده برای ایجاد بازیکنان و انجام بازی ها و همچنین تعداد کاربران همزمان ( -u) را تغییر دهید.
مراحل بعدی
پس از شبیه سازی، می خواهید آمارهای مختلف را با جستجوی Spanner بررسی کنید.
6. بازیابی آمار تجارت
اکنون که بازیکنان را شبیه سازی کرده ایم که پول و آیتم ها را بدست می آورند و سپس آن اقلام را در پست معاملاتی می فروشند، بیایید برخی از آمارها را بررسی کنیم.
برای انجام این کار، از Cloud Console برای ارسال درخواستهای جستجو به Spanner استفاده کنید.
بررسی سفارشات تجاری باز در مقابل انجام شده
هنگامی که یک TradeOrder در پست معاملاتی خریداری می شود، قسمت فراداده پر شده به روز می شود.
این پرس و جو به شما کمک می کند تا بررسی کنید تعداد سفارشات باز شده و تعداد آنها تکمیل شده است:
-- 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 |
معاملات باز | 159 |
معاملات پر شده | 454 |
بررسی موجودی حساب بازیکن و تعداد موارد
بازیکنی در حال انجام یک بازی است اگر ستون current_game او تنظیم شده باشد. در غیر این صورت، آنها در حال حاضر بازی نمی کنند.
برای رسیدن به 10 بازیکن برتر که در حال حاضر با بیشترین آیتمها بازی میکنند، با account_balance خود
، از این پرس و جو استفاده کنید:
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، آمارهای مختلف سفارشات بازیکن و معاملات را بررسی کردید.
مراحل بعدی
بعد، زمان تمیز کردن است!
7. تمیز کردن
بعد از کلی بازی سرگرم کننده با Spanner، باید زمین بازی خود را تمیز کنیم. خوشبختانه این یک مرحله آسان است، فقط کافی است به بخش Cloud Spanner در Cloud Console بروید و نمونهای را که برای این Codelab ایجاد کردهایم حذف کنید.
8. تبریک!
تبریک میگوییم، شما با موفقیت یک بازی نمونه را در Spanner اجرا کردید
بعدش چی؟
در این آزمایشگاه، شما راه اندازی دو سرویس را برای رسیدگی به تولید آیتم های بازی و خرید اقلام برای فروش در پست معاملاتی توسط بازیکنان تکمیل کرده اید.
این نمونه کد باید به شما درک بهتری از نحوه عملکرد سازگاری Cloud Spanner در تراکنشها برای جهشهای DML و Spanner بدهد.
با خیال راحت از ژنراتورهای ارائه شده برای کاوش آچار مقیاس پذیر استفاده کنید.