我应该如何在Redis上实现简单的并发缓存?

时间:2013-11-03 18:50:53

标签: concurrency indexing redis

背景

我有一个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时,我永远不必担心有些是缓存而有些则不是。

我已经做了什么

第一个/最简单的答案似乎是使用普通的'SETGET复合键和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));

然而,这有两个方面被打破。

  1. 两个并发操作可以同时读取/修改/写入。后者“赢”了最后的HSET,前者的索引更新丢失了。
  2. 另一个操作可以读取第一行和第二行之间的索引。它会错过它应该找到的Foo。
  3. 那么如何正确索引?

    我想我可以使用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。

    我想WATCHMULTI,重试会有效,但...... ick。

    我知道在某些地方人们使用特殊的Redis键作为其他对象的锁。这是一种合理的方法吗?

    那些应该是ServiceCache:FooLocks:{Id}ServiceCache:Locks:Foo:{Id}等顶级键吗? 或者为它们制作单独的哈希 - ServiceCache:Locks subkeys Foo:{Id}ServiceCache:Locks:Foo子项{Id}

    如果一个事务(或整个服务器)在“持有”锁定时崩溃,我将如何处理被遗弃的锁?

1 个答案:

答案 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和重试机制,因为您没有在同一事务中阅读