Cloud Spanner 게임 트레이딩 게시물

1. 소개

Cloud Spanner는 수평 확장이 가능하며 전 세계에 분산된 완전 관리형 관계형 데이터베이스 서비스로, 성능과 고가용성을 그대로 유지하면서 ACID 트랜잭션과 SQL 시맨틱스를 제공합니다.

이러한 특징 덕분에 Spanner는 전 세계 플레이어층을 확보하고자 하거나 데이터 일관성이 우려되는 게임의 아키텍처에 매우 적합합니다.

이 실습에서는 리전 Spanner 데이터베이스와 상호작용하여 플레이어가 아이템과 돈을 획득 (item-service)한 다음 다른 플레이어가 구매할 수 있도록 거래처에 아이템을 나열 (tradepost-service)하는 2개의 Go 서비스를 만듭니다.

이 실습에서는 Cloud Spanner 게임 개발 시작하기 Codelab을 기반으로 하여 profile-servicematchmaking-service를 사용하여 플레이어와 게임을 생성했습니다.

904c5193ee27626a.png

다음으로 Python 로드 프레임워크인 Locust.io를 활용하여 데이터를 생성하여 '게임 플레이' 과정에서 현금과 아이템을 획득하는 플레이어를 시뮬레이션합니다. 그런 다음 플레이어는 트레이드 포스트에서 판매할 아이템을 나열하고, 충분한 돈을 모은 다른 플레이어는 이러한 아이템을 구매할 수 있습니다.

또한 Spanner를 쿼리하여 플레이어 계정 잔액, 상품 수, 진행 중이거나 완료된 거래 주문에 관한 일부 통계가 포함됩니다.

마지막으로 이 실습에서 만든 리소스를 삭제합니다.

빌드할 항목

이 실습에서 학습할 내용은 다음과 같습니다.

  • Cloud Spanner 게임 개발 시작하기에서 Spanner 인스턴스를 재사용합니다.
  • Go로 작성된 아이템 서비스를 배포하여 아이템과 돈을 획득하는 플레이어를 처리합니다.
  • Go로 작성된 트레이딩 포스트 서비스를 배포하여 판매할 아이템을 등록하는 플레이어와 이러한 아이템을 구매하는 다른 플레이어를 시뮬레이션합니다.

학습할 내용

  • 읽기-쓰기 트랜잭션을 사용하여 데이터 변경사항의 일관성을 보장하는 방법
  • DML 및 Spanner 변형을 활용하여 데이터를 수정하는 방법

필요한 항목

2. 설정 및 요구사항

Cloud Spanner 게임 개발 시작하기 Codelab 완료

Cloud Spanner 게임 개발 시작하기 Codelab 완료 플레이어 및 게임의 데이터 세트를 가져오는 데 필요합니다. 플레이어와 게임은 아이템과 돈을 획득해야 하며, 이를 통해 판매할 아이템을 표시하고 거래처에서 아이템을 구매해야 합니다.

Cloud Shell에서 환경 변수 구성

Cloud Shell 활성화 gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A를 클릭하여 Cloud Shell을 엽니다. 이전에 이 작업을 수행했으므로 환경을 프로비저닝하고 연결하는 데 몇 분 정도 걸립니다.

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

Screen Shot 2017-06-14 at 10.13.43 PM.png

Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 PROJECT_ID로 이미 설정된 것을 확인할 수 있습니다.

Cloud Shell에서 SPANNER 환경 변수 설정

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라는 4개의 새 테이블을 만듭니다.

402ce3310dd7141a.png

플레이어 아이템 관계

5e1229d85b75906.png

무역 주문 관계

게임 항목은 game_items 테이블에 추가된 후에 플레이어가 획득할 수 있습니다. player_items 테이블에는 itemUUID와 playerUUID 모두에 대한 외래 키가 있어 플레이어가 유효한 항목만 획득하도록 합니다.

player_ledger_entries 테이블은 플레이어 계정 잔액의 금전적 변경사항을 추적합니다. 전리품에서 돈을 얻거나 교역소에서 아이템을 판매하는 것도 좋은 방법입니다.

마지막으로 trade_orders 테이블은 판매 주문 게시와 구매자가 이러한 주문을 처리하는 데 사용됩니다.

스키마를 만들려면 Cloud 콘솔에서 Write DDL 버튼을 클릭합니다.

e9ad931beb1d96b.png

여기에서 schema/trading.sql 파일의 스키마 정의를 입력합니다.

CREATE TABLE game_items
(
  itemUUID STRING(36) NOT NULL,
  item_name STRING(MAX) NOT NULL,
  item_value NUMERIC NOT NULL,
  available_time TIMESTAMP NOT NULL,
  duration int64
)PRIMARY KEY (itemUUID);

CREATE TABLE player_items
(
  playerItemUUID STRING(36) NOT NULL,
  playerUUID STRING(36) NOT NULL,
  itemUUID STRING(36) NOT NULL,
  price NUMERIC NOT NULL,
  source STRING(MAX) NOT NULL,
  game_session STRING(36) NOT NULL,
  acquire_time TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()),
  expires_time TIMESTAMP,
  visible BOOL NOT NULL DEFAULT(true),
  FOREIGN KEY (itemUUID) REFERENCES game_items (itemUUID),
  FOREIGN KEY (game_session) REFERENCES games (gameUUID)
) PRIMARY KEY (playerUUID, playerItemUUID),
    INTERLEAVE IN PARENT players ON DELETE CASCADE;

CREATE TABLE player_ledger_entries (
  playerUUID STRING(36) NOT NULL,
  source STRING(MAX) NOT NULL,
  game_session STRING(36) NOT NULL,
  amount NUMERIC NOT NULL,
  entryDate TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
  FOREIGN KEY (game_session) REFERENCES games (gameUUID)
) PRIMARY KEY (playerUUID, entryDate DESC),
  INTERLEAVE IN PARENT players ON DELETE CASCADE;

CREATE TABLE trade_orders
(
  orderUUID STRING(36)  NOT NULL,
  lister STRING(36) NOT NULL,
  buyer STRING(36),
  playerItemUUID STRING(36) NOT NULL,
  trade_type STRING(5) NOT NULL,
  list_price NUMERIC NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()),
  ended TIMESTAMP,
  expires TIMESTAMP NOT NULL DEFAULT (TIMESTAMP_ADD(CURRENT_TIMESTAMP(), interval 24 HOUR)),
  active BOOL NOT NULL DEFAULT (true),
  cancelled BOOL NOT NULL DEFAULT (false),
  filled BOOL NOT NULL DEFAULT (false),
  expired BOOL NOT NULL DEFAULT (false),
  FOREIGN KEY (playerItemUUID) REFERENCES player_items (playerItemUUID)
) PRIMARY KEY (orderUUID);

CREATE INDEX TradeItem ON trade_orders(playerItemUUID, active);

Submit’ 버튼을 클릭합니다. 버튼을 클릭하여 스키마를 수정하고 스키마 업데이트가 완료될 때까지 기다립니다.

94f44b2774bce914.png

다음 단계

다음으로 항목 서비스를 배포합니다.

3. 항목 서비스 배포

서비스 개요

항목 서비스는 Go로 작성된 REST API로, gin 프레임워크를 활용합니다. 이 API에서 공개 게임에 참여하는 플레이어는 현금과 아이템을 획득합니다.

6c29a0831b5f588d.png

./src/golang/item-service/main.go 파일은 게임 아이템 및 해당 아이템을 획득하는 플레이어와 작동하도록 다음 엔드포인트를 구성합니다. 또한 플레이어가 돈을 획득할 수 있는 엔드포인트도 있습니다.

 func main() {
   configuration, _ := config.NewConfig()

   router := gin.Default()
   router.SetTrustedProxies(nil)
   router.Use(setSpannerConnection(configuration))

   router.GET("/items", getItemUUIDs)
   router.POST("/items", createItem)
   router.GET("/items/:id", getItem)
   router.PUT("/players/balance", updatePlayerBalance) 
   router.GET("/players", getPlayer)
   router.POST("/players/items", addPlayerItem)

   router.Run(configuration.Server.URL())
}

구성 및 Spanner 연결 사용은 이전 Codelab의 프로필 서비스 및 랜덤 대결 서비스와 동일하게 처리됩니다.

항목 서비스는 다음 정의가 포함된 GameItem, Player, PlayerLedgerPlayerItem과 함께 작동합니다.

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

먼저 게임에 생성된 아이템이 있어야 합니다. 이를 위해 /items 엔드포인트에 대한 POST 요청이 호출됩니다. 이것은 game_items 테이블에 아주 간단한 DML 삽입입니다.

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

아이템을 획득하려면 /players/items 엔드포인트에 대한 POST 요청이 호출됩니다. 이 엔드포인트의 로직은 게임 항목의 현재 값과 플레이어의 현재 게임 세션을 검색하는 것입니다. 그런 다음 아이템 획득의 소스와 시간을 나타내는 적절한 정보를 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
}

플레이어가 돈을 획득하려면 /players/updatebalance 엔드포인트에 대한 PUT 요청이 호출됩니다.

이 엔드포인트의 로직은 amount를 적용한 후 플레이어의 잔액을 업데이트하고 player_ledger_entries 테이블을 획득 기록으로 업데이트하는 것입니다. 플레이어의 account_balance는 호출자에게 반환되도록 수정됩니다. DML은 player와 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"

다음으로 플레이어가 이 아이템을 획득하도록 하세요. 이렇게 하려면 ItemUUIDPlayerUUID가 필요합니다. 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 서비스는 Go로 작성된 REST API로, gin 프레임워크를 활용합니다. 이 API에서는 플레이어 항목이 sell에 게시됩니다. 그런 다음 게임 플레이어는 오픈 거래를 할 수 있고, 충분한 돈을 모으면 아이템을 구매할 수 있습니다.

c32372f9def89a4a.png

tradepost 서비스의 ./src/golang/tradepost-service/main.go 파일은 다른 서비스와 유사한 설정 및 코드를 따르므로 여기서 반복하지 않습니다. 이 서비스는 다음과 같이 여러 엔드포인트를 노출합니다.

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

거래 주문을 생성하려면 API 엔드포인트 /trades/sell에 POST 요청을 전송합니다. 필수 정보는 판매할 player_item의 playerItemUUID, lister, list_price입니다.

Spanner 변형을 선택하여 거래 주문을 생성하고 player_item을 표시되지 않음으로 표시합니다. 이렇게 하면 판매자가 중복 상품을 판매하는 것을 방지할 수 있습니다.

 func (o *TradeOrder) Create(ctx context.Context, client spanner.Client) error {
   // insert into spanner
   _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       // get the Item to be listed
       pi, err := GetPlayerItem(ctx, txn, o.Lister, o.PlayerItemUUID)
       if err != nil {
           return err
       }

       // Set expires to 1 day by default
       if o.Expires.IsZero() {
           currentTime := time.Now()
           o.Expires = currentTime.Add(time.Hour * 24)
       }

       // Item is not visible or expired, so it can't be listed. That's an error
       if !validateSellOrder(pi) {
           errorMsg := fmt.Sprintf("Item (%s, %s) cannot be listed.", o.Lister, o.PlayerItemUUID)
           return errors.New(errorMsg)
       }

       // Initialize order values
       o.OrderUUID = generateUUID()
       o.Active = true // TODO: Have to set this by default since testing with emulator does not support 'DEFAULT' schema option

       // Insert the order
       var m []*spanner.Mutation
       cols := []string{"orderUUID", "playerItemUUID", "lister", "list_price", "trade_type", "expires", "active"}
       m = append(m, spanner.Insert("trade_orders", cols, []interface{}{o.OrderUUID, o.PlayerItemUUID, o.Lister, o.ListPrice, "sell", o.Expires, o.Active}))

       // Mark the item as invisible
       cols = []string{"playerUUID", "playerItemUUID", "visible"}
       m = append(m, spanner.Update("player_items", cols, []interface{}{o.Lister, o.PlayerItemUUID, false}))

       txn.BufferWrite(m)
       return nil
   })

   if err != nil {
       return err
   }

   // return empty error on success
   return nil
}

실제로 주문을 생성하기 전에 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
}

구매는 /trades/buy 엔드포인트에 대한 PUT 요청으로 진행됩니다. 필수 정보는 orderUUIDbuyer로, 이는 구매하는 플레이어의 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
}
  • 일치하는 원장 항목과 함께 주문의 list_pricelister의 계정 잔액에 추가합니다.
 // 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
}
  • 일치하는 원장 항목을 사용하여 구매자의 계정 잔액에서 주문의 list_price를 뺍니다.
 // Update buyer's account balance
       negAmount := o.ListPrice.Neg(&o.ListPrice)
       buyer.UpdateBalance(ctx, txn, *negAmount)
  • 게임 및 구매자 세부정보가 포함된 게임 항목의 새 인스턴스를 PlayerItems 테이블에 삽입하여 player_item을 새 플레이어로 이동하고 항목의 lister 인스턴스를 삭제합니다.
// 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
}
  • Orders(주문) 항목을 업데이트하여 항목이 채워져 더 이상 활성 상태가 아님을 나타냅니다.

모두 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에서 실행 중인 서비스가 설정됩니다. 이 서비스에는 item-service와 동일한 종속 항목이 많으므로 새 종속 항목이 다운로드되지 않습니다.

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

명령어 결과:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /trades/player_items      --> main.getPlayerItem (4 handlers)
[GIN-debug] POST   /trades/sell              --> main.createOrder (4 handlers)
[GIN-debug] GET    /trades/open              --> main.getOpenOrder (4 handlers)
[GIN-debug] PUT    /trades/buy               --> main.purchaseOrder (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8083

항목 게시

판매할 PlayerItem을 가져오는 GET 요청을 실행하여 서비스를 테스트합니다.

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

명령어 결과:

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

이제 /trades/sel 엔드포인트를 호출하여 판매할 상품을 게시해 보겠습니다.

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-service를 배포했습니다. 이 서비스는 이러한 주문을 구매하는 기능도 처리합니다.

다음 단계

서비스가 실행 중이므로 이제 트레이딩 포스트에서 판매 및 구매하는 플레이어를 시뮬레이션할 차례입니다.

5. 거래 시작

이제 상품 및 tradepost 서비스가 실행 중이므로 제공된 locust 생성기를 사용하여 부하를 생성할 수 있습니다.

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)

다음 명령어는 10초 (t=10s) 동안 게임 항목을 생성하는 item_generator.py 파일을 호출합니다.

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 프레임워크를 활용하여 Google 서비스에 대한 요청을 REST API를 사용할 수 있습니다.

플레이어를 만들고 게임을 플레이하는 데 소요된 시간은 물론 동시 사용자 수 (-u)도 자유롭게 수정할 수 있습니다.

다음 단계

시뮬레이션 후 Spanner를 쿼리하여 다양한 통계를 확인할 수 있습니다.

6. 거래 통계 가져오기

플레이어가 돈과 아이템을 획득하고 교역소에서 그 아이템을 판매하는 것을 시뮬레이션했으므로 몇 가지 통계를 확인해 보겠습니다.

이렇게 하려면 Cloud 콘솔을 사용하여 Spanner에 쿼리 요청을 실행하세요.

b5e3154c6f7cb0cf.png

미결 거래 주문과 처리된 거래 주문 확인하기

교역소에서 TradeOrder가 구매되면 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 열이 설정된 경우 플레이어가 게임을 하고 있는 것입니다. 그렇지 않으면 현재 게임을 하고 있지 않습니다.

account_balance와 함께 현재 가장 많은 아이템이 있는 게임을 플레이 중인 상위 10명의 플레이어로 이동

, 다음 쿼리를 사용합니다.

SELECT playerUUID, account_balance, (SELECT COUNT(*) FROM player_items WHERE playerUUID=p.PlayerUUID) AS numItems, current_game
FROM players AS p
WHERE current_game IS NOT NULL
ORDER BY numItems DESC
LIMIT 10;

결과:

playerUUID

account_balance

numItems

current_game

04d14288-a7c3-4515-9296-44eeae184d6d

1396.55

201

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

03836948-d591-4967-a8a8-80506454916d

2005.085

192

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


snip


snip


snip


snip

요약

이 단계에서는 Cloud 콘솔을 사용하여 Spanner를 쿼리하여 플레이어 및 거래 주문의 다양한 통계를 검토했습니다.

다음 단계

이제 정리할 시간입니다.

7. 삭제

Spanner를 마음껏 사용한 후에는 플레이그라운드를 정리해야 합니다. 다행히 이 단계는 간단합니다. Cloud 콘솔의 Cloud Spanner 섹션으로 이동하여 이 Codelab용으로 만든 인스턴스를 삭제하면 됩니다.

8. 축하합니다.

수고하셨습니다. Spanner에 샘플 게임을 배포했습니다.

다음 단계

이 실습에서는 게임 아이템 생성을 처리하는 서비스와 플레이어가 교역소에서 판매할 아이템을 획득하는 서비스를 처리하는 두 가지 서비스를 설정했습니다.

이 코드 샘플은 트랜잭션 내 Cloud Spanner 일관성이 DML 및 Spanner 변형 모두에서 어떻게 작동하는지를 이해하는 데 도움이 됩니다.

제공된 생성기를 자유롭게 사용하여 Spanner 확장을 살펴보세요.