Redis常用技術-----使用Lua語言
Redis命令的計算能力並不算很強大,使用Lua語言則可以在很大程度上彌補Redis的這個不足。在Redis中,執行Lua語言是原子性,也就是說Redis執行Lua的時候是不會被中斷的,具備原子性,這個特性有助於Redis對併發資料一致性的支援。
Redis支援兩種方法執行指令碼,一種是直接輸入一些Lua語言的程式程式碼,另一種是將Lua語言編寫成檔案。在實際應用中,一些簡單的指令碼可以採取第一種方式,對於有一定邏輯的一般採用第二種。而對於採用簡單指令碼的,Redis支援快取指令碼,只是它會使用SHA-1演算法對指令碼進行簽名,然後把SHA-1標識返回,只要通過這個標識執行就可以了。
(1)執行輸入Lua程式程式碼
命令格式為
eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
其中
- eval代表執行Lua語言的命令。
- lua-script代表Lua語言指令碼。
- key-num表示引數中有多少個key,需要注意的是Redis中key是從1開始的,如果沒有key的引數,那麼寫0。
- [key1 key2 key3…]是key作為引數傳遞給Lua語言,也可以不填,但是需要和key-num的個數對應起來。
- [value1 value2 value3 ….]這些引數傳遞給Lua語言,他們是可填可不填的。
上圖中執行了兩個Lua指令碼
eval "return 'Hello World'" 0
這個指令碼只是返回一個字串,並不需要任何引數,所以key-num為0,代表沒有任何key引數。
eval "redis.call('set',KEYS[0],ARGV[1])" 1 lua-key lua-value
設定一個鍵值對,在Lua語言中採用redis.call(command,key[param1, param2…])進行操作,其中
- command是命令,包括set、get、del等。
- key是被操作的鍵。
- param1,param2…代表給key的引數。
指令碼中的KEYS[1]代表傳遞給Lua指令碼的第一個key引數,而ARGV[1]代表第一個非key引數。
有時可能需要多次執行同一段Lua指令碼。這時可以使用Redis**快取指令碼的功能,在Redis中指令碼會通過SHA-1簽名演算法加密指令碼,然後返回一個標識字串,可以通過這個字串執行加密後的指令碼。這樣的一個好處在於,如果指令碼很長,從客戶端傳輸可能需要很長時間,那麼使用標識字串,則只需要傳遞32位字串即可,這樣就可以提高傳輸的效率,提高效能。**
首先使用命令
script load lua-script
這個指令碼的返回值是一個SHA-1簽名過後的標識字串,記為shastring,通過它就可以使用命令執行簽名後的指令碼,命令格式如下
evalsha shastring keynum [key1 key2 key3....] [param1 param2 param3....]
下面看看結合Spring
/**
* 在Java中使用Lua指令碼
* @author liu
*/
public class TestLua {
@SuppressWarnings({ "resource", "rawtypes" })
@Test
public void testLua() {
// 如果是簡單的物件,使用原來的封裝會容易一些
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
RedisTemplate rt = applicationContext.getBean(RedisTemplate.class);
// 如果是簡單的操作,使用原來的Jedis會簡單些
Jedis jedis = (Jedis)rt.getConnectionFactory().getConnection().getNativeConnection();
// 執行簡單的指令碼
String helloLua = (String)jedis.eval("return 'Hello Lua'");
System.out.println(helloLua);
// 執行帶引數的指令碼
jedis.eval("redis.call('set',KEYS[1],ARGV[1])", 1, "lua-key","lua-value");
String luaKey = jedis.get("lua-key");
System.out.println(luaKey);
// 快取指令碼,返回sha1簽名標識
String sha1 = (String)jedis.scriptLoad("redis.call('set',KEYS[1],ARGV[1])");
// 通過標識執行指令碼
jedis.evalsha(sha1, 1, new String[] {"sha-key","sha-val1"});
// 獲取執行指令碼後的資料
String value = jedis.get("sha-key");
System.out.println(value);
jedis.close();
}
}
上面演示的是簡單字串的儲存,但現實中可能要儲存物件,這時可以考慮使用Spring提供的RedisScript介面,它有一個預設實現類DefaultRedisScript。
先定義一個可序列的物件Role
public class Role implements Serializable {
private static final long serialVersionUID = 247558898916003817L;
private long id;
private String roleName;
private String note;
// get set
}
這個時候,就可以通過Spring提供的DefaultRedisScript物件執行Lua指令碼來操作物件了。
/**
* 通過DefaultRedisScript來操作Lua
*/
@SuppressWarnings({ "resource", "rawtypes", "unchecked" })
@Test
public void testRedisScript() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
RedisTemplate rt = applicationContext.getBean(RedisTemplate.class);
// 定義預設指令碼封裝類
DefaultRedisScript<Role> rs = new DefaultRedisScript<>();
// 設定指令碼
rs.setScriptText("redis.call('set',KEYS[1],ARGV[1]) return redis.call('get', KEYS[1])");
// 定義操作的key列表
List<String> keyList = new ArrayList<>();
keyList.add("role1");
// 需要序列化儲存和獲取的物件
Role role = new Role();
role.setId(1L);
role.setNote("note1");
role.setRoleName("roleName1");
// 獲得標識字串
String sha1 = rs.getSha1();
System.out.println(sha1);
// 設定返回結果型別,如果沒有這句,則返回為空
rs.setResultType(Role.class);
// 定義序列化器
JdkSerializationRedisSerializer jdk = new JdkSerializationRedisSerializer();
// 執行指令碼,第一個引數是RedisScript介面物件,第二個是引數序列化器
// 第三個是結果序列化器,第四個是Redis的key列表,最後是引數列表
Role obj = (Role)rt.execute(rs, jdk, jdk, keyList, role);
System.out.println(obj);
}
(2)執行Lua檔案
當Lua指令碼存在比較多的邏輯時,顯然使用上面的方式明顯不合適,這時就有必要單獨編寫一個Lua檔案。
且看下面的一個Lua檔案
redis.call('set', KEYS[1], ARGV[1])
redis.call('set', KEYS[2], ARGV[2])
local n1 = tonumber(redis.call('get', KEYS[1]))
local n2 = tonumber(redis.call('get', KEYS[2]))
if n1 > n2 then
return 1
end
if n1 == n2 then
return 0
end
if n1 < n2 then
return 2
end
這是一個可以輸入兩個鍵和兩個數字(記為n1和n2)的指令碼,其意義是先按鍵儲存兩個數字,然後去比較這兩個數字的大小。儲存為test.lua。
在linux中執行下面的命令
redis-cli --eval test.lua key1 key2 , 2 4
注意逗號的左右兩邊的都有一個空格。
可以看到逗號左右兩邊如果沒有空格,會報錯。
在Java中無法執行這樣的檔案指令碼,可以考慮使用evalsha命令,這裡更多的時候我們會考慮evalsha而不是eval,因為evalsha可以快取指令碼,並返回32位sha1標識,這樣可以提高傳輸效能。
/**
* 執行Lua檔案指令碼
* @author liu
*/
public class TestLuaFile {
@SuppressWarnings({ "resource", "rawtypes" })
@Test
public void testLuaFile() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
RedisTemplate rt = applicationContext.getBean(RedisTemplate.class);
// 讀入檔案流
String path = this.getClass().getClassLoader().getResource("test.lua").getPath();
System.out.println(path);
File file = new File(path);
byte[] bytes = getFileToByte(file);
Jedis jedis = (Jedis)rt.getConnectionFactory().getConnection().getNativeConnection();
// 傳送檔案二進位制給Redis,返回sha1標識
byte[] sha1 = jedis.scriptLoad(bytes);
// 使用返回的標識執行,2表示有兩個鍵
Object obj = jedis.evalsha(sha1, 2, "key1".getBytes(), "key2".getBytes(), "2".getBytes(), "4".getBytes());
System.out.println(obj);
}
/**
* 把檔案轉化為二進位制陣列
* @param file 檔案
* @return 二進位制陣列
*/
public byte[] getFileToByte(File file) {
byte[] by = new byte[(int)file.length()];
InputStream is = null;
try {
is = new FileInputStream(file);
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
byte[] bb = new byte[2048];
// 從此輸入流中讀入bb.length個位元組放進bb陣列
int ch = is.read(bb);
while(ch != -1) {
// 將bb陣列中的內容寫入到輸出流
bytestream.write(bb, 0, ch);
ch = is.read(bb);
}
// 將輸出流中的內容複製到by陣列
by = bytestream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return by;
}
}
如果我們將sha1這個標識字串儲存起來,那麼就可以通過這個標識反覆執行Lua指令碼檔案。只需傳遞sha1標識和引數即可,無需傳遞指令碼,有利於系統性能的提高。這裡是採用的Java Redis操作Redis,還可以使用Spring的RedisScript操作檔案,這樣就可以序列化直接操作物件。