2024 Clojure 状态调查!中分享您的看法。

欢迎!请查看关于页面以获得更多有关该工作方式的信息。

+1
by by 携 Gregory Haskins in core.cache by by

我试图调和 clojure.cache 的行为,一种是文档/代码所暗示的,另一种是我实际观察到的。我指的是这条线

https://github.com/clojure/core.cache/blob/99fbd669a429bc800ff32b64b21cf82b6b486f06/src/main/clojure/clojure/core/cache/wrapped.clj#L43

value-fn(以及 wrap-fn)即使在重试的情况下也只会被调用(最多)一次,因此没有缓存洪水风险。
当缓存发生冲突时,似乎很容易让 value-fn 被多次调用。我在 repl 中模拟了这种行为,如下所示

在这种情况下,当线程竞争时,错过函数在 10 次中有 8 次被调用。我在现实世界中也看到了这种行为。

(require '[clojure.core.cache.wrapped :as cache])
=> nil
(def c (cache/fifo-cache-factory {}))
=> #'user/c
(pmap (fn [_] (cache/lookup-or-miss c :foo (fn [k] (println "miss") k))) (range 10))
miss
miss
miss
miss
miss
miss
miss
miss
=> (:foo :foo :foo :foo :foo :foo :foo :foo :foo :foo)

我不完全理解的是为什么当 swap! 保护 c/through-cache 的调用时。我猜这与 swap 解决冲突的方式有关。但是,如果是这样,难道这不意味着文档是错误的吗?

这在 fifo 和 ttl 缓存中都可重复,我还没有尝试其他的。

commented by comment by comment by Gregory Haskins by

我了解到更多关于发生的事情,所以我在这里更新的,为了永世。
我还找到了解决我特定问题的方法。如果有必要考虑支持有效用例的话,我会将其修改为通用问题陈述。

所以,基本问题与 swap! 解决冲突的方式有关。我用以下代码演示了这一点

(def d (atom 0))

(pmap (fn [_] (swap! d (fn [x] (println "x:" x) (inc x)))) (range 10))
x: 0
0x: 0
x: x: 0

0

0x: x:x: 0
 0

x: 1
2x
 0
 1
2x
x: x: 3
2x

4x: x: 24x

 2
x: x: 5 2

5
5
 1
6
2x
 7
  
 1
7
 2
8
76x
=> (1 10 2 9 4 3 6 5 8 7)

最终结果是正确的(增加了10个值),但是为了得到这个结果,交换函数被调用超过30次,且存在许多内部状态的重复(例如,“0”)。我可以推测,在内部,swap! 在并行运行函数时,当发生冲突时会重新播放一些。

如果我们把这个问题转换到缓存中,有一些情况下,c/通过缓存的并行实例see has? = false,因此运行 missed 分支,然后在更新发生冲突时再次运行。

这没有问题,尽管我仍然可能会说,至少文档说明很混乱,也许应该指出这是关于单线程执行的。

我将就一个一般性的问题陈述及其潜在解决方案作出第二条评论。
by
问题陈述

我需要在某些I/O操作前实现缓存语义。这些I/O操作是不可变的,但与缓存查找的开销相比,它们相对较昂贵。

一个例子是OpenID JSON Web Key Set (JWKS)协议:来自Clojure HTTP服务器集群的并行HTTP请求可能包含一个JSON Web Token (JWT),需要验证它是否由受信任的发行者签名。JWKS协议提供了一种机制从受信任的发行者检索用于验证的公钥。请求与密钥的比例可以达到亿比1,因此这种形式的外观查找非常有用。

迄今为止,我已经使用clojure.cache.wrapped构建了这个缓存,其中缓存本质上持有关于JWT的响应的承诺,通过JWT key-id进行索引。基本思想是,当遇到一个新的JWT key-id时,缓存未命中,就会启动一个到IDP的HTTP请求以检索密钥,并缓存一个承诺。启动请求的开销很小,即使往返响应可能需要几毫秒。

缓存客户端可以以任何合理的方式解引用这个承诺。长时间已解决密钥会立即解引用。正在解决中的密钥可能有多个客户端在等待承诺。

我现在遇到的问题与clojure.cache在缓存高度争用时的效率低下有关。在上面的场景中,一个给定的key-id通常同时到期。因此,API客户端倾向于大约在同一时间刷新JWT,并给系统提供一个新的尚未缓存的密钥。

结果是,从多个线程涌向同一个键,从而对缓存中的特定条目施加了很大的压力。尽管响应是稳定的,但我们只需要一个唯一的key ID,但由于每种value-fn都启动了针对相同key ID的JWKS操作,因此效率较低。 sistema仍在当前行为下正确运行,但效率较低,因为我们发布了多个冗余的I/O操作以获取同一个key ID。

有人可能会认为这种I/O是“副作用”的,因此与(swap!)不兼容。虽然我同意突变副作用确实是个问题,但这些“只读”副作用与缓存的一般概念并不矛盾,因此如果缓存库能够通过消除对相同键的value-fn的重复重入来支持它们,那就太理想了。

我已经通过将查找或错过包裹在构造中解决了当前的行为

```
(:require [clojure.core.cache.wrapped :as cache]))

(defn lookup-or-miss
  [缓存原子 e value-fn]
  (或 (cache/lookup cache-atom e)
      (锁定 cache-atom
        (cache/lookup-or-miss cache-atom e value-fn))))
```  
然而,也许考虑向上游库贡献是有意义的,前提是其他人认为解决所提出的问题有价值。

以下是一些相关想法

1) 在当前包装实现中引入锁定,或者作为需要更强保证的用户的一个选项/替代方案。
2) 重实现 clojure.cache.wrapped 的内部以支持至多一次语义,摆脱 (swap!) 并选择类似于 (volatile!) + (锁定) 的方案。

登录注册以回答此问题。

...