Hành trình Xây dựng Ứng dụng Ghi chú Theo Thời gian Thực: Thử thách với Operational Transformation (OT) từ A đến Z
Lê Lân
0
Hành Trình Xây Dựng Ứng Dụng Ghi Chú Cộng Tác Thời Gian Thực Với Operational Transformation
Mở Đầu
Bạn từng nghĩ xây dựng một ứng dụng ghi chú đơn giản sẽ dễ dàng đến mức nào? Tôi cũng vậy, cho tới khi đối mặt với những thách thức chưa từng có về cộng tác thời gian thực và xử lý xung đột trong chỉnh sửa văn bản.
Cách đây hơn một năm, tôi quyết định bắt tay vào một dự án để nâng cao kỹ năng phát triển phần mềm: một ứng dụng ghi chú hoạt động mượt mà trên nhiều thiết bị, làm việc offline và đặc biệt có thể undo/redo mọi thao tác. Để thực sự học hỏi, tôi tránh dùng các thư viện hỗ trợ sẵn như ShareDB hay Yjs mà chọn tự xây dựng thuật toán xử lý xung đột dựa trên Operational Transformation (OT) từ đầu. Bài viết này sẽ đưa bạn vào sâu bên trong thử thách hấp dẫn của một kỹ sư phần mềm khi tự thiết kế OT và cách những nguyên lý này được áp dụng trong thực tế, thông qua các ví dụ minh họa cụ thể.
Khái Niệm Operational Transformation (OT)
OT Có Phải Chỉ Là Đơn Giản Điều Chỉnh Vị Trí Không?
Đơn giản nhất, OT là kỹ thuật giúp đồng bộ tài liệu được chỉnh sửa đồng thời bởi nhiều người dùng. Ý tưởng ban đầu là điều chỉnh lại chỉ số (index) của các thay đổi để tránh ghi đè lẫn nhau.
Tuy nhiên, thực tế không hề đơn giản như vậy. Tôi mất nhiều vòng lặp thử nghiệm mới có thể làm đúng hàm transform và đặc biệt là xử lý undo/redo trong môi trường cộng tác mà không làm sai lệch tài liệu hay ý định người dùng.
Các Thành Phần Chính Trong OT
Composition (Ghép nối): Gộp hai thay đổi thành một thay đổi mới.
Transform (Biến đổi): Điều chỉnh hai thay đổi đồng thời để tránh xung đột.
Undo/Redo: Quản lý việc hoàn tác và làm lại trong môi trường có nhiều người thay đổi.
Các Ví Dụ Thực Tế Về OT
Ví Dụ 1: Xử Lý Việc Chỉnh Sửa Đồng Thời Không Xung Đột
Alice và Bob cùng bắt đầu với văn bản "Hello!".
Alice chèn " world" trước dấu chấm than.
Bob chèn " Bob!" sau dấu chấm than.
Trong quá trình xử lý, server:
Áp dụng thay đổi của Alice trước.
Khi áp dụng thay đổi của Bob, nó thực hiện transform, dịch chuyển các vị trí chèn của Bob sang phải để không đè lên phần mới của Alice.
Kết quả cả hai đều thấy:
"Hello world! Bob!"
Bước
Hành động
1
Alice chèn " world" trước dấu
!
2
Bob chèn " Bob!" sau dấu
!
3
Server điều chỉnh vị trí chèn của Bob dịch sang phải 6 ký tự
4
Kết quả cuối cùng đã đồng bộ với cả hai người dùng
Ví Dụ 2: Xử Lý Chỉnh Sửa Đồng Thời Có Xung Đột
Bối cảnh vẫn là Hello!.
Alice chèn " world" trước dấu chấm than.
Bob chèn " cat" cũng trước dấu chấm than.
Server xử lý:
Áp dụng thay đổi của Alice trước.
Chuyển đổi thay đổi của Bob dựa trên đầu vào đã thay đổi của Alice.
Server chọn left bias: chèn chỉnh sửa đầu tiên vào bên trái.
Kết quả:
"Hello world cat!"
<b>Chú Ý:</b> Cách xử lý đồng thời chèn ký tự cùng vị trí cần một chính sách (bias), vì kết quả có thể khác nhau nhưng đều hợp lệ.
Những Thách Thức Và Bài Học Khi Tự Xây Dựng OT
Lý Do Tôi Không Dùng Thư Viện Có Sẵn
Tôi muốn hiểu từng chi tiết kỹ thuật, không muốn các thuật toán giống như hộp đen. Do đó, tôi lựa chọn xây dựng OT từ đầu, giới hạn trong:
Chỉ xử lý văn bản thuần (không định dạng phong phú).
Kiến trúc server tập trung.
Tối giản giao diện API để dễ duy trì.
Sai Lầm Về Lexicographical Bias
Ban đầu tôi thử chọn bias dựa trên thứ tự từ điển (lexicographical), tưởng hợp lý nhưng nó gây lỗi khó nhằn khi áp dụng undo/redo và còn chưa ổn định khi dùng với các ngôn ngữ có quy tắc đặc biệt.
Giải pháp cuối cùng: Left bias (ưu tiên thao tác xử lý trước).
Quản Lý Trạng Thái Mutable Khó Khăn
Sử dụng trạng thái mutable làm tôi gặp rất nhiều lỗi khó debug. Chuyển sang sử dụng immutable state với thư viện immer.js đã cải thiện đáng kể độ ổn định.
Thiết Kế API Tối Giản
API ban đầu rối rắm, dễ gây lỗi và khó bảo trì. Sau nhiều iteration, tôi thu gọn giao diện chỉ còn những thành phần thiết yếu.
Xử Lý Undo/Redo Khi Cộng Tác
Undo trong môi trường nhiều người dùng có thể bị các thay đổi ngoài tác động trực tiếp lên cú undo này. Phải transform thao tác undo sao cho hợp lý với các thay đổi ngoại cảnh.
Việc Viết Lại Mã Nguồn Toàn Bộ OT
Do nhiều lỗi và kiến trúc chưa tối ưu, tôi quyết định viết lại OT hoàn toàn lần thứ hai với immutable state và API được thiết kế lại.
Việc xây dựng OT từ đầu tuy cực kỳ thách thức, nhưng giúp tôi có cái nhìn sâu sắc về sự phức tạp của hệ thống chỉnh sửa cộng tác thời gian thực.
Các Khối Xây Dựng Cốt Lõi Của OT: Composition, Transform, Undo
Composition (Ghép Nối Thay Đổi)
Hai thay đổi được kết hợp tạo thành thay đổi tổng thể mới. Ký hiệu: A ⋅ B (áp dụng B trên A).
Ví Dụ
A: Insert "Hello"
B: Retain "Hello" + Insert " world!"
Kết quả:
A ⋅ B = Insert "Hello world!"
Transform (Biến Đổi Song Song)
Core của OT giúp điều chỉnh hai thay đổi cạnh tranh để đảm bảo kết quả áp dụng theo thứ tự nào cũng giống nhau.
Nguyên Tắc Cơ Bản
R1: Các insert trong A trở thành retain trong transform(A, B)
R2: Các insert trong B giữ nguyên trong transform(A, B)
R3: Retain những ký tự được retain trong cả hai thay đổi (có thể cần điều chỉnh vị trí)
Ví Dụ Thực Tế
Giả sử bản gốc T = "is long cute tail":
Bob thay thế "is long " bằng "big "
Alice thay thế "is " bằng "has ", và "cute " bằng "round "
Kết quả cuối cùng cả hai cùng nhìn thấy:
"has big round tail"
Pseudocode Transform
Functiontransform(A, B, isAfirst)
Initialize result list
For each position in changes
If opA and opB retain -> retain character
ElseIf opA and opB insert
If isAfirst -> opA insert becomes retain, opB insert giữ nguyên
Else -> ngược lại
ElseIf opA insert -> trở thành retain
ElseIf opB insert -> giữ nguyên insert
Return result
EndFunction
Undo Giữa Các Thay Đổi Ngoại Cảnh
Undo cần biến đổi để tương thích với các thay đổi do người khác tạo ra sau khi thay đổi được undo thực hiện.
Ví Dụ Undo Sau Khi Có Thay Đổi Ngoại Cảnh
Bob viết "abc", undo stack chứa thao tác insert.
Alice chèn "hi" lên đầu -> tài liệu thành "hiabc".
Bob undo thao tác cuối cùng, vị trí xóa phải được điều chỉnh phù hợp để xóa đúng ký tự "c".
Pseudocode Undo
Procedureundo()
op = pop undo stack
applyOp = inverse(op)
For each external in op.externalChanges
Addtransform(external, applyOp) to next undo context
applyOp = transform(applyOp, external)
Push applyOp vào redo stack
Áp dụng applyOp lên tài liệu
EndProcedure
Undo/Redo Đối Xứng Và No-op
Có thể undo vẫn tạo no-op (không thay đổi gì) nhưng redo no-op thì bị loại bỏ.
No-op undo vẫn giúp phục hồi những đoạn văn bản bị người khác xóa qua thao tác external.
Kết Luận
Việc tự xây dựng bộ máy Operational Transformation từ đầu không chỉ là thử thách kỹ thuật mà còn là hành trình học tập sâu sắc giúp tôi thấu hiểu cơ chế hoạt động của các app cộng tác thời gian thực. Mặc dù dùng thư viện có sẵn sẽ tiết kiệm rất nhiều công sức, lựa chọn tự trải nghiệm giúp tôi nâng cao đáng kể kỹ năng lập trình và kiến trúc hệ thống.
Dự án mở rộng hơn cả ứng dụng ghi chú đơn thuần - nó là minh chứng cho tinh thần học hỏi và tự thử thách bản thân của tôi trong lĩnh vực phát triển phần mềm. Nếu bạn quan tâm, hãy thử trải nghiệm demo trực tiếp hoặc khám phá mã nguồn trên GitHub. Tôi rất mong nhận được phản hồi và trao đổi thêm từ các bạn!
Tham Khảo
Etherpad Foundation. Etherpad EasySync Technical Manual. Link PDF
Sun, C., Ellis, C. Operational Transformation in Real-Time Group Editors: Issues, Algorithms, and Achievements.
Koskinen, J. Understanding Operational Transformation.