8

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

例子:

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

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

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

对此的替代方法是让每个组件获取一个IDatabase实例,并RedisKey使用它为所有键添加前缀。(我正在使用StackExchange.Redis

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

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

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

4

2 回答 2

8

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

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

    ConnectionMultiplexer multiplexer = ConnectionMultiplexer.Connect("localhost");
    IDatabase fullDatabase = multiplexer.GetDatabase();
    IDatabase partitioned = fullDatabase.GetKeyspacePartition("my-partition");

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

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

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

CreateBatchand方法只是为这些CreateTransaction接口创建包装器,但使用相同的基本包装器类(因为大多数包装方法由 定义IDatabaseAsync)。

不支持KeyRandomAsyncand方法。KeyRandom调用将抛出一个NotSupportedException. 这不是我关心的问题,引用@Marc Gravell:

我想不出任何明智的方法来实现这一点,但我怀疑 NotSupportedException("RANDOMKEY is not supported when a key-prefix is specified") 是完全合理的(无论如何这不是常用的命令)

我还没有实现ScriptEvaluateScriptEvaluateAsync因为我不清楚我应该如何处理RedisResult返回值。这些方法的输入参数接受RedisKey应该加前缀的参数,但是脚本本身可以返回键,在这种情况下,我认为(最)不加前缀是有意义的。暂时,这些方法会抛出一个NotImplementedException......

排序方法(SortSortAsync和)对SortAndStore和参数有SortAndStoreAsync特殊处理。除非它们具有特殊值之一: for和for ,否则这些前缀正常。bygetnosortby#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;
    }
}

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

internal Condition ToInner(Condition outer)
{
    if (outer == null)
    {
        return outer;
    }
    else
    {
        return outer.Rewrite(this.ToInner);
    }
}

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

internal RedisKey ToInner(RedisKey outer)
{
    return this.Prefix + outer;
}

我现在为此创建了一个拉取请求:

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

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

于 2014-09-25T10:03:28.573 回答
3

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

// note: default database is 0
var logdb = muxer.GetDatabase(1);
var userdb = muxer.GetDatabase(2);

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

优点:

  • 内置
  • 与所有客户合作
  • 提供完整的键空间隔离
  • 前缀不需要额外的每个键空间
  • KEYS, SCAN, FLUSHDB, RANDOMKEY,SORT等一起使用
  • 您可以通过INFO

缺点:

  • 不支持 redis-cluster
  • 不受 twemproxy 等中介机构的支持

笔记:

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

    databases 400 # moar databases!!!
    

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

由于 redis-cluster 不支持数据库分区,这里可能有一个有效的场景,但也需要注意的是,redis 节点很容易自旋,所以另一个有效的选择很简单:为每个(不同的端口)使用不同的 redis 组数字等) - 这也将具有允许节点之间真正并发的优点(CPU隔离)。


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

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

于 2014-09-25T08:48:58.763 回答