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