ข้ามไปยังเนื้อหาหลัก

ค้นหาใน CooLWJ

选择搜索范围,输入关键词后按回车。

ภาษา

ยาวเกินไปก็ผิดด้วยหรือ?——การเดินทางแก้ปัญหาสุดพิสดารเมื่อ ID ชนิด Long ฝั่งแบ็กเอนด์สูญเสียความแม่นยำ

บทนำ

ในยุคที่ส่วนหน้าและส่วนหลังแยกจากกัน ชีวิตของเราเต็มไปด้วยโอกาสและความท้าทายนับไม่ถ้วน——รวมถึง Bug ที่โผล่มาอย่างกะทันหันจนทำให้แทบคลั่ง วันนี้เราจะพูดถึงปัญหาคลาสสิกที่ทำให้นักพัฒนานับไม่ถ้วนทั้งขำทั้งปวดหัว:แบ็กเอนด์ Long ID ประเภทยาวเกินไปทำให้ฟรอนต์เอนด์สูญเสียความแม่นยำของตัวเลข。พูดถึงปัญหานี้แล้ว ก็เรียกได้ว่าเป็น “ต้นตอแห่งความชั่วร้ายทั้งปวง” จริง ๆ ก็ใครใช้ให้ JavaScript สามารถจัดการได้อย่างปลอดภัยเฉพาะ Number.MAX_SAFE_INTEGER(ก็คือ 9007199254740991)ล่ะ?

หากคุณเคยขบคิดอย่างหนักกับปัญหานี้ ก็อย่ากังวลไป คุณไม่ได้เผชิญอยู่คนเดียว วันนี้เราจะไม่เพียงวิเคราะห์ “ปัญหาระดับศตวรรษ” นี้อย่างมีอารมณ์ขัน แต่ยังพาคุณแก้ไขถึงต้นตอ เพื่อให้โค้ดของคุณไม่ “สูญเสียความแม่นยำ” อีกต่อไป。

ที่มาของปัญหา:เพราะเหตุใด Long ยาวขนาดนี้เลยหรือ?

ก่อนอื่น เราต้องเริ่มจาก Long เริ่มจากชนิดข้อมูล。Long,ก็คือ Java ชนิดจำนวนเต็ม 64 บิตในนั้น สำหรับผู้ที่ชอบจัดการกับข้อมูลขนาดใหญ่、สำหรับ Java ที่จัดการตัวเลขขนาดใหญ่ ชนิดข้อมูลนี้แทบจะเป็นพรจากสวรรค์เลยทีเดียว ทว่าโลกฝั่งฟรอนต์เอนด์กลับค่อนข้าง “รู้น้อยเห็นน้อย” เพราะมันจัดการได้เพียงจำนวนเต็มที่มีขนาดไม่เกิน 53 บิต ใช่แล้ว คุณฟังไม่ผิด ในเรื่องนี้ JavaScript มันเหมือนปราชญ์เฒ่าที่ “ช้ากว่าครึ่งจังหวะ” เมื่อเจอตัวเลขที่ใหญ่ขึ้น ก็เริ่มขยับจุดทศนิยมและเลขชี้กำลัง สุดท้ายคายตัวเลขที่ทำให้คุณสิ้นหวังออกมา แล้วปัญหาการสูญเสียความแม่นยำก็เริ่มล่องลอยอยู่ในโปรเจกต์ของคุณราวกับผี。

การพบกันครั้งแรก:เรื่องราวของการสูญเสียความแม่นยำ

เรื่องนี้ต้องเริ่มจากรายงาน Bug ในวันหนึ่ง:“พี่นักพัฒนา ดูสิว่าทำไม ID นี้ถึงเปลี่ยนหน้าตาไป?นี่ไม่ใช่ที่ฉันฐานข้อมูลID ตัวนั้นที่อยู่ข้างในไง!”คุณขมวดคิ้วมองดู ใช่แล้ว,9223372036854775807 กลายเป็น 9223372036854776000,โธ่ ตัวเลขที่เกินมานี่ราวกับเล่นกลเลย ทั้งที่ค่าที่แบ็กเอนด์ส่งมาก็ถูกต้องแท้ ๆ!คุณเกาหัวพลางคิดว่า:“ต้องเป็นความผิดของฟรอนต์เอนด์แน่ ๆ!”

ความผิดอยู่ที่ฟรอนต์เอนด์หรือแบ็กเอนด์?——สืบหาต้นตอ

เอาเข้าจริง การโยนความผิดให้ฝั่งฟรอนต์เอนด์สำหรับปัญหานี้ก็ใช่ว่าจะไร้เหตุผลเสียทีเดียว มาดูกันที่ JavaScript “จุดอ่อน” ในการจัดการกับตัวเลข。JavaScript ของ Number ชนิดข้อมูลเป็นรูปแบบเลขทศนิยมลอยตัวความแม่นยำสองเท่าตามมาตรฐาน IEEE 754 ซึ่งแทนเลขฐานสองได้อย่างปลอดภัยเพียง 53 บิต หรือก็คือ Number.MAX_SAFE_INTEGER ค่า——9007199254740991。

กล่าวอีกอย่างคือ จำนวนเต็มที่อยู่นอกช่วงนี้,JavaScript ก็จะเริ่ม“ลดทอนความแม่นยำ” จู่ ๆ “ความจุสมอง”ของมันก็ไม่พอเสียแล้ว ดังนั้น ของคุณ Long ID ชนิดนี้ก็กลายเป็นเพียงชุดตัวเลขที่ดูไม่ค่อย“เซ็กซี่”ในสายตาของมัน ด้วยเหตุนี้ เวลาส่วนหน้าจัดการ ID ที่ยาวเกินเหตุเหล่านี้ จึงจำเป็นต้อง“เสียสละ”กันสักหน่อย ผลก็คือ ID ของคุณที่เดิมตรงตามต้นฉบับถูกตัดทอน กลายเป็นตัวเลขใหม่ที่“น่าสนใจ”。

จากตกตะลึงสู่ความสงบ:ค้นหาวิธีแก้ไข

เมื่อพบต้นตอของปัญหาแล้ว ก็ถึงคราวที่นักพัฒนาอย่างพวกเราจะแสดงฝีมือกัน ต่อไปผมจะพาคุณไปทำความเข้าใจวิธีแก้ปัญหาหลายแบบอย่างลึกซึ้ง พร้อมบอกข้อดีและข้อเสียของแต่ละแบบ เพราะท้ายที่สุดถนนทุกสายย่อมมุ่งสู่กรุงโรม

1. แปลงเป็นสตริงโดยตรง:เรียบง่าย ตรงไปตรงมา แต่ได้ผลดี

เมื่อต้องเผชิญกับปัญหาแบบนี้ วิธีแรกที่เรานึกถึงย่อมเป็นวิธีที่ง่ายและตรงไปตรงมาที่สุด:นำ Long ชนิดการแปลงข้อมูลให้เป็นสตริงก็ได้แล้วนี่! ในเมื่อ JavaScript เป็นแฟนตัวยงของเลขทศนิยมแบบลอยตัว ถ้าอย่างนั้นเราก็โยนปัญหากลับไปเลย บอกมันว่า:“แค่ถือว่านี่เป็นสตริงก็พอ ไม่ต้องกังวลว่ามันจะยาวแค่ไหน!”

จะทำเช่นนี้ได้อย่างไร?อันที่จริงง่ายมาก เพียงใช้ Jackson ที่ให้มา ToStringSerializer,เราสามารถนำ Long แปลงเป็นสตริง。

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

public class UserDto {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    // 其他字段
}


เมื่อเป็นเช่นนี้ เมื่อแบ็กเอนด์ส่งกลับ UserDto ในขณะที่,Long ชนิด id จะถูกซีเรียลไลซ์เป็นสตริง สิ่งที่ฟรอนต์เอนด์ได้รับจึงเป็นสตริงเช่นกัน ปัญหาความแม่นยำก็แก้ได้ทันที ราวกับแปะกระดาษโน้ตไว้บนหัว JavaScript ว่า:“นี่เป็นสตริงนะ ไม่ต้องเป็นห่วง!”

2. การใช้งานฝั่งฟรอนต์เอนด์ BigInt:ช่วยให้คำนวณจำนวนขนาดใหญ่ได้อย่างแม่นยำด้วย

แน่นอนว่าวิธีที่เรียบง่ายและตรงไปตรงมาไม่ได้เหมาะกับทุกสถานการณ์เสมอไป ลองนึกดูว่าหากฟรอนต์เอนด์ต้องนำ ID นี้ไปคำนวณทางคณิตศาสตร์บางอย่าง การแปลงเป็นสตริงโดยตรงก็คงไม่เหมาะนัก แล้วฟรอนต์เอนด์ควรจัดการกับ ID ที่ “ยาวเป็นพิเศษ” เหล่านี้อย่างไร?

โชคดีที่,JavaScript ก็ใช่ว่าจะไม่มีข้อดีเลย มีการนำ BigInt หลังจากนั้น,JavaScript ในที่สุดก็ไม่ใช่คนทึ่มที่ทำได้แค่ขยับจุดทศนิยมอีกต่อไป。BigInt เป็นชนิดข้อมูลพื้นฐานชนิดใหม่ที่ออกแบบมาเพื่อจัดการจำนวนเต็มที่มีความแม่นยำได้ไม่จำกัดโดยเฉพาะ คุณทำได้ดังนี้:

const id = BigInt("9223372036854775807");
console.log(id + 1n); // 输出:9223372036854775808n

ด้วยวิธีนี้ คุณก็สามารถจัดการที่ฝั่งฟรอนต์เอนด์ได้อย่างแม่นยำ Long ข้อมูลชนิดนี้ เพื่อหลีกเลี่ยงปัญหาการสูญเสียความแม่นยำ แน่นอนว่า มีข้อเตือนใจเล็กน้อยตรงนี้:BigInt ไม่ใช่ทุกเบราว์เซอร์จะรองรับ ดังนั้นคุณต้องแน่ใจว่าสภาพแวดล้อมที่แอปพลิเคชันทำงานเข้ากันได้

3. การทำซีเรียลไลซ์แบบกำหนดเอง:ทำปัญหาซับซ้อนให้ง่ายขึ้น

แม้ว่าวิธีข้างต้นจะสามารถแก้ปัญหาส่วนใหญ่ได้แล้ว แต่บางครั้งเราก็พบสถานการณ์ที่ต้องการการควบคุมอย่างละเอียดมากขึ้น ในเวลานี้,Jackson ตัวแปลงข้อมูลเป็นอนุกรมแบบกำหนดเองของมันก็ได้แสดงประโยชน์

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

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

public class CustomLongSerializer extends JsonSerializer<Long> {

    @Override
    public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) {
            gen.writeString(value.toString());
        }
    }
}


จากนั้นนำตัวแปลงข้อมูลเป็นรูปแบบอนุกรมที่กำหนดเองนี้ไปใช้ในจุดที่ต้องการ:

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

public class UserDto {
    @JsonSerialize(using = CustomLongSerializer.class)
    private Long id;
    // 其他字段
}


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

4. จัดการทั่วทั้งระบบ:แนวทางประหยัดเวลาและแรง

หากคุณต้องการแก้ปัญหานี้ในระดับส่วนกลาง เพื่อไม่ต้องยุ่งยากกับการกำหนดแอนโนเทชันในแต่ละฟิลด์ ก็สามารถพิจารณาการกำหนดค่าส่วนกลางได้ ObjectMapper。โดยการทำใน Spring Boot กำหนดค่าแบบส่วนกลางใน ObjectMapper,คุณสามารถทำให้ทั้งหมดของ Long ฟิลด์ชนิดดังกล่าวจะถูกแปลงเป็นสตริงโดยอัตโนมัติทั้งหมด

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        mapper.registerModule(module);
        return mapper;
    }
}


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

กรณีจริง:เส้นทางแก้ Bug ที่พลิกผันหลายตลบ

เพื่อให้ทุกคนเข้าใจผลที่เกิดขึ้นจริงของแนวทางแก้ไขเหล่านี้ได้ดีขึ้น ผมจะเล่าเรื่องจากโครงการจริงโครงการหนึ่ง โครงการนี้เกี่ยวข้องกับการจัดการ ID ผู้ใช้ เนื่องจากมีผู้ใช้จำนวนมาก แบ็กเอนด์จึงใช้ Long ตัวระบุที่ไม่ซ้ำกันของชนิดนี้ หลังจากโครงการเปิดใช้งานได้เพียงไม่กี่วัน ก็มีผู้ใช้แจ้งว่า:“ทำไม ID ของฉันถึงเปลี่ยนไปแบบนี้?”

ขั้นตอนแรก:สืบหาต้นตอของปัญหา

เราเริ่มจากตรวจสอบโค้ดฟรอนต์เอนด์ แล้วพบว่า JavaScript ของ Number ชนิดข้อมูลนี้ไม่สามารถแทนตัวเลขที่ยาวถึง 19 หลักได้อย่างแม่นยำจริง ๆ จึงทำให้สูญเสียความแม่นยำ จากนั้นเราตรวจสอบโค้ดแบ็กเอนด์ และพบว่าแม้ Long ข้อมูลชนิดนี้ถูกต้องที่ฝั่งแบ็กเอนด์ แต่เมื่อส่งผ่าน REST API เมื่อส่งกลับไปยังฟรอนต์เอนด์ ความแม่นยำของตัวเลขสูญหายไป

ขั้นตอนที่สอง:เลือกวิธีแก้ปัญหาที่เหมาะสม

เพื่อแก้ปัญหาอย่างรวดเร็ว เราตัดสินใจเริ่มต้นด้วยการใช้ @JsonSerialize(using = ToStringSerializer.class) วิธีที่เรียบง่ายและได้ผลนี้ ด้วยวิธีนี้ เราจึงป้องกันไม่ให้ส่วนหน้ารับตัวเลขที่สูญเสียความแม่นยำได้สำเร็จ。

อย่างไรก็ตาม ปัญหายังไม่ได้รับการแก้ไขทั้งหมด ในข้อกำหนดที่ตามมา ส่วนหน้าจำเป็นต้องคำนวณบางอย่างกับ ID เช่น เรียงลำดับ ID ของผู้ใช้ เมื่อถึงตอนนี้ สตริงก็เริ่มรับมือได้ไม่ค่อยไหวแล้ว。

ขั้นตอนที่สาม:ใช้ BigInt แก้ปัญหาการคำนวณฝั่งส่วนหน้า

ดังนั้น เราจึงตัดสินใจนำมาใช้ในฟรอนต์เอนด์ ได้แก่ BigInt。โดยใช้ BigInt,ส่วนหน้าจึงไม่เพียงจัดเก็บ ID ที่ยาวมากเหล่านี้ได้อย่างแม่นยำ แต่ยังทำการคำนวณทางคณิตศาสตร์ที่จำเป็นได้ด้วย จากการทดสอบ วิธีนี้ทำงานได้ดีบนเบราว์เซอร์หลักทุกตัว ข้อเสียเพียงอย่างเดียวคือรองรับเบราว์เซอร์รุ่นเก่าบางรุ่นได้ไม่ดีนัก

ขั้นตอนที่สี่:การกำหนดค่าส่วนกลางขั้นสุดท้าย

เพื่อป้องกันไม่ให้เกิดปัญหาลักษณะเดียวกันอีกในอนาคต เราตัดสินใจที่จะนำ ToStringSerializer ตั้งค่าให้มีผลทั่วทั้งระบบ ซึ่งทำให้ทั้งหมดของ Long ข้อมูลชนิดนี้จะถูกแปลงเป็นสตริงโดยอัตโนมัติทั้งหมดขณะซีเรียลไลซ์ ซึ่งช่วยรับประกันความถูกต้องของข้อมูลฝั่งหน้า และลดการกำหนดค่าซ้ำในโค้ดไปพร้อมกัน。

บทส่งท้าย:ผู้ยุติปัญหาการสูญเสียความแม่นยำ

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

หวังว่าบล็อกที่ทั้งมีอารมณ์ขันและให้รายละเอียดครบถ้วนนี้ จะช่วยให้คุณขณะจัดการ Long เมื่อพบปัญหาการสูญเสียความแม่นยำของชนิดข้อมูล จะได้ไม่ต้องเสียเวลาอ้อมไปมา หากคุณเคยมีประสบการณ์คล้ายกัน ก็เชิญแบ่งปันเรื่องราวของคุณในช่องความคิดเห็น ประสบการณ์ของคุณอาจช่วยจุดประกายความคิดให้กับนักพัฒนาคนอื่นได้มากขึ้น。

สุดท้าย จำไว้ว่า โค้ดก็เหมือนชีวิต การ “สูญหาย” เป็นครั้งคราวไม่ใช่เรื่องน่ากลัว สิ่งสำคัญคือการหา “ตัวซีเรียลไลซ์” ที่เหมาะสมเพื่อพามันกลับเข้าที่เข้าทาง ขอให้โค้ดของทุกคนไม่ “สูญเสียความแม่นยำ” อีก รักษาความเที่ยงตรงและทำงานได้อย่างราบรื่นตลอดทาง!


ความคิดเห็น

กำลังส่ง

บทความยอดนิยม

เลือกธีมที่ชอบ

ธีมที่เลือกจะใช้ทั่วทั้งเว็บไซต์