国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

68. redis計(jì)數(shù)與限流中incr+expire的坑以及解決辦法(Lua+TTL)

這篇具有很好參考價(jià)值的文章主要介紹了68. redis計(jì)數(shù)與限流中incr+expire的坑以及解決辦法(Lua+TTL)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

一、簡(jiǎn)介

在日常工作中,經(jīng)常會(huì)遇到對(duì)某種操作進(jìn)行頻次控制或者統(tǒng)計(jì)次數(shù)的需求,此時(shí)常用的做法是采用redisincr來(lái)遞增,記錄訪問次數(shù), 以及 expire 來(lái)設(shè)置失效時(shí)間。本文將以一個(gè)實(shí)際的例子來(lái)說(shuō)明incr存在的一個(gè)"坑",以及給出解決方案。

如:
26.redis實(shí)現(xiàn)日限流、周限流(含黑名單、白名單)
27.Go實(shí)現(xiàn)一月(30天)內(nèi)不發(fā)送重復(fù)內(nèi)容的站內(nèi)信給用戶

有這么一個(gè)場(chǎng)景,用戶需要進(jìn)行ocr識(shí)別,為了防止接口被刷,這里面做了一個(gè)限制(每分鐘調(diào)用次數(shù)不能超過xx次)。 經(jīng)過調(diào)研后,決定使用redisincrexpire來(lái)實(shí)現(xiàn)這個(gè)功能
68. redis計(jì)數(shù)與限流中incr+expire的坑以及解決辦法(Lua+TTL),go,redis,lua,數(shù)據(jù)庫(kù)

二、代碼演進(jìn)

第一版代碼(存在bug隱患)

// 執(zhí)行ocr調(diào)用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){
	// 如果調(diào)用次數(shù)超過了指定限制,就直接拒絕此次請(qǐng)求
	ok,err := o.checkMinute(uid)
	if err != nil {
		return nil,err
	}
	if !ok {
		return nil,errors.News("frequently called")
	}
	// 執(zhí)行第三方ocr調(diào)用(偽代碼:模擬一個(gè)rpc接口)
	ocrRes,err := doOcrByThird()
	if err != nil {
		return nil,err
	}
	// 調(diào)用成功則執(zhí)行 incr操作
	if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{
	   return nil,err
	}
	return ocrRes,nil
}
 
// 校驗(yàn)每分鐘調(diào)用次數(shù)是否超過限制
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {
	minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))
	if err != nil && !errors.Is(err, eredis.Nil) {
		log.Error("checkMinute: redis.Get failed", zap.Error(err))
		return false, constx.ErrServer
	}
	if errors.Is(err, eredis.Nil) {
		// 過期了,或者沒有該用戶的調(diào)用次數(shù)記錄(設(shè)置初始值為0,過期時(shí)間為1分鐘)
		o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)
		return true, nil
	}
	// 已經(jīng)超過每分鐘的調(diào)用次數(shù)
	if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {
		log.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))
		return false, nil
	}
	return true, nil
}

詳解
這一版代碼存在什么問題呢?問題出在了 開始判斷沒有超出限制,然后執(zhí)行第三方rpc接口調(diào)用也成功了,接下來(lái)直接進(jìn)行計(jì)數(shù)加一(incr)操作有問題,如下圖所示

68. redis計(jì)數(shù)與限流中incr+expire的坑以及解決辦法(Lua+TTL),go,redis,lua,數(shù)據(jù)庫(kù)

說(shuō)明:

  1. 假設(shè)當(dāng)前用戶在進(jìn)行ocr識(shí)別時(shí),未超過調(diào)用次數(shù)。但是在redis中的ttl還剩1秒鐘
  2. 然后調(diào)用第三方ocr進(jìn)行識(shí)別,加入耗時(shí)超過了1
  3. 識(shí)別成功后,調(diào)用次數(shù)+1。這里就很有可能出問題,比如:在incr的時(shí)候剛好該key1s前過期了,那么redis是怎么做的呢,它會(huì)將該key的值設(shè)置為1,ttl設(shè)置為-1,ttl設(shè)置為-1,ttl設(shè)置為-1重要的事情說(shuō)三遍),-1表示沒有過期時(shí)間
  4. 這時(shí)候bug就出現(xiàn)了,用戶的調(diào)用次數(shù)一直在漲,并且也不會(huì)過期,達(dá)到臨界值時(shí)用戶的請(qǐng)求就會(huì)被拒掉,相當(dāng)于該用戶之后都不能訪問這個(gè)接口了,并且這種key變多后,由于沒有過期時(shí)間,還會(huì)一直占用redis的內(nèi)存。

總結(jié)
以上代碼說(shuō)明了一個(gè)問題,也就是increxpire必須具備原子性。而我們第一版代碼顯然在邊界條件下是不滿足要求的,極有可能造成bug,影響用戶體驗(yàn),強(qiáng)烈不推薦使用,接下來(lái)一步一步引入修正后的代碼

第二版代碼(幾乎無(wú)隱患)

從對(duì)第一版代碼的分析可知,是由于查詢次數(shù)還沒有達(dá)到限制后,又進(jìn)行了一些rpc調(diào)用,或者處理了一些其他業(yè)務(wù)邏輯,這個(gè)時(shí)間內(nèi),可能key過期了,然后我們直接使用incr進(jìn)行計(jì)數(shù)加一,導(dǎo)致了永不過期的key產(chǎn)生。那么我們是不是可以在incr前先保證key還沒有過期就行呢?答案是可以的,代碼如下:

// 執(zhí)行ocr調(diào)用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){
	// 如果調(diào)用次數(shù)超過了指定限制,就直接拒絕此次請(qǐng)求
	ok,err := o.checkMinute(uid)
	if err != nil {
		return nil,err
	}
	if !ok {
		return nil,errors.News("frequently called")
	}
	// 執(zhí)行第三方ocr調(diào)用(偽代碼:模擬一個(gè)rpc接口)
	ocrRes,err := doOcrByThird()
	if err != nil {
		return nil,err
	}
	
	// 調(diào)用成功則執(zhí)行 incr操作
	exists, err := o.redis.Exists(ctx, buildUserOcrCountKey(uid)).Result()
	if err != nil {
		log.Error("doOcr: redis.Exists failed", zap.Error(err))
		return nil, err
	}
	if exists == 1 { // key存在,計(jì)數(shù)加1
		if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{
	    return nil,err
	}
	} else { // key不存在,設(shè)置key與過期時(shí)間
		if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{
		   return nil,err
		}
	}
	return ocrRes,nil
}
 
// 校驗(yàn)每分鐘調(diào)用次數(shù)是否超過限制
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {
	minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))
	if err != nil && !errors.Is(err, eredis.Nil) {
		log.Error("checkMinute: redis.Get failed", zap.Error(err))
		return false, constx.ErrServer
	}
	if errors.Is(err, eredis.Nil) {
		// 過期了,或者沒有該用戶的調(diào)用次數(shù)記錄(設(shè)置初始值為0,過期時(shí)間為1分鐘)
		o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)
		return true, nil
	}
	// 已經(jīng)超過每分鐘的調(diào)用次數(shù)
	if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {
		log.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))
		return false, nil
	}
	return true, nil
}

與第一版的差異主要在于如下代碼:

  • 在需要incr操作前,我們先查看key是否存在(且沒有過期)
    • 確保存在后,立即incr(這兩步間隔幾乎可以忽略,所以幾乎可以避免第一版中的問題)
    • 如果不存在則設(shè)置key并設(shè)置過期時(shí)間。

注:redis中的incr命令是不會(huì)改變key的過期時(shí)間的

// 調(diào)用成功則執(zhí)行 incr操作
	exists, err := o.redis.Exists(ctx, buildUserOcrCountKey(uid)).Result()
	if err != nil {
		log.Error("doOcr: redis.Exists failed", zap.Error(err))
		return nil, err
	}
	if exists == 1 { // key存在,計(jì)數(shù)加1
		if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{
	    return nil,err
	}
	} else { // key不存在,設(shè)置key與過期時(shí)間
		if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{
		   return nil,err
		}
	}

還有一種方式是查看key的過期時(shí)間,使用ttl,這樣即使在極端情況下通過incr設(shè)置出了沒有過期時(shí)間的key,也會(huì)在第二次訪問的時(shí)候通過Set設(shè)置過期時(shí)間了。

注:ttl命令返回值是鍵的剩余時(shí)間(單位是秒)。當(dāng)鍵不存在時(shí),ttl命令會(huì)返回-2。沒有為鍵設(shè)置過期時(shí)間(即永久存在,這是建立一個(gè)鍵后的默認(rèn)情況)返回-1。

// 調(diào)用成功則執(zhí)行 incr操作
	cnt, err := o.redis.Ttl(ctx, buildUserOcrCountKey(uid)).Result()
	if err != nil {
		log.Error("doOcr: redis.Ttl failed", zap.Error(err))
		return nil, err
	}
	if cnt >= 1 { // key存在,且還沒有過期
		if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{
	    return nil,err
	}
	} else { // key馬上過期0,key沒有過期時(shí)間-1, key不存在-2
		if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{
		   return nil,err
		}
	}

第三版代碼(完美無(wú)瑕)

第二版代碼中的兩種方式其實(shí)已經(jīng)可以在工作中使用了,但如果追求完美無(wú)瑕的話,ttl版本的代碼在極端情況下還是有點(diǎn)瑕疵,比如極端情況下,key過期時(shí)間還有1s過期,然后我們用incr去累加,但是網(wǎng)絡(luò)延遲了,導(dǎo)致命令到達(dá)redis服務(wù)器的時(shí)候,key已經(jīng)過期了,盡管第二次訪問會(huì)用set重置key并設(shè)置過期時(shí)間,但是萬(wàn)一該用戶再也不來(lái)訪問了呢?這時(shí)候這個(gè)key就會(huì)永遠(yuǎn)占據(jù)著內(nèi)存了。

incr+expire放在lua腳本中執(zhí)行保證原子性是最完美的。廢話不多說(shuō)了,直接上代碼文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-811885.html

// 執(zhí)行ocr調(diào)用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){
	// 如果調(diào)用次數(shù)超過了指定限制,就直接拒絕此次請(qǐng)求
	ok,err := o.checkMinute(uid)
	if err != nil {
		return nil,err
	}
	if !ok {
		return nil,errors.News("frequently called")
	}
	// 執(zhí)行第三方ocr調(diào)用((偽代碼:模擬一個(gè)rpc接口))
	ocrRes,err := doOcrByThird()
	if err != nil {
		return nil,err
	}
	// 調(diào)用成功則執(zhí)行 incr操作
	if err := o.incrCount(ctx,buildUserOcrCountKey(uid));err!=nil{
	   return nil,err
	}
	return ocrRes,nil
}
 
func (o *ocrSvc) incrCount(ctx context.Context, uid int64) error {
   /*
   此段lua腳本的作用:
     第一步,先執(zhí)行incr操作
     local current = redis.call('incr',KEYS[1])
     第二步,看下該key的ttl
     local t = redis.call('ttl',KEYS[1]); 
     第三步,如果ttl為-1(永不過期)
     if t == -1 then
         則重新設(shè)置過期時(shí)間為 「一分鐘」
		 redis.call('expire',KEYS[1],ARGV[1])
	 end;
   */
	script := redis.NewScript(
		`local current = redis.call('incr',KEYS[1]);
		 local t = redis.call('ttl',KEYS[1]); 
		 if t == -1 then
		 	redis.call('expire',KEYS[1],ARGV[1])
		 end;
		 return current
	`)
	var (
		expireTime = 60 // 60 秒
	)
	_, err := script.Run(ctx, b.redis.Client(), []string{buildUserOcrCountKey(uid)}, expireTime).Result()
	if err != nil {
		return err
	}
	return nil
}
 
// 校驗(yàn)每分鐘調(diào)用次數(shù)是否超過
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {
	minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))
	if err != nil && !errors.Is(err, eredis.Nil) {
		elog.Error("checkMinute: redis.Get failed", zap.Error(err))
		return false, constx.ErrServer
	}
	if errors.Is(err, eredis.Nil) {
	    // 第二版代碼中在check時(shí)不進(jìn)行初始化操作
		// 過期了,或者沒有該用戶的調(diào)用次數(shù)記錄(設(shè)置初始值為0,過期時(shí)間為1分鐘)
		// o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)
		return true, nil
	}
	// 已經(jīng)超過每分鐘的調(diào)用次數(shù)
	if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {
		elog.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))
		return false, nil
	}
	return true, nil
}

到了這里,關(guān)于68. redis計(jì)數(shù)與限流中incr+expire的坑以及解決辦法(Lua+TTL)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • SpringCloud-Alibaba之Sentinel熔斷與限流

    SpringCloud-Alibaba之Sentinel熔斷與限流

    一、下載安裝運(yùn)行 http://localhost:8080進(jìn)行訪問 登錄賬號(hào)和密碼均為sentinel 二、創(chuàng)建工程,并注冊(cè)到nacos服務(wù)中心 依賴spring-cloud-starter-alibaba-nacos-discovery,spring-cloud-starter-alibaba-sentinel sentine-datasource-nacos (持久化) 配置文件 啟動(dòng)類 業(yè)務(wù)類 三、啟動(dòng)sentinel java -jar sentinel-dashboard-1.7.0

    2024年02月16日
    瀏覽(27)
  • 微服務(wù)韌性工程:利用Sentinel實(shí)施有效服務(wù)容錯(cuò)與限流降級(jí)

    微服務(wù)韌性工程:利用Sentinel實(shí)施有效服務(wù)容錯(cuò)與限流降級(jí)

    ? ? ? ? 目錄 一、雪崩效應(yīng) 二、Sentinel 服務(wù)容錯(cuò) ? ? ? ? 2.1?Sentinel容錯(cuò)思路 ? ? ? ? 2.2 內(nèi)部異常兼容 ? ? ? ? 2.3 外部流量控制 三、Sentinel 項(xiàng)目搭建 四、Sentinel 工作原理 ????????服務(wù)容錯(cuò)是微服務(wù)設(shè)計(jì)中一項(xiàng)重要原則和技術(shù)手段,主要目標(biāo)是在服務(wù)出現(xiàn)故障、網(wǎng)絡(luò)波

    2024年03月15日
    瀏覽(23)
  • 熔斷降級(jí)與限流在開源SpringBoot/SpringCloud微服務(wù)框架的最佳實(shí)踐

    熔斷降級(jí)與限流在開源SpringBoot/SpringCloud微服務(wù)框架的最佳實(shí)踐

    前期內(nèi)容導(dǎo)讀: Java開源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介紹 Java開源AES/SM4/3DES對(duì)稱加密算法介紹及其實(shí)現(xiàn) Java開源AES/SM4/3DES對(duì)稱加密算法的驗(yàn)證說(shuō)明 Java開源RSA/SM2非對(duì)稱加密算法對(duì)比介紹 Java開源RSA非對(duì)稱加密算法實(shí)現(xiàn) Java開源SM2非對(duì)稱加密算法實(shí)現(xiàn) Java開源接口微服務(wù)代碼框架

    2024年02月12日
    瀏覽(25)
  • SpringCloud(17~21章):Alibaba入門簡(jiǎn)介、Nacos服務(wù)注冊(cè)和配置中心、Sentinel實(shí)現(xiàn)熔斷與限流、Seata處理分布式事務(wù)

    SpringCloud(17~21章):Alibaba入門簡(jiǎn)介、Nacos服務(wù)注冊(cè)和配置中心、Sentinel實(shí)現(xiàn)熔斷與限流、Seata處理分布式事務(wù)

    Spring Cloud Netflix項(xiàng)目進(jìn)入維護(hù)模式 https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now 說(shuō)明 Spring Cloud Netflix Projects Entering Maintenance Mode 什么是維護(hù)模式 將模塊置于維護(hù)模式,意味著 Spring Cloud 團(tuán)隊(duì)將不會(huì)再向模塊添加新功能。我們將修復(fù) block 級(jí)別的 bug 以及安全問題,我

    2024年01月19日
    瀏覽(43)
  • go限流、計(jì)數(shù)器固定窗口算法/計(jì)數(shù)器滑動(dòng)窗口算法

    go限流、計(jì)數(shù)器固定窗口算法/計(jì)數(shù)器滑動(dòng)窗口算法

    問題1:后端接口只能支撐每10秒1w個(gè)請(qǐng)求,要怎么來(lái)保護(hù)它呢? 問題2:發(fā)短信的接口,不超過100次/時(shí),1000次/24小時(shí),要怎么實(shí)現(xiàn)? 所謂固定窗口,就是只設(shè)置了一個(gè)時(shí)間段,給這個(gè)時(shí)間段加上一個(gè)計(jì)數(shù)器。 常見的就是統(tǒng)計(jì)每秒鐘的請(qǐng)求量。 這里就是一個(gè)QPS計(jì)數(shù)器。 在這一

    2024年04月26日
    瀏覽(21)
  • springboot 自定義注解 ,實(shí)現(xiàn)接口限流(計(jì)數(shù)器限流)【強(qiáng)行喂飯版】

    springboot 自定義注解 ,實(shí)現(xiàn)接口限流(計(jì)數(shù)器限流)【強(qiáng)行喂飯版】

    思路:通過AOP攔截注解標(biāo)記的方法,在Redis中維護(hù)一個(gè)計(jì)數(shù)器來(lái)記錄接口訪問的頻率, 并根據(jù)限流策略來(lái)判斷是否允許繼續(xù)處理請(qǐng)求。 另一篇:springboot 自定義注解 ,aop切面@Around; 為接口實(shí)現(xiàn)日志插入【強(qiáng)行喂飯版】 不多說(shuō),直接上代碼: 一:創(chuàng)建限流類型 二:創(chuàng)建注解

    2024年02月15日
    瀏覽(25)
  • java進(jìn)行系統(tǒng)的限流實(shí)現(xiàn)--Guava RateLimiter、簡(jiǎn)單計(jì)數(shù)、滑窗計(jì)數(shù)、信號(hào)量、令牌桶

    本文主要介紹了幾種限流方法:Guava RateLimiter、簡(jiǎn)單計(jì)數(shù)、滑窗計(jì)數(shù)、信號(hào)量、令牌桶,漏桶算法和nginx限流等等 1、引入guava集成的工具 pom.xml 文件 demo代碼實(shí)現(xiàn) 2.令牌桶算法 3、滑窗計(jì)數(shù)器 4、信號(hào)量

    2024年02月09日
    瀏覽(17)
  • go-zero 是如何實(shí)現(xiàn)計(jì)數(shù)器限流的?

    go-zero 是如何實(shí)現(xiàn)計(jì)數(shù)器限流的?

    原文鏈接: 如何實(shí)現(xiàn)計(jì)數(shù)器限流? 上一篇文章 go-zero 是如何做路由管理的? 介紹了路由管理,這篇文章來(lái)說(shuō)說(shuō)限流,主要介紹計(jì)數(shù)器限流算法,具體的代碼實(shí)現(xiàn),我們還是來(lái)分析微服務(wù)框架 go-zero 的源碼。 在微服務(wù)架構(gòu)中,一個(gè)服務(wù)可能需要頻繁地與其他服務(wù)交互,而過多

    2024年02月13日
    瀏覽(18)
  • 限流:計(jì)數(shù)器、漏桶、令牌桶 三大算法的原理與實(shí)戰(zhàn)(史上最全)

    限流:計(jì)數(shù)器、漏桶、令牌桶 三大算法的原理與實(shí)戰(zhàn)(史上最全)

    限流是面試中的常見的面試題(尤其是大廠面試、高P面試) 注:本文以 PDF 持續(xù)更新,最新尼恩 架構(gòu)筆記、面試題 的PDF文件,請(qǐng)到文末《技術(shù)自由圈》公號(hào)獲取 為什么要限流 簡(jiǎn)單來(lái)說(shuō): 限流在很多場(chǎng)景中用來(lái)限制并發(fā)和請(qǐng)求量,比如說(shuō)秒殺搶購(gòu),保護(hù)自身系統(tǒng)和下游系統(tǒng)

    2023年04月17日
    瀏覽(20)
  • 68、Redis:緩存雪崩、緩存穿透、緩存擊穿

    緩存雪崩是指緩存同一時(shí)間大面積的失效,所以,后面的請(qǐng)求都會(huì)落到數(shù)據(jù)庫(kù)上,造成數(shù)據(jù)庫(kù)短時(shí)間內(nèi)承受大量請(qǐng)求而崩掉。 緩存數(shù)據(jù)的過期時(shí)間設(shè)置隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過期現(xiàn)象發(fā)生 給每一個(gè)緩存數(shù)據(jù)增加相應(yīng)的緩存標(biāo)記,記錄緩存是否失效,如果緩存標(biāo)記失效

    2024年02月16日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包