Đồng Bộ Hóa Phân Biệt Bảng Lua - nói dối e blog

Đồng Bộ Hóa Phân Biệt Bảng Lua

Một đồng nghiệp gần đây đã gặp phải một yêu cầu thú vị: cần thường xuyên truyền một nhóm dữ liệu qua mạng trong hệ thống skynet, tuy nhiên thực tế dữ liệu này thay đổi không thường xuyên. Điều này dẫn đến việc lặp lại quá trình tuần tự hóa và truyền tải một cách không hiệu quả.

Để cụ thể hơn, anh ấy đã thiết kế một nút cổng trong skynet, với nhiệm vụ chính là phát tán thông điệp đến nhóm khách hàng qua giao tiếp nội bộ. Mỗi khách hàng được nhận diện bằng một chuỗi UUID đặc biệt, trong khi mỗi thông điệp đều mang theo danh sách UUID các khách hàng cần nhận. Tuy nhiên, các danh sách này có mức độ trùng lặp rất cao - cùng một danh sách UUID bị đóng gói lặp đi lặp lại trong từng lần phát tán.

Ban đầu tôi nghĩ đến giải pháp thiết kế một giao thức đặc thù cho tình huống này: gán ID cho các nhóm dữ liệu đã gửi, sau đó cả bên gửi và nhận đều sử dụng ID để nén dữ liệu truyền đi. Cụ thể, lần đầu gửi sẽ truyền toàn bộ dữ liệu (full data), các lần sau chỉ cần gửi phần thay đổi (delta). Nếu không có bất kỳ thay đổi nào, chỉ cần truyền ID tương ứng.

Tiếp tục suy nghĩ, tôi nhận thấy cần phải xây dựng một phương pháp đồng bộ hóa phân biệt mang tính tổng quát hơn. Phương pháp này nên có khả năng nhận diện và tránh truyền lại dữ liệu trùng lặp khi truyền nhóm dữ liệu giữa các nút xử lý.

Sau bữa tối, tôi bắt tay xây dựng một mô-đun đơn giản như ý tưởng:

Chúng ta có thể thiết lập kênh truyền đồng bộ hai đầu bằng cách gọi syncobj.source() ở đầu gửi và syncobj.clone() ở đầu nhận. Khi cần xây dựng dữ liệu, đối tượng được tạo thông qua obj = source:new() sẽ phụ thuộc vào kênh truyền này. Để đơn giản hóa, mỗi đối tượng có cấu trúc dạng bảng Lua một lớp (single-layer table).

Trong quá trình xử lý, ta có thể thao tác với obj như một bảng thông thường. Khi cần đồng bộ sang đầu nhận, gọi diff = source:diff(obj) để tạo ra gói phân biệt so với phiên bản trước. Gói phân biệt này có thể tuần tự hóa và gửi đi theo cách thông thường.

Đầu nhận sử dụng cobj = clone:patch(diff) để khôi phục đối tượng. Vì diff chỉ chứa phần dữ liệu thay đổi, kích thước truyền đi sẽ nhỏ hơn nhiều so với việc truyền toàn bộ obj. Trong trường hợp xảy ra lỗi truyền thông, có thể gọi diff = source:reset(obj) để tạo gói dữ liệu toàn phần. Đầu nhận có thể xử lý cả gói phân biệt và gói toàn phần thông qua cùng phương thức clone:patch(diff).

Về mặt giao thức nội bộ, mỗi nhóm dữ liệu đều được gắn kèm một ID tăng dần. ID này nằm trong gói phân biệt, cho phép clone:patch xác định chính xác cách khôi phục dữ liệu. Khi gọi source:reset, thực chất là gán cho đối tượng một ID hoàn toàn mới.

Tuy nhiên, nếu mô-đun hoạt động lâu dài sẽ xuất hiện các đối tượng đã bị hủy bỏ ở đầu nguồn. Mặc dù Lua GC sẽ thu hồi bộ nhớ không sử dụng, đầu nhận vẫn lưu giữ các bản sao cũ. Tương tự, mỗi lần source:reset cũng sẽ tạo ra “rác” ở đầu nhận (các phiên bản cũ).

Để giải quyết vấn đề này, mô-đun cung cấp removeset = source:collect() để trả về danh sách ID không còn sử dụng. Chỉ cần định kỳ truyền danh sách này sang clone:collect(removeset), đầu nhận sẽ tiến hành dọn dẹp các đối tượng không cần thiết.

Giải pháp này mang lại hiệu quả đáng kể trong các tình huống cần truyền tải dữ liệu có tần suất thay đổi thấp nhưng yêu cầu độ cập nhật cao. Đặc biệt, khả năng xử lý tự động giữa gói phân biệt và gói toàn phần giúp tăng độ tin cậy trong môi trường truyền thông không ổn định.

0%