redis

References

Brief

in-memory database,支援眾多 data structures,可利用它作為進程間通訊的中介

Install

Windows

可以用 scoop 安裝

Docker

註:RedisStack 包含進階功能,如:RedisJSON, RediSearch, RedisTimeSeries, RedisBloom

image 包含內容 適用情境
redis Redis 簡單或 legacy 專案
redis/redis-stack-server Redis + Redis Stack 部署到生產環境
redis/redis-stack Redis + Redis Stack + RedisInsight 開發、測試
☢️ WARNING
要使用 redis-omfind() 功能,必須要使用含 Redis Stack 的 image
  1. 運行 container
    ​​​​docker run --name test-redis -p 6379:6379 -d redis
    
    • 上 DockerHub 抓 image (redis)
    • 創建一個 container 叫 test-redis
    • daemon 運行 container
    • redis-server 也會在 container 內運行
      • 資料會儲存在這裡 (in-memory)
      • listening on port 6379 (預設)
    • 將其 port mapping 到本機 port 6379,方便後續在本機操作
  2. 進入 container
    ​​​​docker exec -it test-redis redis-cli
    
    • 進入 container,一進去就執行 redis-cli,讓我們 debug
    • redis-cli 可以選擇加一個 --raw options
      • 字串以 UTF-8 編碼存入,以 byte 格式顯示
      • --raw options 讓字串得以用 Unicode 格式顯示 (正常顯示非英文字串)

Deletion Policies

記憶體空間注定是有限的,鍵值沒辦法永久留存在記憶體中,需要定期清理。

Key Expiration

如何剔除已過期的鍵值

  • 主動刪除 active expiration隨機挑選剔除

    每隔 100 ms 隨機挑選 20 個鍵值,然後刪除過期鍵值,若這 20 個鍵值中,有超過 25% 是過期的鍵值,則會重複這個動作,直到過期比例低於 25%。 (每秒做 10 次,每次挑 20 個鍵值)

  • 被動刪除 passive expiration查詢發現過期時剔除

    當該鍵值被存取到時,若該鍵值已過期,則會先刪除該鍵值,而後回覆空值給應用程式。

Memory Eviction

記憶體不夠用時,如何剔除鍵值

  • noeviction:返回錯誤,不會剔除任何鍵值。
  • allkeys-lru:針對所有鍵值,使用 LRU 算法剔除鍵值。
  • allkeys-lfu:針對所有鍵值,使用 LFU 算法剔除鍵值。
  • allkeys-random:針對所有鍵值,隨機剔除鍵值。
  • volatile-lru:在設有過期時間的鍵值中,使用 LRU 算法剔除鍵值。
  • volatile-lfu:在設有過期時間的鍵值中,使用 LRU 算法剔除鍵值。
  • volatile-random:在設有過期時間的鍵值中,隨機剔除鍵值。
  • volatile-ttl:在設有過期時間的鍵值中,剔除即將過期的鍵 (最短 TTL)。

Cache Miss Problems

  • 快取擊穿 hotspot invalid
    • 說明:某個超熱門鍵值過期,進而導致大量 request 打在資料庫臉上。
    • 解決方案:Ops Engineer 要進行流量分析,針對熱點 key 將其設置為永不過期。
  • 快取雪崩 cache avalanche
    • 說明:在短時間內發生大批鍵值過期,進而導致大量 request 打在資料庫臉上。
    • 解決方案:加上一些 jitter。即給予每個鍵值的固定過期時間加上一些隨機偏移,去分散流量。
  • 快取穿透 cache penetration
    • 說明:出現大量既不存在於 Redis 也不存在於資料庫的 GET request,這些 request 都會赤裸裸的打在資料庫臉上。
    • 解決方案:在 backend app 層實作 bloom filter,藉由其所耗空間超小及不會有偽陰性 (說你不存在就是不存在) 判定的特性,去大量刷掉這些 GET request。
      Image Not Showing Possible Reasons
      • The image was uploaded to a note which you don't have access to
      • The note which the image was originally uploaded to has been deleted
      Learn More →

Commands

  • FLUSHALL:清除所有資料

Exists

  • 1:存在
  • 0:不存在

TTL

  • -1:存在但 forever
  • -2:不存在

Data Structure

  • 列表 list (命令 L 開頭)
  • 集合 set (命令 S 開頭)
  • 單層字典 hash (命令 H 開頭)
  • 有序集合 sorted set (命令 Z 開頭)
    • key:value (value 為 floating point,依此排序)

Subscribe & Publish

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Stream

(命令 X 開頭)

Geo

(命令 GEO 開頭)

HyberLogLog

(命令 PF 開頭)

BitMap

字串就是 bit 的集合,所以結構上就是字串

BitField

字串就是 bit 的集合,所以結構上就是字串

Python

  • Third Party Library
    • redis-py
    • redis-om (將 Pydantic Model 物件存入 Redis)
  • 範例 1:同步
    ​​import redis
    ​​
    ​​REDIS_URL = "redis://localhost:6379"
    ​​redis_connection_pool = ConnectionPool.from_url(REDIS_URL, max_connections=10)
    ​​redis_connection = redis.Redis(connection_pool=redis_connection_pool)
    ​​value = "bar"
    ​​redis_connection.set("foo", value)
    ​​result = redis_connection.get("foo")  # 回傳 bytes | None
    ​​redis_connection.close()
    
    ​​assert result.decode() == value
    
  • 範例 2:異步
    ​​import redis.asyncio as redis
    
    ​​REDIS_URL = "redis://localhost:6379"
    ​​redis_connection = redis.Redis.from_url(REDIS_URL)
    ​​value = "bar_async"
    ​​await redis_connection.set("foo_async", value)
    ​​result = await redis_connection.get("foo_async")
    ​​await redis_connection.close()
    
    ​​assert result.decode() == value
    
  • 範例 3:redis-om,get 功能
    ​​from pydantic import ConfigDict
    ​​from redis_om import Field, HashModel, get_redis_connection
    
    ​​# HashModel 來讓我們可以將 Object 存成 Hash
    ​​# JsonModel 來讓我們可以將 Object 存成 JSON
    
    ​​class UserReadCache(HashModel):
    ​​    id: Annotated[int, Field(index=True)]
    ​​    name: Annotated[str, Field(index=True)]
    ​​    email: Annotated[str, Field(index=True)]
    ​​    avatar: Annotated[str, Field()] | None = None
    
    ​​    model_config = ConfigDict(database=get_redis_connection(url=REDIS_URL))
    
    ​​new_user = UserReadCache(
    ​​    id=1,
    ​​    name="json_user",
    ​​    email="json_user@email.com",
    ​​    avatar="image_url",
    ​​)
    ​​new_user.save()
    ​​pk = new_user.pk
    ​​result = UserReadCache.get(pk)
    ​​print(result)
    
    ​​assert result == new_user
    
  • 範例 4:redis-om,find 功能 (需有 Redis Stack)
    ​​from pydantic import ConfigDict
    ​​from redis_om import Field, JsonModel, Migrator, get_redis_connection
    
    ​​class ProductCache(JsonModel):
    ​​    name: Annotated[str, Field(index=True)]
    ​​    price: Annotated[float, Field(index=True)]
    ​​    in_stock: Annotated[bool, Field(index=True)]
    
    ​​    model_config = ConfigDict(database=get_redis_connection(url=REDIS_URL))
    
    ​​Migrator().run() # 🔥 記得要有這行
    
    ​​# 建立產品 1
    ​​product1 = ProductCache(
    ​​    id=1,
    ​​    name="Apple iPhone 15",
    ​​    price=999.99,
    ​​    in_stock=True,
    ​​)
    ​​product1.save()
    ​​# 建立產品 2
    ​​product2 = ProductCache(
    ​​    id=2,
    ​​    name="Samsung Galaxy S24",
    ​​    price=899.99,
    ​​    in_stock=True,
    ​​)
    ​​product2.save()
    ​​# 建立產品 3
    ​​product3 = ProductCache(
    ​​    id=3,
    ​​    name="Google Pixel 8",
    ​​    price=799.99,
    ​​    in_stock=False,
    ​​)
    ​​product3.save()
    ​​# 建立產品 4
    ​​product4 = ProductCache(
    ​​    id=4,
    ​​    name="OnePlus 12",
    ​​    price=749.99,
    ​​    in_stock=True,
    ​​)
    ​​product4.save()
    ​​# 建立產品 5
    ​​product5 = ProductCache(
    ​​    id=5,
    ​​    name="Sony Xperia 5 V",
    ​​    price=949.99,
    ​​    in_stock=False,
    ​​)
    ​​product5.save()
    
    ​​products = ProductCache.find(ProductCache.price < 800).all()
    ​​print(products)