淺入淺出 Redis

Redis 可謂是當前最知名的 In-memory Database ,常常被拿來當快取資料庫。

由 C 語言撰寫的 Redis 為鍵值對( Key - Value )資料庫,資料皆儲存於 Memory 所以讀寫的速度非常快,只不過這導致 Redis 若不小心關閉,所有資料會直接消失。因此它也提供可選的持久化設定,開啟的話可以在某種程度上避免資料遺失。

可以想成 Redis 是一個獨立於程式外的高級 Map (或稱 Dict ),提供許多更進階的功能。

Redis 特色?

Redis 採用單執行緒設計,雖然 4.0 之後有選擇多執行緒,但這不改變核心概念,那就是:『操作命令皆為單執行緒處理』。

單執行緒?

單執行緒最大的一個特點就是不必擔心資料競爭

假如使用了多執行緒,為了避免競爭問題勢必得加入鎖( Lock )的機制,有經驗的人都很清楚這個機制要妥善處理有多麻煩吧?除了每次上鎖與解鎖的額外消耗,還得注意忘了取鎖、解鎖引發的問題。

此外, Redis 性能瓶頸不是來自於 CPU ,官方的 FQA 也說了:

It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

即使在普通的 Linux 系統上啟動 Redis ,也能在 1sec 的時間內服務 1,000,000 個請求。

引入多執行緒?

確實在 4.0 版本中 Redis 引入多執行緒,這是因為後續加入了可異步處理的刪除操作。

Async Sync
UNLINK DEL
FLUSHALL ASYNC FLUSHALL
FLUSHDB ASYNC FLUSHDB

要刪除容量不大( Memory Size )的鍵值對不會花太多功夫,所以就算單執行緒同步地刪除也不太會阻塞到其它操作。

不過 Redis 也可能有巨型資料存在,動輒十幾 MB 或是幾百 MB 都可能,而這種龐大的資料不可能短時間內處理完,這樣會導致其它操作卡住。

異步版本的刪除命令,原理是先將 Key 給移除,而實際上 Value 的刪除則是透過其它執行緒來執行,這樣就可以在刪除大型資料時也不導致 Redis 卡住。

至於在 6.0 的多執行緒 I/O 主要是用來處理網路的讀寫,也就是實際上命令的執行依然維持在單執行緒。

啟動與連線

Redis 服務啟動時沒有傳入任何參數的話,預設會監聽 Port 6379 ,可以透過 --port 或是指定設定檔,但是設定檔要自己建立。

1
2
3
4
5
# 透過參數指定 Port
$ redis-server --port 6666

# 透過設定檔
$ redis-server redis.conf

之後就可以藉由 redis-cli 來連線到 Redis ,直接執行時如果沒設定參數,會連向本地的 6379 。

1
2
$ redis-cli
127.0.0.1:6379>

要遠端連接需要透過 -h 指定 Host ; -p 指定 Port 。

密碼

Redis 是可以設置密碼的,有兩種方式,分別是設定檔與操作命令。

設定檔只需要加入 requirepass PASSWORD 這段訊息,並在 Redis Server 啟動時指定使用該設定檔即可。

透過命令的話先連線至已啟動的 Redis Server :

1
2
3
4
5
6
7
8
9
10
11
12
# 先連線到 Redis Server
$ redis-cli
127.0.0.1:6379>

# 下面的命令可以看目前密碼是否有設置
127.0.0.1:6379> CONFIG GET requirepass
1) "requirepass"
2) ""

# 透過 SET 來設置密碼( requirepass )
127.0.0.1:6379> CONFIG SET requirepass "PASSWORD"
OK

當看到 OK 後如果在執行一次 GET 會出現錯誤訊息「 (error) NOAUTH Authentication required. 」,這樣就得透過認證命令來獲取權限:

1
2
127.0.0.1:6379> AUTH "PASSWORD"
OK

除了透過這種方式登入有設定密碼的 Redis ,也可以在執行 redis-cli 時透過 -a 來傳遞密碼:

1
2
$ redis-cli -a PASSWORD
127.0.0.1:6379>

但這種方式並不推薦,原因在於 -a 送出的密碼會是明文傳遞,很容易被他人攔截到,所以大多情況還是建議使用 AUTH 命令來驗證。

注意要移除密碼不是透過 DEL ,而是要透過 SET 將密碼設定為 “” (空字串)

設定檔

Redis 啟動時都會使用一組設定檔,沒有指定時會使用預設值。

設定檔可以指定多種不同的資訊,如前述的密碼,或是開啟主從式架構的關係等,如果想知道當前的設定檔資訊,可以透過前綴 CONFIG 搭配 GET

1
2
3
4
5
6
7
8
9
# 回傳的訊息中,奇數為設定的名稱;偶數為設定的值
127.0.0.1:6379> CONFIG GET *
1) "dbfilename"
2) "dump.rdb"
3) "requirepass"
4) ""
...
213) "bind"
214) ""

另外也能用 INFO 命令可以看當前 Redis 的相關資訊。

常用操作

說起 Map 結構,其實核心操作不過就是 Get 與 Set 而已。

Redis 支援多種資料型態:

  1. String
  2. List
  3. Set
  4. Hash
  5. Sorted Set
  6. Bitmap
  7. HyperLogLog

後面有兩個詭異的型態,但這邊先不介紹,通常我們會使用的應該只有前幾個,大部分的命令可以到此查詢。

上列表中沒有出現 Number / Numeric 型態,因為 Redis 會把值都會轉換成 String 儲存,因此部份與數字相關的操作( INCRDECR ),實際上都是先將其轉換為數字才處理

String

可以透過 SET 設置指定 Key 與其相對的 Value (型態為 String ),也能藉由 GET 取出。

1
2
3
4
5
6
7
8
9
10
11
# 設定 name - Neko 鍵值對
127.0.0.1:6379> SET name "Neko"
OK

# 藉由 Key 取出對應的 Value
127.0.0.1:6379> GET name
"Neko"

# 讀取不存在的 Key 會收到 nil
127.0.0.1:6379> GET neko
(nil)

如果值不包含空白,可以不加入雙引號:

1
2
3
4
5
127.0.0.1:6379> SET name Neko
OK

127.0.0.1:6379> GET name
"Neko"

當儲存的 Value 是可以被轉換成數字( Integer 、 Float ),就可以使用數字相關的操作,好比說加減的 INCRDECR (各自等於 INCRBY 1DECRBY 1 )。

如果增減的值屬於浮點數請改用 INCRBYFLOAT ,否則會出現型態轉換錯誤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 設置數字的 Value
127.0.0.1:6379> SET num "1"
OK

# 透過 INCR 加一,會回傳加完後的值
127.0.0.1:6379> INCR num
(integer) 2

# 想指定增加的值就需使用 INCRBY
127.0.0.1:6379> INCRBY num 3
(integer) 5

# 減的操作相似,不用加上負號
127.0.0.1:6379> DECRBY num 3
(integer) 2

# 增減的值如果是 Float 就需用 INCRBYFLOAT (不論加或減)
127.0.0.1:6379> INCRBYFLOAT num 0.5
"2.5"

127.0.0.1:6379> INCRBYFLOAT num -0.5
"2"

注意,對於 INCRBYDECRBY 命令的回傳值都會做 Integer 轉換,所以假使你的值為 Float ,那麼回傳值可能會出現錯誤「 (error) ERR value is not an integer or out of range 」。

1
2
3
4
5
6
7
8
127.0.0.1:6379> SET f 0.5

# 這種情況下會視為失敗,會保持原本的值
127.0.0.1:6379> INCR f
(error) ERR value is not an integer or out of range

127.0.0.1:6379> GET f
"0.5"

也有命令可以一次存取多個鍵值對,這些命令前方會多 M ( MSETMGET ),用法基本上是沒有差別的。

1
2
3
4
5
6
7
8
# 透過空白隔離多個鍵值對
127.0.0.1:6379> MSET name "Doge" age "66"
OK

# 取出時會依照輸入 Key 的順序
127.0.0.1:6379> MGET age name
1) "66"
2) "Doge"

Redis 在設值時有提供多種方式,可以在特定條件下才設值。
原本這些命令都被拆分開來,但在 2.6.12 時可以透過 SET 參數方式使用。

Command Option Desc
SETEX EX 指定過期時間,到了以後會移除( second )
PSETEX PX 指定過期時間,到了以後會移除( millisecond )
SETNX NX 當 Key 不存在時才設值
- XX 當 Key 存在時才設值

透過參數形式的好處是可以混合多種 Option ,好比說我希望在沒有 Key 時設值,且這個值有效時間只有 10 秒,這種情況只能透過 Option 的方式設定。

官方已不再建議使用 Command 形式,上面的三個命令可能會在未來被移除

1
2
3
4
5
6
7
8
9
# 下方兩種方式結果是一樣的
127.0.0.1:6379> SET secret "abc123" EX 10
OK

127.0.0.1:6379> SETEX secret 10 "abc123"
OK

# 混合 Option 當 Key 不存在才設值且有效時間為 5 秒
127.0.0.1:6379> SET secret "abc123" NX EX 5

List

Redis 的 List 是單純的 String List ,使用時可以將元素放入頭或尾。
其實就是 Linked List ,據官方的說明可以儲存 2^32 - 1 個元素( 4,294,967,295 )。

增加元素時會確認是否已存在 List ,不存在時會自動建立,而透過命令移除元素時如果會使 List 為空,則會移除該 Key 。

意思是如果 List 被清空,那麼等於此 Key 沒被設置過

1
2
3
4
5
6
7
8
9
10
11
# 元素放入的方式有左與右( LPUSH 、 RPUSH ),並回傳當前 List 長度
127.0.0.1:6379> LPUSH nums "10"
(integer) 1

127.0.0.1:6379> RPUSH nums "20"
(integer) 2

# LRANGE 可以取出指定 Key 的 Start Index 到 End Index 的值
127.0.0.1:6379> LRANGE nums 0 -1
1) "10"
2) "20"

Index 的部分支援逆向,所以例子的 -1 意思是倒數最後一個元素,整條命令就是取出 List 上所有元素。

取出元素可以基於 Index 或是如 Queue 一樣在取值時會將其移出 List ,前者就如同程式語言內常用的 List[Index] 操作;後者則是 Queue 的 Pop 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 以下會建立出 [10, 20, 30] 的陣列
127.0.0.1:6379> RPUSH nums "10" "20" "30"
(integer) 3

# 基於 Index 取值,但不會移出 List
127.0.0.1:6379> LINDEX nums 0
"10"

# 從左取出第一個元素,並移出 List
127.0.0.1:6379> LPOP nums
"10"

# 從右取出第一個元素,並移出 List
127.0.0.1:6379> RPOP nums
"30"

Pop 相關的操作還有提供 Block 版本,表示如果 Pop 的對象沒有任何元素可以取出,那麼連線將 Block 直到有元素可以回傳或超時。

命令需給予超時時限( second ),但如果給 0 則表示無限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 如果 nums 左有元素可以取出,則效果同 LPOP 否則會卡在此處直到有元素可回傳或 30 秒後
127.0.0.1:6379> BLPOP nums 30
(nil)
(30.9s)

# 有元素的情況就如同一般 POP
127.0.0.1:6379> LPUSH nums "1"
(integer) 1

127.0.0.1:6379> BLPOP nums 30
"1"

# 可以一次 Block 監聽多個 Key 有沒有元素,會回傳第一個不為空的元素
127.0.0.1:6379> BLPOP nums names ages 30
...

單執行緒的 Redis 在處理 Block 時,會透過 Dict 來儲存 Key 與等待者的資訊,好比說 ClientID 。並在每次處理命令時都檢查是否有元素可以提供。

當然如果是這樣會導致每次跑命令都浪費時間,所以 Redis 利用兩個 Dict 來記錄,名稱分別為 blocking_keysready_keys

  • blocking_keys
    為 Key : List<Client> 的 Dict ,記錄著有人等待的 Key 與等待者相關訊息。
  • ready_keys
    當 Push 的 Key 為空 List 時會檢查該 Key 是否存在於 blocking_keys 內,有的話會將相關訊息放入 ready_keys ,也是每次 Redis 處理命令時會檢查的對象。

所以實際上只有在處理 Push 相關訊息時才會檢查 Block 名單,如果該 key 剛好在 Block 名單中就透過 Ready 名單通知 Server 與 Client 。

MULTI / EXEC 中使用 Block 的命令,會因為交易原子性問題馬上回應,效用等同於非 Block 操作

Set

Redis 的 Set 為無序的 String 集合,據官方所說可儲存的數量與 List 同樣為 2^32 - 1 個( 4,294,967,295 )。

與常見的 Set 結構一樣,內部的元素是不允許重複的。

透過 SADD 可以替 Set 放入元素,要是想確認某個元素是否存在於 Set 需要透過 SISMEMBER

1
2
3
4
5
6
7
8
9
10
# SADD 可以一次放入多樣值,回傳 Set 操作後的元素數量
127.0.0.1:6379> SADD unique "Neko" "Doge" "Neko"
(integer) 2

# 檢查指定元素是否存在,回傳 Integer 存在為 1 ;不存在為 0
127.0.0.1:6379> SISMEMBER unique "Neko"
(integer) 1

127.0.0.1:6379> SISMEMBER unique "Jacky"
(integer) 0

移除 Set 內的資料可以透過 SPOPSREM
但前者是隨機地移除指定 Set 的元素,後者才可以指定移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> SADD unique "A" "B" "C" "D" "E"
(integer) 5

# 隨機移除兩個元素,數量省略時預設為 1
127.0.0.1:6379> SPOP unique 2
1) "B"
2) "D"

# 移除指定元素, 2.4 之後可以一次移除多個,會回傳被移除的數量
127.0.0.1:6379> SREM unique "A"
(integer) 1

# 假使移除不存在的元素也可以,但不會包含在移除數量中
127.0.0.1:6379> SREM unique "A" "B" "C" "D" "E"
(integer) 2

如果想確認 Set 目前的數量可以用 SCARD ,不過想知道目前裡面所有的元素內容就得透過 SMEMBERS 。後者的速度比較慢,以時間複雜度來說前者為 O(1) 後者為 O(N) 。

Set 本身也有比較進階的操作,好比說交集( SINTER )、聯集( SUNION )、差集( SDIFF )等,除了聯集以外,另外兩個都是以第一個 Set 為基礎去處理,而非把每個 Set 都視為平等。

Hash

Hash 就是鍵值皆為 String 的 Map 結構,也因此很適合用來模擬 Object 。當然,據官方所說 Hash 同樣可以儲存 2^32 - 1 組鍵值對。

以一個 JSON 物件來當例子可以更好理解 Hash 如何模擬物件:

1
2
3
4
5
6
{
"id": 100,
"name": "Neko",
"age": 66,
"level": "normal"
}

上述為擁有編號、名稱、等級等資訊的 User 物件,透過 HSET 來設置這個物件。

1
2
3
4
5
6
7
8
9
10
11
# 設置成功後會回傳成功加入的鍵值對數量
127.0.0.1:6379> HSET user:100 id 100
(integer) 1

# 4.0 之後 HSET 允許一次設定多組鍵值對
127.0.0.1:6379> HSET user:100 name "Neko" age 66 level "normal"
(integer) 3

# 4.0 之前要設定多組得透過 HMSET
127.0.0.1:6379> HMSET user:100 name "Neko" age 66 level "normal"
(integer) 3

回傳的值是被新加入的鍵值對數量,換句話說修改是不包含在內的,這種情況會收到 0 。

Redis 建議命名 Key 時如果有不同區段訊息,應該透過 : 來區隔而不是 _

雖說上面的例子是模擬 Object ,但是身為 Map 結構該有的操作都沒少,例如讀取某個 Key 對應的 Value ,或取出目前所有 Key 、 Value 等命令。

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
# 單獨取用某個 Key 的值
127.0.0.1:6379> HGET user:100 name
"Neko"

# 只取出對象目前所有的 Key
127.0.0.1:6379> HKEYS user:100
1) "id"
2) "name"
3) "age"
4) "level"

# 只取出對象目前所有的 Value
127.0.0.1:6379> HVALS user:100
1) "100"
2) "Neko"
3) "66"
4) "normal"

# 把全部鍵值對都取出來,奇數為 Key ;偶數為 Value
127.0.0.1:6379> GETALL user:100
1) "id"
2) "100"
3) "name"
4) "Neko"
5) "age"
6) "66"
7) "level"
8) "normal"

如果要移除 Hash 的某組鍵值對,需要透過 HDEL

1
2
3
4
5
6
7
# 移除時會回傳成功移除掉的鍵值對數量
127.0.0.1:6379> HDEL user:100 name
(integer) 1

# 在 2.4 版之後可以一次移除多個
127.0.0.1:6379> HDEL user:100 id age level
(integer) 3

如果 Hash 只儲存少數 Fields (一百個左右),只會佔用很小的空間,也就是說即使是一個微小的 Redis Server 也可以用來儲存百萬個 Object 而不用擔心。

Sorted Set

可排序的 Set ,
對,就這樣,連原理都蠻單純的。

Sorted Set 在儲存資料時會對應到一個分數( Score ),這個分數就是拿來做排序的關鍵,排序上通常會以低 -> 高。

1
2
3
4
5
6
7
# 一般的 Set
127.0.0.1:6379> SADD set:normal "Neko"
(integer) 1

# 可排序的 Set
127.0.0.1:6379> ZADD set:sorted 1 "Neko"
(integer) 1

雖然 Set 結構不允許出現重複的元素,但是分數是可以重複的,官方對於分數重複的描述為:

While the same element can’t be repeated in a sorted set since every element is unique, it is possible to add multiple different elements having the same score. When multiple elements have the same score, they are ordered lexicographically (they are still ordered by score as a first key, however, locally, all the elements with the same score are relatively ordered lexicographically).

當分數一致時,會透過位元排序,也就是會將 String 視為 Byte Array 來做比對。

Sorted Set 除了最基礎的 Set 操作,還有許多獨自擁有的,例如說檢查某個元素當前排名 ZRANK ,查看指定元素目前的分數的 ZSCORE 等。

Sorted Set 有排序的關係,取出存在的元素不是 ZMEMBERS ,而是要改用 ZRANGE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> ZADD sorted 1 "A" 2 "B" 3 "C"
(integer) 3

# 此處的 0 與 -1 是表示排名,也就是第一名到最後一名全部顯示
127.0.0.1:6379> ZRANGE sorted 0 -1
1) "A"
2) "B"
3) "C"

# 如果想一併知道分數,可以透過給予 WITHSCORES 參數
127.0.0.1:6379> ZRANGE sorted 0 -1 WITHSOCRES
1) "A"
2) "1"
3) "B"
4) "2"
5) "C"
6) "3"

也可以透過可排序功能與 Pop 操作,來對任務做權重分配,例如新增會員 Score = 1 而修改會員資料 Score = 3 ,配置對應的 Worker 來取出目前等待中的任務。

但這樣處理時需要注意,假如進來的任務全都是權重高的,就會變成權重低的任務一直沒有處理的情況。

1
2
3
4
5
6
7
8
9
10
11
12
# 透過 Block 系列的 Pop 來等待任務, MIN 指的是 Score 最小的
127.0.0.1:6379> BZPOPMIN events 0
...

# 先建立一個 Sorted Set
127.0.0.1:6379> ZADD tasks 1 "create" 2 "modify"

# 一樣可以監聽多個 Key ,成功收到後的訊息為三個 Key, Value, Score
127.0.0.1:6379> BZPOPMIN events tasks 0
1) "tasks"
2) "create"
3) "1"

Block 相關的原理請參考 List 末段 Block 操作。

事務

Transaction 是個術語,意思是操作是否能提供 ACID 特性。

這種需求通常跟交易有關,譬如轉賬是從 A 帳戶轉移金錢到 B 帳戶,雖然看起來只需要兩步驟:

  1. A 扣錢
  2. B 加錢

但這涉及到操作的不安全性就沒這麼簡單。

好比說 A 已經扣除 1000$ ,但這時幫 B 增加 1000$ 失敗呢?會變成 A 損失 1000$ ,
又好比 A 扣錢失敗,但 B 卻成功增加 1000$ 。

Redis 提供了開始事務的 MULTI 與執行事務的 EXEC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 先設置帳戶 A 與帳戶 B 的金額(各 5000 )
127.0.0.1:6379> HSET bank:a money 5000
(integer) 1

127.0.0.1:6379> HSET bank:b money 5000
(integer) 1

# 開始事務,此命令永遠都會回傳 OK
127.0.0.1:6379> MULTI
OK

# 將帳戶 A 的金額扣除 1000 的操作加入事務中
127.0.0.1:6379> HINCRBY bank:a money -1000
QUEUED

# 將帳戶 B 的金額增加 1000 的操作加入事務中
127.0.0.1:6379> HINCRBY bank:b money 1000
QUEUED

# 執行事務中的操作,會回傳每個操作的結果
127.0.0.1:6379> EXEC
1) (integer) 4000
2) (integer) 6000

但是與大多數人理解的事務不同的是, Redis 透過 MULTI / EXEC 的操作只能保持原子性,卻無法保持成功一同成功,失敗一同失敗,因為它不具有 Roll Back (回朔)功能。

換句話說依然可能存在 A 扣款 B 沒入款的情況,而 Redis 之所以不支援 Roll Back 的原因有二:

  1. Redis 命令失敗的情況只有錯誤的語法使用(且無法於 QUEUED 時發現),這種情況屬於程式邏輯錯誤,不應該出現在生產環境中
  2. 因為不支援 Roll Back 可以使 Redis 保持簡單與快速

即使如此 Redis 也提供 WATCH 命令以確保與程式搭配時的 Check-And-Set ( CAS )行為,透過 WATCH 可以監視指定的 Keys 是否有被更改過,如果被更改會會導致 Transaction 執行失敗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# WATCH 必須在 MULTI 前使用
127.0.0.1:6379> WATCH counter
OK

# 開始事務
127.0.0.1:6379> MULTI
OK

# 把 counter 增加 1
127.0.0.1:6379> INCR counter
QUEUED

# 執行事務,此時假設有其它 Client 修改過 counter ,回應會收到 nil
127.0.0.1:6379> EXEC
(nil)

很明顯是種樂觀鎖,等於對事務執行設置了前置條件,以沒人改變監視的 Keys 為依據。

使用 WATCH 時要小心,一旦碰上 EXECDISCARD 都會取消目前所有監視的 Keys ,因為前者是執行事務後者則是放棄事務。

原理

實際上使用 MULTI 時會改變 Client 端的模式,也就是轉換為事務模式。

當處在事務模式的 Client 只要不執行 EXECDISCARD 都不會取消事務模式,這個模式中的所有命令都會被保存在一個 Queue 內,並回應訊息 『 QUEUED 』。

當執行 EXEC 後會將 Queue 內的命令傳送給 Server ,它便會依照順序依次執行與記錄結果,最終將事務的結果回傳給 Client 。

Lua Script

2.6 時 Redis 引入了 Lua 執行環境,也就是 2.6 版本之後就內建一個 Lua 的 Interpreter ,而 Lua 是一個極輕量的語言,它的目標是成為最容易嵌入其它語言的程式語言。

Redis 對於 Lua 的環境做了許多修改,以避免產生漏洞遭利用,此外也提供由 Lua 呼叫 Redis 命令的函式等。並且為了支援持久化功能, Redis 限制了 Lua 函式必須符合三點:

  1. 沒有副作用
  2. 沒有有害的隨機性(比如隨機寫一個鍵值對)
  3. 同樣的輸入必得出同樣的結果

Redis 保證了 Lua 的原子性,也就是當執行 Lua Script 時不會在中間執行其它 Lua Script 或 Redis Command 。

換句話說 Lua Script 本身就是一種 Transaction ,而且還比原本 MULTI / EXEC 更快更簡單。官方雖然短時間內不會移除舊版本的事務機制,但假使未來的使用者都傾向使用 Lua Script 來處理的話,最終可能會把舊有機制給移除。

我自己感覺 Lua Script 提供更多功能與靈活性,若沒特殊原因應該也會選擇 Lua ,但這邊不會再詳細介紹 Lua ,可能未來會找時間寫相關的雜學。

參考資料