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

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

>从Redis读取值,如果不存在则通过查询SQL生成值,并将其推入Redis,这样我们就不必再次计算.
>给Redis写入值,因为我们只是对我们的SQL数据库进行了一些更改,我们知道我们可能已经缓存了它,现在它已经无效了.
>删除值,因为我们知道Redis中的值现在已经过时,我们怀疑没有人会想要它,但现在重新计算的工作太多了.我们可以让操作#1的下一个客户再次计算它.

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

>客户#1想要从上面进行操作#1 [读取].它试图读取密钥,看到它不在那里.
>客户端#1调用SQL数据库来生成值.
>客户端#2对SQL执行某些操作,然后执行上面的操作#2 [写入].它将一些新计算的值推送到Redis中.
>客户端#3很长,在SQL中做了一些其他操作,并且想要对Redis进行操作#3 [删除]知道如果有缓存的东西,它就不再有效了.
>客户端#1将其(现在陈旧的)值推送到Redis.

那么我该如何实现我的操作#1? Redis提供了一个WATCH原语,可以很容易地对裸机进行操作,在那里我可以观察客户端#1上的密钥发生的其他事情,但是it’s not supported by StackExchange.Redis because of how it multiplexes commands.这里的条件操作不够充分,因为如果我尝试说“仅当钥匙不存在时才按下”,这并不妨碍我如上所述的比赛.这里使用的是模式/最佳实践吗?这似乎是人们想要实现的相当普遍的模式.

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

最佳答案 它似乎是关于正确的缓存失效策略的问题,而不是关于Redis的问题.为什么我这么认为–Redis WATCH / MULTI是一种乐观的锁定策略和这种

锁定不适合大多数具有缓存的情况,其中db read query可能是一个解决缓存的问题.在您的操作#3描述中,您写道:

It’s too much work to recompute now. We’re OK letting the next client who does operation #1 compute it again.

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

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

为什么不乐观锁定

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

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

If there is no conflict – you update your data. If there is a conflict, resolve it, typically by aborting the transaction and restart it if still need to update data.

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

什么有用?

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

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

>每个缓存键都包含一些关于值的元数据 – 状态,版本和生命周期.最后一个不是Redis TTL – 通常是你的密钥应该在缓存中保存X时间,终身
在元数据中有X Y时间,有一段时间来进行garantie进程更新.
>您永远不会直接删除密钥 – 您只需更新状态或生命时间.
>每次应用程序从缓存中读取数据时应该做出决定 – 如果数据的状态为“有效” – 请使用它.如果数据状态为“无效”,请尝试更新或使用过时数据.

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

>尝试设置悲观锁定(在Redis中使用SETEX – read more here).
>如果失败 – 返回过时数据(记住我们仍然需要可用性).
>如果成功执行SQL查询并写入缓存.
>再次阅读Redis的版本,并与之前的版本进行比较.
>如果版本相同 – 将状态标记为“有效”.
>解锁.

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

>增加缓存版本并设置状态“无效”.
>如果需要,更新生命时间/ ttl.

为何如此困难

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

我排队了!

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

>每个修改操作都应该将作业推送到队列.
>只有异步工作者才能执行SQL并更新密钥.
>您仍然需要使用“状态”(有效/无效)缓存密钥来使用缓存分离应用程序逻辑.

这是答案吗?

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

点赞