You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nest.js WebSocket网关结合Redis Broker推送失效问题排查与解决

Nest.js多实例WebSocket推送失效问题及解决方案

问题背景

单实例环境下,Nest.js的WebSocket网关可正常推送消息,但部署到负载均衡后的多实例环境时,客户端可能连接到与消息推送端不同的实例,导致推送失效。

尝试通过Redis Pub/Sub作为消息代理实现跨实例消息同步:消息能正常存入Redis并被订阅端读取,但调用推送方法时,目标房间始终显示无连接客户端——尽管已在handleConnection中确认客户端成功加入对应房间。调整Gateway作用域为默认值后问题仍存在。


初始实现代码

SocketIOAdapter

export class SocketIOAdapter extends IoAdapter {
  constructor(private app: INestApplicationContext, private configService: ConfigService) {
    super(app);
  }

  createIOServer(port: number, options: ServerOptions) {
    const clientPort = parseInt(this.configService.getOrThrow("PORT"));

    const cors = {
      origin: [
        `http://localhost:${clientPort}`,
        new RegExp(`/^http:\/\/192\.168\.1\.([1-9]|[1-9]\d):${clientPort}$/`),
      ],
    };

    const optionsWithCORS: ServerOptions = {
      ...options,
      cors,
    };

    const server: Server = super.createIOServer(port, optionsWithCORS);

    const orderRepository = this.app.get(OrderRepository);
    server
      .of("/orders")
      .use(createTokenMiddleware(orderRepository));

    return server;
  }
}

const createTokenMiddleware =
  (orderRepository: OrderRepository) =>
  async (socket: Socket, next) => {
    // 此处使用orderRepository执行逻辑
    next();
  } catch {
    next(new Error("FORBIDDEN"));
  }
};

OrderGateway

@WebSocketGateway({
  namespace: "/orders",
})
@Injectable()
export class OrderGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(OrderGateway.name);

  @WebSocketServer() io: Namespace;

  afterInit(): void {
    this.logger.log("Websocket Gateway initialized.");
  }

  async handleConnection(client: Socket) {
    const sockets = this.io.sockets;

    // 此处逻辑确定客户端需要加入的房间
    const roomsToJoin = [...]
    await client.join(roomsToJoin);
  }

  async handleDisconnect(client: Socket) {
    this.logger.log(`Disconnected socket id: ${client.id}`);
  }

  public emitOrderStatusChangeNotification(order: OrderDTO) {
    this.io
      .to("根据order生成的roomId")
      .emit("order_status_changed", JSON.stringify(order));
  }
}

尝试的Redis Pub/Sub方案

消息发布服务

@Injectable()
export class NotificationPublisherService {
  constructor(@Inject("ORDER_NOTIFICATION_SERVICE") private client: ClientProxy) {}

  async publishEvent(order: OrderDTO) {
    console.log("将消息发送到redis");
    this.client.emit(Constants.notificationEventName, order);
  }
}

消息订阅控制器

@Controller()
export class NotificationSuscriberController {
  private readonly logger = new Logger(NotificationSuscriberController.name);

  constructor(private readonly orderGateway: OrderGateway) {}

  @EventPattern(Constants.notificationEventName)
  async handleOrderStatusChangeEvent(order: OrderDTO) {
    try {
      this.orderGateway.emitOrderStatusChangeNotification(order);
    } catch (err) {
      this.logger.log("发送通知出错");
    }
  }
}

将原有的emitOrderStatusChangeNotification调用替换为publishEvent后,消息能正常在Redis中收发,但推送时目标房间无客户端连接的问题未解决。


最终解决方案

通过扩展IoAdapter并集成Redis适配器,实现跨实例的WebSocket房间状态同步,修改后的代码如下:

修改后的SocketIOAdapter

export class SocketIOAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;
  constructor(private app: INestApplicationContext, private configService: ConfigService) {
    super(app);
  }

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({ url: "redis://localhost:6379" });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options: ServerOptions) {
    const clientPort = parseInt(this.configService.getOrThrow("PORT"));

    const cors = {
      origin: [
        `http://localhost:${clientPort}`,
        new RegExp(`/^http:\/\/192\.168\.1\.([1-9]|[1-9]\d):${clientPort}$/`),
      ],
    };

    const optionsWithCORS: ServerOptions = {
      ...options,
      cors,
    };

    const server: Server = super.createIOServer(port, optionsWithCORS);

    const orderRepository = this.app.get(OrderRepository);
    server
      .adapter(this.adapterConstructor)
      .of(`/orders`)
      .use(createTokenMiddleware(orderRepository));

    return server;
  }
}

const createTokenMiddleware =
  (orderRepository: OrderRepository) =>
  async (socket: Socket, next) => {
    // 此处使用orderRepository执行逻辑
    next();
  } catch {
    next(new Error("FORBIDDEN"));
  }
};

main.ts配置

const redisIoAdapter = new SocketIOAdapter(app, configService);
await redisIoAdapter.connectToRedis();

通过上述修改,所有实例共享Redis作为WebSocket的状态存储,跨实例的消息推送可正常触达目标房间的客户端。


内容的提问来源于stack exchange,提问作者Juan Chaves

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 06:45:38