深入理解Spring Redis的使用 (四)、RedisTemplate執行Redis腳本


對於Redis腳本使用過的同學都知道,這個主要是為了防止競態條件而用的。因為腳本是順序執行的。(不用擔心效率問題)比如我在工作用,用來設置考試最高分。

如果還沒有用過的話,先去看Redis腳本的介紹,發送腳本,緩存腳本,發送sha1執行腳本,以及基本的lua腳本的語法。

 

1. Redis腳本的使用場景

在一些緩存的設置中,經常會出現競態條件,由於並發導致數據有誤。比如大家熟知的++操作。我們自己通過Redis實現++的話,很容易在並發下出現誤差。所以Redis提供了incr函數。我在設置最高分的時候,獲取分數70,A得分80,然后設置80。然后在同時,B獲取分數70,但是B得分78,78>70,然后設置78。這樣的話,B的修改就把A的修改替換了。這也是數據庫隔離級別中的第一類更新丟失。

為了防止這個問題,Redis提供了順序執行命令的方法,就是使用腳本。

 

2. 腳本類RedisScript

RedisTemplate對腳本提供了很高的支持,執行方法同之前的類似,都是通過connection回調。但是這里要注意的是:腳本不支持事務,所以腳本之前不能進行connection.multi()開啟事務,也不能用@Transactional注解讓spring來開啟事務,這些都會拋出異常,大家可以自行測試下。

 

看到這里,我們新建一個最高分的腳本類,MaxscoreScript,繼承自接口RedisScript,需要實現三個方法:

public interface RedisScript<T> {

    /**
     * @return The SHA1 of the script, used for executing Redis evalsha command
     */
    String getSha1();

    /**
     * @return The script result type. Should be one of Long, Boolean, List, or deserialized value type. Can be null if
     *         the script returns a throw-away status (i.e "OK")
     */
    Class<T> getResultType();

    /**
     * @return The script contents
     */
    String getScriptAsString();

}

 

getSha1(),是獲取腳本摘要。看過腳本的應該知道,Redis支持緩存腳本,第一次發送腳本以后,后面都直接發送該腳本的sha1摘要信息來直接執行,大概是為了節省傳輸成本吧。畢竟摘要只需要32個字符。

getResultType(),是獲取返回類型的class,需要和定義的泛型T一致。其實之前在Hibernate的Basedao里面,都用過直接通過實例來獲取泛型的class。雖然不明白這里為什么加,但是實現這個方法,絕對是沒有錯。

getScriptAsString(),毫無疑問,這個是返回腳本內容。至於怎么寫腳本,這里不做說明。

 

3. 使用RedisTemplate執行腳本

    public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
        return scriptExecutor.execute(script, keys, args);
    }

    public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
            List<K> keys, Object... args) {
        return scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args);
    }

 

 

相比之下,第二個是自己傳入序列化器,第一個是用默認的key和value序列化器。

強烈建議使用第二個。然后統一傳入(StringSerializer),里面的參數全部轉為String。我在使用的時候,lua進行大小比較,發現比較的有誤。后來調試才發現,是JdkSerializer中,Redis把他當一串\xu..這樣的字符串,字符串比較顯然不准確。但是如果是"11","12"這樣的字符串,就可以通過lua轉為num類型,再進行比較。

當然,也許有別的業務場景,不需要這么做,也有可能。自行分析斟酌。

 

 

我們看到執行腳本的源碼

    protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys,
            byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {

        Object result;
        try {
            result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
        } catch (Exception e) {

            if (!exceptionContainsNoScriptError(e)) {
                throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e);
            }

            result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
        }

        if (script.getResultType() == null) {
            return null;
        }

        return deserializeResult(resultSerializer, result);
    }

 

這一段代碼是最終調用的,之前還有一些序列化參數的操作沒有貼出來。

默認直接提交sha1摘要,得到異常,如果異常屬於exceptionContainsNoScriptError ,再發送執行腳本,獲取返回結果后通過用戶定義的結果序列化器進行反序列化。

 

4. 總結

 

這一篇講了RedisTemplate如何使用腳本。可能對於不懂腳本在connection 或者在redis控制台下使用的同學來說,還是不能理解。但是如果懂得控制台發送腳本,那么通過RedisTemplate的使用,會讓很多問題迎刃而解。

到這里我所能帶給大家的也就說完了。下一篇我將把自己遇到的問題羅列下,前車之鑒,后事之師。希望大家以后遇到后,不會在卡太長時間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM