Redis【有与无】【API-3】在Redis模块中阻塞(Blocking)命令

本文基于Redis 6.0.9版本,前提至少 Redis 3.0或更高版本。

目录

1.在Redis模块中阻塞(Blocking)命令

1.1.阻塞和恢复的工作方式

1.2.解除阻塞时传递回复数据

1.3.中止阻塞客户

1.4.使用单个函数实现命令,回复和超时回调

1.5.在线程内处理数据副本

1.6.未来的工作


1.在Redis模块中阻塞(Blocking)命令

Redis的内置命令集中有一些阻塞命令。 最常用的一种是BLPOP (或symmetric  BLPOP ),它阻塞等待元素到达列表的对象。

关于阻塞命令的一个有趣的事实是,它们不会阻塞整个服务器,而只会阻塞客户端调用它们。 通常,阻塞的原因是我们希望发生一些外部事件:这可能是Redis数据结构中的某些变化(例如在BLPOP 情况下),线程中发生了长时间的计算,从网络接收一些数据等等。 

Redis模块也具有实现阻塞命令的能力,该文档显示了API的工作原理,并描述了一些可用于对阻塞命令进行建模的模式。

注意:此API当前处于试验阶段,因此仅在定义了宏REDISMODULE_EXPERIMENTAL_API时才能使用。 这是必需的,因为这些调用仍未处于设计的最后阶段,因此将来可能会更改,某些部分可能已弃用,依此类推。

要使用模块API的这一部分,请包含以下模块标头:

#define REDISMODULE_EXPERIMENTAL_API
#include "redismodule.h"

1.1.阻塞和恢复的工作方式

注意:你可能想查看 src/modules 目录内Redis源树中的helloblock.c示例,以获取有关如何应用阻塞API的简单易懂的示例。

在Redis模块中,命令由回调函数实现,当用户调用特定命令时,该回调函数由Redis核心调用。 通常,回调会终止其执行,并向客户端发送一些回复。 改为使用以下函数,实现module命令的函数可能会要求将客户端置于阻塞状态:

RedisModuleBlockedClient *RedisModule_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms);

该函数返回RedisModuleBlockedClient对象,该对象以后将用于解除阻塞客户端。 自变量具有以下含义:

  • ctx 是API其他部分中通常的命令执行上下文。
  • reply_callback 是回调,具有与普通命令功能相同的原型,即在解除阻塞客户端以将回复返回给客户端时调用。
  • timeout_callback 是回调,具有与客户端达到ms超时时调用的普通命令函数相同的原型。
  • free_privdata 是为了释放私有数据而调用的回调。 私有数据是指向一些数据的指针,这些数据在用于解锁客户端的API和将回复发送给客户端的回调之间传递。 我们将在本文后面的部分中看到这种机制的工作原理。
  • ms 是超时(以毫秒为单位)。 达到超时时,将调用超时回调,并且客户端将自动中止。

客户端被阻塞后,可以使用以下API对其进行阻塞:

int RedisModule_UnblockClient(RedisModuleBlockedClient *bc, void *privdata);

该函数将上次对 RedisModule_BlockClient() 的调用返回的已阻塞客户端对象作为参数,并取消阻塞该客户端。 在客户端被解除阻塞之前,立即调用在客户端被阻塞时指定的reply_callback 函数:该函数将有权访问此处使用的privdata 指针。

重要说明:上面的函数是线程安全的,可以在执行某些工作的线程内调用该函数,以实现阻塞客户端的命令。

解除阻塞客户端后,将使用free_privdata回调自动释放privdata数据。 这很有用,因为在客户端超时或与服务器断开连接的情况下,永远不会调用Reply回调,因此由外部函数来负责在需要时释放传递的数据非常重要。

为了更好地理解API的工作原理,我们可以想象编写一条命令阻塞客户端一秒钟,然后发送回复“Hello!”。

注意:Arity检查和其他非重要事项未在其命令中实现,以使示例简单。

int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
                         int argc)
{
    RedisModuleBlockedClient *bc =
        RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);

    pthread_t tid;
    pthread_create(&tid,NULL,threadmain,bc);

    return REDISMODULE_OK;
}

void *threadmain(void *arg) {
    RedisModuleBlockedClient *bc = arg;

    sleep(1); /* Wait one second and unblock. */
    RedisModule_UnblockClient(bc,NULL);
}

上面的命令会尽快阻塞客户端,并生成一个线程,该线程将等待一秒钟并取消阻止该客户端。 让我们检查一下回复和超时回调,在我们的例子中,它们非常相似,因为它们只是使用不同的回复类型来回复客户端。

int reply_func(RedisModuleCtx *ctx, RedisModuleString **argv,
               int argc)
{
    return RedisModule_ReplyWithSimpleString(ctx,"Hello!");
}

int timeout_func(RedisModuleCtx *ctx, RedisModuleString **argv,
               int argc)
{
    return RedisModule_ReplyWithNull(ctx);
}

回复回调仅发送“Hello!”。 字符串到客户端。 这里重要的一点是,当客户端不受线程阻塞时,将调用Reply回调。

timeout命令返回NULL,这通常在实际的Redis阻塞命令超时时发生。

1.2.解除阻塞时传递回复数据

上面的示例很容易理解,但是缺少实际的阻塞命令实现的重要现实方面:通常,回复功能将需要知道对客户机进行回复的内容,并且当客户机不受阻塞时,通常会提供此信息。

我们可以修改上面的示例,以便线程在等待一秒钟后生成一个随机数。 您可以将其视为某种实际的扩展操作。 然后,这个随机数可以传递给回复函数,以便我们将其返回给命令调用者。 为了使它起作用,我们将功能修改如下:

void *threadmain(void *arg) {
    RedisModuleBlockedClient *bc = arg;

    sleep(1); /* Wait one second and unblock. */

    long *mynumber = RedisModule_Alloc(sizeof(long));
    *mynumber = rand();
    RedisModule_UnblockClient(bc,mynumber);
}

如您所见,现在,取消阻塞调用正在将一些私有数据(即mynumber指针)传递给回复回调。 为了获取此私有数据,回复回调将使用以下函数:

void *RedisModule_GetBlockedClientPrivateData(RedisModuleCtx *ctx);

所以我们的回复回调是这样修改的:

int reply_func(RedisModuleCtx *ctx, RedisModuleString **argv,
               int argc)
{
    long *mynumber = RedisModule_GetBlockedClientPrivateData(ctx);
    /* IMPORTANT: don't free mynumber here, but in the
     * free privdata callback. */
    return RedisModule_ReplyWithLongLong(ctx,mynumber);
}

请注意,当使用RedisModule_BlockClient(),阻塞客户端时,我们还需要传递free_privdata函数,因为必须释放分配的long值。 我们的回调将如下所示:

void free_privdata(void *privdata) {
    RedisModule_Free(privdata);
}

注意:必须强调一点,最好在free_privdata回调中释放私有数据,因为如果客户端断开连接或超时,则可能不会调用Reply函数。

还请注意,始终可以使用 GetBlockedClientPrivateData() API从超时回调中访问私有数据。

1.3.中止阻塞客户

有时会出现一个问题,就是我们需要分配资源才能实现非阻塞命令。 因此,我们阻塞了客户端,例如,尝试创建线程,但是线程创建函数返回错误。 在这种情况下该如何恢复? 我们不想让客户端被阻塞,也不想调用UnblockClient() ,因为这将触发调用回调。

在这种情况下,最好的方法是使用以下功能:

int RedisModule_AbortBlock(RedisModuleBlockedClient *bc);

实际上这是如何使用它:

int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
                         int argc)
{
    RedisModuleBlockedClient *bc =
        RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);

    pthread_t tid;
    if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
        RedisModule_AbortBlock(bc);
        RedisModule_ReplyWithError(ctx,"Sorry can't create a thread");
    }

    return REDISMODULE_OK;
}

客户端将被解除阻塞,但不会调用回复回调。

1.4.使用单个函数实现命令,回复和超时回调

可以使用以下功能,以与实现主要命令功能的功能相同的功能来实现回复和回调:

int RedisModule_IsBlockedReplyRequest(RedisModuleCtx *ctx);
int RedisModule_IsBlockedTimeoutRequest(RedisModuleCtx *ctx);

因此,我可以重写示例命令,而无需使用单独的回复和超时回调:

int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
                         int argc)
{
    if (RedisModule_IsBlockedReplyRequest(ctx)) {
        long *mynumber = RedisModule_GetBlockedClientPrivateData(ctx);
        return RedisModule_ReplyWithLongLong(ctx,mynumber);
    } else if (RedisModule_IsBlockedTimeoutRequest) {
        return RedisModule_ReplyWithNull(ctx);
    }

    RedisModuleBlockedClient *bc =
        RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);

    pthread_t tid;
    if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
        RedisModule_AbortBlock(bc);
        RedisModule_ReplyWithError(ctx,"Sorry can't create a thread");
    }

    return REDISMODULE_OK;
}

功能上是相同的,但是有些人会更喜欢冗长的实现,它将大多数命令逻辑集中在一个函数中。

1.5.在线程内处理数据副本

为了与实现命令的slow部分的线程一起使用,一个有趣的模式是使用数据的副本,以便在对键执行某些操作的同时,用户仍可以看到旧版本。 但是,当线程终止其工作时,将交换表示形式,并使用新的经过处理的版本。

这种方法的一个示例是Neural Redis module,该模块在不同线程中训练神经网络,而用户仍然可以执行和检查其旧版本。

1.6.未来的工作

API正在开发中,以便允许从线程以安全的方式调用Redis模块的API,以便线程化命令可以访问数据空间并执行增量操作。

此功能没有ETA,但它可能会在Redis 4.0发行过程中的某些时候出现。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

琴 韵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值