”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Go 中的概率提前过期

Go 中的概率提前过期

发布于2024-11-09
浏览:539

关于缓存踩踏

我经常遇到需要缓存这个或那个的情况。通常,这些值会被缓存一段时间。您可能熟悉这种模式。您尝试从缓存中获取一个值,如果成功,则将其返回给调用者并结束。如果该值不存在,您将获取它(很可能从数据库中)或计算它并将其放入缓存中。在大多数情况下,这非常有效。但是,如果您用于缓存条目的密钥被频繁访问,并且计算数据的操作需要一段时间,您最终会遇到多个并行请求同时发生缓存未命中的情况。所有这些请求都将从源独立加载并将值存储在缓存中。这会导致资源浪费,甚至可能导致拒绝服务。

让我举个例子来说明一下。我将使用 Redis 进行缓存,并在顶部使用一个简单的 Go http 服务器。完整代码如下:

package main

import (
    "errors"
    "log"
    "net/http"
    "time"

    "github.com/redis/go-redis/v9"
)

type handler struct {
    rdb *redis.Client
    cacheTTL time.Duration
}

func (ch *handler) simple(w http.ResponseWriter, r *http.Request) {
    cacheKey := "my_cache_key"
    // we'll use 200 to signify a cache hit & 201 to signify a miss
    responseCode := http.StatusOK
    cachedData, err := ch.rdb.Get(r.Context(), cacheKey).Result()
    if err != nil {
        if !errors.Is(err, redis.Nil) {
            log.Println("could not reach redis", err.Error())
            http.Error(w, "could not reach redis", http.StatusInternalServerError)
            return
        }

        // cache miss - fetch & store
        res := longRunningOperation()
        responseCode = http.StatusCreated

        err = ch.rdb.Set(r.Context(), cacheKey, res, ch.cacheTTL).Err()
        if err != nil {
            log.Println("failed to set cache value", err.Error())
            http.Error(w, "failed to set cache value", http.StatusInternalServerError)
            return
        }
        cachedData = res
    }
    w.WriteHeader(responseCode)
    _, _ = w.Write([]byte(cachedData))
}

func longRunningOperation() string {
    time.Sleep(time.Millisecond * 500)
    return "hello"
}

func main() {
    ttl := time.Second * 3
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    handler := &handler{
        rdb: rdb,
        cacheTTL: ttl,
    }

    http.HandleFunc("/simple", handler.simple)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("Could not start server: %s\n", err.Error())
    }
}

让我们在 /simple 端点上施加一些负载,看看会发生什么。我将使用 vegeta 来实现此目的。

我运行 vegeta Attack -duration=30s -rate=500 -targets=./targets_simple.txt > res_simple.bin。 Vegeta 最终每秒发出 500 个请求,持续 30 秒。我将它们绘制为 HTTP 结果代码的直方图,其中每个桶的跨度为 100 毫秒。结果如下图。

Probabilistic Early Expiration in Go

当我们开始实验时,缓存是空的——我们没有存储任何值。当一堆请求到达我们的服务器时,我们得到了最初的踩踏。他们都检查缓存,没有发现任何内容,调用 longRunningOperation 并将其存储在缓存中。由于 longRunningOperation 大约需要 500 毫秒才能完成前 500 毫秒内发出的任何请求,最终都会调用 longRunningOperation。一旦其中一个请求设法将值存储在缓存中,所有后续请求都会从缓存中获取该值,我们开始看到状态代码为 200 的响应。然后,随着 Redis 上的过期机制启动,该模式每 3 秒重复一次。

在这个玩具示例中,这不会导致任何问题,但在生产环境中,这可能会导致系统上不必要的负载、用户体验下降,甚至自我诱导的拒绝服务。那么我们怎样才能防止这种情况发生呢?嗯,有几种方法。我们可以引入锁——任何缓存未命中都会导致代码尝试实现锁。分布式锁定并不是一件简单的事情,通常它们有微妙的边缘情况,需要微妙的处理。我们还可以使用后台作业定期重新计算该值,但这需要运行一个额外的进程,引入另一个需要在我们的代码中维护和监视的齿轮。如果您有动态缓存键,则此方法也可能不可行。还有另一种方法,称为概率提前过期,这是我想进一步探索的方法。

概率提前到期

这种技术允许人们根据概率重新计算该值。从缓存中获取值时,您还可以根据概率计算是否需要重新生成缓存值。越接近现有价值到期,概率就越高。

我基于 A. Vattani、F.Chierichetti 和 K. Lowenstein 在 Optimal Probabilistic Cache Stampede Prevention 中基于 XFetch 的具体实现。

我将在 HTTP 服务器上引入一个新端点,该端点也将执行昂贵的计算,但这次在缓存时使用 XFetch。为了使 XFetch 工作,我们需要存储昂贵的操作花费了多长时间(增量)以及缓存键何时过期。为了实现这一目标,我将引入一个结构体来保存这些值以及消息本身:

type probabilisticValue struct {
    Message string
    Expiry time.Time
    Delta time.Duration
}

我添加一个函数来用这些属性包装原始消息并将其序列化以存储在redis中:

func wrapMessage(message string, delta, cacheTTL time.Duration) (string, error) {
    bts, err := json.Marshal(probabilisticValue{
        Message: message,
        Delta: delta,
        Expiry: time.Now().Add(cacheTTL),
    })
    if err != nil {
        return "", fmt.Errorf("could not marshal message: %w", err)
    }

    return string(bts), nil
}

我们还编写一个方法来重新计算并将值存储到redis中:

func (ch *handler) recomputeValue(ctx context.Context, cacheKey string) (string, error) {
    start := time.Now()
    message := longRunningOperation()
    delta := time.Since(start)

    wrapped, err := wrapMessage(message, delta, ch.cacheTTL)
    if err != nil {
        return "", fmt.Errorf("could not wrap message: %w", err)
    }
    err = ch.rdb.Set(ctx, cacheKey, wrapped, ch.cacheTTL).Err()
    if err != nil {
        return "", fmt.Errorf("could not save value: %w", err)
    }
    return message, nil
}

为了确定是否需要根据概率更新值,我们可以在 probabilisticValue 中添加一个方法:

func (pv probabilisticValue) shouldUpdate() bool {
    // suggested default param in XFetch implementation
    // if increased - results in earlier expirations
    beta := 1.0
    now := time.Now()
    scaledGap := pv.Delta.Seconds() * beta * math.Log(rand.Float64())
    return now.Sub(pv.Expiry).Seconds() >= scaledGap
}

如果我们将其全部连接起来,我们最终会得到以下处理程序:

func (ch *handler) probabilistic(w http.ResponseWriter, r *http.Request) {
    cacheKey := "probabilistic_cache_key"
    // we'll use 200 to signify a cache hit & 201 to signify a miss
    responseCode := http.StatusOK
    cachedData, err := ch.rdb.Get(r.Context(), cacheKey).Result()
    if err != nil {
        if !errors.Is(err, redis.Nil) {
            log.Println("could not reach redis", err.Error())
            http.Error(w, "could not reach redis", http.StatusInternalServerError)
            return
        }

        res, err := ch.recomputeValue(r.Context(), cacheKey)
        if err != nil {
            log.Println("could not recompute value", err.Error())
            http.Error(w, "could not recompute value", http.StatusInternalServerError)
            return
        }
        responseCode = http.StatusCreated
        cachedData = res

        w.WriteHeader(responseCode)
        _, _ = w.Write([]byte(cachedData))
        return
    }

    pv := probabilisticValue{}
    err = json.Unmarshal([]byte(cachedData), &pv)
    if err != nil {
        log.Println("could not unmarshal probabilistic value", err.Error())
        http.Error(w, "could not unmarshal probabilistic value", http.StatusInternalServerError)
        return
    }

    if pv.shouldUpdate() {
        _, err := ch.recomputeValue(r.Context(), cacheKey)
        if err != nil {
            log.Println("could not recompute value", err.Error())
            http.Error(w, "could not recompute value", http.StatusInternalServerError)
            return
        }
        responseCode = http.StatusAccepted
    }

    w.WriteHeader(responseCode)
    _, _ = w.Write([]byte(cachedData))
}

处理程序的工作方式与第一个处理程序非常相似,但是,在获得缓存命中后,我们就掷骰子。根据结果​​,我们要么只返回刚刚获取的值,要么提前更新该值。

我们将使用 HTTP 状态代码来确定 3 种情况:

  • 200 - 我们从缓存返回值
  • 201 - 缓存未命中,不存在值
  • 202 - 缓存命中,触发概率更新

这次我再次启动 vegeta 在新端点上运行,结果如下:

Probabilistic Early Expiration in Go

那里的微小蓝色斑点表明我们实际上何时提前结束了缓存值的更新。在初始预热期后,我们不再看到缓存未命中。为了避免初始峰值,如果这对您的用例很重要,您可以预先存储缓存值。

如果您想更积极地进行缓存并更频繁地刷新值,您可以使用 beta 参数。以下是将 beta 参数设置为 2 时相同实验的结果:

Probabilistic Early Expiration in Go

我们现在看到概率更新更加频繁。

总而言之,这是一个巧妙的小技术,可以帮助避免缓存踩踏。但请记住,这仅在您定期从缓存中获取相同密钥时才有效 - 否则您不会看到太多好处。

有另一种方法来处理缓存踩踏吗?注意到一个错误吗?请在下面的评论中告诉我!

版本声明 本文转载于:https://dev.to/vkuznecovas/probabilistic-early-expiration-in-go-48h?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    The Mystery of "Broken" Two-Phase Template Instantiation in Microsoft Visual C Problem Statement:Users commonly express concerns that Micro...
    编程 发布于2025-03-12
  • UTF-8 vs. Latin-1:字符编码大揭秘!
    UTF-8 vs. Latin-1:字符编码大揭秘!
    [utf-8和latin1 在他们的应用中,出现了一个基本问题:什么辨别特征区分了这两个编码?超出其字符表现能力,UTF-8具有额外的几个优势。从历史上看,MySQL对UTF-8的支持仅限于每个字符的三个字节,这阻碍了基本多语言平面(BMP)之外的字符的表示。但是,随着MySQL 5.5的出现,...
    编程 发布于2025-03-12
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-03-12
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-03-12
  • Part SQL注入系列:高级SQL注入技巧详解
    Part SQL注入系列:高级SQL注入技巧详解
    [2 Waymap pentesting工具:单击此处 trixsec github:单击此处 trixsec电报:单击此处 高级SQL注入利用 - 第7部分:尖端技术和预防 欢迎参与我们SQL注入系列的第7部分!该分期付款将攻击者采用的高级SQL注入技术 1。高...
    编程 发布于2025-03-12
  • 为什么PYTZ最初显示出意外的时区偏移?
    为什么PYTZ最初显示出意外的时区偏移?
    与pytz 最初从pytz获得特定的偏移。例如,亚洲/hong_kong最初显示一个七个小时37分钟的偏移: 差异源利用本地化将时区分配给日期,使用了适当的时区名称和偏移量。但是,直接使用DateTime构造器分配时区不允许进行正确的调整。 example pytz.timezone(...
    编程 发布于2025-03-12
  • 如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    How to Resolve "General error: 2006 MySQL server has gone away" While Inserting RecordsIntroduction:Inserting data into a MySQL database can...
    编程 发布于2025-03-12
  • 我们如何保护有关恶意内容的文件上传?
    我们如何保护有关恶意内容的文件上传?
    对文件上载上传到服务器的安全性问题可以引入重大的安全风险,因为用户可能会提供潜在的恶意内容。了解这些威胁并实施有效的缓解策略对于维持应用程序的安全性至关重要。用户可以将文件名操作以绕过安全措施。避免将其用于关键目的或使用其原始名称保存文件。用户提供的MIME类型可能不可靠。使用服务器端检查确定实际...
    编程 发布于2025-03-12
  • 如何使用JavaScript中的正则表达式从字符串中删除线路断裂?
    如何使用JavaScript中的正则表达式从字符串中删除线路断裂?
    在此代码方案中删除从字符串在JavaScript中解决此问题,根据操作系统的编码,对线断裂的识别不同。 Windows使用“ \ r \ n”序列,Linux采用“ \ n”,Apple系统使用“ \ r。” 来满足各种线路断裂的变化,可以使用以下正则表达式: [&& && &&&&&&&&&&&...
    编程 发布于2025-03-12
  • 为什么使用Firefox后退按钮时JavaScript执行停止?
    为什么使用Firefox后退按钮时JavaScript执行停止?
    导航历史记录问题:JavaScript使用Firefox Back Back 此行为是由浏览器缓存JavaScript资源引起的。要解决此问题并确保在后续页面访问中执行脚本,Firefox用户应设置一个空功能。 警报'); }; alert('inline Alert')...
    编程 发布于2025-03-12
  • 如何使用PHP将斑点(图像)正确插入MySQL?
    如何使用PHP将斑点(图像)正确插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call ...
    编程 发布于2025-03-12
  • 我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
    我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
    将我的加密库从mcrypt升级到openssl 问题:是否可以将我的加密库从McRypt升级到OpenSSL?如果是这样,如何?答案:是的,可以将您的Encryption库从McRypt升级到OpenSSL。可以使用openssl。附加说明: [openssl_decrypt()函数要求iv参...
    编程 发布于2025-03-12
  • 在Java中使用for-to-loop和迭代器进行收集遍历之间是否存在性能差异?
    在Java中使用for-to-loop和迭代器进行收集遍历之间是否存在性能差异?
    For Each Loop vs. Iterator: Efficiency in Collection TraversalIntroductionWhen traversing a collection in Java, the choice arises between using a for-...
    编程 发布于2025-03-12
  • 如何检查对象是否具有Python中的特定属性?
    如何检查对象是否具有Python中的特定属性?
    方法来确定对象属性存在寻求一种方法来验证对象中特定属性的存在。考虑以下示例,其中尝试访问不确定属性会引起错误: >>> a = someClass() >>> A.property Trackback(最近的最新电话): 文件“ ”,第1行, AttributeError: SomeClass...
    编程 发布于2025-03-12
  • Java HashSet/LinkedHashSet随机元素获取方法详解
    Java HashSet/LinkedHashSet随机元素获取方法详解
    在编程中找到一个随机元素,在编程中找到一个随机元素,从集合(例如集合)中选择一个随机元素很有用。 Java提供了多种类型的集合,包括障碍物和链接HASHSET。本文将探讨如何从这些特定集合实现的过程中选择一个随机元素。的java的hashset和linkedhashset a HashSet代表...
    编程 发布于2025-03-12

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3