【Redis數據結構 String類型】String類型生產中的應用 緩存、計數器、限速器的實現

想要看更加舒服的排版、更加準時的推送
關注公衆號“不太靈光的程序員”
每日八點有乾貨推送
公衆號“不太靈光的程序員” 同時發佈《【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)

在這裏插入圖片描述
限制當前發文次數、接口調用次數限制、遊戲種體力都可以用到限速器。

推薦閱讀:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章