Cơn ác mộng 2 giờ sáng: Khi dữ liệu biến thành dấu hỏi chấm
Điện thoại rung bần bật. Tin nhắn Slack từ team vận hành nhảy liên hồi: “App lỗi rồi anh ơi, comment của khách toàn hiện ??? với ký tự lạ”. Mình bật dậy check log, đập vào mắt là một mớ hỗn độn. Toàn bộ Emoji và tiếng Việt có dấu đã bị biến dạng hoàn toàn khi lưu vào database.
Sai lầm nằm ở chỗ mình đã quá tin vào cái tên utf8. Trong MySQL, utf8 không thực sự là UTF-8 chuẩn mà chúng ta vẫn biết. Nếu bạn đang khốn khổ vì lỗi hiển thị hoặc không thể lưu được icon 😭, bài viết này là chìa khóa dành cho bạn. Đây là những kinh nghiệm xương máu mình rút ra từ việc quản lý các cụm DB hàng Terabyte.
Sự thật phũ phàng: Tại sao utf8 của MySQL là một “cú lừa”?
Hầu hết chúng ta đều chọn utf8 khi tạo database vì nghĩ nó hỗ trợ mọi thứ. Thực tế, utf8 trong MySQL chỉ là utf8mb3 (tối đa 3 bytes mỗi ký tự). Trong khi đó, Emoji hiện đại hoặc các ký tự đặc biệt lại cần tới 4 bytes.
| Đặc điểm | latin1 | utf8 (utf8mb3) | utf8mb4 |
|---|---|---|---|
| Số byte tối đa/ký tự | 1 byte | 3 bytes | 4 bytes |
| Hỗ trợ Emoji | Không | Thất bại (Lỗi 100%) | Hỗ trợ đầy đủ |
| Dung lượng lưu trữ | Thấp nhất | Trung bình | Tốn thêm ~10-20% so với utf8 |
| Phù hợp cho | Dữ liệu tiếng Anh thuần | Legacy system | Mọi dự án hiện đại |
Khi bạn cố nhét một ký tự 4-byte vào cột utf8mb3, MySQL sẽ thẳng tay cắt cụt dữ liệu hoặc ném ra lỗi Incorrect string value. Để an toàn, hãy quên utf8 đi và luôn luôn mặc định sử dụng utf8mb4.
Chọn Collation thế nào cho đúng?
Nếu Character Set là cách lưu trữ, thì Collation là bộ quy tắc để so sánh và sắp xếp. Việc chọn sai Collation thường dẫn đến những lỗi “ngớ ngẩn” như tìm kiếm chữ “a” lại ra cả chữ “á”.
- utf8mb4_general_ci: Tốc độ nhanh nhất nhờ lược bỏ các quy tắc phức tạp. Tuy nhiên, nó xử lý các ký tự đặc biệt hơi “ẩu”, đôi khi coi ‘ß’ bằng ‘s’.
- utf8mb4_unicode_ci: Chuẩn xác theo tiêu chuẩn Unicode. Nó nhận diện chính xác các biến thể ngôn ngữ nhưng sẽ tốn CPU hơn một chút (khoảng 5-10% tùy query) để tính toán.
- utf8mb4_0900_ai_ci: Lựa chọn tối ưu trên MySQL 8.0. Nó nhanh hơn
unicode_civà hỗ trợ Accent Insensitive (không phân biệt dấu) cực tốt.
Quy tắc vàng cho dự án mới
Đừng đợi đến khi database đầy hàng chục GB mới đi convert. Hãy áp dụng bộ quy tắc này ngay từ ngày đầu:
- Dùng MySQL 8.0+: Ưu tiên
utf8mb4kết hợputf8mb4_0900_ai_ci. - Dùng MySQL 5.7: Sử dụng
utf8mb4vàutf8mb4_unicode_ci. - Kích thước cột: Cẩn thận với
VARCHAR(255). Vớiutf8mb4, mỗi ký tự chiếm tối đa 4 bytes, dễ chạm trần giới hạn index 767 bytes của InnoDB đời cũ.
Các bước triển khai và cấu hình chuẩn
Chuyển đổi database đang chạy là một việc nhạy cảm. Hãy luôn backup dữ liệu trước khi thực hiện bất kỳ lệnh ALTER nào.
1. Kiểm tra trạng thái hiện tại
-- Xem charset của database hiện tại
SELECT @@character_set_database, @@collation_database;
2. Chuyển đổi Database và Table
Thay vì sửa từng cột, hãy convert toàn bộ table để MySQL tự động xử lý lại metadata.
-- Chuyển đổi toàn bộ Database
ALTER DATABASE my_project CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Chuyển đổi Table (Lưu ý: lệnh này sẽ khóa table trong chốc lát)
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3. Cấu hình Server-side (my.cnf)
Nhiều bạn sửa DB xong vẫn lỗi vì connection client gửi lên vẫn là latin1. Hãy ép MySQL dùng utf8mb4 cho mọi kết nối bằng cách sửa file cấu hình:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# Bỏ qua việc handshake charset để tránh client tự ý đổi về latin1
character-set-client-handshake = FALSE
4. Đồng bộ phía Application
Code của bạn cũng phải biết nó đang nói chuyện bằng ngôn ngữ gì. Với Node.js (mysql2) hoặc PHP, hãy chỉ định charset ngay trong connection string.
// Node.js configuration
const db = mysql.createConnection({
host: 'localhost',
charset: 'utf8mb4' // Thiếu dòng này thì DB xịn đến mấy vẫn lỗi font
});
Kinh nghiệm thực chiến: Những cái bẫy ít người ngờ tới
Có lần mình debug mất nửa ngày dù DB và Code đã chuẩn utf8mb4. Hóa ra lỗi nằm ở ProxySQL – lớp trung gian điều phối connection. ProxySQL lúc đó mặc định dùng utf8, nó âm thầm cắt mất byte thứ 4 của Emoji trước khi gửi xuống DB. Bài học là: Phải kiểm tra tính đồng bộ trên toàn bộ stack, từ App -> Proxy -> DB.
Một lỗi kinh điển khác là Specified key was too long; max key length is 767 bytes. Khi chuyển sang utf8mb4, một cột VARCHAR(255) sẽ chiếm tối đa 1020 bytes (255×4), vượt quá giới hạn index của InnoDB cũ. Giải pháp là nâng cấp lên MySQL 8.0 hoặc giảm chiều dài cột xuống còn VARCHAR(191) để đảm bảo an toàn cho index.
Làm chủ Character Set không khó, cái khó là sự tỉ mỉ. Hãy chuẩn hóa utf8mb4 ngay hôm nay để khách hàng của bạn có thể thoải mái thả tim ❤️ hay gửi icon 🚀 mà không lo biến thành những dấu hỏi chấm vô hồn.

