Bài đăng giao dịch trò chơi Cloud Spanner

1. Giới thiệu

Cloud Spanner là một dịch vụ cơ sở dữ liệu quan hệ, được phân phối toàn cầu, có thể mở rộng theo chiều ngang, được quản lý toàn diện. Dịch vụ này cung cấp các giao dịch ACID và ngữ nghĩa SQL mà không làm giảm hiệu suất và khả năng hoạt động cao.

Những tính năng này giúp Spanner rất phù hợp với kiến trúc của những trò chơi muốn thúc đẩy lượng người chơi ở quy mô toàn cầu hoặc lo ngại về tính nhất quán của dữ liệu

Trong phòng thí nghiệm này, bạn sẽ tạo 2 dịch vụ Go tương tác với cơ sở dữ liệu Spanner theo khu vực để cho phép người chơi đổi vật phẩm và tiền (item-service), sau đó niêm yết các vật phẩm trên bài đăng giao dịch để những người chơi khác mua (tradepost-service).

Phòng thí nghiệm này phụ thuộc vào lớp học lập trình Bắt đầu phát triển trò chơi Cloud Spanner để tạo trình phát và trò chơi bằng profile-servicematchmaking-service.

904c5193ee27626a.pngS

Tiếp theo, bạn sẽ tạo dữ liệu bằng cách tận dụng khung tải Python Locust.io để mô phỏng người chơi thu được tiền và vật phẩm thông qua quá trình "chơi trò chơi". Sau đó, người chơi có thể niêm yết các vật phẩm để bán trên một sàn giao dịch, nơi những người chơi khác có đủ tiền có thể mua những vật phẩm đó.

Bạn cũng sẽ truy vấn Spanner để xác định số dư tài khoản và số lượng mặt hàng, cũng như một vài số liệu thống kê về các đơn đặt hàng giao dịch đang mở hoặc đã được thực hiện.

Cuối cùng, bạn sẽ dọn dẹp các tài nguyên đã tạo trong phòng thí nghiệm này.

Sản phẩm bạn sẽ tạo ra

Trong phòng thí nghiệm này, bạn sẽ:

  • Sử dụng lại thực thể Spanner trong tài liệu Cloud Spanner Started with Games Development (Bắt đầu sử dụng Cloud Spanner cho hoạt động phát triển trò chơi).
  • Triển khai dịch vụ vật phẩm được viết trong Go để xử lý người chơi thu được vật phẩm và tiền
  • Triển khai dịch vụ Trade Post được viết trong Go để mô phỏng người chơi liệt kê các vật phẩm được bán và những người chơi khác mua những vật phẩm đó.

Kiến thức bạn sẽ học được

  • Cách sử dụng giao dịch đọc-ghi để đảm bảo tính nhất quán cho các thay đổi về dữ liệu
  • Cách tận dụng đột biến DML và Spanner để sửa đổi dữ liệu

Bạn cần có

2. Thiết lập và yêu cầu

Hoàn thành lớp học lập trình Bắt đầu về phát triển trò chơi của Cloud Spanner

Hoàn thành lớp học lập trình Cloud Spanner Bắt đầu phát triển trò chơi. Bạn phải làm việc này để có được tập dữ liệu về người chơi và trò chơi. Người chơi và trò chơi cần thiết để thu được các vật phẩm và tiền. Sau đó, những vật phẩm này được dùng để niêm yết các vật phẩm để bán và mua các vật phẩm từ bưu điện.

Định cấu hình các biến môi trường trong Cloud Shell

Mở Cloud Shell bằng cách nhấp vào Kích hoạt Cloud Shell gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (chỉ mất vài phút để cấp phép và kết nối với môi trường vì bạn đã làm việc này trước đó).

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

Ảnh chụp màn hình lúc 10:13.43 chiều 14/6/2017.png

Sau khi kết nối với Cloud Shell, bạn sẽ thấy rằng mình đã được xác thực và dự án đã được đặt thành PROJECT_ID.

Đặt các biến môi trường SPANNER trong Cloud Shell

export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game

Tạo giản đồ

Bây giờ, cơ sở dữ liệu của bạn đã được tạo, bạn có thể xác định giản đồ trong cơ sở dữ liệu sample-game.

Phòng thí nghiệm này sẽ tạo 4 bảng mới: game_items, player_items, player_ledger_entriestrade_orders.

402ce3310dd7141a.pngS

Mối quan hệ của mục trình phát

5e1229d85b75906.pngS

Mối quan hệ với đơn đặt hàng thương mại

Các vật phẩm trong trò chơi được thêm vào bảng game_items, sau đó người chơi có thể thu nạp. Bảng player_items có các khoá ngoại cho cả itemUUID và PlayerUUID để đảm bảo người chơi chỉ mua những vật phẩm hợp lệ.

Bảng player_ledger_entries theo dõi mọi thay đổi về tiền đối với số dư tài khoản của người chơi. Người chơi có thể kiếm tiền từ chiến lợi phẩm hoặc bán vật phẩm trên bưu điện.

Cuối cùng, bảng trade_orders được dùng để xử lý các đơn đặt hàng đã đăng và để người mua thực hiện các đơn đặt hàng đó.

Để tạo giản đồ, bạn sẽ nhấp vào nút Write DDL trong Cloud Console:

e9ad931beb1d96b.png

Tại đây, bạn sẽ nhập định nghĩa giản đồ từ tệp 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);

Nhấp vào "Submit" sửa đổi giản đồ và chờ cho đến khi cập nhật giản đồ hoàn tất:

94f44b2774bce914.pngS

Tiếp theo

Tiếp theo, bạn sẽ triển khai dịch vụ mục.

3. Triển khai dịch vụ mặt hàng

Tổng quan về dịch vụ

Dịch vụ mục là một API REST được viết bằng Go, có sử dụng khung gin. Trong API này, người chơi đang tham gia trò chơi mở sẽ nhận được tiền và vật phẩm.

6c29a0831b5f588d.png.

Tệp ./src/golang/item-service/main.go định cấu hình các điểm cuối sau để hoạt động với các vật phẩm trong trò chơi và người chơi nhận được các vật phẩm đó. Ngoài ra, còn có một điểm cuối để người chơi kiếm tiền.

 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())
}

Cấu hình và sử dụng Kết nối Spanner được xử lý giống hệt như dịch vụ hồ sơ và dịch vụ tìm kiếm người chơi trong lớp học lập trình trước.

Dịch vụ vật phẩm này hoạt động với GameItem, Player, PlayerLedger,PlayerItem với các định nghĩa sau:

// 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"`
}

Trước tiên, trò chơi phải tạo một số mục. Để thực hiện việc này, hệ thống sẽ gọi một yêu cầu POST đến điểm cuối /items. Đây là một thao tác chèn DML rất đơn giản vào bảng 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
}

Để lấy một vật phẩm, hệ thống sẽ gọi một yêu cầu POST tới điểm cuối /players/items. Logic của điểm cuối này là truy xuất giá trị hiện tại của một vật phẩm trong trò chơi và phiên chơi hiện tại của người chơi. Sau đó, hãy chèn thông tin thích hợp vào bảng player_items cho biết nguồn và thời gian thu nạp vật phẩm.

Hàm này liên kết với các hàm sau:

 // 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
}

Để người chơi kiếm tiền, hệ thống sẽ gọi một yêu cầu PUT đến điểm cuối /players/updatebalance.

Logic của điểm cuối này là cập nhật số dư của người chơi sau khi áp dụng số tiền, cũng như cập nhật bản ghi về việc thu nạp trong bảng player_ledger_entries. Tài khoản account_balance của người chơi được sửa đổi để trả lại cho người gọi. DML được dùng để sửa đổi cả người chơi và play_ledger_entries.

Hàm này liên kết với các hàm sau:

 // 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
}

Theo mặc định, dịch vụ được định cấu hình bằng các biến môi trường. Xem phần có liên quan của tệp ./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
}

Bạn có thể thấy hành vi mặc định là chạy dịch vụ trên localhost:8082.

Với thông tin này, đã đến lúc chạy dịch vụ.

Chạy dịch vụ

Khi chạy dịch vụ sẽ tải các phần phụ thuộc xuống và thiết lập dịch vụ chạy trên cổng 8082:

cd ~/spanner-gaming-sample/src/golang/item-service
go run . &

Kết quả của lệnh:

[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

Kiểm thử dịch vụ bằng cách tạo lệnh curl để tạo một mục:

curl http://localhost:8082/items \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"item_name": "test_item","item_value": "3.14"}'

Kết quả của lệnh:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <Date>
Content-Length: 38

"aecde380-0a79-48c0-ab5d-0da675d3412c"

Tiếp theo, bạn muốn người chơi nhận được vật phẩm này. Để thực hiện, bạn cần có ItemUUIDPlayerUUID. ItemUUID là kết quả từ lệnh trước. Trong ví dụ này, mã sẽ là aecde380-0a79-48c0-ab5d-0da675d3412c.

Để nhận PlayerUUID, hãy thực hiện lệnh gọi đến điểm cuối GET /players:

curl http://localhost:8082/players

Kết quả của lệnh:

{
    "playerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352",
    "updated": "0001-01-01T00:00:00Z",
    "account_balance": {},
    "current_game": "7b97fa85-5658-4ded-a962-4c09269a0a79"
}

Để người chơi lấy được vật phẩm đó, hãy gửi yêu cầu đến điểm cuối 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"}'

Kết quả của lệnh:

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
}

Tóm tắt

Ở bước này, bạn đã triển khai dịch vụ vật phẩm cho phép tạo vật phẩm trong trò chơi. Những người chơi được chỉ định mở trò chơi có thể nhận tiền và vật phẩm trong trò chơi.

Các bước tiếp theo

Ở bước tiếp theo, bạn sẽ triển khai dịch vụ bưu điện.

4. Triển khai dịch vụ quảng cáo thương mại

Tổng quan về dịch vụ

Dịch vụ thương mại là một API REST được viết bằng Go, có sử dụng khung gin. Trong API này, các mặt hàng của người chơi sẽ được đăng để bán. Người chơi trò chơi sau đó có thể nhận được các giao dịch mở và nếu có đủ tiền, họ có thể mua vật phẩm.

c32372f9def89a4a.png

Tệp ./src/golang/tradepost-service/main.go cho dịch vụ Tradepost tuân theo cách thiết lập và mã tương tự như các dịch vụ khác, vì vậy, tệp này sẽ không lặp lại ở đây. Dịch vụ này đưa ra một số điểm cuối như sau:

 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())
}

Dịch vụ này cung cấp cấu trúc TradeOrder, cũng như các cấu trúc bắt buộc cho cấu trúc GameItem, PlayerItem, PlayerPlayerLedger:

 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"`
}

Để tạo một lệnh giao dịch, yêu cầu POST sẽ được phát hành đến điểm cuối API /trades/sell. Thông tin bắt buộc là playerItemUUID của Player_item sẽ được bán, listerlist_price.

Đột biến Spanner được chọn để tạo lệnh giao dịch và đánh dấu Player_item là không hiển thị. Khi đó, người bán sẽ không được đăng các mặt hàng trùng lặp để bán.

 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
}

Trước khi tạo đơn đặt hàng, PlayerItem được xác thực để đảm bảo mặt hàng có thể được bày bán. Chủ yếu là việc này có nghĩa là người chơi nhìn thấy mục PlayerItem và chưa hết hạn.

 // 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
}

Giao dịch mua được thực hiện bằng yêu cầu PUT đến điểm cuối /trades/buy. Thông tin bắt buộc là orderUUIDngười mua, là UUID của người chơi thực hiện giao dịch mua.

Do sự phức tạp này và số lượng thay đổi, các lượt đột biến một lần nữa được chọn để mua đơn đặt hàng. Các thao tác sau được thực hiện trong một giao dịch đọc-ghi:

  • Xác thực đơn đặt hàng có thể được thực hiện vì trước đó đơn đặt hàng chưa được thực hiện và chưa hết hạn.
 // 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
}
  • Truy xuất thông tin người mua và xác thực rằng họ có thể mua mặt hàng. Điều này có nghĩa là người mua không thể giống như người liệt kê và họ có đủ tiền.
 // 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
}
  • Thêm list_price của đơn đặt hàng vào số dư tài khoản của danh sách, với một mục sổ cái trùng khớp.
 // 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
}
  • Trừ list_price của đơn đặt hàng khỏi số dư tài khoản của người mua bằng một mục nhập sổ cái trùng khớp.
 // Update buyer's account balance
       negAmount := o.ListPrice.Neg(&o.ListPrice)
       buyer.UpdateBalance(ctx, txn, *negAmount)
  • Di chuyển Player_item sang trình phát mới bằng cách chèn một bản sao mới của mặt hàng trong trò chơi cùng với trò chơi và thông tin chi tiết về người mua vào bảng PlayerItems, đồng thời xoá bản sao của mặt hàng trong trình nghe.
// 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
}
  • Cập nhật mục Đơn đặt hàng để cho biết mặt hàng đã được thực hiện và không còn hoạt động nữa.

Về tổng thể, chức năng Mua sẽ có dạng như sau:

 // 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
}

Theo mặc định, dịch vụ được định cấu hình bằng các biến môi trường. Xem phần có liên quan của tệp ./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
}

Bạn có thể thấy rằng hành vi mặc định là chạy dịch vụ trên localhost:8083 để tránh xung đột với các dịch vụ khác*.*

Với thông tin này, bây giờ đã đến lúc chạy dịch vụ thương mại.

Chạy dịch vụ

Việc chạy dịch vụ sẽ thiết lập dịch vụ chạy trên cổng 8083. Dịch vụ này có nhiều phần phụ thuộc giống như dịch vụ mặt hàng, vì vậy, các phần phụ thuộc mới sẽ không được tải xuống.

cd ~/spanner-gaming-sample/src/golang/tradepost-service
go run . &

Kết quả của lệnh:

[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

Đăng một mục

Kiểm thử dịch vụ bằng cách đưa ra yêu cầu GET để truy xuất một PlayerItem để bán:

curl http://localhost:8083/trades/player_items

Kết quả của lệnh:

{
    "PlayerUUID": "b74cc194-87b0-4a55-a67f-0f0742ef6352",
    "PlayerItemUUID": "a42b1899-4509-4fce-9958-265d2a2838a0",
    "Price": "3.14"
}

Bây giờ, hãy đăng một mặt hàng đang bán bằng cách gọi điểm cuối /trades/sell

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>"}'

Kết quả của lệnh:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <Date> 
Content-Length: 38

"282ea691-b956-4c4c-95ff-f461d6415651"

Tóm tắt

Ở bước này, bạn đã triển khai tradepost-service để xử lý việc tạo đơn đặt hàng. Dịch vụ này cũng xử lý khả năng mua những đơn đặt hàng đó.

Các bước tiếp theo

Hiện tại, các dịch vụ của bạn đã đi vào hoạt động, đã đến lúc mô phỏng hoạt động bán và mua hàng của người chơi trên bưu điện!

5. Bắt đầu giao dịch

Hiện tại, dịch vụ mặt hàng và thương mại đã đi vào hoạt động, bạn có thể tạo ra tải trọng bằng máy tạo cào cào được cung cấp.

Locus cung cấp giao diện web để chạy trình tạo, nhưng trong phòng thí nghiệm này, bạn sẽ sử dụng dòng lệnh (tuỳ chọn –không có giao diện người dùng).

Tạo vật phẩm trong trò chơi

Trước tiên, bạn cần tạo các mục. Tệp ./generators/item_generator.py bao gồm một nhiệm vụ tạo vật phẩm trong trò chơi với chuỗi tên ngẫu nhiên và giá trị giá ngẫu nhiên:

# 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)

Lệnh sau đây gọi tệp item_generator.py. Tệp này sẽ tạo các mục trò chơi trong 10 giây (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

Kết quả của lệnh:

*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

Người chơi nhận được vật phẩm và tiền

Tiếp theo, hãy yêu cầu người chơi thu thập vật phẩm và tiền để có thể tham gia vào bài giao dịch. Để thực hiện việc này, tệp ./generators/game_server.py cung cấp các nhiệm vụ để truy xuất vật phẩm trong trò chơi để chỉ định cho người chơi cũng như số tiền ngẫu nhiên.

 # 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'")

Lệnh này sẽ cho phép người chơi kiếm vật phẩm và tiền trong 60 giây:

locust -H http://127.0.0.1:8082 -f game_server.py --headless -u=1 -r=1 -t=60s

Kết quả của lệnh:

*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

Người chơi mua và bán trên bưu điện

Bây giờ, người chơi đã có vật phẩm và tiền để mua vật phẩm, họ có thể bắt đầu sử dụng bài giao dịch!

Tệp trình tạo ./generators/trading_server.py cung cấp tác vụ để tạo đơn đặt hàng bán và thực hiện các đơn đặt hàng đó.

 # 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'")

Lệnh này sẽ cho phép người chơi liệt kê các vật phẩm họ đã mua để bán và những người chơi khác mua những vật phẩm đó trong 10 giây:

locust -H http://127.0.0.1:8083 -f ./generators/trading_server.py --headless -u=1 -r=1 -t=10s

Kết quả của lệnh:

*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

Tóm tắt

Ở bước này, bạn sẽ mô phỏng người chơi đăng ký chơi trò chơi, sau đó chạy mô phỏng để người chơi chơi trò chơi bằng dịch vụ tìm kiếm người chơi. Những nội dung mô phỏng này tận dụng khung Locus Python để đưa ra yêu cầu cho các dịch vụ của chúng tôi API REST.

Bạn có thể chỉnh sửa thời gian tạo người chơi và chơi trò chơi, cũng như số người dùng đồng thời (-u).

Các bước tiếp theo

Sau khi mô phỏng, bạn nên kiểm tra các số liệu thống kê khác nhau bằng cách truy vấn Spanner.

6. Truy xuất số liệu thống kê thương mại

Hiện tại, chúng ta đã mô phỏng người chơi thu nạp tiền và vật phẩm, sau đó bán những vật phẩm đó trên trang giao dịch. Hãy cùng kiểm tra một vài số liệu thống kê.

Để thực hiện việc này, hãy sử dụng Cloud Console để gửi yêu cầu truy vấn cho Spanner.

b5e3154c6f7cb0cf.png

Kiểm tra đơn đặt hàng giao dịch đang mở so với đơn đặt hàng giao dịch đã hoàn tất

Khi người dùng mua một TradeOrder trên trang giao dịch, trường siêu dữ liệu TradeOrder sẽ được cập nhật.

Khi truy vấn này, bạn sẽ kiểm tra xem có bao nhiêu đơn đặt hàng đang mở và số lượng đơn đặt hàng đã được thực hiện:

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

Kết quả:

Loại

NumTrades

Giao dịch mở

159

Giao dịch được thực hiện

454

Kiểm tra số dư tài khoản của người chơi và số lượng vật phẩm

Một người chơi đang chơi một trò chơi nếu cột current_game được đặt. Nếu không, họ hiện không chơi trò chơi nào.

Để lọt vào danh sách 10 người chơi hàng đầu đang chơi trò chơi có nhiều vật phẩm nhất bằng account_balance của họ

, sử dụng truy vấn này :

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;

Kết quả:

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

Tóm tắt

Trong bước này, bạn đã xem xét nhiều số liệu thống kê về người chơi và đơn đặt hàng giao dịch bằng cách sử dụng Cloud Console để truy vấn Spanner.

Các bước tiếp theo

Tiếp theo, đã đến lúc dọn dẹp!

7. Dọn dẹp

Sau khi vui chơi hết mình cùng Spanner, chúng ta cần dọn dẹp sân chơi. May mắn thay, đây là một bước đơn giản, chỉ cần chuyển đến phần Cloud Spanner của Cloud Console và xoá thực thể chúng ta đã tạo cho lớp học lập trình này.

8. Xin chúc mừng!

Xin chúc mừng! Bạn đã triển khai thành công một trò chơi mẫu trên Spanner

Tiếp theo là gì?

Trong phòng thí nghiệm này, bạn đã hoàn tất việc thiết lập hai dịch vụ để xử lý việc tạo vật phẩm trong trò chơi và người chơi mua vật phẩm sẽ được bán trên bưu điện.

Các mã mẫu này sẽ giúp bạn hiểu rõ hơn về cách tính nhất quán của Cloud Spanner trong các giao dịch đối với cả sự đột biến DML và Spanner.

Bạn có thể dùng các trình tạo được cung cấp để khám phá Spanner mở rộng quy mô.