Giới thiệu
Dự án Zedis, một phiên bản Redis được viết bằng Zig, đã là một hành trình thú vị giúp tôi học hỏi về lập trình hệ thống cấp thấp. Trong bài viết này, tôi sẽ chia sẻ về hai tính năng cốt lõi mà tôi đã triển khai: Pub/Sub và chiến lược quản lý bộ nhớ.
Tại Sao Lại Xây Dựng Redis Clone Bằng Zig?
Tôi đã đặt ra mục tiêu cá nhân là làm chủ Zig trong năm nay, và việc xây dựng một dự án như thế này là cách hoàn hảo để đạt được điều đó. Zig mang lại nhiều tính năng hấp dẫn cho các hệ thống hiệu suất cao, chẳng hạn như comptime (cho phép mã chạy tại thời gian biên dịch) và quản lý bộ nhớ rõ ràng. Zedis đã trở thành sân chơi của tôi để khám phá mọi thứ từ lập trình mạng đến bộ cấp phát tùy chỉnh.
Khám Phá Tính Năng: Pub/Sub
Việc triển khai cơ chế Publish/Subscribe là một thử thách thú vị. Về cơ bản, đây là một hệ thống nhắn tin giúp tách biệt người gửi (nhà xuất bản) và người nhận (người đăng ký).
Cách Thức Hoạt Động Của Pub/Sub Trong Zedis
- Kênh và Người Đăng Ký: Khi một client đăng ký vào một kênh, họ sẽ được thêm vào danh sách người đăng ký của kênh đó. Tôi đã sử dụng
std.StringHashMap([]u64)để ánh xạ tên kênh với danh sách ID client. - Xuất Bản Một Tin Nhắn: Khi một tin nhắn được xuất bản đến một kênh, máy chủ sẽ duyệt qua danh sách người đăng ký và gửi tin nhắn đến mỗi kết nối của họ.
- Chế Độ Pub/Sub: Khi một client đăng ký vào một kênh, họ sẽ vào chế độ “Pub/Sub” đặc biệt, nơi họ chỉ có thể nhận tin nhắn và không thể thực hiện các lệnh khác.
Ví Dụ Về Hàm subscribe
zig
pub fn subscribe(client: *Client, args: []const Value) !void {
var pubsub_context = client.pubsub_context;
// Vào chế độ pubsub khi đăng ký lần đầu
if (!client.is_in_pubsub_mode) {
client.enterPubSubMode();
}
var i: i64 = 0;
for (args[1..]) |item| {
const channel_name = item.asSlice();
// Đảm bảo kênh tồn tại
pubsub_context.ensureChannelExists(channel_name) catch {
try client.writeError("ERR failed to create channel");
continue;
};
// Đăng ký client vào kênh
pubsub_context.subscribeToChannel(channel_name, client.client_id) catch |err| switch (err) {
error.ChannelFull => {
try client.writeError("ERR maximum subscribers per channel reached");
continue;
},
else => {
try client.writeError("ERR failed to subscribe to channel");
continue;
},
};
const subscription_count = i + 1;
const response_tuple = .{
"subscribe",
channel_name,
subscription_count,
};
// Sử dụng writer chung để gửi tuple dưới dạng mảng RESP.
try client.writeTupleAsArray(response_tuple);
i += 1;
}
}
Chiến Lược Quản Lý Bộ Nhớ
Một trong những khía cạnh thú vị nhất của dự án này là thiết kế chiến lược quản lý bộ nhớ. Tôi đã chọn một phương pháp lai để cân bằng giữa hiệu suất và việc sử dụng bộ nhớ:
KeyValueAllocator: Đây là một bộ cấp phát tùy chỉnh mà tôi xây dựng cho kho lưu trữ key-value chính. Nó sử dụng một vùng nhớ cố định và có chính sách loại bỏ đơn giản để giữ trong ngân sách của mình. Khi bộ cấp phát hết bộ nhớ, nó có thể loại bỏ tất cả các khóa để tạo không gian cho những khóa mới.- Bộ Cấp Phát Arena: Đối với các cấp phát tạm thời, ngắn hạn (như phân tích lệnh), tôi sử dụng bộ cấp phát arena. Điều này rất nhanh vì nó chỉ cần tăng một con trỏ cho các cấp phát mới và giải phóng toàn bộ bộ nhớ cùng một lúc khi không còn cần thiết.
- Bể Cố Định: Đối với các đối tượng thường xuyên được cấp phát và giải phóng, như kết nối client, tôi sử dụng một bể kích thước cố định. Điều này giúp tránh chi phí của việc cấp phát và giải phóng động.
Phương pháp này cho phép Zedis sử dụng bộ nhớ hiệu quả trong khi vẫn duy trì hiệu suất cao.
Kế Hoạch Tương Lai
Tôi rất hào hứng để tiếp tục xây dựng trên nền tảng này. Dưới đây là những gì tôi dự định thực hiện:
- Triển khai AOF (Append Only File) logging
- Thêm hỗ trợ cho các cấu trúc dữ liệu khác như danh sách và tập hợp
- Triển khai hết hạn khóa
- Thêm hỗ trợ clustering
Khám Phá Zedis
Tôi rất muốn bạn kiểm tra Zedis trên GitHub, thử nghiệm và cho tôi biết suy nghĩ của bạn. Mọi đóng góp đều được hoan nghênh!
Cảm ơn bạn đã đọc!