Ubuntu 22.04 Redis JSON 模組應用

Ubuntu 22.04 Redis JSON Module Application

前言

RedisJSON 是 Redis 的一個強大模組,讓 Redis 原生支援 JSON 資料類型的儲存與操作。透過 RedisJSON,開發者可以直接在 Redis 中儲存、更新和查詢 JSON 文件,無需將 JSON 序列化為字串。本文將詳細介紹如何在 Ubuntu 22.04 上安裝、設定並使用 RedisJSON 模組。

RedisJSON 模組介紹

什麼是 RedisJSON?

RedisJSON(原名 ReJSON)是 Redis Labs 開發的開源模組,為 Redis 提供原生的 JSON 資料類型支援。它具有以下特點:

  • 原生 JSON 支援:直接儲存和操作 JSON 文件,無需序列化
  • 部分更新:可以只更新 JSON 文件的特定欄位,而非整個文件
  • JSONPath 查詢:支援 JSONPath 語法進行複雜查詢
  • 高效能:基於 Redis 的記憶體架構,提供極快的讀寫速度
  • 原子操作:所有操作都是原子性的,確保資料一致性

為什麼選擇 RedisJSON?

特性傳統 Redis 字串RedisJSON
部分更新需要讀取-修改-寫入原子性部分更新
查詢效率需要反序列化整個文件直接查詢特定路徑
記憶體使用JSON 字串儲存優化的二進制格式
操作複雜度應用層處理Redis 層處理

安裝與設定

方法一:使用 Redis Stack(推薦)

Redis Stack 是 Redis 官方提供的整合套件,包含 RedisJSON、RediSearch 等多個模組。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 添加 Redis 官方 GPG 金鑰
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

# 設定 Redis 儲存庫
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

# 更新套件索引並安裝 Redis Stack
sudo apt update
sudo apt install redis-stack-server -y

# 啟動 Redis Stack 服務
sudo systemctl enable redis-stack-server
sudo systemctl start redis-stack-server

# 驗證服務狀態
sudo systemctl status redis-stack-server

方法二:手動編譯安裝

如果需要自訂安裝,可以從原始碼編譯 RedisJSON 模組。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 安裝編譯依賴
sudo apt update
sudo apt install build-essential git cargo rustc -y

# 確認 Redis 已安裝
sudo apt install redis-server -y

# 克隆 RedisJSON 原始碼
cd /tmp
git clone --recursive https://github.com/RedisJSON/RedisJSON.git
cd RedisJSON

# 編譯模組
cargo build --release

# 複製模組到 Redis 模組目錄
sudo mkdir -p /usr/lib/redis/modules
sudo cp target/release/librejson.so /usr/lib/redis/modules/

設定 Redis 載入模組

編輯 Redis 設定檔:

1
sudo nano /etc/redis/redis.conf

在設定檔中添加以下行:

1
2
# 載入 RedisJSON 模組
loadmodule /usr/lib/redis/modules/librejson.so

重新啟動 Redis:

1
sudo systemctl restart redis-server

驗證模組安裝

1
2
3
4
5
# 連接 Redis CLI
redis-cli

# 列出已載入的模組
MODULE LIST

預期輸出:

1
2
3
4
5
6
7
8
1) 1) "name"
   2) "ReJSON"
   3) "ver"
   4) (integer) 20609
   5) "path"
   6) "/usr/lib/redis/modules/librejson.so"
   7) "args"
   8) (empty array)

JSON 資料操作指令

基本操作

JSON.SET - 設定 JSON 值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 設定完整 JSON 文件
JSON.SET user:1001 $ '{"name": "王小明", "age": 28, "email": "xiaoming@example.com", "skills": ["Python", "JavaScript", "Go"]}'

# 設定巢狀物件
JSON.SET user:1002 $ '{"profile": {"name": "李小華", "department": "Engineering"}, "active": true}'

# NX 選項:只在 key 不存在時設定
JSON.SET user:1003 $ '{"name": "張大偉"}' NX

# XX 選項:只在 key 存在時設定
JSON.SET user:1001 $.age 29 XX

JSON.GET - 取得 JSON 值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 取得完整文件
JSON.GET user:1001

# 取得特定欄位
JSON.GET user:1001 $.name

# 取得多個欄位
JSON.GET user:1001 $.name $.age $.email

# 格式化輸出(縮排)
JSON.GET user:1001 INDENT "\t" NEWLINE "\n" SPACE " "

JSON.DEL - 刪除 JSON 值

1
2
3
4
5
6
7
8
# 刪除特定欄位
JSON.DEL user:1001 $.email

# 刪除陣列元素
JSON.DEL user:1001 $.skills[0]

# 刪除整個 key
JSON.DEL user:1001

數值操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 設定包含數值的 JSON
JSON.SET product:1 $ '{"name": "筆記型電腦", "price": 35000, "stock": 50}'

# 數值遞增
JSON.NUMINCRBY product:1 $.price 1000
# 結果:36000

# 數值遞減
JSON.NUMINCRBY product:1 $.stock -5
# 結果:45

# 浮點數乘法
JSON.SET stats:1 $ '{"rate": 0.15}'
JSON.NUMMULTBY stats:1 $.rate 2
# 結果:0.3

字串操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 設定字串
JSON.SET message:1 $ '{"content": "Hello"}'

# 字串附加
JSON.STRAPPEND message:1 $.content '" World"'
# 結果:"Hello World"

# 取得字串長度
JSON.STRLEN message:1 $.content
# 結果:11

陣列操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 建立包含陣列的 JSON
JSON.SET tasks:user1 $ '{"pending": ["task1", "task2"], "completed": []}'

# 陣列尾端添加元素
JSON.ARRAPPEND tasks:user1 $.pending '"task3"' '"task4"'

# 陣列頭部插入元素
JSON.ARRINSERT tasks:user1 $.pending 0 '"urgent_task"'

# 取得陣列長度
JSON.ARRLEN tasks:user1 $.pending

# 取得元素索引
JSON.ARRINDEX tasks:user1 $.pending '"task2"'

# 彈出最後一個元素
JSON.ARRPOP tasks:user1 $.pending

# 彈出指定位置元素
JSON.ARRPOP tasks:user1 $.pending 0

# 陣列裁剪(保留索引 0-2 的元素)
JSON.ARRTRIM tasks:user1 $.pending 0 2

物件操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 設定巢狀物件
JSON.SET config:app $ '{"database": {"host": "localhost", "port": 5432}, "cache": {"enabled": true}}'

# 取得物件的所有 key
JSON.OBJKEYS config:app $.database
# 結果:["host", "port"]

# 取得物件長度(key 數量)
JSON.OBJLEN config:app $.database
# 結果:2

# 合併物件
JSON.MERGE config:app $.database '{"username": "admin", "password": "secret"}'

類型與除錯

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 取得 JSON 類型
JSON.TYPE user:1001 $.name
# 結果:string

JSON.TYPE user:1001 $.age
# 結果:integer

JSON.TYPE user:1001 $.skills
# 結果:array

# 取得 JSON 記憶體使用量
JSON.DEBUG MEMORY user:1001

JSONPath 查詢語法

RedisJSON 支援 JSONPath 語法進行複雜查詢,以下是常用的語法說明。

基本路徑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 準備測試資料
JSON.SET store:1 $ '{
  "name": "台北門市",
  "products": [
    {"id": 1, "name": "蘋果", "price": 30, "category": "fruit"},
    {"id": 2, "name": "香蕉", "price": 20, "category": "fruit"},
    {"id": 3, "name": "牛奶", "price": 65, "category": "dairy"},
    {"id": 4, "name": "麵包", "price": 45, "category": "bakery"}
  ],
  "staff": {
    "manager": "陳經理",
    "employees": ["小王", "小李", "小張"]
  }
}'

# $ - 根路徑
JSON.GET store:1 $
# 取得整個 JSON 文件

# .key - 子物件
JSON.GET store:1 $.name
# 結果:["台北門市"]

# ['key'] - 括號表示法
JSON.GET store:1 $['name']
# 結果:["台北門市"]

陣列存取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# [index] - 陣列索引
JSON.GET store:1 $.products[0]
# 結果:[{"id":1,"name":"蘋果","price":30,"category":"fruit"}]

# [start:end] - 陣列切片
JSON.GET store:1 $.products[0:2]
# 結果:前兩個產品

# [-1] - 負數索引(最後一個)
JSON.GET store:1 $.products[-1]
# 結果:最後一個產品

# [*] - 所有陣列元素
JSON.GET store:1 $.products[*].name
# 結果:["蘋果","香蕉","牛奶","麵包"]

遞迴下降

1
2
3
4
5
6
7
# .. - 遞迴搜尋所有層級
JSON.GET store:1 $..name
# 結果:["台北門市","蘋果","香蕉","牛奶","麵包"]

# 搜尋所有 price 欄位
JSON.GET store:1 $..price
# 結果:[30,20,65,45]

過濾器表達式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 過濾價格大於 40 的產品
JSON.GET store:1 '$.products[?(@.price>40)]'
# 結果:牛奶和麵包

# 過濾特定類別
JSON.GET store:1 '$.products[?(@.category=="fruit")]'
# 結果:蘋果和香蕉

# 複合條件過濾
JSON.GET store:1 '$.products[?(@.price>=30 && @.category=="fruit")]'
# 結果:蘋果

# 取得過濾後的特定欄位
JSON.GET store:1 '$.products[?(@.price<50)].name'
# 結果:["蘋果","香蕉","麵包"]

萬用字元

1
2
3
# * - 匹配任意 key
JSON.GET store:1 $.staff.*
# 結果:["陳經理",["小王","小李","小張"]]

索引與搜尋整合

RedisJSON 可以與 RediSearch 模組整合,提供強大的全文搜尋和索引功能。

建立索引

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 首先確認 RediSearch 模組已載入
MODULE LIST

# 準備 JSON 資料
JSON.SET article:1 $ '{"title": "Redis 入門指南", "content": "Redis 是一個開源的記憶體資料庫...", "author": "王大明", "views": 1500, "tags": ["redis", "database", "tutorial"]}'
JSON.SET article:2 $ '{"title": "Python 資料處理", "content": "使用 Python 進行資料分析...", "author": "李小華", "views": 2300, "tags": ["python", "data", "analysis"]}'
JSON.SET article:3 $ '{"title": "Redis JSON 進階應用", "content": "深入了解 RedisJSON 模組的使用...", "author": "王大明", "views": 800, "tags": ["redis", "json", "advanced"]}'

# 建立 JSON 索引
FT.CREATE idx:articles ON JSON PREFIX 1 article: SCHEMA $.title AS title TEXT WEIGHT 5.0 $.content AS content TEXT $.author AS author TAG $.views AS views NUMERIC $.tags AS tags TAG

搜尋查詢

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 全文搜尋
FT.SEARCH idx:articles "Redis"
# 搜尋標題或內容包含 "Redis" 的文章

# 特定欄位搜尋
FT.SEARCH idx:articles "@title:Redis"
# 只搜尋標題包含 "Redis" 的文章

# 標籤過濾
FT.SEARCH idx:articles "@tags:{redis}"
# 搜尋帶有 redis 標籤的文章

# 作者過濾
FT.SEARCH idx:articles "@author:{王大明}"

# 數值範圍查詢
FT.SEARCH idx:articles "@views:[1000 +inf]"
# 搜尋瀏覽數大於 1000 的文章

# 複合查詢
FT.SEARCH idx:articles "@author:{王大明} @views:[500 2000]"

# 排序
FT.SEARCH idx:articles "*" SORTBY views DESC

# 分頁
FT.SEARCH idx:articles "*" LIMIT 0 10

聚合查詢

1
2
3
4
5
6
7
8
# 按作者統計文章數量
FT.AGGREGATE idx:articles "*" GROUPBY 1 @author REDUCE COUNT 0 AS article_count

# 計算平均瀏覽數
FT.AGGREGATE idx:articles "*" GROUPBY 1 @author REDUCE AVG 1 @views AS avg_views

# 找出最高瀏覽數
FT.AGGREGATE idx:articles "*" GROUPBY 1 @author REDUCE MAX 1 @views AS max_views

Python 與 Node.js 整合

Python 整合

安裝依賴

1
pip install redis

基本操作範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import redis
import json

# 連接 Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 設定 JSON 資料
user_data = {
    "id": 1001,
    "name": "王小明",
    "email": "xiaoming@example.com",
    "profile": {
        "age": 28,
        "city": "台北",
        "interests": ["程式設計", "攝影", "旅遊"]
    },
    "settings": {
        "theme": "dark",
        "notifications": True
    }
}

# 使用 JSON.SET 儲存
r.execute_command('JSON.SET', 'user:1001', '$', json.dumps(user_data))

# 取得完整 JSON
result = r.execute_command('JSON.GET', 'user:1001')
print(f"完整資料:{result}")

# 取得特定欄位
name = r.execute_command('JSON.GET', 'user:1001', '$.name')
print(f"姓名:{name}")

# 更新特定欄位
r.execute_command('JSON.SET', 'user:1001', '$.profile.age', 29)

# 陣列操作
r.execute_command('JSON.ARRAPPEND', 'user:1001', '$.profile.interests', '"音樂"')

# 取得巢狀物件
profile = r.execute_command('JSON.GET', 'user:1001', '$.profile')
print(f"個人資料:{profile}")

# 使用 JSONPath 過濾
interests = r.execute_command('JSON.GET', 'user:1001', '$.profile.interests[*]')
print(f"興趣:{interests}")

進階封裝類別

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import redis
import json
from typing import Any, Optional, List

class RedisJSONClient:
    """RedisJSON 操作封裝類別"""

    def __init__(self, host: str = 'localhost', port: int = 6379):
        self.client = redis.Redis(host=host, port=port, decode_responses=True)

    def set(self, key: str, path: str, value: Any) -> bool:
        """設定 JSON 值"""
        try:
            if isinstance(value, (dict, list)):
                value = json.dumps(value, ensure_ascii=False)
            elif isinstance(value, str):
                value = f'"{value}"'
            self.client.execute_command('JSON.SET', key, path, value)
            return True
        except Exception as e:
            print(f"Error setting JSON: {e}")
            return False

    def get(self, key: str, *paths: str) -> Any:
        """取得 JSON 值"""
        try:
            if paths:
                result = self.client.execute_command('JSON.GET', key, *paths)
            else:
                result = self.client.execute_command('JSON.GET', key)
            return json.loads(result) if result else None
        except Exception as e:
            print(f"Error getting JSON: {e}")
            return None

    def delete(self, key: str, path: str = '$') -> int:
        """刪除 JSON 值"""
        return self.client.execute_command('JSON.DEL', key, path)

    def arr_append(self, key: str, path: str, *values: Any) -> int:
        """陣列尾端添加元素"""
        json_values = [json.dumps(v, ensure_ascii=False) for v in values]
        return self.client.execute_command('JSON.ARRAPPEND', key, path, *json_values)

    def arr_len(self, key: str, path: str) -> int:
        """取得陣列長度"""
        result = self.client.execute_command('JSON.ARRLEN', key, path)
        return result[0] if result else 0

    def increment(self, key: str, path: str, value: int = 1) -> int:
        """數值遞增"""
        return self.client.execute_command('JSON.NUMINCRBY', key, path, value)

    def type(self, key: str, path: str = '$') -> str:
        """取得 JSON 類型"""
        result = self.client.execute_command('JSON.TYPE', key, path)
        return result[0] if result else None


# 使用範例
if __name__ == "__main__":
    client = RedisJSONClient()

    # 儲存商品資料
    product = {
        "id": "P001",
        "name": "機械鍵盤",
        "price": 2500,
        "specs": {
            "switches": "Cherry MX Blue",
            "layout": "87 keys",
            "backlight": True
        },
        "reviews": []
    }

    client.set("product:P001", "$", product)

    # 添加評論
    review = {"user": "buyer123", "rating": 5, "comment": "手感很好!"}
    client.arr_append("product:P001", "$.reviews", review)

    # 價格調整
    client.increment("product:P001", "$.price", -200)

    # 取得更新後的資料
    updated_product = client.get("product:P001")
    print(json.dumps(updated_product, indent=2, ensure_ascii=False))

Node.js 整合

安裝依賴

1
npm install redis

基本操作範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { createClient } from 'redis';

async function main() {
    // 建立連線
    const client = createClient({
        url: 'redis://localhost:6379'
    });

    client.on('error', err => console.log('Redis Client Error', err));

    await client.connect();

    // 設定 JSON 資料
    const userData = {
        id: 2001,
        name: '李小華',
        email: 'xiaohua@example.com',
        profile: {
            age: 32,
            city: '高雄',
            skills: ['JavaScript', 'React', 'Node.js']
        },
        active: true
    };

    // 儲存 JSON
    await client.sendCommand([
        'JSON.SET',
        'user:2001',
        '$',
        JSON.stringify(userData)
    ]);
    console.log('使用者資料已儲存');

    // 取得完整 JSON
    const fullData = await client.sendCommand(['JSON.GET', 'user:2001']);
    console.log('完整資料:', JSON.parse(fullData));

    // 取得特定欄位
    const name = await client.sendCommand(['JSON.GET', 'user:2001', '$.name']);
    console.log('姓名:', JSON.parse(name));

    // 更新特定欄位
    await client.sendCommand(['JSON.SET', 'user:2001', '$.profile.age', '33']);

    // 陣列操作 - 添加技能
    await client.sendCommand([
        'JSON.ARRAPPEND',
        'user:2001',
        '$.profile.skills',
        '"TypeScript"'
    ]);

    // 取得技能列表
    const skills = await client.sendCommand([
        'JSON.GET',
        'user:2001',
        '$.profile.skills'
    ]);
    console.log('技能:', JSON.parse(skills));

    // 數值遞增
    await client.sendCommand(['JSON.NUMINCRBY', 'user:2001', '$.profile.age', '1']);

    // 取得更新後的年齡
    const age = await client.sendCommand(['JSON.GET', 'user:2001', '$.profile.age']);
    console.log('更新後年齡:', JSON.parse(age));

    await client.disconnect();
}

main().catch(console.error);

進階封裝模組

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { createClient } from 'redis';

class RedisJSONClient {
    constructor(url = 'redis://localhost:6379') {
        this.client = createClient({ url });
        this.connected = false;
    }

    async connect() {
        if (!this.connected) {
            this.client.on('error', err => console.error('Redis Error:', err));
            await this.client.connect();
            this.connected = true;
        }
        return this;
    }

    async disconnect() {
        if (this.connected) {
            await this.client.disconnect();
            this.connected = false;
        }
    }

    async set(key, path, value) {
        const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
        return await this.client.sendCommand(['JSON.SET', key, path, jsonValue]);
    }

    async get(key, ...paths) {
        const args = ['JSON.GET', key, ...paths];
        const result = await this.client.sendCommand(args);
        return result ? JSON.parse(result) : null;
    }

    async delete(key, path = '$') {
        return await this.client.sendCommand(['JSON.DEL', key, path]);
    }

    async arrAppend(key, path, ...values) {
        const jsonValues = values.map(v =>
            typeof v === 'string' ? `"${v}"` : JSON.stringify(v)
        );
        return await this.client.sendCommand([
            'JSON.ARRAPPEND',
            key,
            path,
            ...jsonValues
        ]);
    }

    async arrLen(key, path) {
        const result = await this.client.sendCommand(['JSON.ARRLEN', key, path]);
        return Array.isArray(result) ? result[0] : result;
    }

    async increment(key, path, value = 1) {
        return await this.client.sendCommand([
            'JSON.NUMINCRBY',
            key,
            path,
            value.toString()
        ]);
    }

    async type(key, path = '$') {
        const result = await this.client.sendCommand(['JSON.TYPE', key, path]);
        return Array.isArray(result) ? result[0] : result;
    }
}

// 使用範例
async function example() {
    const json = new RedisJSONClient();
    await json.connect();

    // 建立訂單
    const order = {
        orderId: 'ORD-2026-001',
        customer: {
            id: 'C001',
            name: '張小明'
        },
        items: [
            { product: '鍵盤', quantity: 1, price: 2500 },
            { product: '滑鼠', quantity: 2, price: 800 }
        ],
        total: 4100,
        status: 'pending'
    };

    await json.set('order:ORD-2026-001', '$', order);

    // 更新訂單狀態
    await json.set('order:ORD-2026-001', '$.status', 'processing');

    // 添加商品
    await json.arrAppend('order:ORD-2026-001', '$.items', {
        product: '螢幕',
        quantity: 1,
        price: 8000
    });

    // 更新總金額
    await json.increment('order:ORD-2026-001', '$.total', 8000);

    // 取得訂單
    const updatedOrder = await json.get('order:ORD-2026-001');
    console.log(JSON.stringify(updatedOrder, null, 2));

    await json.disconnect();
}

example().catch(console.error);

效能考量

記憶體優化

RedisJSON 使用優化的二進制格式儲存 JSON,但大型 JSON 文件仍會消耗較多記憶體。以下是一些優化建議:

1
2
3
4
5
# 查看 JSON 記憶體使用量
JSON.DEBUG MEMORY user:1001

# 壓縮 JSON(移除格式化空白)
JSON.SET user:1001 $ '{"name":"王小明","age":28}'

記憶體優化策略

  1. 扁平化結構:避免過深的巢狀結構
  2. 分割大型文件:將大型 JSON 分割成多個 key
  3. 使用引用:對於重複資料,使用 ID 引用而非完整物件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 不建議:巢狀儲存
order = {
    "id": "ORD001",
    "customer": {
        "id": "C001",
        "name": "王小明",
        "address": {"city": "台北", "street": "..."},
        "orders_history": [...]  # 可能很大
    }
}

# 建議:分離儲存
customer = {"id": "C001", "name": "王小明", "address": {...}}
order = {"id": "ORD001", "customer_id": "C001", "items": [...]}

# 分別儲存
r.execute_command('JSON.SET', 'customer:C001', '$', json.dumps(customer))
r.execute_command('JSON.SET', 'order:ORD001', '$', json.dumps(order))

操作效能

批次操作

1
2
3
4
5
6
# 使用 MULTI/EXEC 進行批次操作
MULTI
JSON.SET product:1 $ '{"name": "商品1", "price": 100}'
JSON.SET product:2 $ '{"name": "商品2", "price": 200}'
JSON.SET product:3 $ '{"name": "商品3", "price": 300}'
EXEC

Pipeline(Python 範例)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 使用 pipeline 批次寫入
pipe = r.pipeline()
for i in range(1000):
    product = {"id": i, "name": f"商品{i}", "price": i * 10}
    pipe.execute_command('JSON.SET', f'product:{i}', '$', json.dumps(product))
pipe.execute()

# 使用 pipeline 批次讀取
pipe = r.pipeline()
for i in range(1000):
    pipe.execute_command('JSON.GET', f'product:{i}', '$.price')
results = pipe.execute()

效能基準參考

操作平均延遲每秒操作數
JSON.SET(小型文件)~0.1ms~10,000
JSON.GET(完整文件)~0.08ms~12,500
JSON.GET(單一欄位)~0.05ms~20,000
JSON.NUMINCRBY~0.06ms~16,600
JSON.ARRAPPEND~0.07ms~14,300

注意:實際效能取決於硬體配置、JSON 大小和網路延遲

監控與調校

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 監控 Redis 記憶體使用
redis-cli INFO memory

# 查看 slowlog
redis-cli SLOWLOG GET 10

# 監控即時命令
redis-cli MONITOR

# 設定記憶體上限
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lru

實際應用案例

案例一:電商購物車系統

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import redis
import json
from datetime import datetime

class ShoppingCart:
    def __init__(self):
        self.r = redis.Redis(host='localhost', port=6379, decode_responses=True)

    def get_cart_key(self, user_id):
        return f"cart:{user_id}"

    def initialize_cart(self, user_id):
        """初始化購物車"""
        cart = {
            "user_id": user_id,
            "items": [],
            "created_at": datetime.now().isoformat(),
            "updated_at": datetime.now().isoformat()
        }
        self.r.execute_command(
            'JSON.SET',
            self.get_cart_key(user_id),
            '$',
            json.dumps(cart)
        )
        return cart

    def add_item(self, user_id, product_id, name, price, quantity=1):
        """添加商品到購物車"""
        key = self.get_cart_key(user_id)

        # 檢查購物車是否存在
        if not self.r.exists(key):
            self.initialize_cart(user_id)

        # 檢查商品是否已在購物車中
        items = json.loads(
            self.r.execute_command('JSON.GET', key, '$.items')
        )[0]

        for i, item in enumerate(items):
            if item['product_id'] == product_id:
                # 更新數量
                self.r.execute_command(
                    'JSON.NUMINCRBY',
                    key,
                    f'$.items[{i}].quantity',
                    quantity
                )
                self._update_timestamp(key)
                return self.get_cart(user_id)

        # 添加新商品
        new_item = {
            "product_id": product_id,
            "name": name,
            "price": price,
            "quantity": quantity
        }
        self.r.execute_command(
            'JSON.ARRAPPEND',
            key,
            '$.items',
            json.dumps(new_item)
        )
        self._update_timestamp(key)
        return self.get_cart(user_id)

    def remove_item(self, user_id, product_id):
        """從購物車移除商品"""
        key = self.get_cart_key(user_id)
        items = json.loads(
            self.r.execute_command('JSON.GET', key, '$.items')
        )[0]

        for i, item in enumerate(items):
            if item['product_id'] == product_id:
                self.r.execute_command('JSON.DEL', key, f'$.items[{i}]')
                self._update_timestamp(key)
                break

        return self.get_cart(user_id)

    def update_quantity(self, user_id, product_id, quantity):
        """更新商品數量"""
        key = self.get_cart_key(user_id)
        items = json.loads(
            self.r.execute_command('JSON.GET', key, '$.items')
        )[0]

        for i, item in enumerate(items):
            if item['product_id'] == product_id:
                self.r.execute_command(
                    'JSON.SET',
                    key,
                    f'$.items[{i}].quantity',
                    quantity
                )
                self._update_timestamp(key)
                break

        return self.get_cart(user_id)

    def get_cart(self, user_id):
        """取得購物車內容"""
        key = self.get_cart_key(user_id)
        result = self.r.execute_command('JSON.GET', key)
        return json.loads(result) if result else None

    def get_total(self, user_id):
        """計算購物車總金額"""
        cart = self.get_cart(user_id)
        if not cart:
            return 0
        return sum(item['price'] * item['quantity'] for item in cart['items'])

    def clear_cart(self, user_id):
        """清空購物車"""
        self.r.execute_command('JSON.DEL', self.get_cart_key(user_id))

    def _update_timestamp(self, key):
        """更新時間戳記"""
        self.r.execute_command(
            'JSON.SET',
            key,
            '$.updated_at',
            f'"{datetime.now().isoformat()}"'
        )


# 使用範例
cart = ShoppingCart()

# 添加商品
cart.add_item("user123", "P001", "機械鍵盤", 2500, 1)
cart.add_item("user123", "P002", "電競滑鼠", 1200, 2)
cart.add_item("user123", "P003", "滑鼠墊", 300, 1)

# 取得購物車
print(json.dumps(cart.get_cart("user123"), indent=2, ensure_ascii=False))

# 計算總金額
print(f"總金額: NT${cart.get_total('user123')}")

# 更新數量
cart.update_quantity("user123", "P002", 3)

# 移除商品
cart.remove_item("user123", "P003")

案例二:即時儀表板資料

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import redis
import json
from datetime import datetime
import random

class DashboardMetrics:
    def __init__(self):
        self.r = redis.Redis(host='localhost', port=6379, decode_responses=True)

    def initialize_dashboard(self, dashboard_id):
        """初始化儀表板"""
        dashboard = {
            "id": dashboard_id,
            "metrics": {
                "total_users": 0,
                "active_sessions": 0,
                "requests_per_minute": 0,
                "error_rate": 0.0,
                "avg_response_time": 0.0
            },
            "alerts": [],
            "last_updated": datetime.now().isoformat()
        }
        self.r.execute_command(
            'JSON.SET',
            f'dashboard:{dashboard_id}',
            '$',
            json.dumps(dashboard)
        )

    def update_metric(self, dashboard_id, metric_name, value):
        """更新單一指標"""
        key = f'dashboard:{dashboard_id}'
        self.r.execute_command(
            'JSON.SET',
            key,
            f'$.metrics.{metric_name}',
            str(value)
        )
        self._update_timestamp(key)

    def increment_metric(self, dashboard_id, metric_name, delta=1):
        """遞增指標值"""
        key = f'dashboard:{dashboard_id}'
        self.r.execute_command(
            'JSON.NUMINCRBY',
            key,
            f'$.metrics.{metric_name}',
            delta
        )
        self._update_timestamp(key)

    def add_alert(self, dashboard_id, severity, message):
        """添加警報"""
        key = f'dashboard:{dashboard_id}'
        alert = {
            "severity": severity,
            "message": message,
            "timestamp": datetime.now().isoformat()
        }
        self.r.execute_command(
            'JSON.ARRAPPEND',
            key,
            '$.alerts',
            json.dumps(alert)
        )

        # 只保留最近 100 筆警報
        alerts_len = self.r.execute_command('JSON.ARRLEN', key, '$.alerts')[0]
        if alerts_len > 100:
            self.r.execute_command('JSON.ARRTRIM', key, '$.alerts', -100, -1)

    def get_dashboard(self, dashboard_id):
        """取得儀表板資料"""
        result = self.r.execute_command('JSON.GET', f'dashboard:{dashboard_id}')
        return json.loads(result) if result else None

    def get_metrics(self, dashboard_id):
        """取得所有指標"""
        result = self.r.execute_command(
            'JSON.GET',
            f'dashboard:{dashboard_id}',
            '$.metrics'
        )
        return json.loads(result)[0] if result else None

    def get_recent_alerts(self, dashboard_id, count=10):
        """取得最近的警報"""
        key = f'dashboard:{dashboard_id}'
        result = self.r.execute_command('JSON.GET', key, '$.alerts')
        alerts = json.loads(result)[0] if result else []
        return alerts[-count:]

    def _update_timestamp(self, key):
        self.r.execute_command(
            'JSON.SET',
            key,
            '$.last_updated',
            f'"{datetime.now().isoformat()}"'
        )


# 使用範例
dashboard = DashboardMetrics()

# 初始化
dashboard.initialize_dashboard("main")

# 模擬更新
dashboard.update_metric("main", "total_users", 15000)
dashboard.update_metric("main", "active_sessions", 342)
dashboard.update_metric("main", "requests_per_minute", 1250)
dashboard.update_metric("main", "avg_response_time", 45.2)

# 添加警報
dashboard.add_alert("main", "warning", "CPU 使用率超過 80%")
dashboard.add_alert("main", "critical", "資料庫連線池耗盡")

# 取得儀表板
print(json.dumps(dashboard.get_dashboard("main"), indent=2, ensure_ascii=False))

案例三:使用者設定管理

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { createClient } from 'redis';

class UserSettings {
    constructor() {
        this.client = createClient({ url: 'redis://localhost:6379' });
    }

    async connect() {
        await this.client.connect();
    }

    async disconnect() {
        await this.client.disconnect();
    }

    async initializeSettings(userId) {
        const defaultSettings = {
            userId,
            theme: 'light',
            language: 'zh-TW',
            notifications: {
                email: true,
                push: true,
                sms: false,
                frequency: 'daily'
            },
            privacy: {
                profileVisible: true,
                showEmail: false,
                showPhone: false
            },
            accessibility: {
                fontSize: 'medium',
                highContrast: false,
                reduceMotion: false
            },
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        };

        await this.client.sendCommand([
            'JSON.SET',
            `settings:${userId}`,
            '$',
            JSON.stringify(defaultSettings)
        ]);

        return defaultSettings;
    }

    async getSettings(userId) {
        const result = await this.client.sendCommand([
            'JSON.GET',
            `settings:${userId}`
        ]);
        return result ? JSON.parse(result) : null;
    }

    async updateSetting(userId, path, value) {
        const key = `settings:${userId}`;
        const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);

        await this.client.sendCommand([
            'JSON.SET',
            key,
            `$.${path}`,
            jsonValue
        ]);

        await this.client.sendCommand([
            'JSON.SET',
            key,
            '$.updatedAt',
            `"${new Date().toISOString()}"`
        ]);
    }

    async toggleSetting(userId, path) {
        const key = `settings:${userId}`;
        const currentValue = await this.client.sendCommand([
            'JSON.GET',
            key,
            `$.${path}`
        ]);

        const newValue = !JSON.parse(currentValue)[0];
        await this.updateSetting(userId, path, newValue);
        return newValue;
    }

    async getNotificationSettings(userId) {
        const result = await this.client.sendCommand([
            'JSON.GET',
            `settings:${userId}`,
            '$.notifications'
        ]);
        return result ? JSON.parse(result)[0] : null;
    }

    async updateNotificationSettings(userId, settings) {
        await this.client.sendCommand([
            'JSON.MERGE',
            `settings:${userId}`,
            '$.notifications',
            JSON.stringify(settings)
        ]);
    }
}

// 使用範例
async function main() {
    const settings = new UserSettings();
    await settings.connect();

    // 初始化使用者設定
    await settings.initializeSettings('user456');

    // 更新主題
    await settings.updateSetting('user456', 'theme', 'dark');

    // 切換通知設定
    const newPushSetting = await settings.toggleSetting('user456', 'notifications.push');
    console.log('推播通知狀態:', newPushSetting);

    // 批次更新通知設定
    await settings.updateNotificationSettings('user456', {
        frequency: 'weekly',
        sms: true
    });

    // 取得完整設定
    const userSettings = await settings.getSettings('user456');
    console.log('使用者設定:', JSON.stringify(userSettings, null, 2));

    await settings.disconnect();
}

main().catch(console.error);

總結

RedisJSON 為 Redis 帶來了強大的 JSON 原生支援能力,使其成為現代應用程式的理想選擇。透過本文,我們學習了:

  1. 基本概念:RedisJSON 的核心功能和優勢
  2. 安裝設定:在 Ubuntu 22.04 上部署 RedisJSON
  3. 資料操作:完整的 JSON 操作指令集
  4. 查詢語法:JSONPath 的進階查詢技巧
  5. 搜尋整合:與 RediSearch 的結合應用
  6. 程式整合:Python 和 Node.js 的實作範例
  7. 效能調校:記憶體和操作效能優化
  8. 實戰案例:購物車、儀表板和設定管理

RedisJSON 適合用於需要高效能 JSON 操作的場景,如快取層、即時分析、設定管理和會話存儲等。結合 RediSearch 後,更能提供強大的全文搜尋和聚合分析能力。

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy