2024 Clojure 状态调查!分享你的想法。

欢迎!请查看关于页面,了解更多关于其工作方式的信息。

+1
core.cache

我正在尝试调和 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 中模拟了这个行为,如下所示

在这种情况下,当线程竞争时,miss 函数调用了 8 次/10 次。我还在现实生活中看到了这种行为。

(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 缓存中都是可重现的。我还没有尝试其他缓存。

我了解到更多关于所发生的事情,因此我为后人更新。  
我还找到我的特定问题的解决方案。  我将将其修订为一般问题陈述,以防这可能是一个有效的考虑支持的用例。

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

(def d (atom 0))

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

0x:  0

x: 1
x: 2x

0x:  1
x: 2
x: 2x
x: x:3
x: x: 2
4x: x: 24x
x:  2

x:  2

输入:输入:5 2
5

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

最终结果正确(递增了10个值),但是得到了30多次以上的交换功能应用,内部状态有许多重复(例如,“0”)。我可以猜测swap!在内部并行运行函数,但在出现冲突时重复其中一些。

如果我们把这个问题转到缓存上,有些情况下,c/through-cache看到see? = false的并行实例,因此运行miss分支,然后在更新冲突时重新运行。

这没问题,尽管我可能仍然会争辩说,至少文档字符串很令人困惑,应该指出这仅与单线程执行相关。

我将提出第二个评论,概述一个问题陈述和解决问题的潜在方案。
by
问题说明

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

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

到目前为止,我已经使用了clojure.cache.wrapped来实现这个缓存,其中缓存本质上持有JWKS响应的承诺,以JWT的key-id为键。基本思想是,当遇到新的JWT key-id时,缓存未命中,开始对IDP发送HTTP请求以检索密钥,并缓存一个承诺。启动请求的开销很小,即使往返响应可能需要几毫秒。

缓存客户端可以按照合适的方式解引用这个承诺。长时间已解析的键立即解引用。正在解析中的键可能有多个客户端正在等待该承诺。

我目前遇到的问题与clojure.cache内部在缓存高度竞争时使用swap!导致的低效性相关。在上面的场景中,一个特定的key-id通常会同时过期。因此,API客户端倾向于在相同的时间刷新JWT,并向系统提供一个尚未缓存的新的key。

结果是多个线程从多个线程争抢相同的key,从而对缓存中的特定条目产生了大量压力。对于相同的key ID,会调用多个value-fns来启动JWKS操作,尽管响应是稳定的,我们只需要一个。尽管当前行为是正确的,但由于发出了多个冗余的I/O操作来获取相同的key ID,所以效率较低。

有人可能会争辩说这种输入输出(IO)是“存在间接副作用”的,因此与(swap!)不兼容。  虽然我同意突变副效应肯定是个问题,但这些“只读”副作用并不与缓存的普遍概念冲突,所以如果缓存库能通过消除对同一键值-fn的重入式访问来支持它们,那将是理想的。

我已经通过将查找或不缺失结构封装在一个构造块中绕过了当前的行为

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

(defn lookup-or-miss
  [cache-atom e value-fn]
  (or (cache/lookup cache-atom e)
      (locking cache-atom
        (cache/lookup-or-miss cache-atom e value-fn))))
```  
然而,也许考虑对上游库作出贡献是有意义的,前提是其他人认为解决该问题是有价值的。

以下是一些相关的思考

1) 在当前封装实现中引入锁定,或者作为需要更强保证的人的选择/替代选项。
2) 重新实现clojure.cache.wrapped内部实现,使其支持一次最多一次语义,摆脱(swap!),而是使用类似(volatile!)+(locking)的东西。

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

...