Concurrency 我应该如何在Redis上实现具有并发性的简单缓存? 背景

Concurrency 我应该如何在Redis上实现具有并发性的简单缓存? 背景,concurrency,indexing,redis,Concurrency,Indexing,Redis,我有一个两层的web服务——只有我的应用服务器和一个RDBMS。我想移动到负载平衡器后面相同的应用程序服务器池。我目前正在缓存一堆正在处理的对象。我希望将它们移动到共享的Redis 我有十几个简单的小型业务对象的缓存。例如,我有一组Foos。每个Foo都有一个唯一的FooId和一个所有者ID。 一个“所有者”可能拥有多个foo 在传统的RDBMS中,这只是一个表,其索引位于PK FooId上,索引位于OwnerId上。我将其缓存在一个进程中,只是: Dictionary<int,Foo&g

我有一个两层的web服务——只有我的应用服务器和一个RDBMS。我想移动到负载平衡器后面相同的应用程序服务器池。我目前正在缓存一堆正在处理的对象。我希望将它们移动到共享的Redis

我有十几个简单的小型业务对象的缓存。例如,我有一组
Foos
。每个
Foo
都有一个唯一的
FooId
和一个
所有者ID
。 一个“所有者”可能拥有多个
foo

在传统的RDBMS中,这只是一个表,其索引位于PK FooId上,索引位于OwnerId上。我将其缓存在一个进程中,只是:

Dictionary<int,Foo> _cacheFooById;
Dictionary<int,HashSet<int>> _indexFooIdsByOwnerId;
后来我决定我喜欢:

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;{更新};HSET;EXEC
    因为在我发出
    EXEC
    之前,它甚至不执行
    HGET

    我还阅读了关于使用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
    ,重试会起作用,但是。。。哎呀

    我知道在某些地方,人们使用特殊的Redis钥匙作为其他物体的锁。这是合理的做法吗

    这些应该是像
    ServiceCache:傻瓜锁:{Id}
    ServiceCache:Locks:Foo:{Id}
    这样的顶级键吗? 或者为它们做一个单独的散列-
    ServiceCache:Locks
    带有
    子键Foo:{Id}
    ,或者
    ServiceCache:Locks:Foo
    带有子键
    {Id}


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

    对于您的用例,您不需要使用watch。只需使用
    multi
    +
    exec
    块,就可以消除竞争条件

    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.
    
    伪码-

    
    MULTI();
    SET ("ServiceCache:Foo:" + theFoo.FooId, JsonSerialize(theFoo));
    SADD ("ServiceCache:FooIndexByOwner:" + theFoo.OwnerId, theFoo.FooId);
    SADD ("ServiceCache:FooIndexAll", theFoo.FooId);
    EXEC();
    
    这就足够了,因为
    multi
    做出了以下承诺: “在RIIS事务的执行过程中,另一个客户发出的请求不可能发生”


    您不需要
    监视
    并重试机制,因为您没有在同一事务中读取和写入

    谢谢!这似乎可以使“索引”与主缓存数据保持最新。我想这可以归结为对每个索引使用Redis集,而不是我自己的json序列化列表。我担心这无法解决上一节所述的重叠缓存未命中问题。用块替换简单的HSET会导致相同的问题。在这种情况下,似乎我确实需要在同一事务中读写。也许我应该单独问这个问题。使用事务语义实时维护两个不同的系统并不容易,而且我所知道的任何缓存都不支持。要在同一事务中读/写,还可以使用服务器端Lua脚本。它通常比使用WATCH子句更容易,也不容易出错。