为什么在聊天应用程序中使用Redis?

24

我最近刚刚搭建了一个聊天室,它运行得很好,但我认为我需要将其连接到redis。

据我所知,如果客户端刷新或服务器崩溃,我需要redis来进行扩展和保持一些数据。

1对1聊天的核心组成部分是存储用户,并将socket.id与这些用户关联起来。

var users = {};
io.sockets.on('connection', function (socket) {

  // store the users & socket.id into objects
  users[socket.handshake.headers.user.username] = socket.id;

});

现在,在客户端上,只要输入的是有效用户"Jack",我就可以通过向服务器传递数据来与之聊天,即用户名和消息只会发送给Jack。

var chattingWith = data.nickname; // this is Jack passed from the client side
io.to(users[chattingWith]).emit();
我的问题是,为什么应该使用redis?在redis中应该存储什么?如何与数据进行交互?我正在使用一个io.adapter。
io.adapter(redisIo({ 
  host: 'localhost', 
  port: 6379,
  pubClient: pub,
  subClient: sub
}));

同时,从示例应用程序中阅读代码时,我发现当套接字连接时,它们会将套接字数据保存到Redis中,如下所示。

// store stuff in redis
redisClientPublish.sadd('sockets:for:' + userKey + ':at:' + room_id, socket.id, function(err, socketAdded) {
  if(socketAdded) {
    redisClientPublish.sadd('socketio:sockets', socket.id);
    redisClientPublish.sadd('rooms:' + room_id + ':online', userKey, function(err, userAdded) {
      if(userAdded) {
        redisClientPublish.hincrby('rooms:' + room_id + ':info', 'online', 1);
        redisClientPublish.get('users:' + userKey + ':status', function(err, status) {
          io.sockets.in(room_id).emit('new user', {
            nickname: nickname,
            provider: provider,
            status: status || 'available'
          });
        });
      }
    });
  }
});

他们在进入一个房间时使用它,以获取有关该房间的信息。

app.get('/:id', utils.restrict, function(req, res) {

  console.log(redisClientPublish);

  utils.getRoomInfo(req, res, redisClientPublish, function(room) {

    console.log('Room Info: ' + room); 

    utils.getUsersInRoom(req, res, redisClientPublish, room, function(users) {

      utils.getPublicRoomsInfo(redisClientPublish, function(rooms) {

        utils.getUserStatus(req.user, redisClientPublish, function(status) {
          utils.enterRoom(req, res, room, users, rooms, status);
        });

      });

    });

  });

});

因此,我之所以问这个问题,是因为我有些困惑是否需要在redis中存储任何东西/为什么需要这样做,例如,我们可能有几十万个用户,并且正在聊天的node.js服务器“Jack”和“Mike”出现故障,然后它会更改为指向新的node.js实例。

显然,我希望聊天仍然记得“Jack”的套接字ID是“12333”和“Mike”的套接字ID是“09278”,因此无论何时,“Jack”说嘿,我要发送消息给“Mike/09278”,服务器端套接字都将正确地将其定向。

将用户名存储为键,套接字ID作为值,对于redis来说是明智的使用情况吗?那个socket.id还有效吗?

1个回答

39

Redis是作为聊天数据库相当不错的选择,因为它提供了一些数据结构,不仅对各种聊天使用场景非常方便,而且处理方式也非常高效。它还附带了PubSub消息功能,允许您通过生成多个服务器实例来扩展后端。

使用socket.io-redis适配器扩展socket.io

如果您想运行多个服务器实例,无论是因为单个服务器无法处理越来越多的用户还是为了设置高可用性集群,那么您的服务器实例必须相互通信,以便能够在连接到不同服务器的用户之间传递消息。 socket.io-redis适配器通过使用redis PubSub功能作为中间件来解决此问题。如果您只使用单个服务器实例,则这不会帮助您(实际上我认为它的性能会稍微降低),但是一旦生成第二个服务器,这将完美地解决任何问题,而无需任何烦恼。

想要了解它是如何工作的并获得一些见解吗?在使用它时监视您的开发redis,并查看通过redis推送的内部socket.io消息。

redis-cli
monitor

使用案例及相应的Redis数据类型

将活动对话保存在SET中

Redis集合是一组唯一的字符串。我认为存储socket.io id可能不太可行,因为无法保证用户在重新连接时会获得相同的id。最好将其房间存储起来,并在重新连接时重新加入。您可以将用户输入的每个聊天室(顺便说一句,直接消息可以定义为具有两个参与者的房间,因此在这两种情况下处理方式相同)添加到其房间集中。在服务器重新启动、客户端重新连接或第二个客户端实例时,可以检索整个集合并将用户重新加入他们的房间。

/* note: untested pseudo code just for illustration */
io.sockets.on('connection', function (socket) {
    rooms = await redis.smembers("rooms:userA");
    rooms.foreach (function(room) {
        socket.join(room);
    }

    socket.on('leave', room) {
        socket.leave(room);
        redis.srem("rooms:userA", room);
    } 

    socket.on('join', room) {
        socket.join(room);
        redis.sadd("rooms:userA", room);
    }
}

使用redis LIST保存对话的最后10条消息

Redis列表类似于字符串的持久化数组。您可以将新消息推入列表中,并在列表大小达到阈值时弹出最旧的消息。方便的是,push命令立即返回大小。

socket.on('chatmessage', room, message) {
    if (redis.lpush("conversation:userA:userB", "Hello World") > 10) {
        redis.rpop("conversation:userA:userB");
    }
    io.to(room).emit(message);
}

要获取消息历史记录,请使用lrange:

msgHistory = redis.lrange("conversation:userA:userB", 0, 10)

将一些基本用户详细信息保存在哈希表中

哈希表是一种键/值集合。可以使用它来存储在线状态以及头像 URL 等信息。

io.sockets.on('connection', function (socket) {
    redis.hset("userdata:userA", "status", "online");

    socket.on('disconnect', function () {
        redis.hset("userdata:userA", "status", "offline");
    }
}
在一个排序列表中维护“最近的对话”列表。
排序集与SET类似,但您可以为每个元素分配一个分数值,并按此值检索集合。只需在两个用户之间有互动时使用时间戳作为分数即可。
 socket.on('chatmessage', room, message) {
      io.to(room).emit(message);
      redis.zadd("conversations:userA", new Date().getTime(), room);
 }

 async function getTheTenLatestConversations() {
     return await redis.zrange("conversations:userA", 0, 10);
 }

参考文献


2
最棒的答案!谢谢你! - Alexey Shabramov
我正在使用socket.io-redis进行开发。它确实会跟踪用户订阅的房间,但我不确定具体在哪里。如果我使用集合来跟踪它,是否仍然有好处?我可以只使用socket.io-chat命令来获取用户房间并将其发送到Redis集合中。 - stuartambient
流数据结构可能是最佳选择。 - csandreas1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接