我有一个2层的Web服务 - 只是我的应用服务器和RDBMS。我想转移到负载均衡器后面的相同应用服务器池。我目前正在缓存一堆对象。我希望将它们转移到共享的Redis。
我有十几个简单的小型业务对象缓存。例如,我有一组Foos
。每个Foo
都有一个唯一的FooId
和一个OwnerId
。
一个“所有者”可能拥有多个Foos
。
在传统的RDBMS中,这只是一个在PK FooId上有索引的表,在OwnerId上有一个索引。我只是在一个过程中缓存这个:
Dictionary<int,Foo> _cacheFooById;
Dictionary<int,HashSet<int>> _indexFooIdsByOwnerId;
直接读取来自此处,写入此处并转到RDBMS。 我通常有这个不变量:
“对于给定的组[由OwnerId说],整个组都在缓存中,或者都不是。”
因此,当我在Foo上缓存未命中时,我从RDBMS中提取所有所有者的其他Foo的Foo 和。更新确保使索引保持最新并尊重不变量。当所有者调用GetMyFoos时,我永远不必担心有些是缓存而有些则不是。
第一个/最简单的答案似乎是使用普通的'SET
和GET
复合键和json值:
SET( "ServiceCache:Foo:" + theFoo.Id, JsonSerialize(theFoo));
我后来决定我喜欢:
HSET( "ServiceCache:Foo", theFoo.FooId, JsonSerialize(theFoo));
这让我可以将一个缓存中的所有值都作为HVALS获取。它也感觉正确 - 我实际上是将哈希表移动到Redis,所以也许我的顶级项目应该是哈希值。
这适用于第一顺序。如果我的高级代码如下:
UpdateCache(myFoo);
AddToIndex(myFoo);
转化为:
HSET ("ServiceCache:Foo", theFoo.FooId, JsonSerialize(theFoo));
var myFoos = JsonDeserialize( HGET ("ServiceCache:FooIndex", theFoo.OwnerId) );
myFoos.Add(theFoo.OwnerId);
HSET ("ServiceCache:FooIndex", theFoo.OwnerId, JsonSerialize(myFoos));
然而,这有两个方面被打破。
HSET
,前者的索引更新丢失了。我想我可以使用Redis集代替索引的json编码值。 这将解决部分问题,因为“添加索引 - 如果不存在”将是原子的。
我还读到了使用MULTI
作为“交易”,但它似乎并不像我想要的那样。我是对的,我不能MULTI; HGET; {update}; HSET; EXEC
,因为在发出HGET
之前它甚至没有EXEC
吗?
我还读到了使用WATCH 和 MULTI进行乐观并发,然后在失败时重试。但WATCH仅适用于顶级键。所以它回到SET/GET
而不是HSET/HGET
。现在我需要一个类似索引的新东西来支持获取给定缓存中的所有值。
如果我理解正确的话,我可以将所有这些事情结合起来完成这项工作。类似的东西:
while(!succeeded)
{
WATCH( "ServiceCache:Foo:" + theFoo.FooId );
WATCH( "ServiceCache:FooIndexByOwner:" + theFoo.OwnerId );
WATCH( "ServiceCache:FooIndexAll" );
MULTI();
SET ("ServiceCache:Foo:" + theFoo.FooId, JsonSerialize(theFoo));
SADD ("ServiceCache:FooIndexByOwner:" + theFoo.OwnerId, theFoo.FooId);
SADD ("ServiceCache:FooIndexAll", theFoo.FooId);
EXEC();
//TODO somehow set succeeded properly
}
最后,我必须将此伪代码转换为实际代码,具体取决于我的客户端库如何使用WATCH/MULTI/EXEC
;看起来他们需要某种上下文来将它们连接在一起。
总而言之,这对于一个非常常见的情况来说似乎很复杂; 我不禁想到,有一种更好,更聪明的Redis-ish方法可以做我不会看到的事情。
即使我没有索引,仍然存在(可能是罕见的)竞争条件。
A: HGET - cache miss
B: HGET - cache miss
A: SELECT
B: SELECT
A: HSET
C: HGET - cache hit
C: UPDATE
C: HSET
B: HSET ** this is stale data that's clobbering C's update.
请注意,C可能只是一个非常快的A。
我想WATCH
,MULTI
,重试会有效,但...... ick。
我知道在某些地方人们使用特殊的Redis键作为其他对象的锁。这是一种合理的方法吗?
那些应该是ServiceCache:FooLocks:{Id}
或ServiceCache:Locks:Foo:{Id}
等顶级键吗?
或者为它们制作单独的哈希 - ServiceCache:Locks
subkeys Foo:{Id}
或ServiceCache:Locks:Foo
子项{Id}
?
如果一个事务(或整个服务器)在“持有”锁定时崩溃,我将如何处理被遗弃的锁?
答案 0 :(得分:2)
对于您的使用案例,您不需要使用手表。您只需使用multi
+ exec
块即可消除竞争条件。
在伪代码中 -
MULTI();
SET ("ServiceCache:Foo:" + theFoo.FooId, JsonSerialize(theFoo));
SADD ("ServiceCache:FooIndexByOwner:" + theFoo.OwnerId, theFoo.FooId);
SADD ("ServiceCache:FooIndexAll", theFoo.FooId);
EXEC();
这已足够,因为multi
做出以下承诺:
“在Redis事务执行过程中,不会发生其他客户发出的请求”
您不需要watch
和重试机制,因为您没有在同一事务中阅读和。