Redis中使用Lua语言
在 Redis 的 2.6 以上版本中,除了可以使用命令外,還可以使用 Lua 語(yǔ)言操作 Redis。從前面的命令可以看出 Redis 命令的計(jì)算能力并不算很強(qiáng)大,而使用 Lua 語(yǔ)言則在很大程度上彌補(bǔ)了 Redis 的這個(gè)不足。
只是在 Redis 中,執(zhí)行 Lua 語(yǔ)言是原子性的,也就說(shuō) Redis 執(zhí)行 Lua 的時(shí)候是不會(huì)被中斷的,具備原子性,這個(gè)特性有助于 Redis 對(duì)并發(fā)數(shù)據(jù)一致性的支持。
Redis 支持兩種方法運(yùn)行腳本,一種是直接輸入一些 Lua 語(yǔ)言的程序代碼;另外一種是將 Lua 語(yǔ)言編寫(xiě)成文件。
在實(shí)際應(yīng)用中,一些簡(jiǎn)單的腳本可以采取第一種方式,對(duì)于有一定邏輯的一般采用第二種方式。而對(duì)于采用簡(jiǎn)單腳本的,Redis 支持緩存腳本,只是它會(huì)使用 SHA-1 算法對(duì)腳本進(jìn)行簽名,然后把 SHA-1 標(biāo)識(shí)返回回來(lái),只要通過(guò)這個(gè)標(biāo)識(shí)運(yùn)行就可以了。
執(zhí)行輸入 Lua 程序代碼
它的命令格式為:
eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]解說(shuō):
eval 代表執(zhí)行 Lua 語(yǔ)言的命令。Lua-script 代表 Lua 語(yǔ)言腳本。key-num 整數(shù)代表參數(shù)中有多少個(gè) key,需要注意的是 Redis 中 key 是從 1 開(kāi)始的,如果沒(méi)有 key 的參數(shù),那么寫(xiě) 0。[key1key2key3...] 是 key 作為參數(shù)傳遞給 Lua 語(yǔ)言,也可以不填它是 key 的參數(shù),但是需要和 key-num 的個(gè)數(shù)對(duì)應(yīng)起來(lái)。[value1 value2 value3...] 這些參數(shù)傳遞給 Lua 語(yǔ)言,它們是可填可不填的。這里難理解的是 key-num 的意義,舉例說(shuō)明。
可以看到執(zhí)行了兩個(gè) Lua 腳本。
eval "return'hello java'" 0這個(gè)腳本只是返回一個(gè)字符串,并不需要任何參數(shù),所以 key-num 填寫(xiě)了 0,代表著沒(méi)有任何 key 參數(shù)。按照腳本的結(jié)果就是返回了 hello java,所以執(zhí)行后 Redis 也是這樣返回的。這個(gè)例子很簡(jiǎn)單,只是返回一個(gè)字符串。
eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value設(shè)置一個(gè)鍵值對(duì),可以在 Lua 語(yǔ)言中采用 redis.call(command,key[param1,param2…]) 進(jìn)行操作,其中:
command 是命令,包括 set、get、del 等。Key 是被操作的鍵。param1,param2...代表給 key 的參數(shù)。腳本中的 KEYS[1] 代表讀取傳遞給 Lua 腳本的第一個(gè) key 參數(shù),而 ARGV[1] 代表第一個(gè)非 key 參數(shù)。
這里共有一個(gè) key 參數(shù),所以填寫(xiě)的 key-num 為 1,這樣 Redis 就知道 key-value 是 key 參數(shù),而 lua-value 是其他參數(shù),它起到的是一種間隔的作用。
最后我們可以看到使用 get 命令獲取數(shù)據(jù)是成功的,所以 Lua 腳本運(yùn)行成功了。
有時(shí)可能需要多次執(zhí)行同樣一段腳本,這個(gè)時(shí)候可以使用 Redis 緩存腳本的功能,在 Redis 中腳本會(huì)通過(guò) SHA-1 簽名算法加密腳本,然后返回一個(gè)標(biāo)識(shí)字符串,可以通過(guò)這個(gè)字符串執(zhí)行加密后的腳本。
這樣的一個(gè)好處在于,如果腳本很長(zhǎng),從客戶(hù)端傳輸可能需要很長(zhǎng)的時(shí)間,那么使用標(biāo)識(shí)字符串,則只需要傳遞 32 位字符串即可,這樣就能提高傳輸?shù)男?#xff0c;從而提高性能。
首先使用命令:
script load script這個(gè)腳本的返回值是一個(gè) SHA-1 簽名過(guò)后的標(biāo)識(shí)字符串,我們把它記為 shastring。通過(guò) shastring 可以使用命令執(zhí)行簽名后的腳本,命令的格式是:
evalsha shastring keynum [key1 key2 key3 ...] [param1 param2 param3 ...]下面演示過(guò)程。
對(duì)腳本簽名后就可以使用 SHA-1 簽名標(biāo)識(shí)運(yùn)行腳本了。在 Spring 中演示這樣的一個(gè)過(guò)程,如果是簡(jiǎn)單存儲(chǔ),筆者認(rèn)為原來(lái)的 API 中的 Jedis 對(duì)象就簡(jiǎn)單些,所以先獲取了原來(lái)的 connection 對(duì)象,代碼如下所示。
// 如果是簡(jiǎn)單的對(duì)象,使用原來(lái)的封裝會(huì)簡(jiǎn)易些 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class); applicationContext.getBean(RedisTemplate.class); // 如果是簡(jiǎn)單的操作,使用原來(lái)的Jedis會(huì)簡(jiǎn)易些 Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection(); // 執(zhí)行簡(jiǎn)單的腳本 String helloJava = (String) jedis.eval("return 'hello java'"); System.out.println(helloJava); // 執(zhí)行帶參數(shù)的腳本 jedis.eval("redis.call ('set', KEYS [1],ARGV [1])", 1, "lua-key","lua-value"); String luaKey = (String) jedis.get("lua-key"); System.out.println(luaKey); // 緩存腳本,返回shal簽名標(biāo)識(shí) String shal = jedis.scriptLoad("redis.call('set',KEYS[1], ARGV[1])"); // 通過(guò)標(biāo)識(shí)執(zhí)行腳本 jedis.evalsha(shal, 1, new String[] { "sha-key", "sha-val" }); // 獲取執(zhí)行腳本后的數(shù)據(jù) String shaVal = jedis.get("sha-key"); System.out.println(shaVal); // 關(guān)閉連接 jedis.close();上面演示的是簡(jiǎn)單字符串的存儲(chǔ),但現(xiàn)實(shí)中可能要存儲(chǔ)對(duì)象,這個(gè)時(shí)候可以考慮使用 Spring 提供的 RedisScript 接口,它還是提供了一個(gè)實(shí)現(xiàn)類(lèi)—— DefaultRedisScript,讓我們來(lái)了解它的使用方法。
這里先來(lái)定義一個(gè)可序列化的對(duì)象 Role,因?yàn)橐蛄谢孕枰獙?shí)現(xiàn) Serializable 接口,代碼如下所示。
public class Role implements Serializable {/*** 注意,對(duì)象要可序列化,需要實(shí)現(xiàn)Serializable接口,往往要重寫(xiě)serialVersionUID*/private static final long serialVersionUID = 3447499459461375642L;private long id;private String roleName;private String note;// 省略setter和getter }這個(gè)時(shí)候,就可以通過(guò) Spring 提供的 DefaultRedisScript 對(duì)象執(zhí)行 Lua 腳本來(lái)操作對(duì)象了,代碼如下所示。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class); // 定義默認(rèn)腳本封裝類(lèi) DefaultRedisScript<Role> redisScript = new DefaultRedisScript<Role>(); // 設(shè)置腳本 redisScript.setScriptText("redis.call('set',KEYS[1], ARGV[1]) return redis.call('get', KEYS[1])"); // 定義操作的key列表 List<String> keyList = new ArrayList<String>(); keyList.add("role1"); // 需要序列化保存和讀取的對(duì)象 Role role = new Role(); role.setId(1L); role.setRoleName("role_name_1"); role.setNote("note_1"); // 獲得標(biāo)識(shí)字符串 String sha1 = redisScript.getSha1(); System.out.println(sha1); // 設(shè)置返回結(jié)果類(lèi)型,如果沒(méi)有這句話,結(jié)果返回為空 redisScript.setResultType(Role.class); // 定義序列化器 JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer(); // 執(zhí)行腳本 // 第一個(gè)是RedisScript接口對(duì)象,第二個(gè)是參數(shù)序列化器 // 第三個(gè)是結(jié)果序列化器,第四個(gè)是Reids的key列表,最后是參數(shù)列表 Role obj = (Role) redisTemplate.execute(redisScript, serializer,serializer, keyList, role); // 打印結(jié)果 System.out.println(obj);注意加粗的代碼,兩個(gè)序列化器第一個(gè)是參數(shù)序列化器,第二個(gè)是結(jié)果序列化器。這里配置的是 Spring 提供的 JdkSerializationRedisSerializer,如果在 Spring 配置文件中將 RedisTemplate 的 valueSerializer 屬性設(shè)置為 JdkSerializationRedisSerializer,那么使用默認(rèn)的序列化器即可。
執(zhí)行 Lua 文件
我們把 Lua 變?yōu)橐粋€(gè)字符串傳遞給 Redis 執(zhí)行,而有些時(shí)候要直接執(zhí)行 Lua 文件,尤其是當(dāng) Lua 腳本存在較多邏輯的時(shí)候,就很有必要單獨(dú)編寫(xiě)一個(gè)獨(dú)立的 Lua 文件。比如編寫(xiě)了一段 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 thenreturn 1 end if n1 == n2 thenreturn 0 end if n1 < n2 thenreturn 2 end這是一個(gè)可以輸入兩個(gè)鍵和兩個(gè)數(shù)字(記為 n1 和 n2)的腳本,其意義就是先按鍵保存兩個(gè)數(shù)字,然后去比較這兩個(gè)數(shù)字的大小。當(dāng) n1==n2 時(shí),就返回 0;當(dāng) n1>n2 時(shí),就返回 1;當(dāng) n1<n2 時(shí),就返回 2,且把它以文件名 test.lua 保存起來(lái)。這個(gè)時(shí)候可以對(duì)其進(jìn)行測(cè)試,在 Windows 或者在 Linux 操作系統(tǒng)上執(zhí)行下面的命令:
redis-cli --eval test.lua key1 key2 , 2 4注意:redis-cli 的命令需要注冊(cè)環(huán)境,或者把文件放置在正確的目錄下才能正確執(zhí)行,這樣就能看到效果,如圖所示。
看到結(jié)果就知道已經(jīng)運(yùn)行成功了。只是這里需要非常注意命令,執(zhí)行的命令鍵和參數(shù)是使用逗號(hào)分隔的,而鍵之間用空格分開(kāi)。在本例中 key2 和參數(shù)之間是用逗號(hào)分隔的,而這個(gè)逗號(hào)前后的空格是不能省略的,這是要非常注意的地方,一旦左邊的空格被省略了,那么 Redis 就會(huì)認(rèn)為“key2,”是一個(gè)鍵,一旦右邊的空格被省略了,Redis 就會(huì)認(rèn)為“,2”是一個(gè)鍵。
在 Java 中沒(méi)有辦法執(zhí)行這樣的文件腳本,可以考慮使用 evalsha 命令,這里更多的時(shí)候我們會(huì)考慮 evalsha 而不是 eval,因?yàn)?evalsha 可以緩存腳本,并返回 32 位 sha1 標(biāo)識(shí),我們只需要傳遞這個(gè)標(biāo)識(shí)和參數(shù)給 Redis 就可以了,使得通過(guò)網(wǎng)絡(luò)傳遞給 Redis 的信息較少,從而提高了性能。
如果使用 eval 命令去執(zhí)行文件里的字符串,一旦文件很大,那么就需要通過(guò)網(wǎng)絡(luò)反復(fù)傳遞文件,問(wèn)題往往就出現(xiàn)在網(wǎng)絡(luò)上,而不是 Redis 的執(zhí)行效率上了。參考上面的例子去執(zhí)行,下面我們模擬這樣的一個(gè)過(guò)程,使用 Java 執(zhí)行 Redis 腳本代碼如下所示。
public static void testLuaFile() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);//讀入文件流File file = new File("G:\\dev\\redis\\test.lua");byte[] bytes = getFileToByte(file);Jedis jedis = (Jedis)redisTemplate.getConnectionFactory().getConnection().getNativeConnection();//發(fā)送文件二進(jìn)制給Redis,這樣REdis就會(huì)返回shal標(biāo)識(shí)byte[] shal = jedis.scriptLoad(bytes);//使用返回的標(biāo)識(shí)執(zhí)行,其中第二個(gè)參數(shù)2,表示使用2個(gè)鍵//而后面的字符串都轉(zhuǎn)化為了二進(jìn)制字節(jié)進(jìn)行傳輸Object obj = jedis.evalsha(shal,2, "key1".getBytes(),"key2".getBytes(),"2".getBytes(), "4".getBytes());System.out.println(obj); } /** * 把文件轉(zhuǎn)化為二進(jìn)制數(shù)組 * @param file 文件 * return二進(jìn)制數(shù)組 */ public static byte[] getFileToByte(File file) {byte[] by = new byte[(int) file.length()];try {InputStream is = new FileinputStream(file);ByteArrayOutputStream bytestream = new ByteArrayOutputStream(); byte[] bb = new byte[2048];int ch;ch = is.read(bb);while (ch != -1) {bytestream.write(bb, 0, ch);ch = is.read(bb);}by = bytestream.toByteArray();} catch (Exception ex) {ex.printStackTrace();}return by; }如果我們將 sha1 這個(gè)二進(jìn)制標(biāo)識(shí)保存下來(lái),那么可以通過(guò)這個(gè)標(biāo)識(shí)反復(fù)執(zhí)行腳本,只需要傳遞 32 位標(biāo)識(shí)和參數(shù)即可,無(wú)需多次傳遞腳本。
從對(duì) Redis 的流水線的分析可知,系統(tǒng)性能不佳的問(wèn)題往往并非是 Redis 服務(wù)器的處理能力,更多的是網(wǎng)絡(luò)傳遞,因此傳遞更少的內(nèi)容,有利于系統(tǒng)性能的提高。
這里采用比較原始的 Java Redis 連接操作 Redis,還可以采用 Spring 提供的 RedisScript 操作文件,這樣就可以通過(guò)序列化器直接操作對(duì)象了。
總結(jié)
以上是生活随笔為你收集整理的Redis中使用Lua语言的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Spring Boot 热部署 devt
- 下一篇: MySQL用户授权