StackExchange Redis的分区密钥空间

Mår*_*röm 7 redis stackexchange.redis

在开发使用Redis的组件时,我发现它是一个很好的模式,可以为该组件使用的所有键添加前缀,这样它就不会干扰其他组件.

例子:

  • 管理用户的组件可能使用前缀为的键,user:管理日志的组件可能使用前缀为的键log:.

  • 在多租户系统中,我希望每个客户在Redis中使用单独的密钥空间,以确保他们的数据不会干扰.然后,前缀将类似于customer:<id>:与特定客户相关的所有密钥.

使用Redis对我来说仍然是新的东西.我对这种分区模式的第一个想法是为每个分区使用单独的数据库标识符.但是,这似乎是一个坏主意,因为数据库的数量有限,似乎是一个即将被弃用的功能.

另一种方法是让每个组件获得一个IDatabase实例,并RedisKey使用它来为所有键添加前缀.(我正在使用StackExchange.Redis)

我一直在寻找一个IDatabase自动为所有键添加前缀的包装器,以便组件可以按IDatabase原样使用接口,而不必担心其键空间.我没找到任何东西.

所以我的问题是:在StackExchange Redis上使用分区键空间的推荐方法什么?

我现在正在考虑实现我自己的IDatabase包装器,它将为所有键添加前缀.我认为大多数方法只是将它们的调用转发给内部IDatabase实例.但是,某些方法需要更多工作:例如SORTRANDOMKEY.

Mår*_*röm 6

IDatabase现在创建了一个提供密钥空间分区的包装器.

包装器是使用扩展方法创建的 IDatabase

    ConnectionMultiplexer multiplexer = ConnectionMultiplexer.Connect("localhost");
    IDatabase fullDatabase = multiplexer.GetDatabase();
    IDatabase partitioned = fullDatabase.GetKeyspacePartition("my-partition");
Run Code Online (Sandbox Code Playgroud)

几乎所有分区包装器中的方法都具有相同的结构:

public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{
    return this.Inner.SetAdd(this.ToInner(key), value, flags);
}
Run Code Online (Sandbox Code Playgroud)

它们只是将调用转发到内部数据库,并RedisKey在传递它们之前将键空间前缀添加到任何参数.

CreateBatchCreateTransaction的方法简单地创建用于这些接口包装,但具有相同的基包装类(如最包裹方法由定义IDatabaseAsync).

KeyRandomAsyncKeyRandom不支持的方法.调用将抛出一个NotSupportedException.对我来说这不是一个问题,引用@Marc Gravell:

我无法想到实现这一目标的任何理智方式,但我怀疑NotSupportedException("指定键前缀时不支持RANDOMKEY")完全合理(这不是常用的命令)

我还没有实现ScriptEvaluate,ScriptEvaluateAsync因为我不清楚如何处理RedisResult返回值.这些方法的输入参数接受RedisKey哪些应该加前缀,但脚本本身可以返回键,在这种情况下,我认为(最有意义的是)取消修正这些键.目前,这些方法将抛出NotImplementedException......

排序方式(Sort,SortAsync,SortAndStoreSortAndStoreAsync)有特殊处理byget参数.这些都是正常的前缀,除非它们具有以下特殊值之一:nosortfor by#for get.

最后,为了允许前缀ITransaction.AddCondition我必须使用一点反射:

internal static class ConditionHelper
{
    public static Condition Rewrite(this Condition outer, Func<RedisKey, RedisKey> rewriteFunc)
    {
        ThrowIf.ArgNull(outer, "outer");
        ThrowIf.ArgNull(rewriteFunc, "rewriteFunc");

        Type conditionType = outer.GetType();
        object inner = FormatterServices.GetUninitializedObject(conditionType);

        foreach (FieldInfo field in conditionType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
        {
            if (field.FieldType == typeof(RedisKey))
            {
                field.SetValue(inner, rewriteFunc((RedisKey)field.GetValue(outer)));
            }
            else
            {
                field.SetValue(inner, field.GetValue(outer));
            }
        }

        return (Condition)inner;
    }
}
Run Code Online (Sandbox Code Playgroud)

包装器使用这个帮助器,如下所示:

internal Condition ToInner(Condition outer)
{
    if (outer == null)
    {
        return outer;
    }
    else
    {
        return outer.Rewrite(this.ToInner);
    }
}
Run Code Online (Sandbox Code Playgroud)

有几种其他ToInner方法可以包含不同类型的参数,RedisKey但它们或多或少都会最终调用:

internal RedisKey ToInner(RedisKey outer)
{
    return this.Prefix + outer;
}
Run Code Online (Sandbox Code Playgroud)

我现在已经为此创建了一个pull请求:

https://github.com/StackExchange/StackExchange.Redis/pull/92

现在调用扩展方法,WithKeyPrefix并且不再需要用于重写条件的反射黑客,因为新代码可以访问Condition类的内部.


Mar*_*ell 5

有趣的建议。请注意,redis已经通过数据库编号提供了简单的隔离机制,例如:

// note: default database is 0
var logdb = muxer.GetDatabase(1);
var userdb = muxer.GetDatabase(2);
Run Code Online (Sandbox Code Playgroud)

StackExchange.Redis 将处理向正确的数据库发出命令的所有工作 - 即通过发出的命令logdb将针对数据库 1 发出。

优点:

  • 内置的
  • 与所有客户合作
  • 提供完整的键空间隔离
  • 不需要额外的每键前缀空间
  • KEYS与, SCAN, FLUSHDB, RANDOMKEY,SORT等一起使用
  • 您可以通过以下方式获得高级每个数据库键空间指标INFO

缺点:

  • redis 集群不支持
  • 不通过 twemproxy 等中介提供支持

笔记:

  • 数据库数量是一个配置选项;IIRC 默认为 16(数字 0-15),但可以通过以下方式在配置文件中进行调整:

    databases 400 # moar databases!!!
    
    Run Code Online (Sandbox Code Playgroud)

这实际上就是我们(Stack Overflow)使用 Redis 进行多租户的方式;数据库 0 是“全局”,1 是“stackoverflow”等。还应该清楚的是,如果需要的话,使用 and 将整个数据库迁移到不同的节点是一件相当简单的事情SCANMIGRATE或更可能的是:SCAN, DUMP,PTTLRESTORE- 避免阻塞)。

由于redis-cluster不支持数据库分区,因此这里可能存在一个有效的场景,但还应该注意的是,redis节点很容易启动,因此另一个有效的选项很简单:为每个(不同的端口)使用不同的redis组数字等)——这还有一个优点是允许节点之间真正的并发(CPU 隔离)。


不过,你的建议也不无道理,这里实际上有“先验”...再次,很大程度上与我们(Stack Overflow)如何使用 Redis 有关:虽然数据库可以很好地隔离,但 Redis 目前不为通道(pub/sub)提供隔离。因此,StackExchange.Redis 实际上包含一个ChannelPrefix选项ConfigurationOptions,允许您指定在接收通知时自动添加和删除的前缀PUBLISH。因此,如果您ChannelPrefixfoo:,并且您发布了事件bar,则实际事件将发布到频道foo:bar;同样:您的任何回调都只能看到bar. 这可能对于数据库来说也是可行的,但要强调:目前此配置选项位于多路复用器级别 - 而不是单独的ISubscriber。为了与您呈现的场景进行比较,这需要达到一定的水平IDatabase

可能,但工作量相当大。如果可能的话,我建议研究仅使用数据库编号的选项......