限流
簡單限流
簡單限流的思路是,在規定的時間窗口內,給出規定的最大操作數量限制。使用zset
結構作爲一個用戶行爲的記錄。zset
的value
和score
都用來表示操作的時間戳。每次操作前,先把操作時間戳加入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內可以操作次操作,那麼需要對應數量的時間戳來存儲,浪費空間。
漏斗限流
介紹漏斗限流的思路。漏斗的滴水速度是勻速的,我們可以往漏斗中加水,如果加水的速度小於漏水的速度,那麼這個行爲是可以允許的;但是如果超過了這個速度,那麼是不允許的。
給出單機漏斗限流的基本實現:
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個/60s1
:默認值是1,表示當前添加的quota
該指令返回5個值,分別是:
0
:0 表示允許,1表示拒絕15
:漏斗容量14
:剩餘空間-1
:被拒絕了,需要多久之後再重試2
:多長時間後,漏斗完全空出來
GeoHash算法
該算法用於計算距離,算法的核心思想是把二維空間的距離映射到一維上,然後此時再指定元素時,就可以在一維的距離上進行比較了,減少了複雜度。注意,這裏的映射是有損映射,但是損失的精度較小,對於附近的人等的應用,這些誤差可以忽略。
總共有6個基本的操作,如下:
geoadd person 111 112 foo
:foo
在person
集合中的經緯度是111 & 112geodist person foo1 foo2 km
:返回foo1
和foo2
的距離geohash person foo
:獲取foo
的哈希值georadiusbynumber person foo 20 km count 3 asc
:返回foo
20km內按照升序的3個人,desc
表示降序georadius 111 112 20 km withdist count 3 asc
:把名稱換成了經緯度
注意,GeoHash
的底層數據結構是zset
,集羣環境中,如果單個key
的數據量過多,會對數據遷移造成卡頓。同時,key
集合的大小一般也不要超過1MB。所以實際部署GeoHash
時,最好單獨部署一套,不要和集羣混合在一起。
Scan
keys regex
是搜索出所有滿足regex
正則表達式的key
值,但是該方式一般在key
很少的時候使用,該方式複雜度是,會阻塞redis。
一般對於線上服務來說,更多的是使用scan
命令,該命令的特點
- 複雜度
O(N)
,通過遊標分步,不會阻塞線程。 - 提供
limit
參數,控制返回結果的最大數量 - 提供模式匹配
- 返回結果可能有重複,需要客戶端去重
- 遍歷過程中,如果數據有修改,則結果是不確定的
- 單次返回結果爲空,不意味着遍歷結束,需要看遊標是否是0
比如:
scan 0 match key99* count 1000
redis採用漸進式的rehash技術,每次只是同步部分hash數據;同時,redis的遍歷方式是採用比特位的高位加法。具體可以參考下面的博客。
對於hash
和zset
等結構,也有對應的hscan
和zscan
等的方式。
參考博客:
- 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