首页 文章

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

提问于
浏览
4

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

  • 从Redis读取值,如果's not there then generate the value via querying SQL, and push it in to Redis so we don' t必须再次计算 .

  • 将值写入Redis,因为我们只是对SQL数据库进行了一些更改,我们知道我们可能已经缓存了它,现在它已经无效了 .

  • 删除值,因为我们知道Redis中的值现在已经过时,我们怀疑没有人会想要它,但它确实让下一个执行操作#1的客户端再次计算它 .

如果我尝试使用StackExchange.Redis,我的挑战是理解如何实现#1和#3 . 如果我通过简单的密钥读取和推送来天真地实现#1,那么在我计算SQL的值并推送它之间完全有可能已经发生了任何数量的其他SQL操作并且还试图将它们的值推送到Redis通过#2或#3 . 例如,请考虑以下顺序:

  • 客户#1想要从上面进行操作#1 [读取] . 它试图读取密钥,看到它不在那里 .

  • Client#1调用SQL数据库来生成值 .

  • 客户端#2对SQL执行某些操作,然后执行上面的操作#2 [写入] . 它将一些新计算的值推送到Redis中 .

  • 客户端#3来了很长时间,在SQL中执行了一些其他操作,并希望对Redis进行操作#3 [删除]知道如果's something cached there, it'不再有效 .

  • 客户端#1将其(现在陈旧的)值推送到Redis .

那么如何实现我的操作#1? Redis提供了一个 WATCH 原语,这使得我可以很容易地对裸机进行操作,在那里我可以观察客户端#1上的密钥发生的其他事情,但是it's not supported by StackExchange.Redis because of how it multiplexes commands . 它在这里已经足够了,因为如果我试着说"push only if key doesn't exist",那就不会像我上面解释的那样阻止比赛 . 这里使用的是模式/最佳实践吗?这似乎是人们想要实现的相当普遍的模式 .

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

1 回答

  • 2

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

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

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

    • 当2个客户开始执行#1操作时会发生什么?他们两个都无法在Redis中找到值并执行SQL查询,然后将它们都写入Redis . 那么我们应该只有一个客户端会更新缓存吗?

    • 如何以正确的写入顺序(操作3)进行保护?

    为什么不乐观锁定

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

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

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

    Redis WATCH / MULTY是一种乐观的锁定,所以他们无法帮助你 - 在尝试使用它们之前,你不知道你的缓存密钥被修改了 .

    什么有用?

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

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

    • 每个缓存键都包含一些有关值的元数据 - 状态,版本和生命周期 . 最后一个不是Redis TTL - 通常如果你的密钥应该在缓存中X时间,元数据中的生命时间有X Y时间,那么Y是一段时间来进行garantie进程更新 .

    • 您永远不会直接删除密钥 - 您只需要更新状态或生命时间 .

    • 每次应用程序从缓存中读取数据时,如果要做出决定 - 如果数据有状态"valid" - 请使用它 . 如果数据有状态"invalid"尝试更新或使用过时数据 .

    如何更新读取(非常重要的是这种“手工制作”乐观和悲观锁定的组合):

    • 尝试设置悲观锁定(在Redis中使用SETEX - read more here) .

    • 如果失败 - 返回过时的数据(记住我们仍然需要可用性) .

    • 如果成功执行SQL查询并写入缓存 .

    • 再次从Redis中读取版本,并与之前提供的版本进行比较 .

    • 如果版本相同 - 将状态标记为"valid" .

    • 释放锁定 .

    如何使无效(您的操作#2,#3):

    • 递增缓存版本并设置状态"invalid" .

    • 如果需要,更新生命时间/ ttl .

    为何如此困难

    • 我们总是可以从缓存中获取并返回值,并且很少有缓存未命中的情况 . 所以我们没有缓存失效级联地狱然后很多进程尝试更新一个密钥 .

    • 我们仍然订购了密钥更新 .

    • 每次只需一个进程即可更新密钥 .

    我有队列!

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

    • 每个修改操作都应该将作业推送到队列 .

    • 只有异步工作者才能执行SQL和更新密钥 .

    • 您仍然需要使用"state"(有效/无效)缓存密钥来使用缓存分离应用程序逻辑 .

    这是答案吗?

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

相关问题