Vibe Code
Giáo trình/Phần 5 — Quy trình & vận hành

Sao lưu và khôi phục được

Ô mà ai cũng vẽ thiếu trong sơ đồ. Sao lưu D1 và R2, quy tắc 3-2-1 bản rút gọn, và luật duy nhất: bản sao lưu chưa phục hồi thử thì chưa phải bản sao lưu.

Cấp 530 phút làm

Đây là ô ở lớp 6 của sơ đồ mà người học hay bỏ qua nhất — vì nó không tạo ra tính năng nào. Nó chỉ bảo vệ toàn bộ số tính năng bạn đã làm.

Luật duy nhất cần nhớ

Bản sao lưu chưa từng được phục hồi thử thì chưa phải là bản sao lưu. Nó chỉ là một file bạn hy vọng là đúng.

Phần lớn người có "sao lưu" nhưng chưa bao giờ thử khôi phục. Đến lúc cần thì phát hiện: file rỗng, file thiếu bảng, hoặc không biết cách nạp lại.

Rollback khác khôi phục dữ liệu

Hai thứ này hay bị lẫn, và lẫn thì rất đắt:

Rollback mã Khôi phục dữ liệu
Lùi được cái gì Mã nguồn đang chạy Nội dung cơ sở dữ liệu
Mất bao lâu 30 giây 10 phút – vài giờ
Cứu được tình huống Bản mới làm vỡ giao diện / API Xóa nhầm, ghi đè sai, migration hỏng
Không cứu được Dữ liệu đã bị ghi sai Mã lỗi vẫn còn đó

Một bản deploy lỗi có thể vừa làm hỏng giao diện vừa ghi sai dữ liệu. Rollback xong vẫn phải kiểm dữ liệu.

Sao lưu D1

Cách 1 — Xuất ra file, giữ ngoài repo:

npx wrangler d1 export app-db --remote --output=backup-2026-10-08.sql

File này là SQL thuần, mở ra đọc được, nạp lại được ở bất kỳ đâu. Đây là lớp bạn kiểm soát hoàn toàn.

Cách 2 — Khôi phục theo thời điểm (Time Travel): D1 giữ lại được trạng thái trong một khoảng thời gian gần.

npx wrangler d1 time-travel info app-db
npx wrangler d1 time-travel restore app-db --timestamp=2026-10-08T07:00:00Z

Đừng để Cách 2 là lớp duy nhất. Nó phụ thuộc vào nhà cung cấp, có giới hạn thời gian, và không giúp gì nếu bạn lỡ xóa cả cơ sở dữ liệu.

Luật tối thiểu — xuất một bản TRƯỚC mọi migration đụng cấu trúc bảng

ALTER TABLE, DROP COLUMN, migration đổi kiểu dữ liệu — tất cả đều có thể hỏng theo cách không lùi lại được. Một lệnh wrangler d1 export mất 20 giây.

Viết thẳng vào quy trình của bạn: không có file sao lưu thì không chạy migration.

Sao lưu tự động hằng đêm

Dùng cron đẩy bản sao sang R2:

async function saoLuuDem(env) {
  const ngay = new Date().toISOString().slice(0, 10);
  const bang = ["nguoi_dung", "don_hang", "chi_tiet_don", "nhat_ky"];
  const goi = {};

  for (const b of bang) {
    const { results } = await env.DB.prepare(`SELECT * FROM ${b}`).all();
    goi[b] = results;
  }

  await env.SAO_LUU.put(`d1/${ngay}.json`, JSON.stringify(goi), {
    httpMetadata: { contentType: "application/json" },
  });

  await env.DB.prepare(
    "INSERT INTO nhat_ky (loai, chi_tiet) VALUES ('sao-luu', ?)"
  ).bind(`${ngay}: ${bang.map(b => b + "=" + goi[b].length).join(", ")}`).run();
}

Ba điều về đoạn mã này:

  1. Ghi nhật ký kèm số dòng từng bảng. Đây là cách bạn phát hiện bản sao lưu rỗng — nếu hôm nay don_hang=0 mà hôm qua là 4.820 thì có chuyện.
  2. SELECT * ở đây là ngoại lệ hợp lệ của luật cấm select-all — nhưng chỉ khi bảng còn nhỏ. Bảng lớn thì phải chia mẻ theo id, nếu không việc sao lưu sẽ tự nó làm nghẽn D1.
  3. Chạy vào giờ ít người dùng (2–4 giờ sáng giờ Việt Nam = 0 19 * * * UTC).
Sao lưu nặng chạy trong giờ làm việc làm nghẽn chính app của bạn

D1 xử lý tuần tự cả cơ sở dữ liệu. Một lệnh SELECT * trên bảng 50 MB sẽ giữ D1 trong nhiều giây, và mọi người dùng khác xếp hàng chờ.

Hai cách phòng: chạy vào ban đêm, và chia theo mẻ (LIMIT 1000 OFFSET ...) với khoảng nghỉ nhỏ giữa các mẻ.

Sao lưu R2

Ảnh và file ít khi bị xóa nhầm hàng loạt, nhưng khi mất thì không tái tạo được — khác với dữ liệu có thể nhập lại.

Hai mức:

  • Tối thiểu: bật phiên bản (versioning) cho bucket, và không bao giờ xóa thật — thêm cột da_xoa thay vì DELETE.
  • Đầy đủ: định kỳ liệt kê toàn bộ key trong R2 và lưu danh sách đó vào D1. Danh sách này cho bạn biết cái gì đáng lẽ phải có — để phát hiện file mất.
// Đối chiếu: file nào D1 nói có mà R2 không có?
const { results } = await env.DB.prepare(
  "SELECT id, anh_key FROM don_hang WHERE anh_key IS NOT NULL LIMIT 500"
).all();
const thieu = [];
for (const r of results) {
  const o = await env.ANH.head(r.anh_key);
  if (!o) thieu.push(r);
}

Chạy phép đối chiếu này mỗi tuần. Nó bắt được loại hỏng âm thầm nhất: dữ liệu trỏ tới file không còn tồn tại.

Quy tắc 3-2-1, bản rút gọn cho app nhỏ

Nguyên gốc là 3 bản sao, 2 loại phương tiện, 1 bản ở nơi khác. Bản thực dụng cho Vibe Code:

1. Bản đang chạy          (D1 + R2 trên Cloudflare)
2. Bản tự động hằng đêm   (R2, giữ 30 ngày gần nhất)
3. Bản thủ công hằng tháng (TẢI VỀ MÁY BẠN hoặc ổ cứng ngoài)

Bản thứ 3 quan trọng hơn vẻ ngoài: hai bản đầu cùng nằm trong một tài khoản Cloudflare. Nếu tài khoản đó gặp chuyện — mất quyền truy cập, bị khóa, thanh toán hỏng — bạn mất cả hai cùng lúc.

Mất dữ liệu không giết app, nó giết niềm tinchuyên gia insight & case thực tế

App chết một buổi thì khách bực rồi quên. Mất dữ liệu thì khách không bao giờ quên. Một lần mất hai ngày dữ liệu chấm công là cả công ty phải ngồi khai lại bằng trí nhớ — và từ đó, mỗi lần app chậm một chút họ đều hỏi "dữ liệu còn không?".

Cái đáng nói: hầu hết các vụ mất dữ liệu tôi gặp không phải do hỏng hóc kỹ thuật. Chúng là: - Một migration chạy nhầm trên bản thật thay vì bản thử. - Một script dọn dẹp xóa rộng hơn ý định (WHERE sai một điều kiện). - Ghi đè: hai nơi cùng sửa, nơi lưu sau ghi đè nơi lưu trước.

Cả ba đều do người gây ra, và cả ba đều phòng được bằng hai thói quen rẻ tiền: xuất một bản sao trước khi chạy thứ gì đụng dữ liệu, và luôn chạy thử trước (--thu / --dry-run) để xem sẽ ảnh hưởng bao nhiêu dòng.

Chống ghi đè — thứ không có trong bản sao lưu nào

Sao lưu cứu bạn sau tai nạn. Chống ghi đè ngăn tai nạn xảy ra:

  • Không xóa thật. Thêm cột da_xoa + xoa_luc + xoa_boi, lọc trong truy vấn. Người dùng thấy như đã xóa, bạn vẫn khôi phục được.
  • Chặn bản rút gọn ghi đè bản đầy đủ. Nếu giao diện gửi lên một bản ghi thiếu trường (vì màn hình đó chỉ hiển thị vài trường), máy chủ phải từ chối, không được ghi đè phần còn lại thành rỗng. Đây là lỗi mất dữ liệu phổ biến nhất mà không ai nghi ngờ.
  • Đối chiếu 100% trước khi đổi đường đọc. Khi thay cách lấy dữ liệu (ví dụ chuyển sang bảng tổng hợp mới), đừng tin là đúng — so toàn bộ bản cũ với bản mới. Có lần phải khớp đủ 818/818 báo cáo mới dám chuyển.
Đề dựng sao lưu có kiểm chứngchuyên gia thực thi
Dựng sao lưu tự động cho app:

- Cron 02:00 giờ Việt Nam (= 19:00 UTC hôm trước).
- Xuất các bảng chính ra JSON, lưu R2 theo key sao-luu/YYYY-MM-DD.json.
- Bảng nào trên 5.000 dòng thì chia mẻ 1.000 dòng, nghỉ 100ms giữa mẻ.
- Ghi nhat_ky mỗi lượt: ngày, số dòng TỪNG BẢNG, dung lượng file.
- Tự xóa bản cũ hơn 30 ngày.
- KIỂM CHỨNG NGAY sau khi ghi: đọc lại file vừa ghi từ R2, đếm số
  dòng từng bảng, so với số vừa xuất. Lệch thì ghi nhat_ky loại 'loi'.
- Trang quản trị: hiện 10 bản sao lưu gần nhất kèm số dòng và dung
  lượng, có nút Tải về.

Rồi viết thêm script khôi phục: đọc một file sao lưu, in ra sẽ nạp
bao nhiêu dòng vào bảng nào, có cờ --thu để KHÔNG ghi gì.

Phần kiểm chứng ngay sau khi ghi là phần phân biệt sao lưu thật với sao lưu giả. Và script khôi phục phải viết cùng lúc với script sao lưu — đừng để lúc cần mới viết.

Tôi có sao lưu, và nó vô dụngngười dùng bình thường góp ý

App của tôi sao lưu hằng đêm suốt 4 tháng. Hôm cần dùng thật — tôi lỡ chạy một lệnh xóa sai — tôi mở file sao lưu ra và phát hiện: nó chỉ có 3 bảng trong số 7 bảng. Tôi viết script lúc app mới có 3 bảng, và không bao giờ cập nhật.

Từ đó tôi làm hai việc, mỗi quý một lần, mất 20 phút: 1. Mở file sao lưu mới nhất ra xem — đủ bảng không, số dòng có hợp lý không. 2. Nạp thử vào một cơ sở dữ liệu trống rồi mở app trỏ vào đó, bấm vài màn hình.

Việc 2 là việc duy nhất thật sự chứng minh bản sao lưu dùng được.

Chốt review bài nàychuyên gia review
  • Đủ: phân biệt rollback với khôi phục dữ liệu, xuất D1, sao lưu tự động có kiểm chứng, sao lưu và đối chiếu R2, 3-2-1 rút gọn, ba biện pháp chống ghi đè.
  • Đã trả giá thật: sao lưu thiếu bảng vì script không cập nhật; chặn bản rút gọn ghi đè; đối chiếu 818/818 trước khi đổi đường đọc.
  • Rủi ro còn lại: bài này không nói tới nghĩa vụ pháp lý khi giữ dữ liệu cá nhân (thời hạn lưu, quyền xóa của người dùng). Nếu app giữ dữ liệu cá nhân của nhiều người, đó là việc cần tìm hiểu riêng.
Bài tập chốt
  1. Xuất một bản sao lưu D1 ngay bây giờ, mở file ra đếm số bảng.
  2. Dựng sao lưu tự động theo đề trên, chạy một đêm, sáng kiểm nhật ký.
  3. Phép thử thật — bắt buộc: tạo một cơ sở dữ liệu D1 mới trống, nạp bản sao lưu vào, trỏ app vào đó, mở lên bấm thử. Nếu app chạy bình thường với dữ liệu đúng, bạn mới thật sự có sao lưu.
  4. Chạy phép đối chiếu R2: có file nào D1 nói có mà R2 không có không?

Việc 3 là việc duy nhất trong bài không được bỏ. Mọi việc còn lại chỉ có giá trị nếu việc 3 đã làm.