背景
我有一个 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 时,我不必担心有些被缓存而有些没有。
我已经做了什么
第一个/最简单的答案似乎是使用普通的 ol'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
,而前者的索引更新丢失。 - 另一个操作可以读取第一行和第二行之间的索引。它会错过它应该找到的 Foo 。
那么如何正确索引呢?
我想我可以使用 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 的方式来做我没有看到的事情。
如何正确锁定?
即使我没有索引,仍然存在(可能很少见)竞争条件。
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}
or ServiceCache:Locks:Foo:{Id}
?或者为它们制作一个单独的散列 -ServiceCache:Locks
使用subkeys Foo:{Id}
或ServiceCache:Locks:Foo
使用子键{Id}
?
我将如何解决废弃的锁,比如如果事务(或整个服务器)在“持有”锁时崩溃?