请在2024年Clojure状态调查中分享您的想法!

欢迎!请访问关于页面以了解更多关于 đây làm thế nào.

+1

我正在努力调和clojure.cache的行为,看看它是否符合文档字符串/代码中建议的行为,以及我实际观察到的情况。我指的是此行

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

即使是在重试的情况下,value-fn(以及wrap-fn)也只会被调用一次(最多) —— 因此没有缓存洪水(cache stampede)的风险。
当缓存处于竞争状态时,似乎相当容易让value-fn被调用多次。我在repl中模拟了这种行为,如下所示

在这种情况下,当线程竞争时,miss函数被调用了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缓存中都可以重现。我还没有尝试其他的。

by

我学到了更多关于发生的事情,所以我在这里更新它以供后人参考。
我还找到了解决我特定问题的方法。我将将其修订成一般问题声明,以便可能是一个需要支持的合理用例。

因此,基本问题与swap!解决冲突的方式有关。我用以下代码进行了演示

(def d (atom 0))

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

1

0
1

2
1
1
2
3
2
4

2

5
2

5
6
7
6
5
8

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

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

如果我们把这转化为缓存,有一些情况下,并行实例的 c/through-cache see has? = false,因此运行了缺失分支,然后在更新冲突时重新执行。

这没有问题,尽管我可能还是会认为,至少文档字符串很混乱,也许应该指出这仅适用于单线程执行。

我将提出第二个评论,其中包含一个一般性问题陈述以及解决问题的潜在方案。
by
# 问题陈述

我需要在前台某些IO操作中实现缓存语义。这些IO操作不是在修改的,但与缓存查找的开销相比,它们相对较贵。

一个例子是 OpenID JSON Web Key Set (JWKS) 协议:来自Clojure HTTP服务器集群的并行HTTP请求可能携带一个JSON Web Token (JWT),这需要被验证为来自受信任的发行者。JWKS 协议提供了一种从受信任的发行者检索公钥的机制以进行此验证。请求与密钥的比例可能高达十亿比一;因此,以某种形式的缓存是非常有利的。

到目前为止,我已使用 clojure.cache.wrapped 构建的这个缓存,其中缓存基本上持有保留了JWKS响应的承诺,以 JWT key-id 为键。基本想法是,当遇到一个新的 JWT key-id 时,缓存缺失并启动一个到IDP的HTTP请求以检索密钥,并将承诺缓存起来。即使往返响应可能需要几毫秒,启动请求的开销也是微乎其微的。

缓存客户端可以在任何可供使用的方式中解引用这个承诺。长期解析的密钥立即解引用。正在解析中的密钥可能有多个客户端在等待承诺。

我目前遇到的问题与 clojure.cache 内部使用 (swap!) 造成的效率低下有关,当缓存竞争激烈时。在上面的场景中,一个给定的key-id通常同时过期。因此,API客户端倾向于几乎同时刷新JWT,并向系统提供一个新的尚未缓存的密钥。

结果是,多个线程为了同一个密钥而蜂拥而至,从而给缓存中特定的条目施加了很大压力。即使响应是稳定的,我们只需要一个值,也可能调用多个值函数来为同一个key ID启动JWKS操作。尽管系统仍然按当前行为正确运行,但由于执行了多次冗余的IO操作以获取相同的key ID,所以效率较低。

有人可能会争辩说这个IO是“副作用”,因此与 (swap!) 不兼容。虽然我同意修改副作用确实是一个问题,但这些“只读”副作用并不与一般缓存概念相冲突,所以如果缓存库能够通过消除对相同键值函数重复入侵应激而支持它们,这将是很理想的。

我已经通过用类似于以下结构的查找或缺失来绕过当前行为

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

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

关于这个话题,我有以下几点思考

1) 在当前的包装实现中引入锁定,或作为对需要更强保证的人的一种选项/替代方案。
2) 重新实现 clojure.cache.wrapped 的内部,以支持至一次语义,避免使用 (swap!),而是使用类似 (volatile!) + (锁定) 的机制。

登录注册来回答这个问题。

欢迎使用 Clojure Q&A,您可以在哪里向 Clojure 社区成员提问并获得答案。
...