การเริ่มต้นใช้งาน Cloud Spanner กับการพัฒนาเกม

1. บทนำ

Cloud Spanner เป็นบริการฐานข้อมูลเชิงสัมพันธ์ที่รองรับการปรับขนาดในวงกว้างและกระจายไปทั่วโลกที่มีการจัดการครบวงจร ซึ่งมอบธุรกรรม ACID และความหมายของ SQL โดยไม่สูญเสียประสิทธิภาพการทำงานและความพร้อมใช้งานสูง

ฟีเจอร์เหล่านี้ทำให้ Spanner มีความเหมาะสมกับสถาปัตยกรรมของเกมที่ต้องการสร้างฐานผู้เล่นทั่วโลก หรือมีความกังวลเกี่ยวกับความสอดคล้องของข้อมูล

ในห้องทดลองนี้ คุณจะสร้างบริการ Go 2 บริการที่โต้ตอบกับฐานข้อมูล Spanner ในระดับภูมิภาคเพื่อให้ผู้เล่นลงชื่อสมัครใช้และเริ่มเล่นได้

413fdd57bb0b68bc.png

ถัดไป คุณจะต้องสร้างข้อมูลโดยใช้ประโยชน์จากเฟรมเวิร์กการโหลด Python Locust.io เพื่อจำลองให้ผู้เล่นลงชื่อสมัครใช้และเล่นเกม จากนั้นค้นหา Spanner เพื่อระบุจำนวนผู้เล่นที่เล่น และสถิติบางอย่างเกี่ยวกับผู้เล่น เกมที่ชนะเทียบกับเกมที่เล่น

ขั้นตอนสุดท้าย คุณจะล้างทรัพยากรที่สร้างขึ้นในห้องทดลองนี้

สิ่งที่คุณจะสร้าง

ในห้องทดลองนี้ คุณจะทำสิ่งต่อไปนี้ได้

  • สร้างอินสแตนซ์ Spanner
  • ทำให้บริการโปรไฟล์ที่เขียนใน Go ใช้งานได้เพื่อจัดการการลงชื่อสมัครใช้โปรแกรมเล่น
  • ใช้บริการจับคู่ที่เขียนใน Go เพื่อกำหนดผู้เล่นให้เล่นเกม กำหนดผู้ชนะ และอัปเดตผู้เล่น สถิติเกม

สิ่งที่คุณจะได้เรียนรู้

  • วิธีตั้งค่าอินสแตนซ์ Cloud Spanner
  • วิธีสร้างฐานข้อมูลและสคีมาเกม
  • วิธีทำให้แอป Go ใช้งานได้เพื่อให้ทำงานกับ Cloud Spanner
  • วิธีสร้างข้อมูลโดยใช้ Locust
  • วิธีค้นหาข้อมูลใน Cloud Spanner เพื่อตอบคำถามเกี่ยวกับเกมและผู้เล่น

สิ่งที่คุณต้องมี

  • โปรเจ็กต์ Google Cloud ที่เชื่อมต่อกับบัญชีสำหรับการเรียกเก็บเงิน
  • เว็บเบราว์เซอร์ เช่น Chrome หรือ Firefox

2. การตั้งค่าและข้อกำหนด

สร้างโปรเจ็กต์

หากยังไม่มีบัญชี Google (Gmail หรือ Google Apps) คุณต้องสร้างบัญชีก่อน ลงชื่อเข้าใช้คอนโซล Google Cloud Platform ( console.cloud.google.com) และสร้างโปรเจ็กต์ใหม่

หากคุณมีโปรเจ็กต์อยู่แล้ว ให้คลิกเมนูแบบเลื่อนลงสำหรับการเลือกโปรเจ็กต์ที่ด้านซ้ายบนของคอนโซล

6c9406d9b014760.png

แล้วคลิก "โปรเจ็กต์ใหม่" ในกล่องโต้ตอบที่ปรากฏขึ้นเพื่อสร้างโปรเจ็กต์ใหม่ ดังนี้

949d83c8a4ee17d9.png

หากคุณยังไม่มีโปรเจ็กต์ คุณจะเห็นกล่องโต้ตอบลักษณะนี้ให้สร้างโปรเจ็กต์แรก

870a3cbd6541ee86.png

กล่องโต้ตอบการสร้างโปรเจ็กต์ที่ตามมาจะให้คุณป้อนรายละเอียดของโปรเจ็กต์ใหม่:

6a92c57d3250a4b3.png

จดจำรหัสโปรเจ็กต์ ซึ่งเป็นชื่อที่ไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมด (ระบบใช้ชื่อด้านบนนี้ไปแล้ว และจะใช้ไม่ได้ ขออภัย) โดยจะเรียกใน Codelab ว่า PROJECT_ID ในภายหลัง

ขั้นตอนถัดไป คุณจะต้องเปิดใช้การเรียกเก็บเงินใน Developers Console เพื่อใช้ทรัพยากร Google Cloud และเปิดใช้ Cloud Spanner API หากยังไม่ได้ดำเนินการ

15d0ef27a8fbab27.png

การใช้งาน Codelab นี้น่าจะมีค่าใช้จ่ายไม่เกิน 2-3 ดอลลาร์ แต่อาจมากกว่านี้หากคุณตัดสินใจใช้ทรัพยากรเพิ่มหรือปล่อยให้ทำงาน (ดูส่วน "ล้างข้อมูล" ในตอนท้ายของเอกสารนี้) ดูข้อมูลเกี่ยวกับราคาของ Google Cloud Spanner ได้ที่นี่

ผู้ใช้ใหม่ของ Google Cloud Platform จะมีสิทธิ์ทดลองใช้ฟรี$300 ซึ่งจะทำให้ Codelab นี้ไม่มีค่าใช้จ่ายทั้งหมด

การตั้งค่า Google Cloud Shell

แม้ว่า Google Cloud และ Spanner จะทำงานจากระยะไกลได้จากแล็ปท็อป แต่ใน Codelab นี้ เราจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมแบบบรรทัดคำสั่งที่ทำงานในระบบคลาวด์

เครื่องเสมือนแบบ Debian นี้เต็มไปด้วยเครื่องมือการพัฒนาทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักขนาด 5 GB ที่ทำงานอย่างต่อเนื่องใน Google Cloud ซึ่งจะช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก ซึ่งหมายความว่าสิ่งที่คุณต้องมีสำหรับ Codelab นี้คือเบราว์เซอร์ (ใช่แล้ว ทั้งหมดนี้ทำงานได้บน Chromebook)

  1. หากต้องการเปิดใช้งาน Cloud Shell จาก Cloud Console เพียงคลิกเปิดใช้งาน Cloud Shell gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (การจัดสรรและเชื่อมต่อกับสภาพแวดล้อมซึ่งจะใช้เวลาเพียงไม่นาน)

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

Screen Shot 14-06-2017 เวลา 22.13.43 น.

เมื่อเชื่อมต่อกับ Cloud Shell แล้ว คุณควรเห็นว่าได้รับการตรวจสอบสิทธิ์แล้วและโปรเจ็กต์ได้รับการตั้งค่าเป็น PROJECT_ID แล้ว

gcloud auth list

เอาต์พุตจากคำสั่ง

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

เอาต์พุตจากคำสั่ง

[core]
project = <PROJECT_ID>

หากโปรเจ็กต์ไม่ได้ตั้งค่าไว้ด้วยเหตุผลบางประการ ให้ใช้คำสั่งต่อไปนี้

gcloud config set project <PROJECT_ID>

กำลังมองหา PROJECT_ID อยู่ใช่ไหม ตรวจสอบรหัสที่คุณใช้ในขั้นตอนการตั้งค่าหรือดูในแดชบอร์ด Cloud Console

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Cloud Shell ยังตั้งค่าตัวแปรสภาพแวดล้อมโดยค่าเริ่มต้นด้วย ซึ่งอาจเป็นประโยชน์เมื่อคุณเรียกใช้คำสั่งในอนาคต

echo $GOOGLE_CLOUD_PROJECT

เอาต์พุตจากคำสั่ง

<PROJECT_ID>

ดาวน์โหลดโค้ด

คุณสามารถดาวน์โหลดโค้ดสำหรับห้องทดลองนี้ได้ใน Cloud Shell ข้อมูลนี้อิงตามรุ่น v0.1.0 ดังนั้นโปรดตรวจสอบแท็กดังต่อไปนี้

git clone https://github.com/cloudspannerecosystem/spanner-gaming-sample.git
cd spanner-gaming-sample/

# Check out v0.1.0 release
git checkout tags/v0.1.0 -b v0.1.0-branch

เอาต์พุตจากคำสั่ง

Cloning into 'spanner-gaming-sample'...
*snip*
Switched to a new branch 'v0.1.0-branch'

ตั้งค่าเครื่องมือสร้างโหลด Locust

Locust เป็นเฟรมเวิร์กการทดสอบโหลด Python ที่มีประโยชน์ในการทดสอบปลายทาง API ของ REST ใน Codelab นี้ เรามีการทดสอบโหลด 2 แบบใน "โปรแกรมสร้าง" ไดเรกทอรีที่เราจะเน้น ได้แก่

  • authentication_server.py: มีงานสร้างผู้เล่น และให้ผู้เล่นแบบสุ่มเลียนแบบการค้นหาจุดเดียว
  • match_server.py: มีงานสำหรับสร้างเกมและปิดเกม การสร้างเกมจะกำหนดผู้เล่นแบบสุ่ม 100 คนที่ไม่ได้เล่นเกมอยู่ การปิดเกมจะอัปเดตสถิติ games_played และ games_won และอนุญาตให้กำหนดผู้เล่นเหล่านั้นในเกมในอนาคต

คุณจะต้องมี Python 3.7 ขึ้นไปเพื่อให้ Locust ทำงานใน Cloud Shell Cloud Shell มาพร้อมกับ Python 3.9 คุณจึงไม่ต้องดำเนินการใดๆ นอกจากตรวจสอบเวอร์ชัน

python -V

เอาต์พุตจากคำสั่ง

Python 3.9.12

คุณสามารถติดตั้งข้อกำหนดของ Locust ได้แล้ว

pip3 install -r requirements.txt

เอาต์พุตจากคำสั่ง

Collecting locust==2.11.1
*snip*
Successfully installed ConfigArgParse-1.5.3 Flask-BasicAuth-0.2.0 Flask-Cors-3.0.10 brotli-1.0.9 gevent-21.12.0 geventhttpclient-2.0.2 greenlet-1.1.3 locust-2.11.1 msgpack-1.0.4 psutil-5.9.2 pyzmq-22.3.0 roundrobin-0.0.4 zope.event-4.5.0 zope.interface-5.4.0

ตอนนี้ ให้อัปเดต PATH เพื่อให้ระบบพบไบนารี locust ที่ติดตั้งใหม่ ดังนี้

PATH=~/.local/bin":$PATH"
which locust

เอาต์พุตจากคำสั่ง

/home/<user>/.local/bin/locust

สรุป

ในขั้นตอนนี้ คุณจะตั้งค่าโปรเจ็กต์ของคุณหากยังไม่มี เปิดใช้งาน Cloud Shell และดาวน์โหลดโค้ดสำหรับห้องทดลองนี้

สุดท้าย คุณตั้งค่า Locust สำหรับการสร้างภาระงานภายหลังใน Lab

รายการถัดไป

ต่อไปคุณจะตั้งค่าอินสแตนซ์และฐานข้อมูล Cloud Spanner

3. สร้างอินสแตนซ์และฐานข้อมูล Spanner

สร้างอินสแตนซ์ Spanner

ในขั้นตอนนี้ เราจะตั้งค่าอินสแตนซ์ Spanner สำหรับ Codelab ค้นหารายการ Spanner 1a6580bd3d3e6783.pngในเมนูแฮมเบอร์เกอร์ 3129589f7bc9e5ce.png ด้านบนซ้าย หรือค้นหา Spanner โดยกด "/" และพิมพ์ "SPANer"

36e52f8df8e13b99.png

จากนั้นคลิก 95269e75bc8c3e4d.png และกรอกแบบฟอร์มโดยป้อนชื่ออินสแตนซ์ cloudspanner-gaming สำหรับอินสแตนซ์ เลือกการกำหนดค่า (เลือกอินสแตนซ์ระดับภูมิภาค เช่น us-central1) แล้วกำหนดจำนวนโหนด สำหรับ Codelab นี้ เราจะใช้เพียง 500 processing units เท่านั้น

สุดท้ายแต่ไม่ท้ายสุด ให้คลิก "สร้าง" และคุณก็มีอินสแตนซ์ Cloud Spanner อยู่แล้วภายในไม่กี่วินาที

4457c324c94f93e6.png

สร้างฐานข้อมูลและสคีมา

คุณสร้างฐานข้อมูลได้เมื่ออินสแตนซ์ทำงาน Spanner ช่วยให้สามารถใช้ฐานข้อมูลได้หลายฐานข้อมูลในอินสแตนซ์เดียว

ฐานข้อมูลคือที่ที่คุณกำหนดสคีมา อีกทั้งยังควบคุมผู้ที่มีสิทธิ์เข้าถึงฐานข้อมูล ตั้งค่าการเข้ารหัสที่กำหนดเอง กำหนดค่าเครื่องมือเพิ่มประสิทธิภาพ และตั้งค่าระยะเวลาเก็บรักษาได้ด้วย

ในอินสแตนซ์หลายภูมิภาค คุณยังกำหนดค่าตัวแปรเริ่มต้นเริ่มต้นได้ด้วย อ่านเพิ่มเติมเกี่ยวกับฐานข้อมูลบน Spanner

สำหรับ Code-lab นี้ คุณจะสร้างฐานข้อมูลที่มีตัวเลือกเริ่มต้น และให้สคีมาในขณะที่สร้าง

ห้องทดลองนี้จะสร้างตาราง 2 ตาราง ได้แก่ ผู้เล่นและเกม

77651ac12e47fe2a.png

ผู้เล่นจะเข้าร่วมการแข่งขันหลายรายการได้ในช่วงเวลาหนึ่งๆ แต่จะเข้าร่วมได้เพียงครั้งละ 1 เกม ผู้เล่นยังมีสถิติเป็นประเภทข้อมูล JSON เพื่อติดตามสถิติที่น่าสนใจ เช่น games_played และ games_won เนื่องจากอาจมีการเพิ่มสถิติอื่นๆ เข้ามาภายหลัง นี่จึงเป็นคอลัมน์ที่ไม่มีสคีมาสำหรับผู้เล่น

เกมจะติดตามผู้เล่นที่เข้าร่วมโดยใช้ประเภทข้อมูล ARRAY ของ Spanner ระบบจะไม่ป้อนข้อมูลแอตทริบิวต์ผู้ชนะและรายการที่เสร็จสิ้นของเกมจนกว่าเกมจะปิดลง

มีคีย์นอก 1 คีย์ที่ช่วยให้แน่ใจว่า current_game ของผู้เล่นเป็นเกมที่ถูกต้อง

สร้างฐานข้อมูลด้วยการคลิก "สร้างฐานข้อมูล" ในภาพรวมของอินสแตนซ์

a820db6c4a4d6f2d.png

จากนั้นกรอกรายละเอียด ตัวเลือกที่สำคัญคือชื่อฐานข้อมูลและภาษา ในตัวอย่างนี้ เราตั้งชื่อฐานข้อมูลว่า sample-game และเลือกภาษา SQL มาตรฐานของ Google

สำหรับสคีมา ให้คัดลอกและวาง DDL นี้ลงในช่อง

CREATE TABLE games (
  gameUUID STRING(36) NOT NULL,
  players ARRAY<STRING(36)> NOT NULL,
  winner STRING(36),
  created TIMESTAMP,
  finished TIMESTAMP,
) PRIMARY KEY(gameUUID);

CREATE TABLE players (
  playerUUID STRING(36) NOT NULL,
  player_name STRING(64) NOT NULL,
  email STRING(MAX) NOT NULL,
  password_hash BYTES(60) NOT NULL,
  created TIMESTAMP,
  updated TIMESTAMP,
  stats JSON,
  account_balance NUMERIC NOT NULL DEFAULT (0.00),
  is_logged_in BOOL,
  last_login TIMESTAMP,
  valid_email BOOL,
  current_game STRING(36),
  FOREIGN KEY (current_game) REFERENCES games (gameUUID),
) PRIMARY KEY(playerUUID);

CREATE UNIQUE INDEX PlayerAuthentication ON players(email) STORING (password_hash);

CREATE INDEX PlayerGame ON players(current_game);

CREATE UNIQUE INDEX PlayerName ON players(player_name);

จากนั้นคลิกปุ่มสร้าง และรอสักครู่เพื่อให้ระบบสร้างฐานข้อมูลของคุณ

หน้าสร้างฐานข้อมูลควรมีลักษณะดังนี้

d39d358dc7d32939.png

ตอนนี้คุณต้องตั้งค่าตัวแปรสภาพแวดล้อมบางอย่างใน Cloud Shell เพื่อใช้ในภายหลังในห้องทดลองโค้ด ดังนั้น ให้จดรหัสอินสแตนซ์ และตั้งค่า INSTANCE_ID และ DATABASE_ID ใน Cloud Shell

f6f98848d3aea9c.png

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

สรุป

ในขั้นตอนนี้ คุณจะได้สร้างอินสแตนซ์ Spanner และฐานข้อมูลเกมตัวอย่าง คุณยังกำหนดสคีมาที่เกมตัวอย่างนี้ใช้ได้อีกด้วย

รายการถัดไป

ถัดไป คุณจะทำให้บริการโปรไฟล์ใช้งานได้เพื่อให้ผู้เล่นลงชื่อสมัครใช้เพื่อเล่นเกมได้

4. ทำให้บริการโปรไฟล์ใช้งานได้

ภาพรวมของบริการ

บริการโปรไฟล์คือ REST API ที่เขียนใน Go ซึ่งใช้ประโยชน์จากเฟรมเวิร์ก Gin

4fce45ee6c858b3e.png

ผู้เล่นสามารถลงชื่อสมัครใช้เพื่อเล่นเกมได้ใน API นี้ ซึ่งสร้างโดยคำสั่ง POST พื้นฐานที่ยอมรับชื่อโปรแกรมเล่น อีเมล และรหัสผ่าน รหัสผ่านจะเข้ารหัสด้วย bcrypt และระบบจะเก็บแฮชไว้ในฐานข้อมูล

อีเมลจะถือว่าเป็นตัวระบุที่ไม่ซ้ำกัน ในขณะที่ player_name จะใช้เพื่อแสดงสำหรับเกม

ปัจจุบัน API นี้ไม่สามารถจัดการการเข้าสู่ระบบ แต่คุณสามารถดำเนินการเพิ่มเติมให้กับคุณได้

ไฟล์ ./src/golang/profile-service/main.go สำหรับบริการโปรไฟล์จะแสดงปลายทางหลัก 2 รายการดังนี้

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

   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection(configuration))

   router.POST("/players", createPlayer)
   router.GET("/players", getPlayerUUIDs)
   router.GET("/players/:id", getPlayerByID)

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

และโค้ดสำหรับปลายทางเหล่านั้นจะส่งเส้นทางไปยังโมเดลโปรแกรมเล่น

func getPlayerByID(c *gin.Context) {
   var playerUUID = c.Param("id")

   ctx, client := getSpannerConnection(c)

   player, err := models.GetPlayerByUUID(ctx, client, playerUUID)
   if err != nil {
       c.IndentedJSON(http.StatusNotFound, gin.H{"message": "player not found"})
       return
   }

   c.IndentedJSON(http.StatusOK, player)
}

func createPlayer(c *gin.Context) {
   var player models.Player

   if err := c.BindJSON(&player); err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   ctx, client := getSpannerConnection(c)
   err := player.AddPlayer(ctx, client)
   if err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   c.IndentedJSON(http.StatusCreated, player.PlayerUUID)
}

สิ่งแรกที่บริการทำคือการตั้งค่าการเชื่อมต่อ Spanner ซึ่งจะมีการดำเนินการในระดับบริการเพื่อสร้างพูลเซสชันสำหรับบริการ

func setSpannerConnection() gin.HandlerFunc {
   ctx := context.Background()
   client, err := spanner.NewClient(ctx, configuration.Spanner.URL())

   if err != nil {
       log.Fatal(err)
   }

   return func(c *gin.Context) {
       c.Set("spanner_client", *client)
       c.Set("spanner_context", ctx)
       c.Next()
   }
}

Player และ PlayerStats มีโครงสร้างที่กำหนดไว้ดังนี้

type Player struct {
   PlayerUUID      string `json:"playerUUID" validate:"omitempty,uuid4"`
   Player_name     string `json:"player_name" validate:"required_with=Password Email"`
   Email           string `json:"email" validate:"required_with=Player_name Password,email"`
   // not stored in DB
   Password        string `json:"password" validate:"required_with=Player_name Email"` 
   // stored in DB
   Password_hash   []byte `json:"password_hash"`                                       
   created         time.Time
   updated         time.Time
   Stats           spanner.NullJSON `json:"stats"`
   Account_balance big.Rat          `json:"account_balance"`
   last_login      time.Time
   is_logged_in    bool
   valid_email     bool
   Current_game    string `json:"current_game" validate:"omitempty,uuid4"`
}

type PlayerStats struct {
   Games_played spanner.NullInt64 `json:"games_played"`
   Games_won    spanner.NullInt64 `json:"games_won"`
}

ฟังก์ชันในการเพิ่มโปรแกรมเล่นจะใช้การแทรก DML ภายในธุรกรรม ReadWrite เนื่องจากการเพิ่มผู้เล่นเป็นคำสั่งเดี่ยว ไม่ใช่การแทรกเป็นกลุ่ม ฟังก์ชันจะมีลักษณะดังนี้

func (p *Player) AddPlayer(ctx context.Context, client spanner.Client) error {
   // Validate based on struct validation rules
   err := p.Validate()
   if err != nil {
       return err
   }

   // take supplied password+salt, hash. Store in user_password
   passHash, err := hashPassword(p.Password)

   if err != nil {
       return errors.New("Unable to hash password")
   }

   p.Password_hash = passHash

   // Generate UUIDv4
   p.PlayerUUID = generateUUID()

   // Initialize player stats
   emptyStats := spanner.NullJSON{Value: PlayerStats{
       Games_played: spanner.NullInt64{Int64: 0, Valid: true},
       Games_won:    spanner.NullInt64{Int64: 0, Valid: true},
   }, Valid: true}

   // insert into spanner
   _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       stmt := spanner.Statement{
           SQL: `INSERT players (playerUUID, player_name, email, password_hash, created, stats) VALUES
                   (@playerUUID, @playerName, @email, @passwordHash, CURRENT_TIMESTAMP(), @pStats)
           `,
           Params: map[string]interface{}{
               "playerUUID":   p.PlayerUUID,
               "playerName":   p.Player_name,
               "email":        p.Email,
               "passwordHash": p.Password_hash,
               "pStats":       emptyStats,
           },
       }

       _, err := txn.Update(ctx, stmt)
       return err
   })
   if err != nil {
       return err
   }
   // return empty error on success
   return nil
}

หากต้องการดึงข้อมูลโปรแกรมเล่นโดยอิงตาม UUID ของโปรแกรมเล่นดังกล่าว ระบบจะออกการอ่านอย่างง่าย การดำเนินการนี้จะเรียก playerUUID, Player_name, email และ stats ของโปรแกรมเล่น

func GetPlayerByUUID(ctx context.Context, client spanner.Client, uuid string) (Player, error) {
   row, err := client.Single().ReadRow(ctx, "players",
       spanner.Key{uuid}, []string{"playerUUID", "player_name", "email", "stats"})
   if err != nil {
       return Player{}, err
   }

   player := Player{}
   err = row.ToStruct(&player)

   if err != nil {
       return Player{}, err
   }
   return player, nil
}

โดยค่าเริ่มต้น ระบบจะกำหนดค่าบริการโดยใช้ตัวแปรสภาพแวดล้อม โปรดดูส่วนที่เกี่ยวข้องของไฟล์ ./src/golang/profile-service/config/config.go

func NewConfig() (Config, error) {
   *snip*
   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8080)

   // 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:8080

เมื่อมีข้อมูลนี้แล้ว ก็ถึงเวลาที่จะเรียกใช้บริการ

เรียกใช้บริการโปรไฟล์

เรียกใช้บริการโดยใช้คำสั่ง go การดำเนินการนี้จะดาวน์โหลดทรัพยากร Dependency และสร้างบริการที่ทำงานอยู่บนพอร์ต 8080 ดังนี้

cd ~/spanner-gaming-sample/src/golang/profile-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] POST   /players                  --> main.createPlayer (4 handlers)
[GIN-debug] GET    /players                  --> main.getPlayerUUIDs (4 handlers)
[GIN-debug] GET    /players/:id              --> main.getPlayerByID (4 handlers)
[GIN-debug] GET    /players/:id/stats        --> main.getPlayerStats (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080

ทดสอบบริการโดยการออกคำสั่ง curl ดังนี้

curl http://localhost:8080/players \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"email": "test@gmail.com","password": "s3cur3P@ss","player_name": "Test Player"}'

เอาต์พุตจากคำสั่ง:

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

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

สรุป

ในขั้นตอนนี้ คุณได้ใช้บริการโปรไฟล์ที่อนุญาตให้ผู้เล่นลงชื่อสมัครใช้เพื่อเล่นเกมของคุณ และคุณได้ทดสอบบริการโดยเรียก POST API เพื่อสร้างผู้เล่นใหม่

ขั้นตอนถัดไป

ในขั้นตอนถัดไป คุณจะต้องใช้บริการจับคู่

5. ติดตั้งใช้งานบริการจับคู่

ภาพรวมของบริการ

บริการจับคู่คือ REST API ที่เขียนขึ้นใน Go ซึ่งใช้ประโยชน์จากเฟรมเวิร์ก Gin

9aecd571df0dcd7c.png

เกมใน API นี้สร้างและปิดไปแล้ว เมื่อสร้างเกม ระบบจะกำหนดผู้เล่น 10 คนที่ไม่ได้เล่นเกมอยู่ให้เกมนั้น

เมื่อเกมปิดลง ระบบจะสุ่มเลือกผู้ชนะและผู้เล่นแต่ละคน มีการปรับค่าสถิติสำหรับ games_played และ games_won อยู่ นอกจากนี้ ผู้เล่นแต่ละคนจะได้รับการอัปเดตเพื่อระบุว่าไม่ได้เล่นแล้วและสามารถเล่นเกมในอนาคตได้

ไฟล์ ./src/golang/matchmaking-service/main.go สำหรับบริการจับคู่จะใช้การตั้งค่าและโค้ดที่คล้ายกับบริการ profile จึงไม่สามารถซ้ำกันที่นี่ บริการนี้แสดงปลายทางหลัก 2 รายการดังนี้

func main() {
   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection())

   router.POST("/games/create", createGame)
   router.PUT("/games/close", closeGame)

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

บริการนี้มีโครงสร้างเกม รวมถึงโครงสร้างผู้เล่นและสถิติผู้เล่นที่เล็กลง

type Game struct {
   GameUUID string           `json:"gameUUID"`
   Players  []string         `json:"players"`
   Winner   string           `json:"winner"`
   Created  time.Time        `json:"created"`
   Finished spanner.NullTime `json:"finished"`
}

type Player struct {
   PlayerUUID   string           `json:"playerUUID"`
   Stats        spanner.NullJSON `json:"stats"`
   Current_game string           `json:"current_game"`
}

type PlayerStats struct {
   Games_played int `json:"games_played"`
   Games_won    int `json:"games_won"`
}

ในการสร้างเกม บริการจับคู่จะสุ่มเลือกผู้เล่น 100 คนที่ยังไม่ได้เล่นเกมอยู่

การกลายพันธุ์ของ Spaner จะเลือกมาสร้างเกมและกำหนดผู้เล่น เนื่องจากการกลายพันธุ์จะมีประสิทธิภาพมากกว่า DML เมื่อทำการเปลี่ยนแปลงครั้งใหญ่

// Create a new game and assign players
// Players that are not currently playing a game are eligble to be selected for the new game
// Current implementation allows for less than numPlayers to be placed in a game
func (g *Game) CreateGame(ctx context.Context, client spanner.Client) error {
   // Initialize game values
   g.GameUUID = generateUUID()

   numPlayers := 10

   // Create and assign
   _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       var m []*spanner.Mutation

       // get players
       query := fmt.Sprintf("SELECT playerUUID FROM (SELECT playerUUID FROM players WHERE current_game IS NULL LIMIT 10000) TABLESAMPLE RESERVOIR (%d ROWS)", numPlayers)
       stmt := spanner.Statement{SQL: query}
       iter := txn.Query(ctx, stmt)

       playerRows, err := readRows(iter)
       if err != nil {
           return err
       }

       var playerUUIDs []string

       for _, row := range playerRows {
           var pUUID string
           if err := row.Columns(&pUUID); err != nil {
               return err
           }

           playerUUIDs = append(playerUUIDs, pUUID)
       }

       // Create the game
       gCols := []string{"gameUUID", "players", "created"}
       m = append(m, spanner.Insert("games", gCols, []interface{}{g.GameUUID, playerUUIDs, time.Now()}))

       // Update players to lock into this game
       for _, p := range playerUUIDs {
           pCols := []string{"playerUUID", "current_game"}
           m = append(m, spanner.Update("players", pCols, []interface{}{p, g.GameUUID}))
       }

       txn.BufferWrite(m)

       return nil
   })

   if err != nil {
       return err
   }

   return nil
}

การเลือกผู้เล่นแบบสุ่มจะทำด้วย SQL โดยใช้ความสามารถ TABLESPACE RESERVOIR ของ GoogleSQL

การปิดเกมจะซับซ้อนขึ้นเล็กน้อย ซึ่งรวมถึงการเลือกผู้ชนะแบบสุ่มจากผู้เล่นเกม การระบุเวลาที่เกมจบ และการอัปเดตของผู้เล่นแต่ละคน สถิติของ games_played และ games_won

เนื่องจากความซับซ้อนและปริมาณการเปลี่ยนแปลงนี้ เราจึงต้องเลือกการเปลี่ยนแปลงอีกครั้งเพื่อปิดเกม

func determineWinner(playerUUIDs []string) string {
   if len(playerUUIDs) == 0 {
       return ""
   }

   var winnerUUID string

   rand.Seed(time.Now().UnixNano())
   offset := rand.Intn(len(playerUUIDs))
   winnerUUID = playerUUIDs[offset]
   return winnerUUID
}

// Given a list of players and a winner's UUID, update players of a game
// Updating players involves closing out the game (current_game = NULL) and
// updating their game stats. Specifically, we are incrementing games_played.
// If the player is the determined winner, then their games_won stat is incremented.
func (g Game) updateGamePlayers(ctx context.Context, players []Player, txn *spanner.ReadWriteTransaction) error {
   for _, p := range players {
       // Modify stats
       var pStats PlayerStats
       json.Unmarshal([]byte(p.Stats.String()), &pStats)

       pStats.Games_played = pStats.Games_played + 1

       if p.PlayerUUID == g.Winner {
           pStats.Games_won = pStats.Games_won + 1
       }
       updatedStats, _ := json.Marshal(pStats)
       p.Stats.UnmarshalJSON(updatedStats)

       // Update player
       // If player's current game isn't the same as this game, that's an error
       if p.Current_game != g.GameUUID {
           errorMsg := fmt.Sprintf("Player '%s' doesn't belong to game '%s'.", p.PlayerUUID, g.GameUUID)
           return errors.New(errorMsg)
       }

       cols := []string{"playerUUID", "current_game", "stats"}
       newGame := spanner.NullString{
           StringVal: "",
           Valid:     false,
       }

       txn.BufferWrite([]*spanner.Mutation{
           spanner.Update("players", cols, []interface{}{p.PlayerUUID, newGame, p.Stats}),
       })
   }

   return nil
}

// Closing game. When provided a Game, choose a random winner and close out the game.
// A game is closed by setting the winner and finished time.
// Additionally all players' game stats are updated, and the current_game is set to null to allow
// them to be chosen for a new game.
func (g *Game) CloseGame(ctx context.Context, client spanner.Client) error {
   // Close game
   _, err := client.ReadWriteTransaction(ctx,
       func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
           // Get game players
           playerUUIDs, players, err := g.getGamePlayers(ctx, txn)

           if err != nil {
               return err
           }

           // Might be an issue if there are no players!
           if len(playerUUIDs) == 0 {
               errorMsg := fmt.Sprintf("No players found for game '%s'", g.GameUUID)
               return errors.New(errorMsg)
           }

           // Get random winner
           g.Winner = determineWinner(playerUUIDs)

           // Validate game finished time is null
           row, err := txn.ReadRow(ctx, "games", spanner.Key{g.GameUUID}, []string{"finished"})
           if err != nil {
               return err
           }

           if err := row.Column(0, &g.Finished); err != nil {
               return err
           }

           // If time is not null, then the game is already marked as finished. 
           // That's an error.
           if !g.Finished.IsNull() {
               errorMsg := fmt.Sprintf("Game '%s' is already finished.", g.GameUUID)
               return errors.New(errorMsg)
           }

           cols := []string{"gameUUID", "finished", "winner"}
           txn.BufferWrite([]*spanner.Mutation{
               spanner.Update("games", cols, []interface{}{g.GameUUID, time.Now(), g.Winner}),
           })

           // Update each player to increment stats.games_played 
           // (and stats.games_won if winner), and set current_game 
           // to null so they can be chosen for a new game
           playerErr := g.updateGamePlayers(ctx, players, txn)
           if playerErr != nil {
               return playerErr
           }

           return nil
       })

   if err != nil {
       return err
   }

   return nil
}

ระบบจะจัดการการกำหนดค่าอีกครั้งผ่านตัวแปรสภาพแวดล้อมตามที่อธิบายไว้ใน ./src/golang/matchmaking-service/config/config.go สำหรับบริการ

   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8081)

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

เพื่อหลีกเลี่ยงข้อขัดแย้งกับบริการโปรไฟล์ บริการนี้จะทำงานใน localhost:8081 โดยค่าเริ่มต้น

เมื่อมีข้อมูลนี้ ก็ถึงเวลาที่จะเรียกใช้บริการจับคู่แล้ว

เรียกใช้บริการจับคู่

เรียกใช้บริการโดยใช้คำสั่ง go การดำเนินการนี้จะเริ่มต้นให้บริการบนพอร์ต 8082 บริการนี้มีทรัพยากร Dependency เหมือนกับบริการโปรไฟล์หลายรายการ ดังนั้นระบบจะไม่ดาวน์โหลดทรัพยากร Dependency ใหม่

cd ~/spanner-gaming-sample/src/golang/matchmaking-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] POST   /games/create             --> main.createGame (4 handlers)
[GIN-debug] PUT    /games/close              --> main.closeGame (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8081

สร้างเกม

ทดสอบบริการเพื่อสร้างเกม ก่อนอื่น ให้เปิดเทอร์มินัลใหม่ใน Cloud Shell ดังนี้

90eceac76a6bb90b.png

จากนั้นออกคำสั่ง curl ต่อไปนี้

curl http://localhost:8081/games/create \
    --include \
    --header "Content-Type: application/json" \
    --request "POST"

เอาต์พุตจากคำสั่ง:

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

"f45b0f7f-405b-4e67-a3b8-a624e990285d"

ปิดเกม

curl http://localhost:8081/games/close \
    --include \
    --header "Content-Type: application/json" \
    --data '{"gameUUID": "f45b0f7f-405b-4e67-a3b8-a624e990285d"}' \
    --request "PUT"

เอาต์พุตจากคำสั่ง:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: <date> 19:43:58 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

สรุป

ในขั้นตอนนี้ คุณใช้บริการจับคู่เพื่อจัดการการสร้างเกมและกำหนดให้ผู้เล่นเล่นเกมนั้น บริการนี้ยังจัดการปิดเกม ซึ่งจะสุ่มเลือกผู้ชนะและอัปเดตผู้เล่นเกมทุกคน สถิติของ games_played และ games_won

ขั้นตอนถัดไป

เมื่อบริการของคุณทำงานได้แล้ว ก็ถึงเวลาให้ผู้เล่นลงชื่อสมัครใช้และเล่นเกม

6. เริ่มเล่น

เมื่อโปรไฟล์และบริการจับคู่ทำงานอยู่ คุณสามารถสร้างโหลดโดยใช้โปรแกรมสร้างตัวระบุตำแหน่งที่ให้มา

Locust มีอินเทอร์เฟซเว็บสำหรับเรียกใช้โปรแกรมสร้าง แต่ในห้องทดลองนี้ คุณจะใช้บรรทัดคำสั่ง (ตัวเลือก –headless)

ลงชื่อสมัครใช้ผู้เล่น

ก่อนอื่น คุณจะต้องสร้างผู้เล่น

โค้ด Python สำหรับสร้างโปรแกรมเล่นในไฟล์ ./generators/authentication_server.py จะมีลักษณะดังนี้

class PlayerLoad(HttpUser):
   def on_start(self):
       global pUUIDs
       pUUIDs = []

   def generatePlayerName(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generatePassword(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generateEmail(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32) + ['@'] +
           random.choices(['gmail', 'yahoo', 'microsoft']) + ['.com'])

   @task
   def createPlayer(self):
       headers = {"Content-Type": "application/json"}
       data = {"player_name": self.generatePlayerName(), "email": self.generateEmail(), "password": self.generatePassword()}

       with self.client.post("/players", data=json.dumps(data), headers=headers, catch_response=True) as response:
           try:
               pUUIDs.append(response.json())
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")

ชื่อผู้เล่น อีเมล และรหัสผ่านจะสร้างขึ้นแบบสุ่ม

ระบบจะดึงข้อมูลผู้เล่นที่ลงชื่อสมัครใช้สำเร็จแล้วในงานที่ 2 เพื่อสร้างภาระงานการอ่าน

   @task(5)
   def getPlayer(self):
       # No player UUIDs are in memory, reschedule task to run again later.
       if len(pUUIDs) == 0:
           raise RescheduleTask()

       # Get first player in our list, removing it to avoid contention from concurrent requests
       pUUID = pUUIDs[0]
       del pUUIDs[0]

       headers = {"Content-Type": "application/json"}

       self.client.get(f"/players/{pUUID}", headers=headers, name="/players/[playerUUID]")

คำสั่งต่อไปนี้เรียกไฟล์ ./generators/authentication_server.py ที่จะสร้างโปรแกรมเล่นใหม่เป็นเวลา 30 วินาที (t=30s) โดยมีการเกิดขึ้นพร้อมกัน 2 ชุดข้อความพร้อมกัน (u=2) ดังนี้

cd ~/spanner-gaming-sample
locust -H http://127.0.0.1:8080 -f ./generators/authentication_server.py --headless -u=2 -r=2 -t=30s

ผู้เล่นเข้าร่วมเกม

เมื่อคุณมีผู้เล่นลงชื่อสมัครใช้แล้ว พวกเขาต้องการเริ่มเล่นเกม!

โค้ด Python ที่ใช้สร้างและปิดเกมในไฟล์ ./generators/match_server.py จะมีลักษณะดังนี้

from locust import HttpUser, task
from locust.exception import RescheduleTask

import json

class GameMatch(HttpUser):
   def on_start(self):
       global openGames
       openGames = []

   @task(2)
   def createGame(self):
       headers = {"Content-Type": "application/json"}

       # Create the game, then store the response in memory of list of open games.
       with self.client.post("/games/create", headers=headers, catch_response=True) as response:
           try:
               openGames.append({"gameUUID": response.json()})
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")


   @task
   def closeGame(self):
       # No open games are in memory, reschedule task to run again later.
       if len(openGames) == 0:
           raise RescheduleTask()

       headers = {"Content-Type": "application/json"}

       # Close the first open game in our list, removing it to avoid 
       # contention from concurrent requests
       game = openGames[0]
       del openGames[0]

       data = {"gameUUID": game["gameUUID"]}
       self.client.put("/games/close", data=json.dumps(data), headers=headers)

เมื่อเรียกใช้โปรแกรมสร้างนี้ โปรแกรมจะเปิดและปิดเกมที่อัตราส่วน 2:1 (open:close) คำสั่งนี้จะเรียกใช้โปรแกรมสร้างเป็นเวลา 10 วินาที (-t=10s):

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

สรุป

ในขั้นตอนนี้ คุณได้จำลองให้ผู้เล่นลงชื่อสมัครใช้เกม แล้วทำการจำลองให้ผู้เล่นเล่นเกมโดยใช้บริการจับคู่ การจำลองเหล่านี้ใช้ประโยชน์จากเฟรมเวิร์ก Locust Python เพื่อส่งคำขอไปยังบริการของเรา REST API

แก้ไขเวลาที่ใช้ในการสร้างผู้เล่นและเล่นเกม รวมถึงจำนวนผู้ใช้ที่ใช้งานพร้อมกัน (-u)

ขั้นตอนถัดไป

หลังจากการจำลอง คุณจะต้องตรวจสอบสถิติต่างๆ โดยค้นหา Spanner

7. ดึงข้อมูลสถิติเกม

เมื่อเรามีผู้เล่นจำลองสามารถลงชื่อสมัครใช้และเล่นเกมแล้ว คุณควรตรวจสอบสถิติของคุณ

ซึ่งทำได้โดยใช้ Cloud Console ในการส่งคำขอการค้นหาไปยัง Spanner

b5e3154c6f7cb0cf.png

การตรวจสอบการแข่งขันแบบเปิดเทียบกับแบบปิด

เกมที่ปิดไปแล้วคือเกมที่มีการประทับเวลาจบแล้ว ขณะที่เกมที่เปิดอยู่จะจบแล้วแสดงเป็น NULL ค่านี้จะตั้งเมื่อเกมปิดไปแล้ว

ดังนั้นการค้นหานี้จะช่วยคุณตรวจสอบจำนวนเกมที่เปิดอยู่และจำนวนเกมที่ปิดไปแล้ว

SELECT Type, NumGames FROM
(SELECT "Open Games" as Type, count(*) as NumGames FROM games WHERE finished IS NULL
UNION ALL
SELECT "Closed Games" as Type, count(*) as NumGames FROM games WHERE finished IS NOT NULL
)

ผลลัพธ์

Type

NumGames

Open Games

0

Closed Games

175

การตรวจสอบจำนวนผู้เล่นที่เล่นและไม่เล่น

ผู้เล่นกำลังเล่นเกมหากมีการตั้งค่าคอลัมน์ current_game ไว้ ไม่เช่นนั้น ผู้ใช้จะไม่เล่นเกมอยู่

หากต้องการเปรียบเทียบจำนวนผู้เล่นที่กำลังเล่นและไม่ได้เล่นอยู่ ให้ใช้คำค้นหานี้

SELECT Type, NumPlayers FROM
(SELECT "Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NOT NULL
UNION ALL
SELECT "Not Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NULL
)

ผลลัพธ์

Type

NumPlayers

Playing

0

Not Playing

310

กำหนดผู้ชนะสูงสุด

เมื่อเกมปิดลง ระบบจะสุ่มเลือกผู้เล่นคนใดคนหนึ่งให้เป็นผู้ชนะ สถิติ games_won ของผู้เล่นรายนั้นจะเพิ่มขึ้นในช่วงปิดเกม

SELECT playerUUID, stats
FROM players
WHERE CAST(JSON_VALUE(stats, "$.games_won") AS INT64)>0
LIMIT 10;

ผลลัพธ์

playerUUID

สถิติ

07e247c5-f88e-4bca-a7bc-12d2485f2f2b

{&quot;games_played&quot;:49,&quot;games_won&quot;:1}

09b72595-40af-4406-a000-2fb56c58fe92

{&quot;games_played&quot;:56,&quot;games_won&quot;:1}

1002385b-02a0-462b-a8e7-05c9b27223aa

{&quot;games_played&quot;:66,&quot;games_won&quot;:1}

13ec3770-7ae3-495f-9b53-6322d8e8d6c3

{&quot;games_played&quot;:44,&quot;games_won&quot;:1}

15513852-3f2a-494f-b437-fe7125d15f1b

{&quot;games_played&quot;:49,&quot;games_won&quot;:1}

17faec64-4f77-475c-8df8-6ab026cf6698

{&quot;games_played&quot;:50,&quot;games_won&quot;:1}

1abfcb27-037d-446d-bb7a-b5cd17b5733d

{&quot;games_played&quot;:63,&quot;games_won&quot;:1}

2109a33e-88bd-4e74-a35c-a7914d9e3bde

{&quot;games_played&quot;:56,&quot;games_won&quot;:2}

222e37d9-06b0-4674-865d-a0e5fb80121e

{&quot;games_played&quot;:60,&quot;games_won&quot;:1}

22ced15c-0da6-4fd9-8cb2-1ffd233b3c56

{&quot;games_played&quot;:50,&quot;games_won&quot;:1}

สรุป

ในขั้นตอนนี้ คุณได้ตรวจสอบสถิติต่างๆ ของผู้เล่นและเกมโดยใช้ Cloud Console เพื่อค้นหา Spanner

ขั้นตอนถัดไป

ต่อไปก็ถึงเวลาล้างข้อมูล

8. กำลังล้างข้อมูล (ไม่บังคับ)

หากต้องการล้างข้อมูล เพียงไปที่ส่วน Cloud Spanner ของ Cloud Console แล้วลบอินสแตนซ์ 'cloudspanner-gaming' ที่เราสร้างในขั้นตอน Codelab ที่ชื่อ "Setup a Cloud Spannerอินสแตนซ์"

9. ยินดีด้วย

ขอแสดงความยินดี คุณทำให้เกมตัวอย่างใช้งานได้ใน Spanner เรียบร้อยแล้ว

ขั้นตอนถัดไปคือ

ในห้องทดลองนี้ คุณได้รู้จักกับหัวข้อต่างๆ เกี่ยวกับการทำงานร่วมกับ Spanner โดยใช้ไดรเวอร์โกลัง ซึ่งจะให้พื้นฐานที่ดียิ่งขึ้นเพื่อทำความเข้าใจแนวคิดสำคัญ เช่น

  • การออกแบบสคีมา
  • DML เทียบกับการเปลี่ยนแปลง
  • การทำงานร่วมกับ Golang

อย่าลืมดู Codelab ของ Cloud Spanner Game Trading Post เพื่อใช้เป็นตัวอย่างสำหรับการทำงานกับ Spanner เป็นแบ็กเอนด์สำหรับเกมของคุณ