- Ứng dụng local-first hứa hẹn tốc độ phản hồi nhanh và quyền riêng tư cơ bản được đảm bảo, nhưng trên thực tế việc triển khai hỗ trợ offline đúng nghĩa là cực kỳ khó
- Lý do lớn nhất là độ phức tạp của việc đồng bộ, vì khi dữ liệu bị thay đổi đồng thời trên nhiều thiết bị thì cuối cùng hệ thống phải hội tụ về chính xác cùng một trạng thái
- Có hai thách thức kỹ thuật lớn là sự bất định về thứ tự thời gian và xung đột
- Để giải quyết vấn đề này, cần áp dụng các thiết kế hệ thống phân tán như Hybrid Logical Clocks(HLCs) và CRDTs
- Bằng cách tận dụng các phần mở rộng dựa trên SQLite, có thể cung cấp một kiến trúc đồng bộ đơn giản và đáng tin cậy, và có thể sử dụng trên mọi nền tảng
Lời hứa và thực tế của ứng dụng offline-first
- Ứng dụng offline-first quảng bá rằng có thể mang lại phản hồi tức thì, quyền riêng tư được đảm bảo mặc định, và sử dụng được ngay cả trong môi trường mạng không ổn định mà không phải chờ tải
- Trên thực tế, phần lớn ứng dụng không triển khai hỗ trợ offline một cách đúng đắn; đa số chỉ chọn cách tạm lưu thay đổi ở máy cục bộ rồi gửi đi sau khi có kết nối mạng
- Cách triển khai này kém tin cậy và cuối cùng dẫn đến những cảnh báo như "các thay đổi có thể không được lưu"
Khó khăn cốt lõi của việc đồng bộ
- Khi xây dựng ứng dụng local-first, về bản chất bạn sẽ phải xây dựng một hệ thống phân tán
- Nhiều thiết bị có thể độc lập thay đổi dữ liệu trong môi trường offline, và khi kết nối lại sau đó thì chúng phải hội tụ chính xác về cùng một trạng thái
- Để làm được điều đó, có hai thách thức lớn
- Sự bất định về thứ tự của các sự kiện
- Xung đột trên cùng một dữ liệu
1. Sự bất định về thứ tự sự kiện
- Các sự kiện phát sinh ở những thời điểm khác nhau trên nhiều thiết bị, và trạng thái có thể thay đổi tùy theo thứ tự
- Ví dụ: thiết bị A đặt x=3, thiết bị B đặt x=5 → sau khi mỗi bên thay đổi trong trạng thái offline rồi đồng bộ, có thể xuất hiện các kết quả khác nhau
- Cơ sở dữ liệu tập trung truyền thống giải quyết việc này bằng tính nhất quán mạnh, nhưng cách đó cần đồng bộ toàn cục nên không phù hợp với hệ thống local-first
- Cuối cùng, cần xác định thứ tự phù hợp cho từng sự kiện ngay cả trong môi trường động và phân tán, tức là cần một cách xác định thứ tự mà không cần đồng hồ trung tâm
Đưa Hybrid Logical Clocks(HLCs) vào sử dụng
- Hybrid Logical Clocks(HLCs) là một thuật toán đơn giản nhưng hiệu quả giúp các thiết bị riêng lẻ có thể đi đến đồng thuận thực tế về thứ tự sự kiện
- HLC kết hợp thông tin thời gian vật lý với bộ đếm logic để sử dụng
- Ví dụ:
- Thiết bị A ghi lại sự kiện vào lúc 10:00:00.100, HLC là (10:00:00.100, 0)
- Thiết bị B nhận được thông điệp, dù đồng hồ của nó chậm hơn thì vẫn nâng HLC lên thành (10:00:00.100, 1)
- Nhờ vậy có thể xác định chính xác thứ tự sự kiện bất kể chênh lệch đồng hồ vật lý giữa hai thiết bị
2. Vấn đề xung đột
- Chỉ áp dụng đúng thứ tự thôi là chưa đủ; nếu cùng một dữ liệu bị chỉnh sửa độc lập trên các thiết bị khác nhau thì xung đột là điều không thể tránh khỏi
- Phần lớn hệ thống yêu cầu lập trình viên tự viết mã xử lý xung đột, nhưng điều này gây ra rủi ro lỗi và gánh nặng bảo trì
Ứng dụng CRDTs
- Cách tốt nhất là áp dụng Conflict-Free Replicated Data Types(CRDTs)
- CRDTs đảm bảo rằng dù đồng bộ theo thứ tự nào, hoặc dù áp dụng trùng lặp, trạng thái trên mỗi thiết bị cuối cùng vẫn luôn giống nhau
- Chiến lược CRDT đơn giản nhất là Last-Write-Wins(LWW)
- Gắn timestamp cho mỗi lần cập nhật
- Khi đồng bộ, chọn giá trị có timestamp mới hơn
Ưu điểm của SQLite
- Khi xây dựng ứng dụng local-first, một DB cục bộ nhẹ và đáng tin cậy là yếu tố bắt buộc, và SQLite là lựa chọn tối ưu
- Nếu triển khai chức năng đồng bộ bằng phần mở rộng framework dựa trên SQLite, sẽ có các lợi ích sau
- Việc áp dụng thông điệp rất đơn giản: truy vấn giá trị hiện tại → nếu timestamp mới hơn thì ghi đè → nếu không thì bỏ qua
- Cách này đảm bảo trạng thái hội tụ trên mọi thiết bị bất kể thứ tự đồng bộ
Ý nghĩa của kiến trúc này
- Cấu trúc này hiện thực hóa đồng bộ đơn giản và đáng tin cậy
- Vẫn đáng tin cậy, không mất dữ liệu ngay cả khi offline trong nhiều tuần
- Tính quyết định, luôn hội tụ về trạng thái cuối cùng
- Giải quyết chỉ với phần mở rộng SQLite gọn nhẹ không có phụ thuộc nặng
- Hỗ trợ mọi nền tảng chính như iOS, Android, macOS, Windows, Linux, WASM
Gợi ý cho lập trình viên
- Cần tránh cách chỉ "giả lập" hỗ trợ chế độ offline bằng hàng đợi request đơn giản
- Cần chấp nhận khái niệm eventual consistency và tận dụng các công nghệ hệ thống phân tán đã được kiểm chứng như HLC và CRDT
- Thay vì framework lớn và phức tạp, nên theo đuổi cấu trúc nhỏ gọn, không phụ thuộc
- Kết quả là ứng dụng có thể tận hưởng các lợi thế như khởi chạy tức thì, sử dụng được khi offline, quyền riêng tư được đảm bảo mặc định
Tham khảo mã nguồn mở SQLite-Sync
- Nếu quan tâm đến một engine offline-first đa nền tảng, có thể dùng ngay trong production, bạn có thể tham khảo phần mở rộng mã nguồn mở SQLite-Sync
1 bình luận
Ý kiến trên Hacker News
Cache-Controlđúng cách trong phản hồi API và buộc tầng mạng tuân thủ nó thì giải quyết được rất nhiều vấn đề. Làm vậy cũng cho phép thay đổi thời gian sống của cache từ phía server mà không cần cập nhật app