+1

[PHP] Giải ngố PHP_INT_MAX: Khi những con số khổng lồ "phản bội" bạn

Chào các anh em dev hệ PHP,

Chắc hẳn lúc ngồi lướt doc hoặc tọc vạch mấy file config, anh em thỉnh thoảng sẽ thấy cái hằng số PHP_INT_MAX to đùng ngã ngửa. Thường thì chúng ta sẽ lướt qua luôn vì nghĩ: "Mấy khi mình dùng đến số to thế này, app mình làm gì có chục tỷ user đâu mà lo".

Đừng vội chủ quan! Đã có rất nhiều hệ thống dính bug "tâm linh" liên quan đến thanh toán tiền tỷ hoặc sai lệch ID chỉ vì không hiểu rõ bản chất của cái hằng số này. Hôm nay, mình sẽ bóc trần sự thật về PHP_INT_MAX và cách nó âm thầm "bóp" hệ thống của bạn như thế nào.

1. PHP_INT_MAX rốt cuộc là số mấy?

Đây là một hằng số (constant) được PHP định nghĩa sẵn, đại diện cho số nguyên (integer) lớn nhất mà môi trường PHP hiện tại của bạn có thể hỗ trợ.

Tại sao lại có chữ "môi trường hiện tại"? Vì con số này không cố định, nó phụ thuộc vào hệ điều hành và phiên bản PHP của server (32-bit hay 64-bit).

  • Trên hệ thống 32-bit: Con số này là 2147483647 (khoảng hơn 2 tỷ).
  • Trên hệ thống 64-bit: Con số này là 9223372036854775807 (khoảng 9 tỷ tỷ - một con số khổng lồ).

Anh em có thể dễ dàng test bằng cách gõ echo PHP_INT_MAX; vào terminal. Thời nay đa số server đều là 64-bit rồi, nhưng thi thoảng bảo trì mấy hệ thống legacy từ thời tống, chạy XAMPP 32-bit thì vẫn gặp số 2 tỷ kia bình thường.

2. Sự phản bội mang tên "Ép kiểu ngầm" (Type Casting)

Trong các ngôn ngữ như C hay Java, nếu bạn lấy số nguyên lớn nhất cộng thêm 1, nó sẽ bị tràn (Integer Overflow) và quay vòng về số âm. Nhưng PHP thì "thông minh" hơn (hoặc lươn lẹo hơn). Nó bảo: "Tôi không thích quay về số âm. Thấy số to quá thì tôi tự động ép kiểu sang số thập phân (float) luôn cho rộng rãi!".

$max = PHP_INT_MAX;
var_dump($max);     // int(9223372036854775807)

$max_plus_one = $max + 1;
var_dump($max_plus_one); // float(9.2233720368548E+18) - Ủa alo???

Chính cái sự ép kiểu ngầm này là khởi nguồn của mọi tội lỗi. Khi một số lớn bị ép sang float, nó sẽ bắt đầu mất đi độ chính xác (precision loss). Nghĩa là nếu anh em lấy PHP_INT_MAX + 1 đem so sánh với PHP_INT_MAX + 2, sau khi bị chuyển sang float, PHP có thể đánh giá hai biến này... bằng nhau (true). Dùng nó để query tìm user trong database là đi bụi ngay.

3. Nỗi đau thực chiến: MySQL BIGINT và cơn ác mộng Frontend

Đây là case study kinh điển nhất mà anh em làm hệ thống lớn kiểu gì cũng dính.

Bạn thiết kế database MySQL, dùng kiểu BIGINT cho cột id. Mức max của BIGINT đúng bằng PHP_INT_MAX của PHP 64-bit. Mọi thứ hoạt động hoàn hảo trên server backend. PHP đọc ID từ database lên, xử lý mượt mà. Nhưng, hệ thống của bạn không chạy một mình. Nó còn phải trả kết quả API bằng file JSON cho Frontend (JavaScript) hoặc Mobile xử lý.

Và bùm! JavaScript không dùng 64-bit integer bá đạo như PHP. Chuẩn số nguyên an toàn lớn nhất của JS là Number.MAX_SAFE_INTEGER, con số này chỉ dừng lại ở mức 9007199254740991. Con số này nhỏ hơn rất nhiều so với PHP_INT_MAX.

Hệ quả của chuỗi bi kịch này:

  1. Backend gửi về một ID siêu to: 9007199254740995
  2. Trình duyệt nhận JSON, JS tự parse số này thành kiểu Number của nó.
  3. Do vượt rào giới hạn an toàn, JS tự làm tròn thành: 9007199254740996 (sai mẹ ID).
  4. User bấm "Xóa bài viết", Frontend gửi cái ID đuôi 996 lên Backend.
  5. Backend query database không ra (hoặc đen hơn là xóa nhầm bài của thằng khác). Trầm cảm chưa?

4. Bí kíp sinh tồn để không bị bóp dái

Để không bị những con số khổng lồ này đâm sau lưng, anh em chỉ cần nhớ 2 nguyên tắc vàng:

Nguyên tắc 1: Với những ID siêu lớn (kiểu BIGINT, Snowflake ID của Twitter/Discord), hãy LUÔN LUÔN ép chúng sang kiểu Chuỗi (String) trước khi trả ra API. Trong Laravel, anh em có thể dùng Eloquent Casts siêu nhàn:

protected $casts = [
    'id' => 'string',
];

Nguyên tắc 2: Thao tác với các phép tính cộng trừ nhân chia số tiền cực lớn, hoặc tính toán logic liên quan đến tiền điện tử (crypto), đừng dùng dấu + - * / thông thường của PHP. Hãy xài extension BCMath (Binary Calculator). Nó xử lý các phép toán bằng kiểu chuỗi (string) nên dù số có to đến mấy cũng không bao giờ bị sai số thập phân.

Chốt hạ

PHP_INT_MAX tưởng chừng như một hằng số vô tri vô giác, nhưng nó lại là ranh giới mong manh giữa một hệ thống chạy mượt mà và một cái bug "triệu đô". Nắm vững giới hạn của môi trường, hiểu rõ sự chênh lệch kiểu dữ liệu giữa các hệ thống (Database, Backend, Frontend) sẽ giúp anh em thiết kế API cứng tay hơn rất nhiều.

Anh em nào từng mất ngủ vì cái lỗi làm tròn số bên Frontend hành cho lên bờ xuống ruộng chưa? Comment tâm sự bên dưới nhé. Hẹn gặp lại anh em ở bài viết sau!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.