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

1. مقدمه

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

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

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

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

904c5193ee27626a.png

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

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

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

چیزی که خواهی ساخت

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

  • استفاده مجدد از نمونه آچار در Cloud Spanner شروع با توسعه بازی ها .
  • یک سرویس Item نوشته شده در Go را برای رسیدگی به بازیکنانی که اقلام و پول به دست می آورند، مستقر کنید
  • یک سرویس Trading Post نوشته شده در Go را برای شبیه سازی بازیکنانی که اقلام را برای فروش فهرست می کنند، و سایر بازیکنانی که آن اقلام را خریداری می کنند، مستقر کنید.

چیزی که یاد خواهید گرفت

  • نحوه استفاده از تراکنش های خواندن و نوشتن برای اطمینان از سازگاری برای تغییرات داده ها
  • چگونه از جهش های DML و 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 باز کنید gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK_OgjogdOgdOg3ZHK39gdOg 2A (چون قبلاً این کار را انجام داده اید، برای تهیه و اتصال به محیط فقط چند لحظه طول می کشد).

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjkWkWE2C w

Screen Shot 2017-06-14 at 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 دارای کلیدهای خارجی برای هر دو آیتمUUID و playerUUID است تا مطمئن شود بازیکنان فقط آیتم های معتبر را دریافت می کنند.

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

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

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

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

94f44b2774bce914.png

بعدی

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

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

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

سرویس آیتم یک 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())
}

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

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

جهش های آچار برای ایجاد سفارش تجاری و علامت گذاری بازیکن_آیتم به عنوان غیر قابل مشاهده انتخاب می شوند. انجام این کار از ارسال اقلام تکراری برای فروش توسط فروشنده جلوگیری می کند.

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

b5e3154c6f7cb0cf.png

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

هنگامی که یک 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;

نتیجه:

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

مراحل بعدی

بعد، زمان تمیز کردن است!

7. تمیز کردن

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

8. تبریک!

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

بعدش چی؟

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

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

با خیال راحت از ژنراتورهای ارائه شده برای کاوش آچار مقیاس پذیر استفاده کنید.