Ca nâng cấp MySQL tưởng an toàn nhưng hóa ra không hề an toàn

29 tháng 8, 2026·5 phút đọc

Bài viết kể lại một sự cố thực tế khi nâng cấp MySQL trên AWS: sau khi chuyển replica thành primary, một bảng dữ liệu bị sai tham chiếu khóa ngoại do sự khác biệt trong cách gán ID tự động tăng (AUTO_INCREMENT) giữa source và replica. Nguyên nhân sâu xa đến từ cấu hình binlog_format MIXED — một số lệnh UPDATE chạy theo STATEMENT mode, số khác lại chuyển sang ROW mode, dẫn đến dữ liệu không đồng nhất. Bài viết là lời cảnh báo quan trọng cho các quản trị viên cơ sở dữ liệu về rủi ro tiềm ẩn khi nâng cấp trên môi trường replication.

Ca nâng cấp MySQL tưởng an toàn nhưng hóa ra không hề an toàn

Mọi chuyện bắt đầu từ một thông báo quen thuộc: phiên bản cơ sở dữ liệu của tôi đã hết hạn hỗ trợ và cần được nâng cấp. Chi phí extended support của AWS là động lực đủ mạnh để tôi hành động ngay lập tức.

Tôi có sẵn một bản replica đang hoạt động tốt, nên kế hoạch rất đơn giản: nâng cấp replica trước, kiểm tra mọi thứ hoạt động bình thường, rồi chuyển primary sang replica đó.

Nhanh gọn, dễ dàng, đúng không? Tôi đã nghĩ vậy. Nhưng sự thật không như mong đợi.

Sự cố bất ngờ sau hơn một giờ

Chỉ một giờ sau khi chuyển đổi, tôi nhận được báo cáo về một lỗi kỳ lạ. Khi kiểm tra database, tôi phát hiện một bảng cụ thể (gọi là bảng X) có thứ tự ID bị xáo trộn hoàn toàn. Row trước đây có ID 1 trong database cũ giờ lại mang ID 26.

Điều đáng nói là bảng X này được tham chiếu bởi 6 bảng khác. Trong đó, 5 bảng tham chiếu đúng theo ID mới, nhưng một bảng còn lại lại sử dụng ID từ database cũ — giờ trỏ đến những row hoàn toàn khác.

Điều này thật khó hiểu. Làm sao mọi thứ lại rối tung lên như vậy?

Nguồn cơn: migration thêm khóa chính tự động tăng

Trước đó, trước khi nâng cấp, một migration đã được chạy để thêm một khóa chính mới kiểu auto-increment vào bảng X:

ALTER TABLE X ADD COLUMN id INT NOT NULL AUTO_INCREMENT PRIMARY KEY;

Migration này cũng cập nhật 6 bảng liên quan để chúng tham chiếu ID mới thay vì ID cũ. Với mỗi bảng, câu lệnh cập nhật trông đại khái như thế này:

UPDATE some_table
JOIN x ON x.old_id = some_table.x_old_id
SET some_table.x_id = x.id;

Bản chất của cột AUTO_INCREMENT trong replication

Hóa ra, việc thêm cột AUTO_INCREMENT vào một bảng đang được replicate có thể dẫn đến việc các row nhận được ID khác nhau giữa source và replica.

Theo tài liệu chính thức của MySQL về replication và AUTO_INCREMENT, việc thêm cột AUTO_INCREMENT bằng ALTER TABLE có thể không tạo ra cùng một thứ tự row trên source và replica. Thứ tự gán ID phụ thuộc vào storage engine và thứ tự xử lý row của từng máy.

Tôi đã không hề hay biết điều này. Nhưng câu hỏi lớn hơn vẫn còn đó: tại sao 5/6 bảng tham chiếu đúng, còn một bảng thì sai bét?

Vai trò của binlog format

Đây chính là lúc mọi chuyện trở nên rối rắm hơn.

MySQL replication sử dụng “binary log” để ghi lại các thay đổi trên source. Những thay đổi này sau đó được gửi tới các replica và được áp dụng lại để tái tạo các transaction tương ứng.

Nội dung được ghi và cách áp dụng trên replica phụ thuộc vào “format” của binary log. MySQL hỗ trợ 3 chế độ:

  • STATEMENT: Câu lệnh SQL được ghi vào binary log, replica sẽ thực thi lại câu lệnh đó.
  • ROW: Các thay đổi trên từng row được ghi vào binary log và được áp dụng trực tiếp lên replica.
  • MIXED: Mặc định dùng STATEMENT, nhưng tự động chuyển sang ROW trong một số trường hợp.

Hóa ra, database source của tôi được cấu hình với binlog_format = MIXED.

Vậy nên lý do 5 bảng kia tham chiếu đúng ID mới là vì các câu lệnh UPDATE của chúng được replicate bằng STATEMENT mode. Câu lệnh UPDATE chính xác được chạy lại trên replica, tra cứu đúng bản x.id của replica và ghi ID cục bộ chính xác vào các bảng liên quan.

Nhưng riêng bảng cuối cùng, MySQL quyết định dùng ROW mode. Trong chế độ này, replica không chạy lại câu lệnh UPDATE gốc, mà nhận trực tiếp kết quả thay đổi row từ source và áp lên bản sao của nó.

Hệ quả là các giá trị x_id được sinh trên source được copy nguyên xi sang replica. Nhưng vì bảng X có ID khác trên replica, những giá trị này giờ trỏ đến những row hoàn toàn khác.

Tại sao chỉ một bảng bị ảnh hưởng?

Bạn có thể thắc mắc: tại sao MySQL lại quyết định dùng ROW mode chỉ cho một bảng duy nhất?

Điểm khác biệt duy nhất tôi có thể tìm thấy giữa bảng này và 5 bảng còn lại là nó có một cột AUTO_INCREMENT.

MySQL có những trường hợp cụ thể liên quan đến AUTO_INCREMENT mà nó xem là không an toàn cho statement-based replication, vì vậy dưới chế độ MIXED, nó sẽ ghi log bằng ROW.

Thật trớ trêu, phải không? Mọi thứ cuối cùng đều quy về tính năng AUTO_INCREMENT — thứ đã gây ra toàn bộ vấn đề này.

Kết luận

Hãy thật cẩn thận với replica MySQL. Điều đáng sợ nhất của loại sự cố này là nó hoàn toàn không lường trước được và rất dễ bị bỏ sót. Nhưng nó có thể nhanh chóng biến thành thảm họa trong môi trường production, và bạn chỉ còn biết tự hỏi: "Tại sao mọi thứ lại đi đến mức này?"

Lời khuyên dành cho anh em quản trị database: trước khi nâng cấp trên môi trường replication, hãy kiểm tra kỹ binlog format, đặc biệt là các bảng có cột AUTO_INCREMENT vừa được thêm qua migration. Và luôn luôn có phương án rollback rõ ràng — bởi vì "an toàn" không bao giờ là điều chắc chắn tuyệt đối.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗