ทำไมจำนวนเต็มขนาดใหญ่ถึงสูญเสียความแม่นยำ
สรุปสำคัญ: จำนวนเต็มที่เกิน 2^53-1 สูญเสียความแม่นยำใน JSON.parse snowflake ID, หมายเลขคำสั่งซื้อ และ bigint ในฐานข้อมูลล้วนเจอปัญหานี้ จึงควรเก็บเป็น string ตลอดทั้งเส้นทาง

สรุปสั้น ๆ: ให้ถือ snowflake ID, หมายเลขคำสั่งซื้อ และ primary key แบบ bigint เป็น string ตลอดการ serialize การส่งผ่าน และการ parse; ใช้ชนิดตัวเลขเฉพาะเมื่อพิสูจน์ได้ว่าฟิลด์อยู่ในช่วง 2^53-1 และบังคับด้วย constraint maximum ใน Schema
เพราะความเสียหายเกิดขึ้นระหว่างการ parse อย่างเงียบ ๆ
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 }
หลักที่หายไปแล้ว ไม่มีอะไรกู้คืนได้
2^53 มาจากไหน
JavaScript มีชนิดตัวเลขเดียว: IEEE 754 double precision ที่มี significand 53 บิต (โดยนัยหนึ่งบิต ชัดเจน 52 บิต) จำนวนเต็มที่แทนได้แม่นยำจึงหยุดที่ ±(2^53-1):
-9007199254740991 .. 9007199254740991
เหนือเส้นนั้น ค่าที่แทนได้ติดกันจะห่างกัน 2, 4, 8 9007199254740993 เป็นจำนวนคี่ และที่ขนาดนั้นค่าที่แทนได้ทุกตัวเป็นจำนวนคู่ มันจึงปัดไปเป็น 9007199254740992
ฟิลด์ที่โดนผลกระทบ
| ฟิลด์ | จำนวนหลัก | ความเสี่ยง |
|---|---|---|
| snowflake ID | 18–19 | สูง หลักท้ายเปลี่ยน |
| หมายเลขคำสั่งซื้อ / ธุรกรรม | 16–20 | สูง |
| primary key แบบ bigint ในฐานข้อมูล | 19 | สูง |
| เงินในหน่วยย่อย | — | ต่ำ |
| timestamp ระดับมิลลิวินาที | 13 | ปลอดภัย |
สิ่งที่เหมือนกัน: ค่าเหล่านี้เป็น identifier ไม่ใช่ปริมาณ identifier ถูกส่งผ่าน ไม่เคยถูกบวกหรือคูณ string จึงเป็นชนิดที่ถูกต้อง
วิธีแก้สี่แบบ
serialize เป็น string backend แปลงฟิลด์ bigint ก่อนเขียน response ง่ายและยั่งยืน; ให้ระบุในเอกสารว่าฟิลด์เหล่านี้เป็น string
ให้ serializer จัดการ ToStringSerializer ของ Jackson สำหรับ Long, tag json:",string" ของ Go แนวคิดเดียวกัน: ให้ค่าอยู่นอกเส้นทางตัวเลข
parse ด้วยการรองรับ BigInt json-bigint และไลบรารีที่คล้ายกัน โปรดทราบว่า BigInt ผสมกับการคำนวณด้วย Number ไม่ได้ และ JSON.stringify ปฏิเสธมัน
จำกัดช่วงค่า ถ้าฟิลด์ไม่สามารถเกิน 2^53 ได้จริง ให้เพิ่ม maximum ใน Schema เพื่อให้ข้อมูลนอกช่วงล้มเหลวตอน validation แทนที่จะล้มเหลวอย่างเงียบ ๆ ในเบราว์เซอร์
จะรู้ได้อย่างไรว่าคุณมี bug นี้
เอาค่า ID ที่รู้มา round-trip: serialize, ส่งผ่าน, parse, เทียบทีละอักขระ หรือตรวจ raw response body — ตัวเลข 19 หลักที่ไม่มีเครื่องหมายคำพูดและมาถึงโดยหลักท้ายผิด เกือบแน่นอนว่าเสียหายตอน parse
คำถามที่พบบ่อย
ทำไม JSON.parse เปลี่ยนหลักท้ายโดยไม่ error?
JavaScript มีชนิดตัวเลขเดียวคือ IEEE 754 double precision จำนวนเต็มที่เกิน 2^53-1 จึงลงได้เพียงค่าที่แทนได้ใกล้ที่สุด JSON.parse ถือว่านั่นเป็นการแปลงตัวเลขปกติ: มันไม่ throw และไม่เตือน
frontend จะเก็บจำนวนเต็มขนาดใหญ่ไว้ครบได้อย่างไร?
ใช้ไลบรารีอย่าง json-bigint เพราะ JSON.parse ทำไม่ได้ ต้นทุนคือ BigInt ผสมกับการคำนวณด้วย Number ไม่ได้ และ JSON.stringify ปฏิเสธมัน โค้ดรอบข้างจึงต้องเปลี่ยนด้วย
เอกสาร API ควรระบุว่าฟิลด์เหล่านี้เป็น string ไหม?
ควร การ serialize เป็น string เป็นวิธีที่ง่ายและยั่งยืนที่สุด แต่จะได้ผลก็ต่อเมื่อผู้ใช้ถือค่าเป็นข้อความ ชนิดของฟิลด์จึงต้องถูกเขียนลงใน API contract