---
# System prepended metadata

title: redis
tags: [Web]

---

# redis

## References
+ 🔗 [**菜鳥教程 - Redis**](https://www.runoob.com/redis/redis-tutorial.html)
+ 🎬 [**GeekHour - 一小时 Redis 教程**](https://youtu.be/WgpP7-XAI5Y)
+ 🎬 [**轩辕的编程宇宙 - 1. 我是Redis，MySQL大哥被我坑惨了！**](https://www.bilibili.com/video/BV1Fd4y1T7pD/?)
+ 🎬 [**轩辕的编程宇宙 - 2. Redis数据持久化AOF和RDB原理一次搞懂！**](https://www.bilibili.com/video/BV1sV4y147Jz/?)
+ 🎬 [**轩辕的编程宇宙 - 3. Redis的高可用是怎么实现的？哨兵是什么原理？**](https://www.bilibili.com/video/BV1MW4y187AH/?)
+ 🎬 [**轩辕的编程宇宙 - 4. Redis集群是如何工作的？**](https://www.bilibili.com/video/BV1ge411L7Sh/?)

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

## Install

### Windows
可以用 `scoop` 安裝

### Docker
註：[RedisStack](https://redis.io/about/redis-stack/) 包含進階功能，如：RedisJSON, RediSearch, RedisTimeSeries, RedisBloom
| image | 包含內容 | 適用情境 |
| --- | --- | --- |
| `redis` | Redis | 簡單或 legacy 專案 |
| `redis/redis-stack-server` | Redis + Redis Stack | 部署到生產環境 |
| `redis/redis-stack` | Redis + Redis Stack + RedisInsight | 開發、測試 |

|☢️ <span class="warning">WARNING</span>|
|:---|
|要使用 `redis-om` 的 `find()` 功能，必須要使用含 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
<mark>如何剔除已過期的鍵值</mark>

+ **主動刪除 active expiration**：<mark>隨機挑選剔除</mark>

    每隔 100 ms 隨機挑選 20 個鍵值，然後刪除過期鍵值，若這 20 個鍵值中，有超過 25% 是過期的鍵值，則會重複這個動作，直到過期比例低於 25%。 (每秒做 10 次，每次挑 20 個鍵值)
  
+ **被動刪除 passive expiration**：<mark>查詢發現過期時剔除</mark>

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

### Memory Eviction
<mark>記憶體不夠用時，如何剔除鍵值</mark>

+ `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**
  + 說明：<mark>某個超熱門鍵值過期</mark>，進而導致大量 request 打在資料庫臉上。
  + 解決方案：Ops Engineer 要進行流量分析，針對熱點 key 將其設置為永不過期。
+ **快取雪崩 cache avalanche**
  + 說明：在<mark>短時間內發生大批鍵值過期</mark>，進而導致大量 request 打在資料庫臉上。
  + 解決方案：加上一些 <mark>jitter</mark>。即給予每個鍵值的固定過期時間加上一些隨機偏移，去分散流量。
+ **快取穿透 cache penetration**
  + 說明：出現<mark>大量既不存在於 Redis 也不存在於資料庫的 GET request</mark>，這些 request 都會赤裸裸的打在資料庫臉上。
  + 解決方案：在 backend app 層實作 <mark>bloom filter</mark>，藉由其所耗空間超小及不會有偽陰性 (說你不存在就是不存在) 判定的特性，去大量刷掉這些 GET request。
    ![bloom filter](https://hackmd.io/_uploads/HkWfUws1gg.png)

## 

## 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
![](https://hackmd.io/_uploads/HJzT8Ztygg.png)


### Stream
(命令 `X` 開頭)

### Geo
(命令 `GEO` 開頭)


### HyberLogLog
(命令 `PF` 開頭)

### BitMap
字串就是 bit 的集合，所以結構上就是字串


### BitField
字串就是 bit 的集合，所以結構上就是字串


## Python

+ Third Party Library
  + `redis-py`
  + `redis-om` (將 Pydantic Model 物件存入 Redis)
+ 範例 1：同步
  ```py
  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：異步
  ```py
  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 功能
  ```py
  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)
  ```py
  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)
  ```