พื้นหลัง
Roblox มีชุด API สำหรับเชื่อมต่อกับ data stores ผ่าน DataStoreService กรณีการใช้งานที่พบบ่อยที่สุดสำหรับ API เหล่านี้คือการบันทึก โหลด และทำซ้ำ ข้อมูลผู้เล่น นั่นคือ ข้อมูลที่เกี่ยวข้องกับความก้าวหน้า การซื้อ และลักษณะเซสชันอื่น ๆ ของผู้เล่นที่ยังคงอยู่ระหว่างเซสชันการเล่นแต่ละครั้ง
เกมส่วนใหญ่ใน Roblox ใช้ API เหล่านี้เพื่อสร้างระบบข้อมูลผู้เล่นในรูปแบบใดรูปแบบหนึ่ง การนำไปใช้งานเหล่านี้แตกต่างกันในแนวทาง แต่โดยทั่วไปแล้วมุ่งหวังที่จะแก้ไขปัญหาชุดเดียวกัน
ปัญหาทั่วไป
ด้านล่างนี้คือปัญหาทั่วไปบางประการที่ระบบข้อมูลผู้เล่นพยายามแก้ไข:
การเข้าถึงในหน่วยความจำ: คำขอ DataStoreService จะทำการร้องขอเว็บที่ทำงานแบบอะซิงโครนัสและอยู่ภายใต้ข้อจำกัดอัตรา นี่เหมาะสมสำหรับการโหลดเริ่มต้นในช่วงเริ่มต้นของเซสชัน แต่ไม่เหมาะสำหรับการอ่านและเขียนที่มีความถี่สูงในระหว่างการเล่นเกมตามปกติ ระบบข้อมูลผู้เล่นของนักพัฒนาส่วนใหญ่จะเก็บข้อมูลนี้ในหน่วยความจำบนเซิร์ฟเวอร์ Roblox โดยจำกัดคำขอ DataStoreService ให้กับสถานการณ์ต่อไปนี้:
- การอ่านเริ่มต้นในช่วงเริ่มต้นของเซสชัน
- การเขียนสุดท้ายในตอนท้ายของเซสชัน
- การเขียนเป็นระยะ ๆ ในช่วงเวลาหนึ่งเพื่อลดความเสี่ยงที่การเขียนสุดท้ายจะล้มเหลว
- การเขียนเพื่อให้แน่ใจว่าข้อมูลถูกบันทึกในขณะที่กำลังประมวลผลการซื้อ
การจัดเก็บที่มีประสิทธิภาพ: การจัดเก็บข้อมูลเซสชันทั้งหมดของผู้เล่นในตารางเดียวช่วยให้คุณสามารถอัปเดตค่าหลายค่าได้อย่างอะตอมิกและจัดการข้อมูลในปริมาณที่น้อยลงในคำขอเดียว นอกจากนี้ยังช่วยลดความเสี่ยงของการไม่ซิงโครไนซ์ระหว่างค่าและทำให้การย้อนกลับทำได้ง่ายขึ้น
นักพัฒนาบางคนยังใช้การจัดเรียงแบบกำหนดเองเพื่อบีบอัดโครงสร้างข้อมูลขนาดใหญ่ (โดยทั่วไปเพื่อบันทึกเนื้อหาที่ผู้ใช้สร้างขึ้นในเกม)
การทำซ้ำ: ไคลเอนต์ต้องเข้าถึงข้อมูลของผู้เล่นเป็นประจำ (เช่น เพื่ออัปเดต UI) วิธีการทั่วไปในการทำซ้ำข้อมูลผู้เล่นไปยังไคลเอนต์ช่วยให้คุณสามารถส่งข้อมูลนี้ได้โดยไม่ต้องสร้างระบบการทำซ้ำที่กำหนดเองสำหรับแต่ละส่วนประกอบของข้อมูล นักพัฒนามักต้องการตัวเลือกในการเลือกสิ่งที่ทำซ้ำและสิ่งที่ไม่ทำซ้ำไปยังไคลเอนต์
การจัดการข้อผิดพลาด: เมื่อไม่สามารถเข้าถึง DataStores ได้ โซลูชันส่วนใหญ่จะใช้กลไกการลองใหม่และการสำรองข้อมูลไปยังข้อมูล 'เริ่มต้น' ต้องระมัดระวังเป็นพิเศษเพื่อให้แน่ใจว่าข้อมูลสำรองจะไม่เขียนทับข้อมูล 'จริง' ในภายหลัง และต้องสื่อสารกับผู้เล่นอย่างเหมาะสม
การลองใหม่: เมื่อไม่สามารถเข้าถึง data stores ได้ โซลูชันส่วนใหญ่จะใช้กลไกการลองใหม่และการสำรองข้อมูลไปยังข้อมูลเริ่มต้น ต้องระมัดระวังเป็นพิเศษเพื่อให้แน่ใจว่าข้อมูลสำรองจะไม่เขียนทับข้อมูล "จริง" ในภายหลัง และสื่อสารสถานการณ์กับผู้เล่นอย่างเหมาะสม
การล็อกเซสชัน: หากข้อมูลของผู้เล่นคนเดียวถูกโหลดและอยู่ในหน่วยความจำบนเซิร์ฟเวอร์หลายเครื่อง อาจเกิดปัญหาที่เซิร์ฟเวอร์หนึ่งบันทึกข้อมูลที่ล้าสมัย สิ่งนี้อาจนำไปสู่การสูญเสียข้อมูลและช่องโหว่ในการทำซ้ำไอเท็มทั่วไป
การจัดการการซื้ออย่างอะตอมิก: ตรวจสอบ มอบรางวัล และบันทึกการซื้ออย่างอะตอมิกเพื่อป้องกันไม่ให้ไอเท็มสูญหายหรือมอบรางวัลหลายครั้ง
ตัวอย่างโค้ด
Roblox มีโค้ดอ้างอิงเพื่อช่วยคุณในการออกแบบและสร้างระบบข้อมูลผู้เล่น เนื้อหาที่เหลือของหน้านี้จะตรวจสอบพื้นหลัง รายละเอียดการนำไปใช้ และข้อควรระวังทั่วไป
หลังจากที่คุณนำเข้ารูปแบบไปยัง Studio คุณควรเห็นโครงสร้างโฟลเดอร์ดังต่อไปนี้:

สถาปัตยกรรม
แผนภาพระดับสูงนี้แสดงให้เห็นถึงระบบหลักในตัวอย่างและวิธีที่พวกเขาเชื่อมต่อกับโค้ดในเกมที่เหลือ

การลองใหม่
Class: DataStoreWrapper
พื้นหลัง
เนื่องจาก DataStoreService ทำการร้องขอเว็บภายใต้ฝากระโปรง คำขอของมันจึงไม่รับประกันว่าจะสำเร็จ เมื่อเกิดเหตุการณ์นี้ วิธีการ DataStore จะโยนข้อผิดพลาด ทำให้คุณสามารถจัดการกับมันได้
"ข้อผิดพลาด" ที่พบบ่อยอาจเกิดขึ้นหากคุณพยายามจัดการกับความล้มเหลวของ data store ดังนี้:
local MAX_ATTEMPTS = 5
local BASE_DELAY = 2
local MAX_DELAY = 32
local function retrySetAsync(dataStore, key, value)
for attempt = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
return result
end
if attempt < MAX_ATTEMPTS then
local backoff = math.min(MAX_DELAY, BASE_DELAY * (2 ^ (attempt - 1)))
local jitter = math.random() * backoff
task.wait(math.min(MAX_DELAY, backoff + jitter))
end
end
endลองใหม่สำหรับความล้มเหลวชั่วคราวด้วยการหน่วงเวลาแบบเอ็กซ์โพเนนเชียลและการสั่นแบบสุ่มเพื่อให้เซิร์ฟเวอร์ไม่ลองใหม่พร้อมกัน จำกัดการหน่วงเวลาและจำนวนความพยายาม
แม้จะมีรูปแบบการหน่วงเวลานั้น กลไกการลองใหม่นี้ไม่เหมาะสำหรับคำขอ DataStoreService เนื่องจากมันไม่รับประกันลำดับที่คำขอจะถูกทำ การรักษาลำดับของคำขอเป็นสิ่งสำคัญสำหรับคำขอ DataStoreService เนื่องจากพวกเขามีปฏิสัมพันธ์กับสถานะ พิจารณาสถานการณ์ต่อไปนี้:
- คำขอ A ถูกส่งเพื่อกำหนดค่าของคีย์ K เป็น 1
- คำขอล้มเหลว ดังนั้นการลองใหม่จึงถูกกำหนดให้ทำงานหลังจากการหน่วงเวลา
- ก่อนที่การลองใหม่จะเกิดขึ้น คำขอ B ตั้งค่าของ K เป็น 2 แต่การลองใหม่ของคำขอ A จะเขียนทับค่าดังกล่าวทันทีและตั้งค่า K เป็น 1
แม้ว่า UpdateAsync จะทำงานกับเวอร์ชันล่าสุดของค่าของคีย์ แต่คำขอ UpdateAsync ยังคงต้องได้รับการประมวลผลตามลำดับเพื่อหลีกเลี่ยงสถานะชั่วคราวที่ไม่ถูกต้อง (เช่น การซื้อหักเหรียญก่อนที่การเพิ่มเหรียญจะถูกประมวลผล ส่งผลให้มีเหรียญติดลบ)
ระบบข้อมูลผู้เล่นของเราใช้คลาสใหม่ DataStoreWrapper ซึ่งให้การลองใหม่ที่รับประกันว่าจะถูกประมวลผลตามลำดับต่อคีย์
แนวทาง

DataStoreWrapper มีวิธีการที่สอดคล้องกับวิธีการ DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() และ DataStore:RemoveAsync().
เมื่อเรียกใช้วิธีการเหล่านี้:
เพิ่มคำขอไปยังคิว คีย์แต่ละคีย์มีคิวของตัวเอง ซึ่งคำขอจะถูกประมวลผลตามลำดับและเป็นลำดับ เธรดที่ร้องขอจะหยุดจนกว่าคำขอจะเสร็จสิ้น
ฟังก์ชันนี้อิงจากคลาส ThreadQueue ซึ่งเป็นตัวจัดกำหนดงานที่ใช้ coroutine และตัวจำกัดอัตรา แทนที่จะส่งคืนคำสัญญา ThreadQueue จะหยุดเธรดปัจจุบันจนกว่าการดำเนินการจะเสร็จสิ้นและโยนข้อผิดพลาดหากล้มเหลว สิ่งนี้สอดคล้องกับรูปแบบอะซิงโครนัส Luau ที่เป็นอัตลักษณ์มากขึ้น
หากคำขอล้มเหลว จะมีการลองใหม่ด้วยการหน่วงเวลาแบบเอ็กซ์โพเนนเชียลที่กำหนดค่าได้ การลองใหม่เหล่านี้เป็นส่วนหนึ่งของการเรียกกลับที่ส่งไปยัง ThreadQueue ดังนั้นจึงรับประกันว่าจะเสร็จสิ้นก่อนที่คำขอถัดไปในคิวสำหรับคีย์นี้จะเริ่มต้น
เมื่อคำขอเสร็จสิ้น วิธีการคำขอจะส่งคืนด้วยรูปแบบ success, result
DataStoreWrapper ยังเปิดเผยวิธีการเพื่อรับความยาวของคิวสำหรับคีย์ที่กำหนดและล้างคำขอที่ล้าสมัย ตัวเลือกหลังนี้มีประโยชน์โดยเฉพาะในสถานการณ์เมื่อเซิร์ฟเวอร์กำลังปิดตัวลงและไม่มีเวลาในการประมวลผลคำขอใด ๆ นอกจากคำขอที่ล่าสุดที่สุด
ข้อควรระวัง
DataStoreWrapper ปฏิบัติตามหลักการที่ว่า นอกเหนือจากสถานการณ์ที่รุนแรง ทุกคำขอ data store ควรได้รับอนุญาตให้เสร็จสิ้น (สำเร็จหรือไม่ก็ตาม) แม้ว่าคำขอที่ใหม่กว่าจะทำให้มันไม่จำเป็น เมื่อมีการร้องขอใหม่ คำขอที่ล้าสมัยจะไม่ถูกลบออกจากคิว แต่จะได้รับอนุญาตให้เสร็จสิ้นก่อนที่คำขอใหม่จะเริ่มต้น เหตุผลสำหรับเรื่องนี้มีรากฐานมาจากการที่โมดูลนี้สามารถนำไปใช้เป็นยูทิลิตี้ data store ทั่วไปแทนที่จะเป็นเครื่องมือเฉพาะสำหรับข้อมูลผู้เล่น และมีดังนี้:
เป็นเรื่องยากที่จะตัดสินชุดกฎที่เข้าใจได้ว่าเมื่อใดคำขอจึงจะปลอดภัยที่จะลบออกจากคิว พิจารณาคิวต่อไปนี้:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
พฤติกรรมที่คาดหวังคือ GetAsync() จะส่งคืน 1 แต่หากเราลบคำขอ SetAsync() ออกจากคิวเนื่องจากมันถูกทำให้ไม่จำเป็นโดยคำขอล่าสุด มันจะส่งคืน 0
ความก้าวหน้าทางตรรกะคือเมื่อมีการเพิ่มคำขอเขียนใหม่ จะต้องตัดคำขอที่ล้าสมัยออกเท่าที่จะทำได้จากคำขออ่านล่าสุด UpdateAsync ซึ่งเป็นการดำเนินการที่พบบ่อยที่สุด (และเป็นเพียงการดำเนินการเดียวที่ใช้โดยระบบนี้) สามารถอ่านและเขียนได้ทั้งคู่ ดังนั้นจึงยากที่จะปรับให้เข้ากับการออกแบบนี้โดยไม่เพิ่มความซับซ้อนเพิ่มเติม
DataStoreWrapper อาจต้องการให้คุณระบุว่าคำขอ UpdateAsync() ได้รับอนุญาตให้อ่านและ/หรือเขียนหรือไม่ แต่จะไม่มีความเกี่ยวข้องกับระบบข้อมูลผู้เล่นของเรา ซึ่งไม่สามารถกำหนดได้ล่วงหน้าเนื่องจากกลไกการล็อกเซสชัน (จะกล่าวถึงในรายละเอียดเพิ่มเติมในภายหลัง)
เมื่อถูกลบออกจากคิว จะยากที่จะตัดสินกฎที่เข้าใจได้ว่า อย่างไร ควรจัดการกับสิ่งนี้ เมื่อมีการร้องขอ DataStoreWrapper เธรดปัจจุบันจะถูกหยุดจนกว่าจะเสร็จสิ้น หากเราลบคำขอที่ล้าสมัยออกจากคิว เราจะต้องตัดสินใจว่าจะส่งคืน false, "Removed from queue" หรือไม่ส่งคืนและทิ้งเธรดที่ใช้งานอยู่ ทั้งสองวิธีมีข้อเสียของตนเองและเพิ่มความซับซ้อนเพิ่มเติมให้กับผู้บริโภค
ท้ายที่สุด มุมมองของเราคือวิธีการที่เรียบง่าย (การประมวลผลคำขอทุกคำขอ) เป็นสิ่งที่ดีกว่าในที่นี้และสร้างสภาพแวดล้อมที่ชัดเจนในการนำทางเมื่อเผชิญกับปัญหาที่ซับซ้อนเช่นการล็อกเซสชัน ข้อยกเว้นเพียงอย่างเดียวสำหรับเรื่องนี้คือในระหว่าง DataModel:BindToClose(), ซึ่งการล้างคิวจะกลายเป็นสิ่งจำเป็นเพื่อบันทึกข้อมูลของผู้ใช้ทั้งหมดในเวลาและค่าที่การเรียกฟังก์ชันแต่ละรายการส่งคืนจะไม่เป็นปัญหาที่กำลังดำเนินอยู่ เพื่อให้สอดคล้องกับเรื่องนี้ เราจึงเปิดเผยวิธีการ skipAllQueuesToLastEnqueued สำหรับข้อมูลเพิ่มเติม โปรดดูที่ ข้อมูลผู้เล่น.
การล็อกเซสชัน
Class: SessionLockedDataStoreWrapper
พื้นหลัง
ข้อมูลผู้เล่นถูกเก็บในหน่วยความจำบนเซิร์ฟเวอร์และจะถูกอ่านและเขียนไปยัง data stores ที่อยู่เบื้องหลังเมื่อจำเป็น คุณสามารถอ่านและอัปเดตข้อมูลผู้เล่นในหน่วยความจำได้ทันทีโดยไม่ต้องร้องขอเว็บและหลีกเลี่ยงการเกินขีดจำกัดของ DataStoreService
เพื่อให้โมเดลนี้ทำงานได้ตามที่ตั้งใจไว้ สิ่งสำคัญคือไม่ให้เซิร์ฟเวอร์มากกว่าหนึ่งเครื่องสามารถโหลดข้อมูลของผู้เล่นคนเดียวกันเข้าสู่หน่วยความจำจาก DataStore ได้ในเวลาเดียวกัน
ตัวอย่างเช่น หากเซิร์ฟเวอร์ A โหลดข้อมูลของผู้เล่น เซิร์ฟเวอร์ B จะไม่สามารถโหลดข้อมูลนั้นได้จนกว่าเซิร์ฟเวอร์ A จะปล่อยการล็อกในระหว่างการบันทึกสุดท้าย หากไม่มีกลไกการล็อก เซิร์ฟเวอร์ B อาจโหลดข้อมูลผู้เล่นที่ล้าสมัยจาก data store ก่อนที่เซิร์ฟเวอร์ A จะมีโอกาสบันทึกเวอร์ชันที่ใหม่กว่าที่มีอยู่ในหน่วยความจำ จากนั้นหากเซิร์ฟเวอร์ A บันทึกข้อมูลที่ใหม่กว่าหลังจากเซิร์ฟเวอร์ B โหลดข้อมูลที่ล้าสมัย เซิร์ฟเวอร์ B จะเขียนทับข้อมูลที่ใหม่กว่านั้นในระหว่างการบันทึกครั้งถัดไป
แม้ว่า Roblox จะอนุญาตให้ไคลเอนต์เชื่อมต่อกับเซิร์ฟเวอร์เพียงเครื่องเดียวในเวลาเดียวกัน แต่คุณไม่สามารถสมมติได้ว่าข้อมูลจากเซสชันหนึ่งจะถูกบันทึกเสมอก่อนที่เซสชันถัดไปจะเริ่มต้น พิจารณาสถานการณ์ต่อไปนี้ที่อาจเกิดขึ้นเมื่อผู้เล่นออกจากเซิร์ฟเวอร์ A:
- เซิร์ฟเวอร์ A ทำการร้องขอ DataStore เพื่อบันทึกข้อมูลของพวกเขา แต่คำขอล้มเหลวและต้องการการลองใหม่หลายครั้งเพื่อให้เสร็จสิ้น ในระหว่างช่วงการลองใหม่ ผู้เล่นเข้าร่วมเซิร์ฟเวอร์ B
- เซิร์ฟเวอร์ A ทำการเรียก UpdateAsync() มากเกินไปต่อคีย์เดียวกันและถูกจำกัด คำขอการบันทึกสุดท้ายถูกวางในคิว ขณะที่คำขออยู่ในคิว ผู้เล่นเข้าร่วมเซิร์ฟเวอร์ B
- บนเซิร์ฟเวอร์ A โค้ดบางส่วนที่เชื่อมต่อกับเหตุการณ์ PlayerRemoving จะหยุดก่อนที่ข้อมูลของผู้เล่นจะถูกบันทึก ก่อนที่การดำเนินการนี้จะเสร็จสิ้น ผู้เล่นเข้าร่วมเซิร์ฟเวอร์ B
- ประสิทธิภาพของเซิร์ฟเวอร์ A ลดลงจนการบันทึกสุดท้ายล่าช้าจนกว่าผู้เล่นจะเข้าร่วมเซิร์ฟเวอร์ B
สถานการณ์เหล่านี้ควรเกิดขึ้นได้ยาก แต่ก็เกิดขึ้น โดยเฉพาะในสถานการณ์ที่ผู้เล่นตัดการเชื่อมต่อจากเซิร์ฟเวอร์หนึ่งและเชื่อมต่อกับอีกเซิร์ฟเวอร์หนึ่งอย่างรวดเร็ว (เช่น ขณะกำลังโทรไปยังเซิร์ฟเวอร์อื่น) ผู้ใช้ที่ไม่หวังดีบางคนอาจพยายามใช้ประโยชน์จากพฤติกรรมนี้เพื่อทำการกระทำโดยไม่ให้ข้อมูลคงอยู่ สิ่งนี้อาจส่งผลกระทบอย่างมากในเกมที่อนุญาตให้ผู้เล่นทำการซื้อขายและเป็นแหล่งที่มาของการใช้ประโยชน์จากการทำซ้ำไอเท็มทั่วไป
การล็อกเซสชันจัดการกับช่องโหว่นี้โดยการรับประกันว่าเมื่อคีย์ DataStore ของผู้เล่นถูกอ่านครั้งแรกโดยเซิร์ฟเวอร์ เซิร์ฟเวอร์จะเขียนล็อกไปยังข้อมูลเมตาของคีย์ในระหว่างการเรียก UpdateAsync() เดียวกัน หากค่าล็อกนี้มีอยู่เมื่อเซิร์ฟเวอร์อื่นพยายามอ่านหรือเขียนคีย์ เซิร์ฟเวอร์จะไม่ดำเนินการต่อ
แนวทาง

SessionLockedDataStoreWrapper เป็นเมต้า-Wrapper รอบคลาส DataStoreWrapper ซึ่ง DataStoreWrapper ให้ฟังก์ชันการจัดคิวและการลองใหม่ ซึ่ง SessionLockedDataStoreWrapper จะเสริมด้วยการล็อกเซสชัน
SessionLockedDataStoreWrapper จะส่งคำขอ DataStore ทุกคำขอ—ไม่ว่าจะเป็น GetAsync, SetAsync หรือ UpdateAsync—ผ่าน UpdateAsync เนื่องจาก UpdateAsync อนุญาตให้คีย์ถูกอ่านและเขียนได้อย่างอะตอมิก นอกจากนี้ยังสามารถละทิ้งการเขียนตามค่าที่อ่านได้โดยการส่งคืน nil ในฟังก์ชันการแปลง
ฟังก์ชันการแปลงที่ส่งไปยัง UpdateAsync สำหรับแต่ละคำขอจะดำเนินการดังต่อไปนี้:
ตรวจสอบว่าคีย์ปลอดภัยต่อการเข้าถึง โดยละทิ้งการดำเนินการหากไม่ปลอดภัย "ปลอดภัยต่อการเข้าถึง" หมายถึง:
วัตถุข้อมูลเมตาของคีย์ไม่มีค่า LockId ที่ไม่รู้จักซึ่งได้รับการอัปเดตล่าสุดน้อยกว่าช่วงเวลาหมดอายุของล็อก ซึ่งจะช่วยให้เคารพล็อกที่วางโดยเซิร์ฟเวอร์อื่นและเพิกเฉยต่อล็อกนั้นหากหมดอายุ
หากเซิร์ฟเวอร์นี้ได้วางค่า LockId ของตนเองในข้อมูลเมตาของคีย์ก่อนหน้านี้ ค่านั้นยังคงอยู่ในข้อมูลเมตาของคีย์ ซึ่งจะช่วยให้สามารถจัดการกับสถานการณ์ที่เซิร์ฟเวอร์อื่นได้เข้ายึดล็อกของเซิร์ฟเวอร์นี้ (โดยการหมดอายุหรือโดยการบังคับ) และปล่อยล็อกในภายหลัง กล่าวอีกนัยหนึ่ง แม้ว่าค่า LockId จะเป็น nil เซิร์ฟเวอร์อื่นอาจยังคงแทนที่และลบล็อกในช่วงเวลาที่คุณล็อกคีย์
UpdateAsync จะดำเนินการตามการดำเนินการ DataStore ที่ผู้บริโภคของ SessionLockedDataStoreWrapper ขอ ตัวอย่างเช่น GetAsync() จะแปลเป็น function(value) return value end.
ขึ้นอยู่กับพารามิเตอร์ที่ส่งไปยังคำขอ UpdateAsync จะล็อกหรือปลดล็อกคีย์:
หากคีย์จะถูกล็อก UpdateAsync จะตั้งค่า LockId ในข้อมูลเมตาของคีย์เป็น GUID GUID นี้จะถูกเก็บในหน่วยความจำบนเซิร์ฟเวอร์เพื่อให้สามารถตรวจสอบได้ในครั้งถัดไปที่เข้าถึงคีย์ หากเซิร์ฟเวอร์มีล็อกอยู่แล้วในคีย์นี้ จะไม่มีการเปลี่ยนแปลงใด ๆ นอกจากนี้ยังมีการกำหนดงานเพื่อเตือนคุณหากคุณไม่เข้าถึงคีย์อีกครั้งเพื่อรักษาล็อกภายในช่วงเวลาหมดอายุของล็อก
หากคีย์จะถูกปลดล็อก UpdateAsync จะลบ LockId ในข้อมูลเมตาของคีย์
มีการส่งตัวจัดการการลองใหม่ที่กำหนดเองไปยัง DataStoreWrapper ที่อยู่เบื้องหลังเพื่อให้การดำเนินการถูกลองใหม่หากถูกยกเลิกในขั้นตอนที่ 1 เนื่องจากเซสชันถูกล็อก
ข้อความข้อผิดพลาดที่กำหนดเองยังถูกส่งคืนไปยังผู้บริโภค ทำให้ระบบข้อมูลผู้เล่นสามารถรายงานข้อผิดพลาดทางเลือกในกรณีที่การล็อกเซสชันไปยังไคลเอนต์
ข้อควรระวัง
ระบอบการล็อกเซสชันขึ้นอยู่กับเซิร์ฟเวอร์ที่ปล่อยล็อกของมันบนคีย์เมื่อเสร็จสิ้นการใช้งาน ควรเกิดขึ้นเสมอผ่านคำสั่งในการปลดล็อกคีย์เป็นส่วนหนึ่งของการเขียนสุดท้ายใน PlayerRemoving หรือ BindToClose()
อย่างไรก็ตาม การปลดล็อกอาจล้มเหลวในบางสถานการณ์ ตัวอย่างเช่น:
- เซิร์ฟเวอร์ล่มหรือ DataStoreService ไม่สามารถใช้งานได้สำหรับการเข้าถึงคีย์ทั้งหมด
- เนื่องจากข้อผิดพลาดในตรรกะหรือข้อบกพร่องที่คล้ายกัน คำสั่งในการปลดล็อกคีย์ไม่ได้ถูกสร้างขึ้น
เพื่อรักษาล็อกบนคีย์ คุณต้องเข้าถึงมันเป็นประจำตราบเท่าที่มันถูกโหลดในหน่วยความจำ โดยปกติจะทำเป็นส่วนหนึ่งของลูปการบันทึกอัตโนมัติที่ทำงานอยู่เบื้องหลังในระบบข้อมูลผู้เล่นส่วนใหญ่ แต่ระบบนี้ยังเปิดเผยวิธีการ refreshLockAsync หากคุณต้องการทำด้วยตนเอง
หากเวลาหมดอายุของล็อกถูกเกินโดยไม่อัปเดตล็อก เซิร์ฟเวอร์ใด ๆ ก็สามารถเข้ายึดล็อกได้ หากเซิร์ฟเวอร์อื่นเข้ายึดล็อก ความพยายามของเซิร์ฟเวอร์ปัจจุบันในการอ่านหรือเขียนคีย์จะล้มเหลว เว้นแต่จะสร้างล็อกใหม่
การประมวลผลผลิตภัณฑ์ของนักพัฒนา
Singleton: ReceiptHandler
พื้นหลัง
การเรียกกลับ ProcessReceipt ทำหน้าที่สำคัญในการกำหนดว่าเมื่อใดควรสรุปการซื้อ ProcessReceipt จะถูกเรียกในสถานการณ์ที่เฉพาะเจาะจงมาก สำหรับชุดการรับประกันของมัน โปรดดูที่ MarketplaceService.ProcessReceipt.
แม้ว่าความหมายของ "การจัดการ" การซื้ออาจแตกต่างกันไปในแต่ละเกม แต่เราจะใช้เกณฑ์ต่อไปนี้
การซื้อยังไม่ได้รับการจัดการก่อนหน้านี้
การซื้อสะท้อนอยู่ในเซสชันปัจจุบัน
สิ่งนี้ต้องดำเนินการตามขั้นตอนต่อไปนี้ก่อนที่จะส่งคืน PurchaseGranted:
- ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการจัดการ
- มอบรางวัลการซื้อในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
- บันทึก PurchaseId ว่าได้รับการจัดการในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
- เขียนข้อมูลผู้เล่นในหน่วยความจำของผู้เล่นไปยัง DataStore.
การล็อกเซสชันทำให้กระบวนการนี้ง่ายขึ้น เนื่องจากคุณไม่ต้องกังวลเกี่ยวกับสถานการณ์ต่อไปนี้:
- ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์ปัจจุบันอาจล้าสมัย ซึ่งต้องการให้คุณดึงค่าล่าสุดจาก DataStore ก่อนที่จะตรวจสอบประวัติ PurchaseId
- การเรียกกลับสำหรับการซื้อเดียวกันทำงานในเซิร์ฟเวอร์อื่น ซึ่งต้องการให้คุณอ่านและเขียนประวัติ PurchaseId และบันทึกข้อมูลผู้เล่นที่อัปเดตพร้อมการซื้ออย่างอะตอมิกเพื่อป้องกันการเกิดเงื่อนไขการแข่งขัน
การล็อกเซสชันรับประกันว่า หากความพยายามในการเขียนไปยัง DataStore ของผู้เล่นประสบความสำเร็จ จะไม่มีเซิร์ฟเวอร์อื่นที่อ่านหรือเขียนไปยัง DataStore ของผู้เล่นสำเร็จระหว่างการโหลดข้อมูลและการบันทึกในเซิร์ฟเวอร์นี้ สั้น ๆ ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เป็นเวอร์ชันที่ทันสมัยที่สุดที่มีอยู่ มีข้อควรระวังบางประการ แต่จะไม่ส่งผลกระทบต่อพฤติกรรมนี้
แนวทาง
ความคิดเห็นใน ReceiptProcessor อธิบายแนวทาง:
ตรวจสอบว่าข้อมูลของผู้เล่นถูกโหลดอยู่ในเซิร์ฟเวอร์นี้ในขณะนี้และโหลดโดยไม่มีข้อผิดพลาดใด ๆ
เนื่องจากระบบนี้ใช้การล็อกเซสชัน การตรวจสอบนี้ยังยืนยันว่าข้อมูลในหน่วยความจำเป็นเวอร์ชันที่ทันสมัยที่สุด
หากข้อมูลของผู้เล่นยังไม่ได้โหลด (ซึ่งคาดหวังเมื่อผู้เล่นเข้าร่วมเกม) ให้รอให้ข้อมูลของผู้เล่นโหลด ระบบยังฟังการออกจากเกมของผู้เล่นก่อนที่ข้อมูลของพวกเขาจะโหลด เนื่องจากไม่ควรหยุดนิ่งตลอดไปและบล็อกการเรียกกลับนี้จากการถูกเรียกอีกครั้งในเซิร์ฟเวอร์นี้สำหรับการซื้อหากผู้เล่นเข้าร่วมใหม่
ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการประมวลผลในข้อมูลผู้เล่น
เนื่องจากการล็อกเซสชัน อาร์เรย์ของ PurchaseIds ที่ระบบมีในหน่วยความจำเป็นเวอร์ชันที่ทันสมัยที่สุด หาก PurchaseId ถูกบันทึกว่าได้รับการประมวลผลและสะท้อนในค่าที่ถูกโหลดหรือบันทึกไปยัง DataStore ให้ส่งคืน PurchaseGranted หากถูกบันทึกว่าได้รับการประมวลผล แต่ ไม่ สะท้อนใน DataStore ให้ส่งคืน NotProcessedYet.
อัปเดตข้อมูลผู้เล่นในท้องถิ่นในเซิร์ฟเวอร์นี้เพื่อ "มอบรางวัล" การซื้อ
ReceiptProcessor ใช้แนวทางการเรียกกลับทั่วไปและกำหนดการเรียกกลับที่แตกต่างกันสำหรับแต่ละ DeveloperProductId.
อัปเดตข้อมูลผู้เล่นในท้องถิ่นในเซิร์ฟเวอร์นี้เพื่อจัดเก็บ PurchaseId.
ส่งคำขอเพื่อบันทึกข้อมูลในหน่วยความจำไปยัง DataStore โดยส่งคืน PurchaseGranted หากคำขอนั้นสำเร็จ หากไม่สำเร็จ ให้ส่งคืน NotProcessedYet.
หากคำขอการบันทึกนี้ไม่สำเร็จ คำขอในภายหลังเพื่อบันทึกข้อมูลเซสชันในหน่วยความจำของผู้เล่นอาจยังคงสำเร็จ ในระหว่างการเรียก ProcessReceipt ครั้งถัดไป ขั้นตอนที่ 2 จัดการกับสถานการณ์นี้และส่งคืน PurchaseGranted.
ข้อมูลผู้เล่น
Singletons: PlayerData.Server, PlayerData.Client
พื้นหลัง
โมดูลที่ให้ส่วนติดต่อสำหรับโค้ดในการอ่านและเขียนข้อมูลเซสชันของผู้เล่นแบบซิงโครนัสเป็นเรื่องปกติในเกม Roblox ส่วนนี้ครอบคลุม PlayerData.Server และ PlayerData.Client.
แนวทาง
PlayerData.Server และ PlayerData.Client จัดการดังต่อไปนี้:
- โหลดข้อมูลของผู้เล่นเข้าสู่หน่วยความจำ รวมถึงการจัดการกรณีที่ล้มเหลวในการโหลด
- ให้ส่วนติดต่อสำหรับโค้ดเซิร์ฟเวอร์ในการสอบถามและเปลี่ยนแปลงข้อมูลผู้เล่น
- ทำซ้ำการเปลี่ยนแปลงในข้อมูลของผู้เล่นไปยังไคลเอนต์เพื่อให้โค้ดของไคลเอนต์สามารถเข้าถึงได้
- ทำซ้ำข้อผิดพลาดในการโหลดและ/หรือบันทึกไปยังไคลเอนต์เพื่อให้สามารถแสดงกล่องโต้ตอบข้อผิดพลาดได้
- บันทึกข้อมูลของผู้เล่นเป็นระยะ ๆ เมื่อผู้เล่นออกจากเกม และเมื่อเซิร์ฟเวอร์ปิดตัวลง
โหลดข้อมูลผู้เล่น

SessionLockedDataStoreWrapper ทำการร้องขอ getAsync ไปยัง data store
หากคำขอนี้ล้มเหลว ข้อมูลเริ่มต้นจะถูกใช้และโปรไฟล์จะถูกทำเครื่องหมายว่า "มีข้อผิดพลาด" เพื่อให้แน่ใจว่าจะไม่ถูกเขียนไปยัง data store ในภายหลัง
ตัวเลือกทางเลือกคือการเตะผู้เล่น แต่เราขอแนะนำให้ปล่อยให้ผู้เล่นเล่นด้วยข้อมูลเริ่มต้นและมีการสื่อสารที่ชัดเจนเกี่ยวกับสิ่งที่เกิดขึ้นแทนที่จะนำพวกเขาออกจากเกม
ส่งข้อมูลเริ่มต้นไปยัง PlayerDataClient ซึ่งประกอบด้วยข้อมูลที่โหลดและสถานะข้อผิดพลาด (ถ้ามี)
เธรดใด ๆ ที่หยุดอยู่โดยใช้ waitForDataLoadAsync สำหรับผู้เล่นจะถูกดำเนินการต่อ
ให้ส่วนติดต่อสำหรับโค้ดเซิร์ฟเวอร์
- PlayerDataServer เป็น singleton ที่สามารถถูกเรียกใช้และเข้าถึงได้โดยโค้ดเซิร์ฟเวอร์ใด ๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน
- ข้อมูลผู้เล่นจะถูกจัดระเบียบในพจนานุกรมของคีย์และค่า คุณสามารถจัดการค่าต่าง ๆ เหล่านี้บนเซิร์ฟเวอร์โดยใช้วิธีการ setValue, getValue, updateValue และ removeValue วิธีการเหล่านี้ทั้งหมดทำงานแบบซิงโครนัสโดยไม่ต้องหยุด
- วิธีการ hasLoaded และ waitForDataLoadAsync มีให้เพื่อให้แน่ใจว่าข้อมูลได้โหลดก่อนที่คุณจะเข้าถึงมัน เราขอแนะนำให้ทำเช่นนี้ครั้งเดียวในระหว่างหน้าจอการโหลดก่อนที่ระบบอื่น ๆ จะเริ่มต้นเพื่อหลีกเลี่ยงการตรวจสอบข้อผิดพลาดในการโหลดก่อนการโต้ตอบกับข้อมูลทุกครั้งบนไคลเอนต์
- วิธีการ hasErrored สามารถสอบถามได้ว่าการโหลดเริ่มต้นของผู้เล่นล้มเหลวหรือไม่ ทำให้พวกเขาใช้ข้อมูลเริ่มต้น ตรวจสอบวิธีนี้ก่อนที่จะอนุญาตให้ผู้เล่นทำการซื้อ เนื่องจากไม่สามารถบันทึกการซื้อไปยังข้อมูลได้หากการโหลดไม่สำเร็จ
- สัญญาณ playerDataUpdated จะถูกส่งออกพร้อมกับ player, key, และ value ทุกครั้งที่ข้อมูลของผู้เล่นมีการเปลี่ยนแปลง ระบบแต่ละระบบสามารถสมัครรับข้อมูลนี้ได้
ทำซ้ำการเปลี่ยนแปลงไปยังไคลเอนต์
- การเปลี่ยนแปลงใด ๆ ในข้อมูลผู้เล่นใน PlayerDataServer จะถูกทำซ้ำไปยัง PlayerDataClient เว้นแต่คีย์นั้นจะถูกทำเครื่องหมายว่าเป็นส่วนตัวโดยใช้ setValueAsPrivate
- setValueAsPrivate ใช้เพื่อระบุคีย์ที่ไม่ควรส่งไปยังไคลเอนต์
- PlayerDataClient รวมถึงวิธีการในการรับค่าของคีย์ (get) และสัญญาณที่ถูกส่งเมื่อมีการอัปเดต (updated) นอกจากนี้ยังมีวิธีการ hasLoaded และสัญญาณ loaded เพื่อให้ไคลเอนต์สามารถรอให้ข้อมูลโหลดและทำซ้ำก่อนที่จะเริ่มระบบของตน
- PlayerDataClient เป็น singleton ที่สามารถถูกเรียกใช้และเข้าถึงได้โดยโค้ดไคลเอนต์ใด ๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน
ทำซ้ำข้อผิดพลาดไปยังไคลเอนต์
- สถานะข้อผิดพลาดที่พบเมื่อบันทึกหรือโหลดข้อมูลผู้เล่นจะถูกทำซ้ำไปยัง PlayerDataClient
- เข้าถึงข้อมูลนี้ได้ด้วยวิธีการ getLoadError และ getSaveError พร้อมกับสัญญาณ loaded และ saved
- มีข้อผิดพลาดสองประเภท: DataStoreError (คำขอ DataStoreService ล้มเหลว) และ SessionLocked (ดูที่ การล็อกเซสชัน)
- ใช้เหตุการณ์เหล่านี้เพื่อปิดการแจ้งเตือนการซื้อของไคลเอนต์และดำเนินการกล่องโต้ตอบเตือน ข้อความนี้แสดงตัวอย่างกล่องโต้ตอบ:

บันทึกข้อมูลผู้เล่น

เมื่อผู้เล่นออกจากเกม ระบบจะดำเนินการตามขั้นตอนต่อไปนี้:
- ตรวจสอบว่าปลอดภัยในการเขียนข้อมูลของผู้เล่นไปยัง data store หรือไม่ สถานการณ์ที่ไม่ปลอดภัยรวมถึงข้อมูลของผู้เล่นล้มเหลวในการโหลดหรือยังอยู่ระหว่างการโหลด
- ทำการร้องขอผ่าน SessionLockedDataStoreWrapper เพื่อเขียนค่าข้อมูลในหน่วยความจำปัจจุบันไปยัง data store และลบการล็อกเซสชันเมื่อเสร็จสิ้น
- ล้างข้อมูลของผู้เล่น (และตัวแปรอื่น ๆ เช่น ข้อมูลเมตาและสถานะข้อผิดพลาด) ออกจากหน่วยความจำของเซิร์ฟเวอร์
ในลูปเป็นระยะ ระบบจะเขียนข้อมูลของผู้เล่นแต่ละคนไปยัง data store (โดยมีเงื่อนไขว่าปลอดภัยในการบันทึก) ความซ้ำซ้อนนี้ช่วยลดการสูญเสียในกรณีที่เซิร์ฟเวอร์ล่มและยังจำเป็นต้องรักษาการล็อกเซสชัน
ตัวอย่างเริ่มต้นลูปที่ใช้ร่วมกันหนึ่งลูปหลังจาก AUTO_SAVE_INTERVAL วินาที (180 โดยค่าเริ่มต้น) และจากนั้นบันทึกผู้เล่นที่โหลดทุกคนในเวลาเดียวกัน ลูปนั้นไม่เลื่อนเซิร์ฟเวอร์หรือผู้เล่น ดังนั้นเซิร์ฟเวอร์ที่เริ่มในเวลาใกล้เคียงกันสามารถล้างข้อมูลได้พร้อมกัน
เลื่อนการบันทึกครั้งแรกของผู้เล่นแต่ละคนโดยระยะเวลาสุ่มภายในช่วงเวลาเพื่อให้เซิร์ฟเวอร์ที่ใช้งานอยู่ไม่เขียนพร้อมกัน:
local AUTO_SAVE_INTERVAL = 180local function startAutoSave(player)task.spawn(function()task.wait(math.random() * AUTO_SAVE_INTERVAL)while player.Parent doif canSave(player) thensavePlayerData(player)endtask.wait(AUTO_SAVE_INTERVAL)endend)endเมื่อได้รับคำขอให้ปิดเซิร์ฟเวอร์ จะเกิดเหตุการณ์ต่อไปนี้ใน BindToClose callback:
- จะมีการร้องขอให้บันทึกข้อมูลของผู้เล่นแต่ละคนในเซิร์ฟเวอร์ โดยทำตามกระบวนการที่ปกติจะดำเนินการเมื่อผู้เล่นออกจากเซิร์ฟเวอร์ คำขอเหล่านี้จะถูกทำในเวลาเดียวกัน เนื่องจาก BindToClose callbacks มีเวลาเพียง 30 วินาทีในการเสร็จสิ้น
- เพื่อเร่งการบันทึก คำขออื่น ๆ ทั้งหมดในคิวของแต่ละคีย์จะถูกล้างออกจาก DataStoreWrapper ที่อยู่เบื้องหลัง (ดูที่ การลองใหม่).
- การเรียกกลับจะไม่ส่งคืนจนกว่าคำขอทั้งหมดจะเสร็จสิ้น