redis筆記2 限流、GeoHash和Scan

限流

簡單限流

簡單限流的思路是,在規定的時間窗口內,給出規定的最大操作數量限制。使用zset結構作爲一個用戶行爲的記錄。zsetvaluescore都用來表示操作的時間戳。每次操作前,先把操作時間戳加入zset結構,然後移除超時的操作時間戳;之後比較總的個數和最大個數的關係,用來表示是否可以操作。

import time
import redis

client = redis.StrictRedis()

def is_action_allowed(usr_id, action_key, priod, max_count):
    key = 'hist:%s:%s' % (user_id, action_key)
    now_ts = int(time.time() * 1000) # 毫秒時間戳
    with client.pipiline() as pipe:
        pipe.zadd(key, now_ts, now_ts)
        # 移除超時時間
        pipe.zremrangebyscore(key, 0, now_ts - period * 1000)
        pipe.zcard(key) # 獲取當前時間段內總的操作次數
        # 設置超時時間,移除冷操作節約空間
        pipe.expore(key, period + 1)  
        _, _, current_count, _ = pipe.execute()
    return current_count <= max_count

這種做法的缺陷是,如果規定時間段內允許的操作數非常多,假設1s內可以操作10510^5次操作,那麼需要對應數量的時間戳來存儲,浪費空間。

漏斗限流

介紹漏斗限流的思路。漏斗的滴水速度是勻速的,我們可以往漏斗中加水,如果加水的速度小於漏水的速度,那麼這個行爲是可以允許的;但是如果超過了這個速度,那麼是不允許的。

給出單機漏斗限流的基本實現:

import time

class Funnel(object):
    def __init__(self, capacity, leaking_rate):
        self.capacity = capacity            # 漏斗的容量
        self.leaking_rate = leaking_rate  # 漏水的速度
        self.left_quota = capacity         # 初始化的水量
        self.leaking_ts = time.time()     # 上次漏水的時間

    # 加水順便檢驗空間,算法的核心
    def make_space(self):
        now_ts = time.time()
        delta_ts = now_ts - self.leaking_ts  # 距離上次加水時間的間隔
        delta_quota = delta_ts * self.leaking_rate  # 減少的水量,可以認爲是騰出的空間
        if delta_quota < 1:  # 騰出的空間太少
            return
        self.left_quota += delta_quota  # 增加剩餘的空間
        self.leaking_ts = now_ts  # 更新漏水的時間戳
        if self.left_quota > self.capacity:  # 不能超過容量
            self.left_quota = self.capacity

    def watering(self, quota):  # 判斷加入的水是否滿足
        self.make_space()
        if self.left_quota >= quota:  # 判斷剩餘的空間是否充足
            self.left_quota -= quota
            return True
        return False

funnels = {}

def is_action_allowed(user_id, action_key, capacity, leaking_rate):
    key = '%s:%s' % (user_id, action_key)
    funnel = funnels.get(key)
    if not funnel:
        funnel = Funnel(capacity, leaking_rate)
        funnels[key] = funnel
    return funnel.watering(1)

for i in range(20):
    print is_action_allowed('erick', 'reply', 15, 0.5)

漏斗限流的缺陷:該方式不適用於分佈式的系統。因爲從funnels這個hash中取出結構,然後把數據放到內存中計算,最後再放回數據的過程不是原子的。這意味着我們需要加鎖操作。如果加鎖失敗,需要重試或者放棄,複雜度高。

Redis-Cell實現漏斗限流

redis的漏斗限流算法,而且提供了原子限流指令。只有一條指令:

cl.throttle erick:reply 15 30 60 1

意義分別是:

  • erick:reply:鍵值
  • 15:漏斗的容量
  • 30:規定時間內最大的操作個數
  • 60:規定的時間,這裏是30個/60s
  • 1:默認值是1,表示當前添加的quota

該指令返回5個值,分別是:

  • 0:0 表示允許,1表示拒絕
  • 15:漏斗容量
  • 14:剩餘空間
  • -1:被拒絕了,需要多久之後再重試
  • 2:多長時間後,漏斗完全空出來

GeoHash算法

該算法用於計算距離,算法的核心思想是把二維空間的距離映射到一維上,然後此時再指定元素時,就可以在一維的距離上進行比較了,減少了複雜度。注意,這裏的映射是有損映射,但是損失的精度較小,對於附近的人等的應用,這些誤差可以忽略。

總共有6個基本的操作,如下:

  • geoadd person 111 112 foofooperson集合中的經緯度是111 & 112
  • geodist person foo1 foo2 km:返回foo1foo2的距離
  • geohash person foo:獲取foo的哈希值
  • georadiusbynumber person foo 20 km count 3 asc:返回foo20km內按照升序的3個人,desc表示降序
  • georadius 111 112 20 km withdist count 3 asc:把名稱換成了經緯度

注意,GeoHash的底層數據結構是zset,集羣環境中,如果單個key的數據量過多,會對數據遷移造成卡頓。同時,key集合的大小一般也不要超過1MB。所以實際部署GeoHash時,最好單獨部署一套,不要和集羣混合在一起。

Scan

keys regex是搜索出所有滿足regex正則表達式的key值,但是該方式一般在key很少的時候使用,該方式複雜度是O(N)O(N),會阻塞redis。

一般對於線上服務來說,更多的是使用scan命令,該命令的特點

  • 複雜度O(N),通過遊標分步,不會阻塞線程。
  • 提供limit參數,控制返回結果的最大數量
  • 提供模式匹配
  • 返回結果可能有重複,需要客戶端去重
  • 遍歷過程中,如果數據有修改,則結果是不確定的
  • 單次返回結果爲空,不意味着遍歷結束,需要看遊標是否是0

比如:

scan 0 match key99* count 1000

redis採用漸進式的rehash技術,每次只是同步部分hash數據;同時,redis的遍歷方式是採用比特位的高位加法。具體可以參考下面的博客。

對於hashzset等結構,也有對應的hscanzscan等的方式。

參考博客:

  • http://tech-happen.site/32ad6396.html

大key掃描

redis中,如果有很大的hash或者set等的結構,那麼可能會造成卡頓,可能的原因如下:

  • 如果key的結構空間不夠,則需要重新分配空間,並拷貝數據,耗費時間
  • 集羣之間遷移數據,速度變慢、

如果redis的內存消耗出現較大波動,或者耗時出現較大波動,說明可能出現大key,此時需要藉助scan的方式掃描並判斷:

redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章