使用StackExchange.Redis安全地设置密钥,同时允许删除

时间:2016-01-21 02:38:48

标签: redis stackexchange.redis

我正在尝试将Redis用作位于SQL数据库前面的缓存。在高层次上我想实现这些操作:

  1. 从Redis读取值,如果不存在,则通过查询SQL生成值,然后将其推送到Redis,这样我们就不必再次计算。
  2. 向Redis写入值,因为我们只是对我们的SQL数据库进行了一些更改,并且我们知道我们可能已经缓存它并且它现在无效。
  3. 删除值,因为我们知道Redis中的值现在已经过时,我们怀疑没有人会想要它,但是现在重新计算它的工作太多了。我们可以让操作#1的下一个客户再次计算它。
  4. 如果我尝试使用StackExchange.Redis,我的挑战是理解如何实现#1和#3。如果我通过简单的密钥读取和推送来天真地实现#1,那么我之间完全有可能在计算SQL的值并推送它之前可能已经发生了任何其他SQL操作并且还试图推送他们通过#2或#3进入Redis的价值观。例如,请考虑以下顺序:

    1. 客户#1想从上面进行操作#1 [读取]。它试图读取密钥,看不到它。
    2. 客户端#1调用SQL数据库来生成值。
    3. 客户端#2对SQL执行某些操作,然后执行上面的操作#2 [写入]。它将一些新计算的值推送到Redis。
    4. 客户端#3来了很长时间,在SQL中执行了一些其他操作,并希望对Redis进行操作#3 [删除],因为知道如果那里有缓存的东西,它就不再有效了。
    5. 客户端#1将其(现在陈旧的)值推送到Redis。
    6. 那么如何实施我的操作#1? Redis提供了一个WATCH原语,这可以很容易地对付裸机,我可以观察客户端#1上的密钥发生的其他事情,但是it's not supported by StackExchange.Redis because of how it multiplexes commands。它的有条件操作在这里是不够的,因为如果我尝试说"仅当键不存在时才按下#34;这不会阻止比赛,因为我如上所述。这里使用的是模式/最佳实践吗?这似乎是人们想要实现的一种相当普遍的模式。

      我有一个想法是我可以使用一个单独的键,每次我对主键执行一些操作时会增加,然后可以使用StackExchange.Redis'有条件的操作,但这看起来很糟糕。

1 个答案:

答案 0 :(得分:2)

看起来像关于正确的缓存失效策略的问题,而不是关于Redis的问题。为什么我这么认为 - Redis WATCH / MULTI 是一种乐观锁定策略这种 锁定不适合大多数具有缓存的情况,其中db read query可能是一个解决缓存的问题。在您的操作#3描述中,您写道:

  

现在重新计算是太多的工作了。我们可以让操作#1的下一个客户再次计算它。

因此,我们可以继续read update案例作为更新策略。在我们继续之前,还有一些问题:

  1. 当2个客户开始执行#1操作时会发生这种情况?他们两个都无法在Redis中找到值并执行SQL查询,然后将它们写入Redis。那么我们应该只有一个客户端会更新缓存吗?
  2. 我们如何以正确的写入顺序(操作3)进行保护?
  3. 为什么不乐观锁定

    Optimistic concurrency control假设多个交易可以经常完成而不会相互干扰。在运行时,事务使用数据资源而不获取对这些资源的锁定。在提交之前,每个事务都会验证没有其他事务已修改它已读取的数据。如果检查显示有相互矛盾的修改,则提交事务将回滚并重新启动。

    您可以在wikipedia中阅读有关OCC交易阶段的信息,但用几句话来说:

      

    如果没有冲突 - 您更新数据。如果存在冲突,请解决此问题,通常是通过中止事务并在仍需要更新数据时重新启动它。

    Redis WATCH / MULTY是一种乐观的锁定,因此他们无法帮助您 - 在尝试使用它们之前,您不知道您的缓存密钥已被修改。

    什么有效?

    每当你倾听有人讲述锁定时 - 在听完一些话之后你会听到妥协,性能和一致性与可用性。最后一对是最重要的。

    在大多数高负载系统中,可用性是赢家。这意味着缓存?通常这样的情况:

    1. 每个缓存键都包含一些有关值的元数据 - 状态,版本和生命周期。最后一个不是Redis TTL - 通常是你的密钥应该在缓存中保存X时间,终身 在元数据中有X + Y时间,有一段时间来进行garantie进程更新。
    2. 您永远不会直接删除密钥 - 您只需要更新状态或生命时间。
    3. 每次应用程序从缓存中读取数据时,如果要做出决定 - 如果数据的状态为“有效” - 请使用它。如果数据状态为“无效”,请尝试更新或使用过时数据。
    4. 如何更新读取(非常重要的是这种“手工制作”乐观和悲观锁定的组合):

      1. 尝试设置悲观锁定(在Redis中使用SETEX - read more here)。
      2. 如果失败 - 返回过时数据(记住我们仍然需要可用性)。
      3. 如果成功执行SQL查询并写入缓存。
      4. 再次阅读Redis的版本并与之前的版本进行比较。
      5. 如果版本相同 - 将状态标记为“有效”。
      6. 释放锁定。
      7. 如何使(您的操作#2,#3)无效:

        1. 增加缓存版本并设置状态“无效”。
        2. 如果需要,请更新生命时间/ ttl。
        3. 为何如此困难

          1. 我们总是可以从缓存中获取并返回值,并且很少有缓存未命中的情况。所以我们没有缓存失效级联地狱然后很多进程尝试更新 一把钥匙。
          2. 我们仍然订购了密钥更新。
          3. 每次只需一个进程即可更新密钥。
          4. 我有队列!

            对不起,您之前没有说过 - 我不会全部写完。如果队列全部变得更简单:

            1. 每个修改操作都应该将作业推送到队列。
            2. 只有异步工作者才能执行SQL和更新密钥。
            3. 您仍然需要使用“状态”(有效/无效)缓存密钥来分离具有缓存的应用程序逻辑。
            4. 这是答案吗?

              Actualy是,同时没有。这是一种可能的解决方案。缓存失效是许多可能解决方案的复杂问题 - 其中之一 可能很简单,其他 - 复杂。在大多数情况下,取决于混凝土应用的实际商务要求。