پست تجاری بازی Cloud Spanner

۱. مقدمه

کلود اسپنر یک سرویس پایگاه داده رابطه‌ای، توزیع‌شده در سطح جهانی و مقیاس‌پذیر افقی است که تراکنش‌های ACID و مفاهیم SQL را بدون از دست دادن عملکرد و دسترسی‌پذیری بالا ارائه می‌دهد.

این ویژگی‌ها، اسپنر را به گزینه‌ای عالی برای معماری بازی‌هایی تبدیل می‌کند که می‌خواهند پایگاه بازیکنان جهانی داشته باشند یا نگران ثبات داده‌ها هستند.

در این تمرین، شما دو سرویس Go ایجاد خواهید کرد که با یک پایگاه داده منطقه‌ای Spanner تعامل دارند تا بازیکنان بتوانند اقلام و پول ( item-service ) را به دست آورند و سپس اقلام را در فروشگاه برای خرید سایر بازیکنان فهرست کنند ( tradepost-service ).

این آزمایشگاه برای تولید بازیکنان و بازی‌ها با استفاده از profile-service و matchmaking-service به آزمایشگاه کد Cloud Spanner Getting Started with Games Development وابسته است.

904c5193ee27626a.png

در مرحله بعد، با استفاده از چارچوب بارگذاری پایتون Locust.io، داده‌هایی را تولید خواهید کرد تا بازیکنان را در حال کسب پول و اقلام در طول "بازی" شبیه‌سازی کنید. سپس بازیکنان می‌توانند اقلام را برای فروش در یک فروشگاه فهرست کنند، جایی که سایر بازیکنان با پول کافی می‌توانند آن اقلام را خریداری کنند.

همچنین از Spanner درخواست خواهید کرد تا موجودی حساب بازیکنان و تعداد اقلام و برخی آمار مربوط به سفارشات تجاری باز یا پر شده را تعیین کند.

در نهایت، منابعی را که در این آزمایشگاه ایجاد شده‌اند، پاکسازی خواهید کرد.

آنچه خواهید ساخت

به عنوان بخشی از این آزمایشگاه، شما:

  • از نمونه Spanner از Cloud Spanner Getting Started with Games Development دوباره استفاده کنید.
  • یک سرویس آیتم نوشته شده با زبان Go برای مدیریت بازیکنانی که آیتم و پول به دست می‌آورند، مستقر کنید.
  • یک سرویس Trading Post نوشته شده با زبان Go را برای شبیه‌سازی فهرست کردن اقلام توسط بازیکنان و خرید آن اقلام توسط سایر بازیکنان، مستقر کنید.

آنچه یاد خواهید گرفت

  • نحوه استفاده از تراکنش‌های خواندن-نوشتن برای اطمینان از سازگاری تغییرات داده‌ها
  • نحوه استفاده از جهش‌های DML و Spanner برای تغییر داده‌ها

آنچه نیاز دارید

۲. تنظیمات و الزامات

شروع کار با توسعه بازی‌ها در codelab، مراحل Cloud Spanner را کامل کنید

کد آزمایشگاه شروع کار با توسعه بازی‌ها در Cloud Spanner را تکمیل کنید. این کد برای دریافت مجموعه داده‌ای از بازیکنان و بازی‌ها لازم است. بازیکنان و بازی‌ها برای به دست آوردن اقلام و پول مورد نیاز هستند که به نوبه خود برای فهرست کردن اقلام برای فروش و خرید اقلام از پست معاملاتی استفاده می‌شود.

پیکربندی متغیرهای محیطی در Cloud Shell

با کلیک روی فعال کردن Cloud Shell، Cloud Shell را باز کنید. gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (از آنجایی که قبلاً این کار را انجام داده‌اید، آماده‌سازی و اتصال به محیط فقط چند لحظه طول می‌کشد.)

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

اسکرین شات 2017-06-14 ساعت 10.13.43 PM.png

پس از اتصال به 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 .

402ce3310dd7141a.png

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

5e1229d85b75906.png

روابط سفارش تجاری

آیتم‌های بازی در جدول game_items اضافه می‌شوند و سپس می‌توانند توسط بازیکنان خریداری شوند. جدول player_items دارای کلیدهای خارجی برای itemUUID و playerUUID است تا اطمینان حاصل شود که بازیکنان فقط آیتم‌های معتبر را خریداری می‌کنند.

جدول player_ledger_entries هرگونه تغییر مالی در موجودی حساب بازیکن را ثبت می‌کند. این تغییرات می‌تواند از طریق غنیمت یا فروش اقلام در فروشگاه باشد.

و در نهایت، جدول trade_orders برای مدیریت ارسال سفارشات فروش و انجام این سفارشات توسط خریداران استفاده می‌شود.

برای ایجاد طرحواره، روی دکمه‌ی Write DDL در کنسول ابری کلیک کنید:

e9ad931beb1d96b.png

در اینجا، تعریف طرحواره را از فایل 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 » کلیک کنید و منتظر بمانید تا به‌روزرسانی طرحواره تکمیل شود:

۹۴f۴۴b۲۷۷۴bce۹۱۴.png

بعدی

در مرحله بعد، سرویس آیتم را مستقر خواهید کرد.

۳. سرویس آیتم را مستقر کنید

نمای کلی خدمات

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

6c29a0831b5f588d.png

فایل ./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، اقلام بازیکنان برای فروش قرار داده می‌شوند. سپس بازیکنان بازی‌ها می‌توانند معاملات آزاد داشته باشند و در صورت داشتن پول کافی، می‌توانند کالا را خریداری کنند.

c32372f9def89a4a.png

فایل ./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 استفاده کنید.

b5e3154c6f7cb0cf.png

بررسی سفارشات معاملاتی باز در مقابل سفارشات انجام شده

وقتی یک سفارش تجاری در دفتر معاملاتی خریداری می‌شود، فیلد فراداده پر شده به‌روزرسانی می‌شود.

این کوئری به شما کمک می‌کند تا بررسی کنید که چه تعداد سفارش باز و چه تعداد پر شده است:

-- 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;

نتیجه:

playerUUID

account_balance

numItems

current_game

04d14288-a7c3-4515-9296-44eeae184d6d

1396.55

201

79f24cd4-102d-4d7d-bbe8-be69d364ebbf

03836948-d591-4967-a8a8-80506454916d

2005.085

192

053c353e-e56d-4b2e-9431-76eedf58bea5


snip


snip


snip


snip

خلاصه

در این مرحله، شما با استفاده از Cloud Console برای پرس و جو از Spanner، آمارهای مختلف سفارشات بازیکن و معامله را بررسی کردید.

مراحل بعدی

بعدش، وقت تمیزکاریه!

۷. تمیز کردن

بعد از کلی بازی و سرگرمی با Spanner، باید زمین بازی‌مان را تمیز کنیم. خوشبختانه این مرحله آسانی است، فقط کافی است به بخش Cloud Spanner در Cloud Console بروید و نمونه‌ای را که برای این codelab ایجاد کرده‌ایم، حذف کنید.

۸. تبریک می‌گویم!

تبریک می‌گویم، شما با موفقیت یک بازی نمونه را روی Spanner مستقر کردید.

بعدش چی؟

در این آزمایش، شما راه‌اندازی دو سرویس برای مدیریت تولید آیتم‌های بازی و خرید آیتم توسط بازیکنان برای فروش در فروشگاه را به پایان رسانده‌اید.

این نمونه‌های کد باید درک بهتری از نحوه عملکرد سازگاری Cloud Spanner در تراکنش‌ها برای جهش‌های DML و Spanner به شما ارائه دهند.

برای بررسی مقیاس‌بندی Spanner، می‌توانید از مولدهای ارائه شده استفاده کنید.