1. 程式人生 > >Redis常用技術-----使用Lua語言

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語言指令碼

上圖中執行了兩個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....]

使用簽名執行Lua指令碼

下面看看結合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

注意逗號的左右兩邊的都有一個空格。

redis-cli的命令執行

可以看到逗號左右兩邊如果沒有空格,會報錯。

在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操作檔案,這樣就可以序列化直接操作物件。