在开发使用Redis的组件时,我发现它是一个很好的模式,可以为该组件使用的所有键添加前缀,以便它不会干扰其他组件。
示例:
管理用户的组件可能使用以user:
为前缀的密钥,管理日志的组件可能使用以log:
为前缀的密钥。
在多租户系统中,我希望每个客户在Redis中使用单独的密钥空间,以确保他们的数据不会干扰。对于与特定客户相关的所有密钥,前缀将类似于customer:<id>:
。
使用Redis对我来说仍然是新的东西。我对这种分区模式的第一个想法是为每个分区使用单独的数据库标识符。然而,这似乎是一个坏主意,因为数据库的数量有限,似乎是一个即将被弃用的功能。
另一种方法是让每个组件获得一个IDatabase
实例和一个RedisKey
,它将用于为所有键添加前缀。 (我正在使用StackExchange.Redis)
我一直在寻找一个自动为所有键添加前缀的IDatabase
包装器,以便组件可以按原样使用IDatabase
接口,而不必担心其键空间。我没有找到任何东西。
所以我的问题是:在StackExchange Redis上使用分区键空间的推荐方法是什么?
我现在正在考虑实现我自己的IDatabase
包装器,它将为所有键添加前缀。我认为大多数方法只是将它们的调用转发给内部IDatabase
实例。但是,某些方法需要更多工作:例如SORT和RANDOMKEY。
答案 0 :(得分:6)
我现在创建了一个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
参数。
CreateBatch
和CreateTransaction
方法只为这些接口创建包装器,但具有相同的基本包装类(因为大多数包装方法都由IDatabaseAsync
定义)。
不支持KeyRandomAsync
和KeyRandom
方法。调用将抛出NotSupportedException
。这不是我的问题,引用@Marc Gravell:
我无法想到任何理智的方法,但我怀疑NotSupportedException(“指定键前缀时不支持RANDOMKEY”)完全合理(这不是常用的命令)< / p>
我尚未实施ScriptEvaluate
和ScriptEvaluateAsync
,因为我不清楚如何处理RedisResult
返回值。这些方法的输入参数接受应该加前缀的RedisKey
,但是脚本本身可以返回键,在这种情况下,我认为它会使 unsfix <(大多数)感觉到/ em>那些钥匙。目前,这些方法将抛出NotImplementedException
...
排序方法(Sort
,SortAsync
,SortAndStore
和SortAndStoreAsync
)对by
和get
参数进行了特殊处理。这些都是正常的前缀,除非它们具有以下特殊值之一:nosort
by
和#
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
类的内部。
答案 1 :(得分: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
缺点:
注意:
数据库的数量是一个配置选项; IIRC默认为16(数字0-15),但可以通过以下方式在配置文件中调整:
databases 400 # moar databases!!!
这实际上是我们(Stack Overflow)如何使用具有多租户的redis;数据库0是&#34;全局&#34;,1是&#34; stackoverflow&#34;等。还应该清楚的是,如果需要,将整个数据库迁移到不同的节点是相当简单的事情使用SCAN
和MIGRATE
(或更有可能:SCAN
,DUMP
,PTTL
和RESTORE
- 以避免阻止。
由于redis-cluster不支持数据库分区,因此这里可能存在一个有效的方案,但是还应该注意redis节点很容易启动,所以另一个有效选项就是:使用不同的redis每个的组(不同的端口号等) - 这也有利于允许节点之间真正的并发(CPU隔离)。
但是,你的建议并非不合理;实际上有&#34;先前&#34;这里......再次,主要与我们(Stack Overflow)如何使用redis有关:虽然数据库可以很好地隔离密钥,但redis目前没有为频道提供隔离(发布/订阅)。因此,StackExchange.Red实际上在ChannelPrefix
上包含ConfigurationOptions
选项,允许您指定在PUBLISH
期间自动添加并在接收通知时删除的前缀。因此,如果您的ChannelPrefix
为foo:
,并且您发布了事件bar
,则实际事件将发布到频道foo:bar
;同样:你只看到bar
的任何回调。它可能这对于数据库来说也是可行的,但是要强调:此配置选项位于多路复用器级别 - 而不是个人{{1 }}。为了与您提供的方案相比,这需要处于ISubscriber
级别。
可能,但工作量不错。如果可能的话,我建议调查一下只使用数据库号码的选项......