9 điểm bởi GN⁺ 2025-04-21 | 1 bình luận | Chia sẻ qua WhatsApp
  • Bảng ảo của SQLite cũng có thể hỗ trợ ghi và giao dịch, bằng cách triển khai các hook như xUpdate, xSync, xCommit, xRollback
  • SQLite mặc định đảm bảo tính nguyên tử bằng cơ chế rollback journal, và khi xử lý nhiều tệp DB thì dùng super-journal để điều phối việc commit toàn cục
  • Bảng ảo cũng là một phần của giao thức giao dịch của SQLite, nên nếu xSync thất bại thì toàn bộ giao dịch sẽ bị rollback
  • Commit được chia thành 2 giai đoạn; xSync là nơi thực hiện các tác vụ có thể thất bại, còn xCommit chỉ nên làm các tác vụ dọn dẹp đơn giản
  • xCommitxRollback luôn có thể được gọi, vì vậy cần viết chúng như các hàm dọn dẹp có thể chạy mà không thất bại

Bảng ảo và xử lý giao dịch trong SQLite

Trong bài viết trước, tác giả đã giới thiệu cách cơ bản để đăng ký và truy vấn bảng ảo của SQLite bằng ngôn ngữ Go. Bài này tập trung vào cách triển khai bảng ảo có thể ghi và hỗ trợ giao dịch.

Hỗ trợ ghi và giao dịch cho bảng ảo

  • Giao diện bảng ảo của SQLite không chỉ là chỉ đọc

  • Nếu triển khai hook xUpdate thì có thể ghi ra cả nguồn dữ liệu bên ngoài

  • Để có được tính nhất quán giao dịch thực sự, cần các hook giao dịch sau:

    • xBegin: thông báo bắt đầu giao dịch
    • xSync: chuẩn bị để commit an toàn xuống đĩa (nếu thất bại ở đây thì rollback toàn bộ)
    • xCommit: commit cuối cùng và dọn dẹp
    • xRollback: thực hiện rollback nếu giao dịch bị hủy
  • Ngay cả khi được sửa đổi cùng với bảng thường hoặc bảng ảo khác, SQLite vẫn phối hợp tất cả các hook để đảm bảo tính nguyên tử

Cách giao dịch của SQLite hoạt động bên trong

Rollback journal

  • SQLite mặc định lưu trang vào tệp sao lưu (journal) trước khi ghi đè
  • Nếu có sự cố, nó sẽ phục hồi từ journal để đảm bảo tính nguyên tử

> Lưu ý: SQLite cũng hỗ trợ chế độ WAL, nhưng không nằm trong phạm vi bài viết này

Super-journal

  • Khi có nhiều cơ sở dữ liệu được gắn vào, chỉ journal riêng cho từng DB là khó đồng bộ

  • Một tệp cấp cao hơn gọi là super-journal được dùng để điều phối commit giữa nhiều tệp

  • Nếu chỉ xử lý nhiều bảng ảo trong cùng một tệp DB thì có thể đồng bộ mà không cần super-journal

  • Dù trong trường hợp nào, SQLite vẫn tự động gọi các hook xSync, xCommit, xRollback trong luồng giao dịch

Commit hai giai đoạn cùng bảng ảo

Quá trình commit của SQLite gồm hai giai đoạn:

Giai đoạn 1: xSync (đảm bảo durability)

  • Đồng bộ an toàn xuống đĩa mọi trang hoặc journal của mọi B-Tree và tệp DB
  • Mỗi bảng ảo cũng sẽ được gọi hook xSync tương ứng
  • Nếu bất kỳ xSync nào thất bại thì toàn bộ giao dịch sẽ bị rollback → giữ tính nguyên tử

Giai đoạn 2: dọn dẹp (xCommit)

  • Sau khi dữ liệu đã được ghi xong xuống đĩa, SQLite xóa tệp journal và thực hiện dọn dẹp cho bảng ảo

  • Dưới đây là một phần mã từ vdbeaux.c

    disable_simulated_io_errors();  
    sqlite3BeginBenignMalloc();  
    for(i=0; i<db->nDb; i++){  
      Btree *pBt = db->aDb[i].pBt;  
      if( pBt ){  
        sqlite3BtreeCommitPhaseTwo(pBt, 1);  
      }  
    }  
    sqlite3EndBenignMalloc();  
    enable_simulated_io_errors();  
    sqlite3VtabCommit(db);  
    
  • Bên trong sqlite3VtabCommit(), trên thực tế mọi lời gọi xCommit đều bị bỏ qua ngay cả khi thất bại → đây là giai đoạn dọn dẹp thuần túy

    int sqlite3VtabCommit(sqlite3 *db){  
      callFinaliser(db, offsetof(sqlite3_module,xCommit));  
      return SQLITE_OK;  
    }  
    
  • Vì độ bền đã được đảm bảo ở xSync, nên lỗi của xCommit hay xRollback cũng sẽ bị bỏ qua

Những điểm cần lưu ý cho người viết bảng ảo

  • Mọi tác vụ có tính bền vững phải được đặt trong xSync
    • Các thao tác có thể thất bại như network I/O, ghi tệp... cần được xử lý ở đây để giao dịch có thể bị hủy an toàn
  • Ngay cả sau xSync, xRollback vẫn có thể được gọi
    • Nếu xSync của bảng khác thất bại thì toàn bộ giao dịch sẽ rollback
  • xCommitxRollback phải được viết như các hàm dọn dẹp không được thất bại
    • Chúng nên idempotent (tính lũy đẳng), nghĩa là gọi nhiều lần cũng không làm thay đổi trạng thái

Kết luận

  • Cơ chế journaling của SQLite đảm bảo commit nguyên tử cho mọi thành phần, bao gồm cả bảng thường và bảng ảo
  • Các hook giao dịch của bảng ảo được tích hợp tự nhiên vào luồng giao dịch của SQLite
  • Nhà phát triển triển khai bảng ảo nên tập trung vào xSync để đảm bảo tính toàn vẹn dữ liệu, còn tác vụ dọn dẹp nên được tách sang xCommitxRollback

1 bình luận

 
GN⁺ 2025-04-21
Ý kiến trên Hacker News
  • Bài viết về vtab khá hay. Tôi đã triển khai hỗ trợ vtab khi tái hiện thực SQLite bằng Rust. Vì vậy gần đây tôi đã học được rất nhiều điều về vtab. vtab rất mạnh mẽ và có lẽ chưa được tận dụng đủ nhiều
  • Thú vị. Nhưng cái này dùng gói mattn go-sqlite3. Đây là CGO
    • Tôi tự hỏi liệu trong Go hiện đại đây có phải là một yêu cầu phổ biến hoặc được kỳ vọng hay không