標簽: Java與NoSQL
從 2.6版本 起, Redis 開始支持 Lua 腳本 讓開發者自己擴展 Redis …
案例-實現訪問頻率限制: 實現訪問者 $ip 在一定的時間 $time 內只能訪問 $limit 次.
- 非腳本實現
private boolean accessLimit(String ip, int limit, int time, Jedis jedis) {
boolean result = true;
String key = "rate.limit:" + ip;
if (jedis.exists(key)) {
long afterValue = jedis.incr(key);
if (afterValue > limit) {
result = false;
}
} else {
Transaction transaction = jedis.multi();
transaction.incr(key);
transaction.expire(key, time);
transaction.exec();
}
return result;
}
- 以上代碼有兩點缺陷
- 可能會出現競態條件: 解決方法是用
WATCH
監控rate.limit:$IP
的變動, 但較為麻煩; - 以上代碼在不使用
pipeline
的情況下最多需要向Redis請求5條指令, 傳輸過多.
- 可能會出現競態條件: 解決方法是用
-
Lua腳本實現
Redis 允許將 Lua 腳本傳到 Redis 服務器中執行, 腳本內可以調用大部分 Redis 命令, 且 Redis 保證腳本的原子性:- 首先需要准備Lua代碼: script.lua
--
-- Created by IntelliJ IDEA.
-- User: jifang
-- Date: 16/8/24
-- Time: 下午6:11
--
local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
if redis.call("INCR", key) > limit then
return 0
else
return 1
end
else
redis.call("SET", key, 1)
redis.call("EXPIRE", key, expire_time)
return 1
end
- Java
private boolean accessLimit(String ip, int limit, int timeout, Jedis connection) throws IOException {
List<String> keys = Collections.singletonList(ip);
List<String> argv = Arrays.asList(String.valueOf(limit), String.valueOf(timeout));
return 1 == (long) connection.eval(loadScriptString("script.lua"), keys, argv);
}
// 加載Lua代碼
private String loadScriptString(String fileName) throws IOException {
Reader reader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));
return CharStreams.toString(reader);
}
- Lua 嵌入 Redis 優勢:
- 減少網絡開銷: 不使用 Lua 的代碼需要向 Redis 發送多次請求, 而腳本只需一次即可, 減少網絡傳輸;
- 原子操作: Redis 將整個腳本作為一個原子執行, 無需擔心並發, 也就無需事務;
- 復用: 腳本會永久保存 Redis 中, 其他客戶端可繼續使用.
Lua模型
Lua是一種 便於嵌入應用程序 的腳本語言, 具備了作為通用腳本語言的所有功能. 其高速虛擬機實現非常有名(Lua的垃圾回收很有講究- 增量垃圾回收 ), 在很多虛擬機系性能評分中都取得了優異的成績. Home lua.org.
以嵌入式為方針設計的Lua, 在默認狀態下簡潔得嚇人. 除了基本的數據類型外, 其他一概沒有. 標注庫也就 Coroutine、String、Table、Math、 I/O、OS, 再加上Modules包加載而已. 參考: Lua 5.1 Reference Manual - Standard Libraries(中文版: Lua 5.1 參考手冊).
注: 本文僅介紹 Lua 與眾不同的設計模型(對比 Java/C/C++、JavaScript、Python 與 Go), 語言細節可參考文內和附錄推薦的文章以及Lua之父Roberto Ierusalimschy的<Programming in Lua>(中文版: <LUA程序設計(第2版)>)
Base
1. 數據類型
- 作為通用腳本語言, Lua的數據類型如下:
- 數值型:
全部為浮點數型, 沒有整型;
只有nil
和false
作為布爾值的false
, 數字0
和空串(‘’
/‘\0’
)都是true
; - 字符串
- 用戶自定義類型
- 函數(function)
- 表(table)
- 數值型:
變量如果沒有特殊說明為全局變量(那怕是語句塊 or 函數內), 局部變量前需加
local
關鍵字.
2. 關鍵字
3. 操作符
- Tips:
- 數學操作符的操作數如果是字符串會自動轉換成數字;
- 連接
..
自動將數值轉換成字符串; - 比較操作符的結果一定是布爾類型, 且會嚴格判斷數據類型(
'1' != 1
);
函數(function)
在 Lua 中, 函數是和字符串、數值和表並列的基本數據結構, 屬於第一類對象( first-class-object /一等公民), 可以和數值等其他類型一樣賦給變量、作為參數傳遞, 以及作為返回值接收(閉包):
- 使用方式類似JavaScript:
-- 全局函數: 求階乘
function fact(n)
if n == 1 then
return 1
else
return n * fact(n - 1)
end
end
-- 1. 賦給變量
local func = fact
print("func type: " .. type(func), "fact type: " .. type(fact), "result: " .. func(4))
-- 2. 閉包
local function new_counter()
local value = 0;
return function()
value = value + 1
return value
end
end
local counter = new_counter()
print(counter(), counter(), counter())
-- 3. 返回值類似Go/Python
local random_func = function(param)
return 9, 'a', true, "ƒ∂π", param
end
local var1, var2, var3, var4, var5 = random_func("no param is nil")
print(var1, var2, var3, var4, var5)
-- 4. 變數形參
local function square(...)
local argv = { ... }
for i = 1, #argv do
argv[i] = argv[i] * argv[i]
end
return table.unpack(argv)
end
print(square(1, 2, 3))
表(table)
Lua最具特色的數據類型就是表(Table), 可以實現數組、Hash
、對象所有功能的萬能數據類型:
-- array
local array = { 1, 2, 3 }
print(array[1], #array)
-- hash
local hash = { x = 1, y = 2, z = 3 }
print(hash.x, hash['y'], hash["z"], #hash)
-- array & hash
array['x'] = 8
print(array.x, #array)
- Tips:
- 數組索引從
1
開始; - 獲取數組長度操作符
#
其’長度’只包括以(正)整數為索引的數組元素. - Lua用表管理全局變量, 將其放入一個叫
_G
的table內:
- 數組索引從
-- pairs會遍歷所有值不為nil的索引, 與此類似的ipairs只會從索引1開始遞遍歷到最后一個值不為nil的整數索引.
for k, v in pairs(_G) do
print(k, " -> ", v, " type: " .. type(v))
end
用
Hash
實現對象的還有JavaScript, 將數組和Hash
合二為一的還有PHP.
元表
Every value in Lua can have a metatable/元表. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable. For instance, when a non-numeric value is the operand of an addition, Lua checks for a function in the field “__add” of the value’s metatable. If it finds one, Lua calls this function to perform the addition.
The key for each event in a metatable is a string with the event name prefixed by two underscores__
; the corresponding values are called metamethods. In the previous example, the key is “__add” and the metamethod is the function that performs the addition.
metatable中的鍵名稱為事件/event, 值稱為元方法/metamethod, 我們可通過getmetatable()
來獲取任一值的metatable, 也可通過setmetatable()
來替換table的metatable. Lua 事件一覽表:
對於這些操作, Lua 都將其關聯到 metatable 的事件Key, 當 Lua 需要對一個值發起這些操作時, 首先會去檢查其metatable中是否有對應的事件Key, 如果有則調用之以控制Lua解釋器作出響應.
MetaMethods
MetaMethods主要用作一些類似C++中的運算符重載操作, 如重載+
運算符:
local frac_a = { numerator = 2, denominator = 3 }
local frac_b = { numerator = 4, denominator = 8 }
local operator = {
__add = function(f1, f2)
local ret = {}
ret.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator
ret.denominator = f1.denominator * f2.denominator
return ret
end,
__tostring = function(self)
return "{ " .. self.numerator .. " ," .. self.denominator .. " }"
end
}
setmetatable(frac_a, operator)
setmetatable(frac_b, operator)
local frac_res = frac_a + frac_b
setmetatable(frac_res, operator) -- 使tostring()方法生效
print(tostring(frac_res))
關於更多Lua事件處理可參考文檔: Metamethods.
MetaTables 與 面向對象
Lua本來就不是設計為一種面向對象語言, 因此其面向對象功能需要通過元表(metatable)這種非常怪異的方式實現, Lua並不直接支持面向對象語言中常見的類、對象和方法: 其
對象
和類
通過表
實現, 而方法
是通過函數
來實現.
上面的Event一覽表內我們看到有__index
這個事件重載,這個東西主要是重載了find key
操作, 該操作可以讓Lua變得有點面向對象的感覺(類似JavaScript中的prototype). 通過Lua代碼模擬:
local function gettable_event(t, key)
local h
if type(t) == "table" then
local value = rawget(t, key)
if value ~= nil then
return value
end
h = getmetatable(t).__index
if h == nil then
return nil
end
else
h = getmetatable(t).__index
if h == nil then
error("error")
end
end
if type(h) == "function" then
-- call the handler
return (h(t, key))
else
-- or repeat opration on it
return h[key]
end
end
-- 測試
obj = { 1, 2, 3 }
op = {
x = function()
return "xx"
end
}
setmetatable(obj, { __index = op['x'] })
print(gettable_event(obj, x))
- 對於任何事件, Lua的處理都可以歸結為以下邏輯:
- 如果存在規定的操作則執行它;
- 否則從元表中取出各事件對應的
__
開頭的元素, 如果該元素為函數, 則調用; - 如果該元素不為函數, 則用該元素代替
table
來執行事件所對應的處理邏輯.
這里的代碼僅作模擬, 實際的行為已經嵌入Lua解釋器, 執行效率要遠高於這些模擬代碼.
方法調用的實現
面向對象的基礎是創建對象和調用方法. Lua中, 表作為對象使用, 因此創建對象沒有問題, 關於調用方法, 如果表元素為函數的話, 則可直接調用:
-- 從obj取鍵為x的值, 將之視為function進行調用 obj.x(foo)
不過這種實現方法調用的方式, 從面向對象角度來說還有2個問題:
- 首先:
obj.x
這種調用方式, 只是將表obj
的屬性x
這個函數對象取出而已, 而在大多數面向對象語言中, 方法的實體位於類中, 而非單獨的對象中. 在JavaScript等基於原型的語言中, 是以原型對象來代替類進行方法的搜索, 因此每個單獨的對象也並不擁有方法實體. 在Lua中, 為了實現基於原型的方法搜索, 需要使用元表的__index
事件:
如果我們有兩個對象a
和b
,想讓b
作為a
的prototype需要setmetatable(a, {__index = b})
, 如下例: 為obj
設置__index
加上proto
模板來創建另一個實例:
proto = {
x = function()
print("x")
end
}
local obj = {}
setmetatable(obj, { __index = proto })
obj.x()
proto
變成了原型對象, 當obj
中不存在的屬性被引用時, 就會去搜索proto
.
- 其次: 通過方法搜索得到的函數對象只是單純的函數, 而無法獲得最初調用方法的表(接收器)相關信息. 於是, 過程和數據就發生了分離.JavaScript中, 關於接收器的信息可由關鍵字
this
獲得, 而在Python中通過方法調用形式獲得的並非單純的函數對象, 而是一個“方法對象” –其接收器會在內部作為第一參數附在函數的調用過程中.
而Lua准備了支持方法調用的語法糖:obj:x()
. 表示obj.x(obj)
, 也就是: 通過冒號記法調用的函數, 其接收器會被作為第一參數添加進來(obj
的求值只會進行一次, 即使有副作用也只生效一次).
-- 這個語法糖對定義也有效
function proto:y(param)
print(self, param)
end
- Tips: 用冒號記法定義的方法, 調用時最好也用冒號記法, 避免參數錯亂
obj:y("parameter")
更多MetaTable介紹可參考文檔Metatable與博客metatable和metamethod.
基於原型的編程
Lua雖然能夠進行面向對象編程, 但用元表來實現, 仿佛把對象剖開看到五臟六腑一樣.
<代碼的未來>中松本行弘老師向我們展示了一個基於原型編程的Lua庫, 通過該庫, 即使沒有深入解Lua原始機制, 也可以實現面向對象:
--
-- Author: Matz
-- Date: 16/9/24
-- Time: 下午5:13
--
-- Object為所有對象的上級
Object = {}
-- 創建現有對象副本
function Object:clone()
local object = {}
-- 復制表元素
for k, v in pairs(self) do
object[k] = v
end
-- 設定元表: 指定向自身`轉發`
setmetatable(object, { __index = self })
return object
end
-- 基於類的編程
function Object:new(...)
local object = {}
-- 設定元表: 指定向自身`轉發`
setmetatable(object, { __index = self })
-- 初始化
object:init(...)
return object
end
-- 初始化實例
function Object:init(...)
-- 默認不進行任何操作
end
Class = Object:new()
另存為prototype.lua, 使用時只需require()
引入即可:
require("prototype")
-- Point類定義
Point = Class:new()
function Point:init(x, y)
self.x = x
self.y = y
end
function Point:magnitude()
return math.sqrt(self.x ^ 2 + self.y ^ 2)
end
-- 對象定義
point = Point:new(3, 4)
print(point:magnitude())
-- 繼承: Point3D定義
Point3D = Point:clone()
function Point3D:init(x, y, z)
self.x = x
self.y = y
self.z = z
end
function Point3D:magnitude()
return math.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ 2)
end
p3 = Point3D:new(1, 2, 3)
print(p3:magnitude())
-- 創建p3副本
ap3 = p3:clone()
print(ap3.x, ap3.y, ap3.z)
Redis - Lua
在傳入到Redis的Lua腳本中可使用redis.call()
/redis.pcall()
函數調用Reids命令:
redis.call("set", "foo", "bar") local value = redis.call("get", "foo")
redis.call()
返回值就是Reids命令的執行結果, Redis回復與Lua數據類型的對應關系如下:
Reids返回值類型 | Lua數據類型 |
---|---|
整數 | 數值 |
字符串 | 字符串 |
多行字符串 | 表(數組) |
狀態回復 | 表(只有一個ok 字段存儲狀態信息) |
錯誤回復 | 表(只有一個err 字段存儲錯誤信息) |
注: Lua 的
false
會轉化為空結果.
redis-cli提供了EVAL
與EVALSHA
命令執行Lua腳本:
- EVAL
EVAL script numkeys key [key ...] arg [arg ...]
key和arg兩類參數用於向腳本傳遞數據, 他們的值可在腳本中使用KEYS
和ARGV
兩個table訪問:KEYS
表示要操作的鍵名,ARGV
表示非鍵名參數(並非強制). - EVALSHA
EVALSHA
命令允許通過腳本的SHA1來執行(節省帶寬), Redis在執行EVAL
/SCRIPT LOAD
后會計算腳本SHA1緩存,EVALSHA
根據SHA1取出緩存腳本執行.
創建Lua環境
為了在 Redis 服務器中執行 Lua 腳本, Redis 內嵌了一個 Lua 環境, 並對該環境進行了一系列修改, 從而確保滿足 Redis 的需要. 其創建步驟如下:
- 創建基礎 Lua 環境, 之后所有的修改都基於該環境進行;
- 載入函數庫到 Lua 環境, 使 Lua 腳本可以使用這些函數庫進行數據操作: 如基礎庫(刪除了
loadfile()
函數)、Table、String、Math、Debug等標准庫, 以及CJSON、 Struct(用於Lua值與C結構體轉換)、 cmsgpack等擴展庫(Redis 禁用Lua標准庫中與文件或系統調用相關函數, 只允許對 Redis 數據處理). - 創建全局表
redis
, 其包含了對 Redis 操作的函數, 如redis.call()
、redis.pcall()
等; - 替換隨機函數: 為了確保相同腳本可在不同機器上產生相同結果, Redis 要求所有傳入服務器的 Lua 腳本, 以及 Lua 環境中的所有函數, 都必須是無副作用的純函數, 因此Redis使用自制函數替換了 Math 庫中原有的
math.random()
和math.randomseed()
. - 創建輔助排序函數: 對於 Lua 腳本來說, 另一個可能產生數據不一致的地方是那些帶有不確定性質的命令(如: 由於
set
集合無序, 因此即使兩個集合內元素相同, 其輸出結果也並不一樣), 這類命令包括SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS、KEYS 等.
Redis 會創建一個輔助排序函數__redis__compare_helper
, 當執行完以上命令后, Redis會調用table.sort()
以__redis__compare_helper
作為輔助函數對命令返回值排序. - 創建錯誤處理函數: Redis創建一個
__redis__err__handler
錯誤處理函數, 當調用redis.pcall()
執行 Redis 命令出錯時, 該函數將打印異常詳細信息. - Lua全局環境保護: 確保傳入腳本內不會將額外的全局變量導入到 Lua 環境內.
小心: Redis 並未禁止用戶修改已存在的全局變量.
- 完成Redis的
lua
屬性與Lua環境的關聯:
整個 Redis 服務器只需創建一個 Lua 環境.
Lua環境協作組件
-
Redis創建兩個用於與Lua環境協作的組件: 偽客戶端- 負責執行 Lua 腳本中的 Redis 命令,
lua_scripts
字典- 保存 Lua 腳本:- 偽客戶端
執行Reids命令必須有對應的客戶端狀態, 因此執行 Lua 腳本內的 Redis 命令必須為 Lua 環境專門創建一個偽客戶端, 由該客戶端處理 Lua 內所有命令:redis.call()
/redis.pcall()
執行一個Redis命令步驟如下:
lua_scripts
字典
字典key為腳本 SHA1 校驗和, value為 SHA1 對應腳本內容, 所有被EVAL
和SCRIPT LOAD
載入過的腳本都被記錄到lua_scripts
中, 便於實現SCRIPT EXISTS
命令和腳本復制功能.
- 偽客戶端
EVAL命令原理
EVAL
命令執行分為以下三個步驟:
-
定義Lua函數:
在 Lua 環境內定義 Lua函數 : 名為f_
前綴+腳本 SHA1 校驗和, 體為腳本內容本身. 優勢:- 執行腳本步驟簡單, 調用函數即可;
- 函數的局部性可保持 Lua 環境清潔, 減少垃圾回收工作量, 且避免使用全局變量;
- 只要記住 SHA1 校驗和, 即可在不知腳本內容的情況下, 直接調用 Lua 函數執行腳本(
EVALSHA
命令實現).
-
將腳本保存到
lua_scripts
字典; - 執行腳本函數:
執行剛剛在定義的函數, 間接執行 Lua 腳本, 其准備和執行過程如下:
1). 將EVAL
傳入的鍵名和參數分別保存到KEYS
和ARGV
, 然后將這兩個數組作為全局變量傳入到Lua環境;
2). 為Lua環境裝載超時處理hook
(handler
), 可在腳本出現運行超時時讓通過SCRIPT KILL
停止腳本, 或SHUTDOWN
關閉Redis;
3). 執行腳本函數;
4). 移除超時hook
;
5). 將執行結果保存到客戶端輸出緩沖區, 等待將結果返回客戶端;
6). 對Lua環境執行垃圾回收.
對於會產生隨機結果但無法排序的命令(如只產生一個元素, 如 SPOP、SRANDMEMBER、RANDOMKEY、TIME), Redis在這類命令執行后將腳本狀態置為
lua_random_dirty
, 此后只允許腳本調用只讀命令, 不允許修改數據庫值.
實踐
使用Lua腳本重新構建帶有過期時間的分布式鎖.
案例來源: <Redis實戰> 第6、11章, 構建步驟:
- 鎖申請
- 首先嘗試加鎖:
- 成功則為鎖設定過期時間; 返回;
- 失敗檢測鎖是否添加了過期時間;
- wait.
- 首先嘗試加鎖:
- 鎖釋放
- 檢查當前線程是否真的持有了該鎖:
- 持有: 則釋放; 返回成功;
- 失敗: 返回失敗.
- 檢查當前線程是否真的持有了該鎖:
非Lua實現
String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
String identifier = UUID.randomUUID().toString();
String key = "lock:" + lockName;
long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
while (System.currentTimeMillis() < acquireTimeEnd) {
// 獲取鎖並設置過期時間
if (connection.setnx(key, identifier) != 0) {
connection.expire(key, lockTimeOut);
return identifier;
}
// 檢查過期時間, 並在必要時對其更新
else if (connection.ttl(key) == -1) {
connection.expire(key, lockTimeOut);
}
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
}
return null;
}
boolean releaseLock(Jedis connection, String lockName, String identifier) {
String key = "lock:" + lockName;
connection.watch(key);
// 確保當前線程還持有鎖
if (identifier.equals(connection.get(key))) {
Transaction transaction = connection.multi();
transaction.del(key);
return transaction.exec().isEmpty();
}
connection.unwatch();
return false;
}
Lua腳本實現
- Lua腳本: acquire
local key = KEYS[1]
local identifier = ARGV[1]
local lockTimeOut = ARGV[2]
-- 鎖定成功
if redis.call("SETNX", key, identifier) == 1 then
redis.call("EXPIRE", key, lockTimeOut)
return 1
elseif redis.call("TTL", key) == -1 then
redis.call("EXPIRE", key, lockTimeOut)
end
return 0
- Lua腳本: release
local key = KEYS[1]
local identifier = ARGV[1]
if redis.call("GET", key) == identifier then
redis.call("DEL", key)
return 1
end
return 0
- Pre工具: 腳本執行器
/** * @author jifang * @since 16/8/25 下午3:35. */
public class ScriptCaller {
private static final ConcurrentMap<String, String> SHA_CACHE = new ConcurrentHashMap<>();
private String script;
private ScriptCaller(String script) {
this.script = script;
}
public static ScriptCaller getInstance(String script) {
return new ScriptCaller(script);
}
public Object call(Jedis connection, List<String> keys, List<String> argv, boolean forceEval) {
if (!forceEval) {
String sha = SHA_CACHE.get(this.script);
if (Strings.isNullOrEmpty(sha)) {
// load 腳本得到 sha1 緩存
sha = connection.scriptLoad(this.script);
SHA_CACHE.put(this.script, sha);
}
return connection.evalsha(sha, keys, argv);
}
return connection.eval(script, keys, argv);
}
}
- Client
public class Client {
private ScriptCaller acquireCaller = ScriptCaller.getInstance(
"local key = KEYS[1]\n" +
"local identifier = ARGV[1]\n" +
"local lockTimeOut = ARGV[2]\n" +
"\n" +
"if redis.call(\"SETNX\", key, identifier) == 1 then\n" +
" redis.call(\"EXPIRE\", key, lockTimeOut)\n" +
" return 1\n" +
"elseif redis.call(\"TTL\", key) == -1 then\n" +
" redis.call(\"EXPIRE\", key, lockTimeOut)\n" +
"end\n" +
"return 0"
);
private ScriptCaller releaseCaller = ScriptCaller.getInstance(
"local key = KEYS[1]\n" +
"local identifier = ARGV[1]\n" +
"\n" +
"if redis.call(\"GET\", key) == identifier then\n" +
" redis.call(\"DEL\", key)\n" +
" return 1\n" +
"end\n" +
"return 0"
);
@Test
public void client() {
Jedis jedis = new Jedis("127.0.0.1", 9736);
String identifier = acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300);
System.out.println(releaseLock(jedis, "ret1", identifier));
}
String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
String identifier = UUID.randomUUID().toString();
List<String> keys = Collections.singletonList("lock:" + lockName);
List<String> argv = Arrays.asList(identifier,
String.valueOf(lockTimeOut));
long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
boolean acquired = false;
while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {
if (1 == (long) acquireCaller.call(connection, keys, argv, false)) {
acquired = true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
}
}
return acquired ? identifier : null;
}
boolean releaseLock(Jedis connection, String lockName, String identifier) {
List<String> keys = Collections.singletonList("lock:" + lockName);
List<String> argv = Collections.singletonList(identifier);
return 1 == (long) releaseCaller.call(connection, keys, argv, true);
}
}
- 參考 & 推薦
- 代碼的未來
- Redis入門指南
- Redis實戰
- Redis設計與實現
- 雲風的Blog: Lua與虛擬機
- Lua簡明教程- CoolShell
- Lua-newbie
- Lua-Users
- redis.io