想要看更加舒服的排版、更加準時的推送
關注公衆號“不太靈光的程序員”
每日八點有乾貨推送
公衆號“不太靈光的程序員” 同時發佈《【Redis數據結構 String類型】String類型生產中的應用 緩存、計數器、限速器的實現》
本文依舊會對學習內容進行拆分,建議閱讀時間基本保持10分鐘內,想學習之前章節內容點擊《你不瞭解的Redis》閱讀所有章節內容。
Redis數據結構系列是對Redis常用的String、List、Set、Sorted Set、Hashe和Stream6種數據結構進行介紹,並使用redis-py進行實踐操作。
Redis數據結構 String
String是在Redis應用最的數據結構了,使用key-values做緩存、計數器、限流器。
我們先簡單瞭解下String的操作命令再來使用這些功能做些小實驗。
String常用操作命令
SET 將鍵key設定爲指定的字符串值
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- ex - 設置過期時間,單位秒
- px - 設置過期時間,單位毫秒
當SET命令執行成功之後,之前設置的過期時間都將失效,以最新的過期時間爲準
- nx - 如果設置爲True,則只有name不存在時,當前set操作才執行
- xx - 如果設置爲True,則只有name存在時,當前set操作才執行
nx和xx的功能看上去雞肋,可能環境初始的時候能用的上,默認情況都爲False
使用SET創建一個key時,如果key已經存在,操作會直接覆蓋原來的值,不管之前的key保存的數據是什麼類型,也就是說如果我們之前使用message.mq 創建了一個List類型來實現消息隊列,再次 SET 時也操作了 message.mq 這個key後,結果會使你的系統陷入癱瘓。
我們來測試一下:
print(r.lpush("user:server:message.mq", "Hello World"))
print(r.lrange("user:server:message.mq", 0, 0))
print(r.type("user:server:message.mq"))
print(r.set("user:server:message.mq", "Hello World"))
print(r.get("user:server:message.mq"))
print(r.type("user:server:message.mq"))
print(r.lpush("user:server:message.mq", "Hello World"))
> 1
> ['Hello World']
> list
> True
> Hello World
> string
> WRONGTYPE Operation against a key holding the wrong kind of value
規範的命名key值是不就顯的格外重要了,也可以將特定功能的key放在同一個命名空間中來避免錯誤。
GET 獲取key對應的數值
- GET key
GET的操作就比較合理了,只可以獲取時String類型的數值,當key不存在時返回None對象
在redis-py中會返回兩種空值,需要特別注意下
print(r.lpush("user:server:message.mq", "Hello World"))
print(r.rpop("user:server:message.mq"))
print(r.type("user:server:message.mq"))
print(type(r.type("user:server:message.mq")))
print(r.type("user:server:message.mq") is None)
print(r.get("user:1000:index"))
print(r.get("user:1000:index") is None)
> 1
> Hello World
> none
> <class 'str'>
> False
> None
> True
注:我們對一個空的List類型進行操作,獲取它的數值類型返回值是none字符串,並不是None對象,GET獲取一個不存在的key時返回的是None對象。
MSET/MGET 同時對多個key進行讀寫操作
- MSET key value [key value …]
- MGET key [key …]
使用MSET/MGET一次操作多個鍵值對來減少客戶端和服務端的通信次數,從而提升操作效率
print(r.mset({"user:1001:index": 1, "user:1002:index": "100"}))
print(r.mget("user:1001:index", "user:1002:index"))
print(r.mget(["user:1001:index", "user:1002:index"]))
> True
> ['1', '100']
> ['1', '100']
原子性操作命令
- INCR key 對數值執行原子的加1操作
- DECR key 對數值執行原子的減1操作
- INCRBY key increment 對數值執行原子的加increment操作,默認1
- DECRBY key increment 對數值執行原子的減increment操作,默認1
- INCRBYFLOAT key increment 對數值執行原子的加increment(浮點數)操作,默認1.0
- 沒有寫錯 就是沒有命令 DECRBYFLOAT
當values的數值是可以表示數字的字符串,就可以使用該原子性操作對數值進行增加或減少操作,如果操作的key不存在時會先將key的值設定爲0再做加1操作。
原子操作是一個操作或者一系列不可分割的操作,在執行完畢之前不會被任何其它任務或事件中斷,就是說即使有多個客戶端對同一個key同時發出INCR命令,也決不會導致競爭的情況。
舉個簡單的例子,當客戶端1和客戶端2同時讀取key的值是10,並且都INCR將值加1,最終key的值一定是12,Redis服務端收到兩個INCR命令時是順序執行兩次 read-increment-set,read-increment-set 操作,後者遞增前會重新讀取數值,read-increment-set操作完成前,其他客戶端不會在同一時間執行任何命令,這也和單線程不存在數據共享有關吧。
這裏插一句Redis單線程爲什麼效率還高呢?
因爲多線程的本質就是CPU模擬出來多個線程的情況,這種模擬出來的情況就有一個代價,就是上下文的切換,對於一個內存的系統來說,它沒有上下文的切換就是效率最高的。
Redis用單個CPU綁定一塊內存的數據,然後針對這塊內存的數據進行多次讀寫的時候,都是在一個CPU上完成的,所以它是單線程處理這個事。在內存的情況下,這個方案就是最佳方案 — — 阿里 沈詢
爲什麼我們大多時候要使用多線程來提高運行速度呢?
這就和CPU、內存、磁盤的運行速度有關,我們大多數的操作是需要讀取數據庫、接口數據或者文件再進行邏輯計算,需要操作磁盤IO和網絡IO,IO操作的時間遠遠要大於內存、CPU,不能讓CPU就這麼等着,就要切換到不需要等待的任務上繼續執行。
print(r.mget(["user:1000:index", "user:1001:index"]))
print(r.incr("user:1000:index"))
print(r.incr("user:1001:index"))
print(r.mget(["user:1000:index", "user:1001:index"]))
> [None, '1']
> 1
> 2
> ['1', '2']
注: 由於Redis沒有一個明確的類型來表示整型數據,所以這個操作是一個字符串操作。
但事實上,Redis內部採用整數形式來存儲這些整數值的字符串的。
INCR/INCRBYFLOAT可以不可以操作同一個key?
print(r.mget(["user:1000:index", "user:1001:index"]))
print(r.decr("user:1000:index"))
print(r.decrby("user:1000:index", 10))
print(r.incr("user:1001:index"))
print(r.incrbyfloat("user:1001:index", 1.4))
print(r.incrby("user:1001:index", 2))
> ['-43', '18']
> -44
> -54
> 19
> 20.4
> value is not an integer or out of range
注:INCRBYFLOAT 可以將一個整型數值轉化爲浮點數進行操作,如果小數位爲0的浮點數進行 INCR操作是可以的,但是 INCR 不會對浮點的數值進行四捨五入的取整操作,如果小數位存在有效數值會觸發異常。
String的應用
緩存
有一天個人博客“不太靈光的程序員”裏有了一篇爆紅的文章,訪問量巨高都把要把數據庫拉掛了?首先文章的內容的更新頻率是不高的,我們就可考慮緩存。
首先定義格式爲"article:{ids}:details"
的key來表示緩存文章,這裏設不設置緩存時間區別不大,如果對文章做修改了SET新的文章到key裏就好了。
這樣每次有新的請求進來就不會去實時的查數據庫,從而降低頁面的響應時間。
示例代碼:
def get_db():
time.sleep(5)
return "不太靈感的程序帶你瞭解Redis"
def get_article_details(article_id):
details = r.get(f"article:{article_id}:details")
if details:
print('我從緩存來')
return details
else:
print('我從數據庫來')
details = get_db()
r.set(f"article:{article_id}:details", details)
return details
if __name__ == "__main__":
for i in range(3):
start = time.time()
details = get_article_details(1001)
end = time.time()
print(details, end - start)
計數器
String的原子遞增操作最常用的使用場景是計數器。
還是以我們火爆的博客爲例子,怎麼才能記錄它到底有多火爆呢,肯定需要記錄下每篇文章的訪問量,讓後把每日的訪問增量和總量,訪問爆發的時間段出個統計去找廣告商要錢對不對。
先不考慮session的過濾,只要你點進來就算一次訪問,接下來按天來統計每篇文章的訪問量。
定義格式爲"article:{ids}:visits:date"
的key來表示文章一天的訪問量,每次用戶訪問這個頁面的時候對這個key執行一下incr命令,這樣就可以實現一個簡單的計數器了。
示例代碼:
def read_article(article_id, date):
details = get_article_details(article_id)
print('當前訪問量:', r.incr(f"article:{article_id}:visits:{date}"))
return details
if __name__ == "__main__":
for d in pd.date_range(start='2020-02-20', end='2020-02-25', freq='D'):
date = d.date()
for i in range(random.randint(1, 3)):
details = read_article(1001, date)
visits = r.get(f"article:1001:visits:{date}")
print(f"{date}訪問量{visits}")
限速器
限速器是一種可以限制某些操作執行速率的特殊場景。
比如博客裏會存在軍惡意刷留言、刷點讚的情況,就可以用限速器來控制它。
定義格式爲"user:{ids}:gives"
的key來表示用戶點前時段的點贊數,比方我的博客限制10s最多點贊5次,超過5次就提示"您的點贊次數太多了,請休息一下!",在設置個禁言期 20s。
如果你真的現在我的博客和公衆號裏點贊留言,請點死我!!!!
快來微信搜一搜關注 “不太靈光的程序員”,給予他力量。
和計數器的區別在與key的有效期,當前的場景裏我們是不關注key的到底被點擊了多少次,只要在10s裏沒超過5次就不關心,所以需要加超時時間,每次用戶點讚的時候對這個key執行一下incr命令。
示例代碼:
def give_article(user_id):
keyname = f"user:{user_id}:gives"
gives = r.get(keyname)
if gives and int(gives) >= 5:
print('您的點贊次數太多了,請休息一下!')
else:
gives = r.incr(keyname)
if r.ttl(keyname) == -1:
r.expire(keyname, 10)
if gives == 5:
r.expire(keyname, 20)
print(f'當前點贊 {gives} 次')
if __name__ == "__main__":
for i in range(60):
give_article(1001)
time.sleep(1)
限制當前發文次數、接口調用次數限制、遊戲種體力都可以用到限速器。