如何使用aioredis pub / sub实现单生产者多消费者

时间:2019-01-12 11:49:32

标签: python redis python-asyncio aiohttp

我有网络应用。该应用程序具有将一些对象数据推送到redis通道的终结点。
另一个端点处理websocket连接,该数据从通道中获取并通过ws发送到客户端。

当我通过ws连接时,邮件仅获得第一个连接的客户端。

如何从具有多个客户端的redis频道中读取消息,而不创建新的订阅?

Websocket处理程序。
我在这里订阅频道,将其保存到应用(init_tram_channel)。然后在我收听频道并发送消息的地方运行作业(run_tram_listening)。

@routes.get('/tram-state-ws/{tram_id}')
async def tram_ws(request: web.Request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    tram_id = int(request.match_info['tram_id'])
    channel_name = f'tram_{tram_id}'

    await init_tram_channel(channel_name, request.app)
    tram_job = await run_tram_listening(
        request=request,
        ws=ws,
        channel=request.app['tram_producers'][channel_name]
    )

    request.app['websockets'].add(ws)
    try:
        async for msg in ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                if msg.data == 'close':
                    await ws.close()
                    break
            if msg.type == aiohttp.WSMsgType.ERROR:
                logging.error(f'ws connection was closed with exception {ws.exception()}')
            else:
                await asyncio.sleep(0.005)
    except asyncio.CancelledError:
        pass
    finally:
        await tram_job.close()
        request.app['websockets'].discard(ws)

    return ws

订阅和保存频道。
每个通道都与唯一对象相关,并且为了不创建与同一对象相关的许多通道,我仅将一个保存到应用程序。 app['tram_producers']是字典。

async def init_tram_channel(
        channel_name: str,
        app: web.Application
):
    if channel_name not in app['tram_producers']:
        channel, = await app['redis'].subscribe(channel_name)
        app['tram_producers'][channel_name] = channel

正在运行的频道用于收听频道。 我通过aiojobs运行它:

async def run_tram_listening(
        request: web.Request,
        ws: web.WebSocketResponse,
        channel: Channel
):
    """
    :return: aiojobs._job.Job object
    """
    listen_redis_job = await spawn(
        request,
        _read_tram_subscription(
            ws,
            channel
        )
    )
    return listen_redis_job

Coro我在哪里收听和发送消息:

async def _read_tram_subscription(
        ws: web.WebSocketResponse,
        channel: Channel
):
    try:
        async for msg in channel.iter():
            tram_data = msg.decode()
            await ws.send_json(tram_data)
    except asyncio.CancelledError:
        pass
    except Exception as e:
        logging.error(msg=e, exc_info=e)

2 个答案:

答案 0 :(得分:1)

在aioredis github问题中发现了以下代码(我已将其用于任务)。

class TramProducer:
    def __init__(self, channel: aioredis.Channel):
        self._future = None
        self._channel = channel

    def __aiter__(self):
        return self

    def __anext__(self):
        return asyncio.shield(self._get_message())

    async def _get_message(self):
        if self._future:
            return await self._future

        self._future = asyncio.get_event_loop().create_future()
        message = await self._channel.get_json()
        future, self._future = self._future, None
        future.set_result(message)
        return message

那么,它是如何工作的? TramProducer包装了我们获取消息的方式。
如@Messa

  从一个Redis订阅中仅收到一次

消息。

因此,只有TramProducer的一个客户端正在从Redis检索消息,而其他客户端正在等待将来的结果,该结果将在从通道接收消息后设置。

如果self._future已初始化,则意味着有人正在等待来自redis的消息,因此我们将只等待self._future的结果。

TramProducer用法(我从问题中举了一个例子):

async def _read_tram_subscription(
        ws: web.WebSocketResponse,
        tram_producer: TramProducer
):
    try:
        async for msg in tram_producer:
            await ws.send_json(msg)
    except asyncio.CancelledError:
        pass
    except Exception as e:
        logging.error(msg=e, exc_info=e)

TramProducer初始化:

async def init_tram_channel(
        channel_name: str,
        app: web.Application
):
    if channel_name not in app['tram_producers']:
        channel, = await app['redis'].subscribe(channel_name)
        app['tram_producers'][channel_name] = TramProducer(channel)

我认为这可能对某人有用。
完整项目请点击https://gitlab.com/tram-emulator/tram-server

答案 1 :(得分:0)

我猜想从一个Redis订阅中只会收到一条消息,并且如果您的应用程序中有多个侦听器,那么只有一个侦听器会收到它。

因此,您需要在应用程序内部创建类似mini pub / sub之类的东西,以将消息分发给所有侦听器(在这种情况下为websocket连接)。

前段时间,我制作了一个aiohttp websocket聊天示例-不是使用Redis,但至少存在跨websocket分发版:https://github.com/messa/aiohttp-nextjs-demo-chat/blob/master/chat_web/views/api.py

关键是要拥有一个应用程序范围的message_subcriptions,其中每个websocket连接都会注册自己,或者可能注册自己的asyncio.Queue(在示例中我使用了Event,但这不是最优的),并且每当来自Redis的消息被发送到所有相关队列。

当然,当websocket连接结束(客户端退订,断开连接,失败...)时,应删除队列(如果Redis订阅是最后一个监听它的连接,则可能会取消该订阅)。

Asyncio并不意味着我们应该忽略队列:)同样,熟悉一次组合多个任务(从websocket读取,从消息队列读取,也许从某些通知队列读取...)也很熟悉。使用队列还可以帮助您更干净地处理客户端重新连接(不丢失任何消息)。