Wednesday, 9 April 2025

Use Redis Pub/Sub to check user online compare to not use

Khi sử dụng Redis Pub/Sub, hệ thống vẫn phải bắn sự kiện qua Private Channel của Reverb để gửi đến client. Tuy nhiên, Redis Pub/Sub mang lại hiệu suất cao hơn so với cách không dùng nó, và mình sẽ giải thích chi tiết tại sao, đồng thời làm rõ sự khác biệt giữa hai cách.


1. Cách hoạt động khi dùng Redis Pub/Sub

Quy trình

  1. User thay đổi trạng thái (ví dụ: User 500 offline):
    • Server gọi UserStatusService::setOffline(500).
    • Redis xóa trạng thái (user:500:status) và publish sự kiện đến các kênh riêng của bạn bè (ví dụ: user:1:status, user:2:status).
  2. Worker Redis Subscriber:
    • Lắng nghe tất cả kênh user:*:status.
    • Khi nhận message từ Redis (ví dụ: user:1:status), worker broadcast sự kiện qua Reverb đến kênh private-user.1.
  3. Client:
    • Mỗi user lắng nghe kênh riêng của mình (ví dụ: private-user.1) qua Laravel Echo.
    • Nhận sự kiện và cập nhật UI.

Tại sao vẫn cần Private Channel?

  • Redis Pub/Sub không giao tiếp trực tiếp với client: Redis chỉ là hệ thống lưu trữ và truyền tin nội bộ giữa các server/process. Nó không thể gửi dữ liệu trực tiếp đến browser qua WebSocket.
  • Reverb (WebSocket): Đảm nhận việc gửi sự kiện từ server đến client qua kết nối WebSocket. Private Channel giúp gửi đúng đối tượng (chỉ bạn bè nhận được thông báo).
  • Kết hợp: Redis Pub/Sub truyền tin nhanh trong backend, Reverb chuyển tiếp tin đó đến client qua Private Channel.

2. So sánh với cách không dùng Redis Pub/Sub

Không dùng Redis Pub/Sub

  1. Quy trình:
    • User 500 offline → Server truy vấn database để lấy danh sách bạn bè (User 1, User 2, User 3).
    • Server broadcast sự kiện qua Reverb đến các kênh private-user.1, private-user.2, private-user.3 bằng cách gọi event() trực tiếp trong PHP:
      php
      foreach ($friendIds as $friendId) { event(new UserStatusChanged($userId, 'offline', $friendId)); }
  2. Nhược điểm:
    • Truy vấn database: Mỗi lần user thay đổi trạng thái, phải query bảng friends (hoặc tương tự) để lấy danh sách bạn bè → chậm, đặc biệt nếu danh sách lớn (1000 bạn).
    • Tải xử lý: PHP phải thực hiện vòng lặp và gửi từng event trong cùng một request → tốn CPU và thời gian.

Dùng Redis Pub/Sub

  1. Quy trình:
    • User 500 offline → Server lấy danh sách bạn bè từ Redis (nếu đã cache) hoặc database, rồi publish đến các kênh Redis (user:1:status, user:2:status).
    • Worker Redis subscribe nhận message và broadcast qua Reverb.
  2. Ưu điểm:
    • Lấy dữ liệu nhanh hơn: Nếu danh sách bạn bè được cache trong Redis (ví dụ: user:500:friends), truy vấn từ Redis (in-memory) nhanh hơn rất nhiều so với database (disk-based).
    • Xử lý bất đồng bộ: Redis Pub/Sub và worker chạy độc lập, không chặn request chính của PHP → giảm tải server.
    • Tốc độ publish: Redis xử lý hàng nghìn message/giây, nhẹ hơn việc PHP gọi event() trong vòng lặp.

3. Tại sao Redis Pub/Sub nhanh hơn?

Lý do chính

  1. Truy vấn nhanh từ Redis:
    • Không dùng Redis Pub/Sub: Mỗi lần broadcast cần query database để lấy danh sách bạn bè → tốn thời gian (vài ms đến hàng chục ms tùy kích thước bảng).
    • Dùng Redis Pub/Sub: Nếu danh sách bạn bè đã cache trong Redis (ví dụ: user:500:friends dưới dạng Set), truy xuất chỉ mất <1ms vì Redis là in-memory.
  2. Bất đồng bộ:
    • Không dùng Redis: PHP xử lý toàn bộ logic (query + broadcast) trong một request → đồng bộ, dễ gây bottleneck.
    • Dùng Redis Pub/Sub: Publish là hành động tức thời, worker xử lý song song → không chặn request chính.
  3. Hiệu suất Pub/Sub:
    • Redis Pub/Sub được thiết kế để truyền tin nhanh, không cần lưu trữ lâu dài, phù hợp cho sự kiện realtime.
    • Gửi 1000 message qua Redis Pub/Sub nhanh hơn gọi 1000 lần event() trong PHP.

Minh họa thời gian

  • Không Redis: Query database (10ms) + vòng lặp 1000 bạn (50ms) = ~60ms cho 1 sự kiện.
  • Có Redis: Cache Redis (1ms) + publish 1000 kênh (5ms) = ~6ms, cộng với worker xử lý bất đồng bộ.

4. Ví dụ cụ thể

Không dùng Redis Pub/Sub

php
public static function setOffline($userId) { Redis::del("user:$userId:status"); $friendIds = User::find($userId)->friends()->pluck('id')->toArray(); // Query database foreach ($friendIds as $friendId) { event(new \App\Events\UserStatusChanged($userId, 'offline', $friendId)); // Gửi từng event } }
  • Chậm vì: Query database + vòng lặp tuần tự.

Dùng Redis Pub/Sub

php
public static function setOffline($userId) { Redis::del("user:$userId:status"); $friendIds = Redis::smembers("user:$userId:friends"); // Lấy từ Redis if (!$friendIds) { $friendIds = User::find($userId)->friends()->pluck('id')->toArray(); Redis::sadd("user:$userId:friends", $friendIds); // Cache lần đầu } foreach ($friendIds as $friendId) { Redis::publish("user:$friendId:status", json_encode(['userId' => $userId, 'status' => 'offline'])); } }
  • Nhanh vì: Lấy từ Redis (nếu đã cache) + publish bất đồng bộ.

Worker Redis

php
public function handle() { Redis::psubscribe(['user:*:status'], function ($message, $channel) { $data = json_decode($message, true); $friendId = explode(':', $channel)[1]; broadcast(new \App\Events\UserStatusChanged($data['userId'], $data['status'], $friendId)); }); }

5. Kết luận

  • Vẫn cần Private Channel: Redis Pub/Sub chỉ thay thế bước gửi sự kiện nội bộ trong backend. Reverb dùng Private Channel để gửi từ server đến client.
  • Nhanh hơn vì:
    1. Truy xuất danh sách bạn bè từ Redis thay vì database.
    2. Publish qua Redis bất đồng bộ, nhẹ hơn gọi event() tuần tự trong PHP.
  • Hiệu quả: Với 1000 user, cách này giảm tải server và tăng tốc độ xử lý từ vài chục ms xuống vài ms.

Thank you

No comments:

Post a Comment

Golang Advanced Interview Q&A